分頁是面試必考、業務高頻的 SQL 技術難點。本文在 SQL Server 環境下,對五種經典分頁寫法進行 1萬/10萬/100萬三級資料量實測,量化效能差異,並提供生產級存儲過程封裝代碼。
CREATE DATABASE DBTest; USE DBTest;
CREATE TABLE pagetest (
id INT IDENTITY(1,1) NOT NULL,
col01 INT NULL,
col02 NVARCHAR(50) NULL,
col03 DATETIME NULL
);
-- 插入 100 萬測試資料
DECLARE @i INT = 0;
WHILE @i < 1000000 BEGIN
INSERT INTO pagetest VALUES (RAND()*10000, LEFT(NEWID(),10), GETDATE());
SET @i += 1;
END
測試場景:每頁 50 筆,取第 199 頁(OFFSET 9900, LIMIT 50)。
| 編號 | 寫法名稱 | 核心 SQL |
|---|---|---|
| 1 | NOT IN / TOP | SELECT TOP 50 * FROM pagetest WHERE id NOT IN (SELECT TOP 9900 id FROM pagetest ORDER BY id) ORDER BY id |
| 2 | NOT EXISTS | SELECT TOP 50 * FROM pagetest WHERE NOT EXISTS (SELECT 1 FROM (SELECT TOP 9900 id FROM pagetest ORDER BY id) a WHERE a.id = pagetest.id) ORDER BY id |
| 3 | MAX / TOP | SELECT TOP 50 * FROM pagetest WHERE id > (SELECT MAX(id) FROM (SELECT TOP 9900 id FROM pagetest ORDER BY id) a) ORDER BY id |
| 4 | ROW_NUMBER() 標準 | SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY id) rn, * FROM pagetest) a WHERE rn BETWEEN 9901 AND 9950 |
| 5 | ROW_NUMBER() 變體 | SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY temp) rn, * FROM (SELECT TOP 9950 temp=0, * FROM pagetest ORDER BY id) a) b WHERE rn > 9900 |


| 資料量 | 最佳寫法 | 次佳 | 最差 | 備註 |
|---|---|---|---|---|
| 1 萬 | 差異微小(<5ms) | - | - | 所有寫法均可接受 |
| 10 萬 | MAX/TOP、ROW_NUMBER() | NOT EXISTS | NOT IN、ROW_NUMBER() 變體 | 變體寫法效能極差 |
| 100 萬 | MAX/TOP、ROW_NUMBER() | NOT EXISTS | NOT IN、ROW_NUMBER() 變體 | 差距擴大至數倍 |
WHERE rn > ... AND rn < ...、WHERE rn BETWEEN ...、WHERE rn > ... 優化器生成相同計畫。TOP 9950 再排序,破壞索引有序性,導致大量排序溢出到 tempdb。@tbName:表名(防注入需白名單驗證)@tbFields:回傳欄位(* 或逗號分隔)@PageSize、@PageIndex:分頁參數@strWhere:WHERE 條件(不含 WHERE 關鍵字)@strOrder:ORDER BY 子句(含 ORDER BY 關鍵字)@Total OUTPUT:總筆數回傳CREATE PROC [dbo].[spSqlPageByRownumber]
@tbName VARCHAR(255), @tbFields VARCHAR(1000),
@PageSize INT, @PageIndex INT,
@strWhere VARCHAR(1000), @strOrder VARCHAR(255),
@Total INT OUTPUT
AS
BEGIN
DECLARE @sql NVARCHAR(MAX), @sqlCount NVARCHAR(MAX);
-- 總筆數
SET @sqlCount = 'SELECT @Total = COUNT(*) FROM ' + @tbName +
CASE WHEN @strWhere <> '' THEN ' WHERE ' + @strWhere ELSE '' END;
-- 分頁查詢
SET @sql = 'SELECT * FROM (SELECT ROW_NUMBER() OVER(' + @strOrder + ') rn, ' + @tbFields +
' FROM ' + @tbName + CASE WHEN @strWhere <> '' THEN ' WHERE ' + @strWhere ELSE '' END + ') t ' +
'WHERE t.rn > ' + CAST((@PageIndex-1)*@PageSize AS VARCHAR) +
' AND t.rn <= ' + CAST(@PageIndex*@PageSize AS VARCHAR);
EXEC sp_executesql @sqlCount, N'@Total INT OUTPUT', @Total OUTPUT;
EXEC sp_executesql @sql;
END
CREATE PROC [dbo].[spSqlPageByMaxTop]
@tbName VARCHAR(255), @tbFields VARCHAR(1000),
@PageSize INT, @PageIndex INT,
@strWhere VARCHAR(1000), @strOrder VARCHAR(255),
@Total INT OUTPUT
AS
BEGIN
DECLARE @sql NVARCHAR(MAX), @sqlCount NVARCHAR(MAX);
SET @sqlCount = 'SELECT @Total = COUNT(*) FROM ' + @tbName +
CASE WHEN @strWhere <> '' THEN ' WHERE ' + @strWhere ELSE '' END;
SET @sql = 'SELECT TOP ' + CAST(@PageSize AS VARCHAR) + ' * FROM ' + @tbName +
' WHERE id > (SELECT MAX(id) FROM (SELECT TOP ' + CAST((@PageIndex-1)*@PageSize AS VARCHAR) +
' id FROM ' + @tbName + ' ' + @strOrder + ') a) ' + @strOrder;
EXEC sp_executesql @sqlCount, N'@Total INT OUTPUT', @Total OUTPUT;
EXEC sp_executesql @sql;
END
WHERE id > @last_seen_id ORDER BY id LIMIT 50,無 OFFSET 掃描,極致效能,適合無限滾動/翻頁。INDEX idx_cover (id) INCLUDE (col01, col02, col03) 避免書籤查找。COUNT(*) 結果。分頁無銀彈,**MAX/TOP 與 ROW_NUMBER() 是關係型資料庫的標準解**;百萬級以上建議改用 Keyset Pagination 或引入 Elasticsearch 等搜尋引擎分擔查詢壓力。