分頁查詢是資料庫應用中最常見的需求之一。在 SQL Server 2000 時代,開發者常使用 NOT IN 或 MAX(ID) 等技巧實現分頁;而 SQL Server 2005 引入 ROW_NUMBER() 視窗函數後,分頁寫法變得更簡潔且效能更優。本文將透過實際測試,對比兩種分頁存儲過程在 50 萬筆資料下的效能表現,並分析統計總筆數對效能的影響。
CREATE TABLE [dbo].[Xtest](
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[XName] [nvarchar](50) NULL,
[XDemo] [nvarchar](500) NULL,
CONSTRAINT [PK_xtest] PRIMARY KEY CLUSTERED ([ID] ASC)
) ON [PRIMARY]
DECLARE @i INT = 1
WHILE @i < 500001
BEGIN
INSERT INTO dbo.Xtest (XName, XDemo)
VALUES (CAST(@i AS VARCHAR) + 'name' + CAST(@i AS VARCHAR),
N'這是第' + CAST(@i AS VARCHAR) + N'的內容')
SET @i = @i + 1
END
另對 XName 欄位建立非叢集索引以模擬實際查詢場景。
採用 NOT IN (SELECT TOP ...) 子查詢方式跳過前幾頁資料:
ALTER PROCEDURE [dbo].[sp_Pager2000]
@tblName VARCHAR(255),
@strGetFields VARCHAR(1000) = '*',
@pkName NVARCHAR(50) = 'ID',
@strOrder VARCHAR(255) = '',
@strWhere VARCHAR(1500) = '',
@pageIndex INT = 1,
@pageSize INT = 20,
@recordCount INT = 0 OUTPUT,
@doCount BIT = 0
AS
BEGIN
-- 動態 SQL 組裝邏輯,核心為:
-- SELECT TOP @pageSize ... WHERE @pkName NOT IN (SELECT TOP (@pageSize*(@pageIndex-1)) ...)
-- 完整代碼見原文
END
利用視窗函數產生行號,再以 BETWEEN 篩選目標頁:
ALTER PROCEDURE [dbo].[sp_Pager2005]
@tblName VARCHAR(255),
@strGetFields VARCHAR(1000) = '*',
@strOrder VARCHAR(255) = '',
@strWhere VARCHAR(1500) = '',
@pageIndex INT = 1,
@pageSize INT = 20,
@recordCount INT OUTPUT,
@doCount BIT = 0
AS
BEGIN
DECLARE @strSQL VARCHAR(5000)
-- 統計總筆數邏輯同上
-- 核心查詢:
SET @strSQL = 'SELECT * FROM (SELECT ROW_NUMBER() OVER (' + @strOrder + ') AS ROWID, '
+ @strGetFields + ' FROM [' + @tblName + '] ' + @strWhere + ') AS sp '
+ 'WHERE ROWID BETWEEN ' + STR((@pageIndex-1)*@pageSize+1)
+ ' AND ' + STR(@pageIndex*@pageSize)
EXEC (@strSQL)
END
測試條件:WHERE XName LIKE '%222name%',分頁大小 20 筆,測試第 1~6 頁。環境:Core 2 Duo 2.20GHz, 1GB RAM, SQL Server 2008。
| 頁碼 | 不使用 ROW_NUMBER | 使用 ROW_NUMBER |
|---|---|---|
| Page 1 | 2.937 秒 | 2.937 秒 |
| Page 2 | 3.140 秒 | 3.031 秒 |
| Page 3 | 3.453 秒 | 3.156 秒 |
| Page 4 | 3.609 秒 | 3.296 秒 |
| Page 5 | 3.843 秒 | 3.421 秒 |
| Page 6 | 4.156 秒 | 3.515 秒 |
結論:每頁皆執行 COUNT(*) 會導致效能隨頁碼遞增惡化,兩種寫法差異不大,瓶頸在於重複統計。
| 頁碼 | 不使用 ROW_NUMBER | 使用 ROW_NUMBER |
|---|---|---|
| Page 1 | 2.937 秒 | 2.937 秒 |
| Page 2 | 0.343 秒 | 0.218 秒 |
| Page 3 | 0.593 秒 | 0.359 秒 |
| Page 4 | 0.812 秒 | 0.468 秒 |
| Page 5 | 1.046 秒 | 0.578 秒 |
| Page 6 | 1.281 秒 | 0.687 秒 |
結論:避免重複統計總筆數後,ROW_NUMBER() 版本效能顯著優於傳統 NOT IN 寫法,且隨頁碼增長效能衰減較緩慢。
COUNT(*) 全表掃描。ORDER BY 與 WHERE 條件欄位有適當索引,必要時建立覆蓋索引減少 Key Lookup。分頁效能的關鍵在於避免重複統計總筆數與選擇高效的分頁語法。實測證明:搭配 ROW_NUMBER() 並快取總筆數,可將後續頁面回應時間從秒級降至毫秒級。開發者應根據 SQL Server 版本選擇適當方案,並將統計邏輯從分頁存儲過程中剝離,由應用層管理。