SELECT TOP n * FROM t WHERE id NOT IN (SELECT TOP m id FROM t ORDER BY id)。缺點:NOT IN 遇 NULL 值語意陷阱、子查詢重複掃描、深分頁效能急劇下降。MAX(id) 或 MIN(id) 作為分頁邊界,配合 SEEK 運算子,實現 O(1) 定位。| 頁碼 | 方案一:表變量+IDENTITY | 方案二:NOT IN | 方案三:MAX/MIN 定位 |
|---|---|---|---|
| 1 | 60 ms | 30 ms | 76 ms |
| 10 | 46 | 16 | 63 |
| 100 | 1,076 | 720 | 130 |
| 500 | 540 | 12,943 | 83 |
| 1,000 | 17,110 | 470 | 250 |
| 10,000 | 24,796 | 4,500 | 140 |
| 100,000 | 38,326 | 42,283 | 1,553 |
| 250,000 | 28,140 | 128,720 | 2,330 |
| 500,000 | 121,686 | 127,846 | 7,168 |
結論:方案三(MAX/MIN 定位)在深分頁(>1000 頁)效能穩定優異,後勁十足。
-- 第 @PageIndex 頁,每頁 @PageSize 筆,依 id 遞增
DECLARE @PageSize INT = 20, @PageIndex INT = 50000;
SELECT TOP (@PageSize) *
FROM BigTable
WHERE id > (
SELECT MAX(id) FROM (
SELECT TOP ((@PageIndex - 1) * @PageSize) id
FROM BigTable
ORDER BY id
) AS t
)
ORDER BY id;
CREATE PROCEDURE dbo.Pagination_Fast
@tblName NVARCHAR(255), -- 資料表名
@strGetFields NVARCHAR(1000) = N'*', -- 回傳欄位
@fldName NVARCHAR(255), -- 排序欄位(需唯一、有索引)
@PageSize INT = 20,
@PageIndex INT = 1,
@doCount BIT = 0, -- 1=回傳總筆數
@OrderType BIT = 0, -- 0=ASC, 1=DESC
@strWhere NVARCHAR(1500) = N'' -- WHERE 條件(不含 WHERE 關鍵字)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @sql NVARCHAR(MAX),
@order NVARCHAR(100),
@bound NVARCHAR(200);
-- 排序方向
IF @OrderType = 0
BEGIN
SET @order = N' ORDER BY [' + @fldName + N'] ASC';
SET @bound = N'> (SELECT MAX([' + @fldName + N']) FROM (SELECT TOP ((@PageIndex - 1) * @PageSize) [' + @fldName + N'] FROM [' + @tblName + N']' + @order + N') AS t)';
END
ELSE
BEGIN
SET @order = N' ORDER BY [' + @fldName + N'] DESC';
SET @bound = N'< (SELECT MIN([' + @fldName + N']) FROM (SELECT TOP ((@PageIndex - 1) * @PageSize) [' + @fldName + N'] FROM [' + @tblName + N']' + @order + N') AS t)';
END
-- 總筆數查詢
IF @doCount = 1
BEGIN
SET @sql = N'SELECT COUNT(*) AS Total FROM [' + @tblName + N']' +
CASE WHEN @strWhere <> N'' THEN N' WHERE ' + @strWhere ELSE N'' END;
EXEC sp_executesql @sql;
END
-- 第 1 頁直接 TOP 查詢(最快)
IF @PageIndex = 1
BEGIN
SET @sql = N'SELECT TOP (@PageSize) ' + @strGetFields + N' FROM [' + @tblName + N']' +
CASE WHEN @strWhere <> N'' THEN N' WHERE ' + @strWhere ELSE N'' END + @order;
EXEC sp_executesql @sql, N'@PageSize INT', @PageSize;
RETURN;
END
-- 一般頁:鍵值定位
SET @sql = N'SELECT TOP (@PageSize) ' + @strGetFields + N' FROM [' + @tblName + N']' +
N' WHERE [' + @fldName + N']' + @bound +
CASE WHEN @strWhere <> N'' THEN N' AND ' + @strWhere ELSE N'' END + @order;
EXEC sp_executesql @sql,
N'@PageSize INT, @PageIndex INT',
@PageSize = @PageSize,
@PageIndex = @PageIndex;
END
GO
-- 查詢第 3 頁,每頁 20 筆,依 CreateDate 遞減,回傳總筆數
EXEC dbo.Pagination_Fast
@tblName = N'Orders',
@strGetFields = N'OrderID, CustomerID, OrderDate, TotalAmount',
@fldName = N'OrderDate',
@PageSize = 20,
@PageIndex = 3,
@doCount = 1,
@OrderType = 1,
@strWhere = N'CustomerID = ''ALFKI'' AND OrderDate >= ''2023-01-01''';
IDENTITY 欄位。非唯一欄位會導致邊界定位錯誤(同值列跨頁)。sp_executesql 避免 SQL 注入,執行計畫可重用。TOP N ... ORDER BY,省去子查詢開銷。@strWhere 同時套用於子查詢與外層,確保邊界計算與最終結果一致。TOP (巨大數字) 掃描過多列而變慢。極端深分頁建議改用「游標式分頁」(前端傳上一頁最大/最小鍵值)或「預先計算頁碼對應鍵值區間」快取表。@fldName 有**覆蓋索引**(含 @strGetFields 所有欄位)或至少叢集索引。@strWhere 條件欄位有合適索引,避免掃描。SET STATISTICS IO, TIME ON 觀察邏輯讀與耗時。READ UNCOMMITTED(NOLOCK)或快照隔離級別減少封鎖。