場景:產品表 100 萬行,按城市分組取每組最新 10 筆。實測四種方案效能,ROW_NUMBER() 穩居第一,游標次之,CROSS APPLY 與 COUNT 方案不可用。
CREATE TABLE dbo.Products (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50),
AddTime DATETIME,
City NVARCHAR(10)
);
-- 建立支援索引
CREATE INDEX IX_Products_City_AddTime ON dbo.Products (City, AddTime DESC) INCLUDE (Name);
-- 插入 100 萬測試數據...
| 方案 | 語法核心 | 平均耗時 | 評價 |
|---|---|---|---|
| 1. ROW_NUMBER() | ROW_NUMBER() OVER (PARTITION BY City ORDER BY AddTime DESC) | ~8 秒 | ✅ 單掃描、並行計劃、穩定 |
| 2. CROSS APPLY | CROSS APPLY (SELECT TOP 10 ... WHERE City = o.City) | > 3 分 5 秒 | ❌ 執行計劃退化為 Nested Loops,外表掃描 100 萬行每行求內表 |
| 3. COUNT 相關子查詢 | WHERE (SELECT COUNT(*) ... WHERE AddTime > o.AddTime) < 10 | > 13 分鐘 (未完成) | ❌ 極其低效,相關子查詢逐行執行 |
| 4. 游標 + 臨時表 | 遍歷城市 → INSERT TOP 10 INTO #Tmp | ~10 秒 | ⚠️ 可用但較 ROW_NUMBER 慢,串行、臨時表開銷 |
DECLARE @City NVARCHAR(10);
CREATE TABLE #TopN (Id INT, Name NVARCHAR(50), City NVARCHAR(10), AddTime DATETIME);
DECLARE Cur CURSOR LOCAL FAST_FORWARD READ_ONLY FOR
SELECT DISTINCT City FROM dbo.Products ORDER BY City;
OPEN Cur;
FETCH NEXT FROM Cur INTO @City;
WHILE @@FETCH_STATUS = 0
BEGIN
INSERT INTO #TopN (Id, Name, City, AddTime)
SELECT TOP 10 Id, Name, City, AddTime
FROM dbo.Products
WHERE City = @City
ORDER BY AddTime DESC;
FETCH NEXT FROM Cur INTO @City;
END
CLOSE Cur; DEALLOCATE Cur;
SELECT * FROM #TopN ORDER BY City, AddTime DESC;
DROP TABLE #TopN;
ROW_NUMBER()、RANK()、DENSE_RANK() 針對分組排序優化極佳。(PartitionCol, OrderCol DESC) INCLUDE (SelectCols) 實現索引順序掃描 + 覆蓋,避免排序運算符。SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY City ORDER BY AddTime DESC) AS Rn
FROM dbo.Products
) T
WHERE Rn <= 10
ORDER BY City, AddTime DESC
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY; -- 首頁 50 行