HAVING 用於篩選**分組後的聚合結果**,常與 GROUP BY 搭配。但當業務需要「聚合的聚合」(如:每筆訂單總額的平均值) 時,單層查詢無法直接巢狀聚合函數。本文以 AdventureWorks 範例資料庫,示範錯誤場景、UDF 分解法與現代 CTE/子查詢最佳實踐。
需求:找出**所有訂單總額超過 25,000** 的客戶清單。需連接 Customer、SalesOrderHeader、SalesOrderDetail 三表。
SELECT
C.CustomerID,
SOH.SalesOrderID,
SUM(SOD.LineTotal) AS SubTotal
FROM Sales.Customer C
JOIN Sales.SalesOrderHeader SOH ON SOH.CustomerID = C.CustomerID
JOIN Sales.SalesOrderDetail SOD ON SOD.SalesOrderID = SOH.SalesOrderID
GROUP BY C.CustomerID, SOH.SalesOrderID
HAVING SUM(SOD.LineTotal) > 25000.00
ORDER BY C.CustomerID, SOH.SalesOrderID;
LineTotal 為計算欄位 (UnitPrice * (1-Discount) * OrderQty),聚合函數同樣適用。HAVING 篩選分組後的 SUM(LineTotal)。
-- 錯誤嘗試:想求所有訂單的平均總額
SELECT
SOH.SalesOrderID,
AVG(SUM(SOD.LineTotal)) AS Average -- 錯誤!
FROM Sales.SalesOrderHeader SOH
JOIN Sales.SalesOrderDetail SOD ON SOD.SalesOrderID = SOH.SalesOrderID
GROUP BY SOH.SalesOrderID;
錯誤訊息:Msg 130, Level 15, State 1: Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
SQL 標準禁止直接巢狀聚合函數 (聚合的聚合)。
CREATE FUNCTION dbo.SalesTotals_TVF ()
RETURNS TABLE
AS RETURN
(
SELECT
SOD.SalesOrderID,
SUM(SOD.LineTotal) AS TotalSale
FROM Sales.SalesOrderDetail SOD
GROUP BY SOD.SalesOrderID
);
GO
CREATE FUNCTION dbo.AverageSale_Scalar ()
RETURNS MONEY
AS
BEGIN
DECLARE @Result MONEY;
SELECT @Result = AVG(TotalSale) FROM dbo.SalesTotals_TVF();
RETURN @Result;
END
GO
DECLARE @Avg MONEY = dbo.AverageSale_Scalar();
SELECT
ST.*,
@Avg AS OverallAverage,
ST.TotalSale / @Avg AS Ratio,
CASE
WHEN ST.TotalSale / @Avg > 1 THEN N'高於平均'
WHEN ST.TotalSale / @Avg < 1 THEN N'低於平均'
ELSE N'持平'
END AS Level
FROM dbo.SalesTotals_TVF() ST;
WITH OrderTotals AS (
SELECT
SOD.SalesOrderID,
SUM(SOD.LineTotal) AS TotalSale
FROM Sales.SalesOrderDetail SOD
GROUP BY SOD.SalesOrderID
),
OverallStats AS (
SELECT AVG(TotalSale) AS AvgSale FROM OrderTotals
)
SELECT
OT.*,
OS.AvgSale,
OT.TotalSale / OS.AvgSale AS Ratio,
CASE
WHEN OT.TotalSale / OS.AvgSale > 1 THEN N'高於平均'
WHEN OT.TotalSale / OS.AvgSale < 1 THEN N'低於平均'
ELSE N'持平'
END AS Level
FROM OrderTotals OT
CROSS JOIN OverallStats OS;
CROSS JOIN 單列統計表等同標量子查詢,效能極佳。
SELECT
SOD.SalesOrderID,
SUM(SOD.LineTotal) OVER (PARTITION BY SOD.SalesOrderID) AS TotalSale,
AVG(SUM(SOD.LineTotal) OVER (PARTITION BY SOD.SalesOrderID)) OVER () AS AvgSale
FROM Sales.SalesOrderDetail SOD
GROUP BY SOD.SalesOrderID; -- 注意:窗口函數需配合 GROUP BY 或 DISTINCT
窗口函數 AVG(...) OVER () 可直接計算「聚合結果的平均」,但語法較繁瑣,CTE 更清晰。
| 方案 | 優點 | 缺點 | 適用版本 |
|---|---|---|---|
| TVF + 純量函數 | 模組化、可重用、舊版相容 | 建立永久物件、權限管理、統計資料分離 | SQL 2000+ |
| CTE / 子查詢 | 無永久物件、整體最佳化、可讀性佳 | 無 | SQL 2005+ |
| 窗口函數 | 單查詢完成、進階分析 | 語法複雜、易寫錯 | SQL 2012+ |
HAVING SUM(Amt) > 1000 AND COUNT(*) >= 5HAVING SUM(Amt) > (SELECT AVG(SUM(Amt)) FROM ... GROUP BY ...) (需子查詢先算好)SELECT SUM(Amt) FROM Orders HAVING SUM(Amt) > 10000 —— 視為單一群組,等同 WHERE 但語意不同。PascalCase,欄位別名用 PascalCase。