面對千萬列資料表,傳統 ROW_NUMBER() OVER (...) + WHERE rn BETWEEN ... 或 OFFSET/FETCH 在深分頁 (第 10 萬頁) 時效能崩潰。本文提供基於鍵集分頁 思想的通用分頁存儲過程,利用排序鍵單調性實現 O(1) 翻頁效能,支援任意排序、篩選、分組、欄位選擇。
Id=12345, CreatedAt='2024-01-01')WHERE (SortKey > @LastSortKey) ORDER BY SortKey FETCH NEXT @PageSize
IF OBJECT_ID('SP_Pagination') IS NOT NULL DROP PROC SP_Pagination;
GO
CREATE PROCEDURE SP_Pagination
@Tables VARCHAR(2000), -- 表/檢視表名 (可含 Schema)
@PrimaryKey VARCHAR(500), -- 主鍵/唯一排序鍵 (支援複合: Col1, Col2)
@Sort VARCHAR(500) = NULL, -- 排序子句 (不含 ORDER BY),如 'Id DESC, CreateDate ASC'
@CurrentPage INT = 1,
@PageSize INT = 5,
@Fields VARCHAR(2000) = '*', -- 輸出欄位
@Filter VARCHAR(1000) = NULL, -- WHERE 條件 (不含 WHERE)
@Group VARCHAR(1000) = NULL -- GROUP BY 條件 (不含 GROUP BY)
AS
BEGIN
SET NOCOUNT ON;
-- 1. 預設排序 = 主鍵
IF @Sort IS NULL OR @Sort = '' SET @Sort = @PrimaryKey;
-- 2. 解析排序方向與運算子 (簡化版:假設單欄位排序,複合需擴充)
DECLARE @SortColumn VARCHAR(1000), @Operator CHAR(2), @SortTable VARCHAR(1000), @SortName VARCHAR(1000);
IF CHARINDEX('DESC', @Sort) > 0 BEGIN
SET @SortColumn = REPLACE(@Sort, 'DESC', '');
SET @Operator = '<=';
END ELSE BEGIN
SET @SortColumn = REPLACE(@Sort, 'ASC', '');
SET @Operator = '>=';
END
SET @SortColumn = LTRIM(RTRIM(@SortColumn));
-- 3. 解析 Schema.Table.Column
IF CHARINDEX('.', @SortColumn) > 0 BEGIN
SET @SortTable = SUBSTRING(@SortColumn, 1, CHARINDEX('.', @SortColumn)-1);
SET @SortName = SUBSTRING(@SortColumn, CHARINDEX('.', @SortColumn)+1, LEN(@SortColumn));
END ELSE BEGIN
SET @SortTable = @Tables;
SET @SortName = @SortColumn;
END
-- 4. 取得排序欄位資料型別 (用於變數宣告)
DECLARE @Type VARCHAR(1000), @Prec INT;
SELECT @Type = t.name, @Prec = c.prec
FROM sysobjects o
JOIN syscolumns c ON o.id = c.id
JOIN systypes t ON c.xusertype = t.xusertype
WHERE o.name = @SortTable AND c.name = @SortName;
IF CHARINDEX('char', @Type) > 0 SET @Type = @Type + '(' + CAST(@Prec AS VARCHAR) + ')';
-- 5. 計算分頁參數
DECLARE @StrPageSize VARCHAR(100) = CAST(@PageSize AS VARCHAR(100));
DECLARE @StrStartRow VARCHAR(100) = CAST(((@CurrentPage - 1) * @PageSize + 1) AS VARCHAR(100));
DECLARE @StrFilter VARCHAR(1000) = '', @StrSimpleFilter VARCHAR(1000) = '', @StrGroup VARCHAR(1000) = '';
IF @Filter IS NOT NULL AND @Filter <> '' BEGIN
SET @StrFilter = ' WHERE ' + @Filter;
SET @StrSimpleFilter = ' AND ' + @Filter;
END
IF @Group IS NOT NULL AND @Group <> '' SET @StrGroup = ' GROUP BY ' + @Group;
-- 6. 動態 SQL 執行雙查詢法:先取第 N 筆排序鍵值,再取該頁資料
DECLARE @DynSql NVARCHAR(MAX) = N'
DECLARE @SortColumn ' + @Type + N';
SET ROWCOUNT ' + @StrStartRow + N';
SELECT @SortColumn = ' + @SortColumn + N' FROM ' + @Tables + @StrFilter + @StrGroup + N' ORDER BY ' + @Sort + N';
SET ROWCOUNT ' + @StrPageSize + N';
SELECT ' + @Fields + N' FROM ' + @Tables + N' WHERE ' + @SortColumn + @Operator + N' @SortColumn ' + @StrSimpleFilter + @StrGroup + N' ORDER BY ' + @Sort + N';';
EXEC (@DynSql);
END;
GO
-- Traffic_Sites 表,依 Id 遞增分頁,每頁 5 筆,第 1 頁
EXEC SP_Pagination 'Traffic_Sites', 'Id', 'Id ASC', 1, 5, '*', '', '';

| 技術 | 說明 |
|---|---|
SET ROWCOUNT N | 舊版分頁核心:限制後續 SELECT 影響列數。第一次查詢取第 N 筆排序鍵值,第二次取該頁資料。 |
| 雙查詢法 | 查詢 1:SELECT @SortKey = SortCol FROM ... ORDER BY SortCol SET ROWCOUNT @StartRow 取得游標點。查詢 2:WHERE SortCol @Op @SortKey ORDER BY SortCol SET ROWCOUNT @PageSize 取得頁資料。 |
| 動態型別變數 | 透過 syscolumns/systypes 查詢排序欄位實際型別,宣告匹配變數,避免隱式轉換。 |
| 複合排序支援缺口 | 目前程式碼假設單欄位排序。複合排序需將 @SortColumn 拆解為多變數,WHERE 條件改為 (Col1 > @v1) OR (Col1 = @v1 AND Col2 > @v2) ...。 |
CREATE PROCEDURE SP_Pagination_Modern
@Tables NVARCHAR(500),
@SortColumns NVARCHAR(500), -- 如 'Id ASC, CreateDate DESC'
@PageSize INT = 20,
@LastKeys NVARCHAR(MAX) = NULL, -- JSON: '[{"Id":123},{"CreateDate":"2024-01-01"}]'
@Fields NVARCHAR(MAX) = N'*',
@Filter NVARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Sql NVARCHAR(MAX) = N'SELECT ' + @Fields + N' FROM ' + @Tables;
IF @Filter IS NOT NULL SET @Sql += N' WHERE ' + @Filter;
-- 建立動態 WHERE 子句對應 @LastKeys
IF @LastKeys IS NOT NULL BEGIN
-- 此處需解析 JSON 產生 (Col1 > @v1) OR (Col1=@v1 AND Col2 > @v2) ...
-- 簡化:假設單鍵
SET @Sql += N' WHERE Id > @LastId';
END
SET @Sql += N' ORDER BY ' + @SortColumns + N' OFFSET 0 ROWS FETCH NEXT ' + CAST(@PageSize AS NVARCHAR(10)) + N' ROWS ONLY;';
-- 實際需配合 sp_executesql 參數化
EXEC sp_executesql @Sql, N'@LastId BIGINT', @LastId = 123;
END;
| 場景 | 推薦方案 |
|---|---|
| 傳統 APP、順序翻頁、SQL 2000/2005/2008 | 本文 SP_Pagination (ROWCOUNT 雙查詢) |
| 現代 Web API、游標分頁、SQL 2012+ | OFFSET/FETCH + 鍵集游標 (LastId/LastSortKey) |
| 需隨機跳頁、深分頁 | 覆蓋索引 + OFFSET/FETCH (接受效能遞減) 或預先物化分頁表 |
| 極高併發、極大資料量 | 應用層快取首頁、搜尋服務、資料庫只做鍵集查詢 |
分頁優化的本質是避免掃描與排序。鍵集分頁將「第 N 頁」轉化為「大於上一頁游標的前 M 筆」,從根本上解決深分頁效能崩潰問題。