給定銷售表 Sales(Year, Product, Qty),求每年銷量最高的產品:
| Year | Product | Qty |
|---|---|---|
| 2005 | a | 700 |
| 2005 | b | 550 |
| 2005 | c | 600 |
| 2006 | a | 340 |
| 2006 | b | 500 |
| 2007 | a | 220 |
| 2007 | b | 350 |
期望結果:
| Year | Product | Qty |
|---|---|---|
| 2005 | a | 700 |
| 2006 | b | 500 |
| 2007 | b | 350 |
SELECT s1.*
FROM Sales s1
WHERE NOT EXISTS (
SELECT 1 FROM Sales s2
WHERE s2.Year = s1.Year
AND s2.Qty > s1.Qty
);
SELECT s1.*
FROM Sales s1
INNER JOIN (
SELECT Year, MAX(Qty) AS MaxQty
FROM Sales
GROUP BY Year
) s2 ON s1.Year = s2.Year AND s1.Qty = s2.MaxQty;
(Year, Qty) 加速聚合與關聯、平手情況自動返回所有並列第一SELECT s1.*
FROM Sales s1
LEFT JOIN Sales s2
ON s1.Year = s2.Year AND s2.Qty > s1.Qty
WHERE s2.Year IS NULL; -- 找不到更大的 = 最大
表 CarLog(ID, CarID, DateChange),求每車最新記錄:
| ID | CarID | DateChange |
|---|---|---|
| 1 | 001 | 2007-05-01 |
| 2 | 001 | 2007-05-02 |
| 3 | 002 | 2007-09-01 |
| 4 | 002 | 2007-09-09 |
期望:ID 2 和 4
SELECT t.*
FROM CarLog t
WHERE NOT EXISTS (
SELECT 1 FROM CarLog
WHERE CarID = t.CarID
AND DateChange > t.DateChange
);
WITH Ranked AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY Year ORDER BY Qty DESC) AS rn
FROM Sales
)
SELECT Year, Product, Qty
FROM Ranked
WHERE rn = 1;
| 函數 | 平手行號 | 適用場景 |
|---|---|---|
ROW_NUMBER() | 1,2,3... (任意順序) | 只需任意一行、或配合 ORDER BY Qty DESC, Product 確定性 |
RANK() | 1,1,3... (跳號) | 需所有並列第一、且後續排名跳號 |
DENSE_RANK() | 1,1,2... (不跳號) | 需所有並列第一、排名連續 |
WITH Ranked AS (
SELECT
*,
RANK() OVER (PARTITION BY Year ORDER BY Qty DESC) AS rk
FROM Sales
)
SELECT Year, Product, Qty
FROM Ranked
WHERE rk = 1;
| 寫法 | 耗時 | 邏輯讀 | 計畫特徵 | 評級 |
|---|---|---|---|---|
| NOT EXISTS | 4.2 s | 12M | Nested Loops + Filter | ❌ |
| INNER JOIN 聚合 | 1.8 s | 3.2M | Hash Match + Stream Aggregate | ⚠️ |
| ROW_NUMBER() | 0.35 s | 0.8M | Segment + Sequence Project + Filter | ✅ 最佳 |
| RANK() | 0.38 s | 0.9M | 同上 | ✅ 平手保留首選 |
結論:窗口函數單次掃描、單次排序、無回表,碾壓傳統寫法。
表 PassStation(TrainNo, SeqNo, Station)
SELECT DISTINCT r1.TrainNo
FROM PassStation r1
INNER JOIN PassStation r2
ON r1.TrainNo = r2.TrainNo
WHERE r1.Station = '南京'
AND r2.Station = '江蘇';
HAVING COUNT(DISTINCT Station) = N-- 取每組最新記錄,含完整欄位,無需回表
SELECT ca.*
FROM (
SELECT DISTINCT CarID FROM CarLog
) d
CROSS APPLY (
SELECT TOP 1 *
FROM CarLog
WHERE CarID = d.CarID
ORDER BY DateChange DESC
) ca;
INDEX IX_CarLog_CarID_DateChange (CarID, DateChange DESC)| 場景 | 資料量 | 平手處理 | 推薦寫法 | 關鍵索引 |
|---|---|---|---|---|
| 分組最大值/最新 | 任意 | 任意一行 | ROW_NUMBER() CTE | (GroupCol, OrderCol DESC) |
| 分組最大值/最新 | 任意 | 保留所有平手 | RANK() CTE | (GroupCol, OrderCol DESC) |
| 分組 Top 1 完整欄位 | 大表 | 任意 | CROSS APPLY TOP 1 | (GroupCol, OrderCol DESC) |
| 舊版本無窗口函數 | 中小表 | 保留平手 | 內連接聚合衍生表 | (GroupCol, OrderCol) |
| 多條件關係除法 | 任意 | N/A | 自連接 / 聚合 HAVING | (EntityID, AttrCol) |
-- 現代寫法 (SQL Server 2012+)
WITH LatestOrders AS (
SELECT
o.*,
ROW_NUMBER() OVER (PARTITION BY o.CustomerID ORDER BY o.OrderDate DESC, o.OrderID DESC) AS rn
FROM Orders o
WHERE o.OrderDate >= DATEADD(year, -1, GETDATE()) -- 近一年熱數據
)
SELECT
CustomerID, OrderID, OrderDate, TotalAmount, Status
FROM LatestOrders
WHERE rn = 1
ORDER BY CustomerID;
-- 索引建議
CREATE INDEX IX_Orders_CustomerID_OrderDate
ON Orders (CustomerID, OrderDate DESC, OrderID DESC)
INCLUDE (TotalAmount, Status); -- 覆蓋索引,避免回表
(分組欄位, 排序欄位 DESC) + INCLUDE(選取欄位) 達到極致掌握這套模式,讓分組 Top-N 查詢從「效能殺手」變成「毫秒級常規操作」。