首先,FROM 和 JOIN 是 SQL 語句執行的第一步。它們的邏輯結果是一個笛卡爾積,決定了接下來要操作的資料集,但要注意的是,邏輯執行順序並不代表物理執行順序,實際上資料庫在獲取表中的資料之前會使用 ON 和 WHERE 過濾條件進行最佳化查詢!
其次,應用 ON 條件對上一步的結果進行過濾並生成新的資料集;然後,執行 WHERE 子句對上一步的資料集再次進行過濾
接著,基於 GROUP BY 子句指定的表示式進行分組;同時,對於每個分組計算聚合函式 agg_func 的結果。經過 GROUP BY 處理之後,資料集的結構就發生了變化,只保留了分組欄位和聚合函式的結果;如果存在 GROUP BY 子句,可以利用 HAVING 針對分組後的結果進一步進行過濾,通常是針對聚合函式的結果進行過濾;
接下來,SELECT 可以指定要拿的欄位;如果指定了 DISTINCT 關鍵字,需要對結果集進行去重複操作。另外還會為指定了 AS 的欄位生成別名;
然後,應用 ORDER BY 子句對結果進行排序。如果存在 GROUP BY 子句或者 DISTINCT 關鍵字,只能使用分組欄位和聚合函式進行排序;否則,可以使用 FROM 和 JOIN 表中的任何欄位排序;最後,OFFSET 和 FETCH(LIMIT、TOP)限定了最終返回的行數。例如 WHERE 子句在 HAVING 子句之前執行,因此我們應該儘量使用 WHERE 進行資料過濾,避免無謂的操作;除非業務需要針對聚合函式的結果進行過濾
💼 在 WHERE 條件中使用別名的錯誤
1 2 3
SELECT emp_name AS empname FROM employee WHERE empname ='張飛'; -- 只能使用 emp_name
這段 SQL 會出錯,因為 WHERE 子句執行時,SELECT 子句還沒被執行。換句話說,此時 empname 這個別名「還不存在」。
💼 GROUP BY 之後欄位的變化
1 2 3
SELECT dept_id, emp_name, AVG(salary) FROM employee GROUPBY dept_id;
這段 SQL 也會出錯,因為在 GROUP BY 之後,結果集只保留了分組欄位(dept_id)和聚合函式結果(AVG(salary)),非分組欄位(這裡是 emp_name)就「不再存在」於結果集裡。 如果想同時看到員工與部門平均薪資,可以用「視窗函式(Window Function)」
1 2 3 4 5
SELECT emp_name, dept_id, AVG(salary) OVER(PARTITIONBY dept_id) AS dept_avg_salary FROM employee;
💼 篩選條件放在 on 和 where 後的區別
1 2 3 4
SELECT e.emp_name, d.dept_name FROM employee e LEFTJOIN department d ON (e.dept_id = d.dept_id) WHERE e.emp_name ='張飛';
SELECT e.emp_name, d.dept_name FROM employee e LEFTJOIN department d ON (e.dept_id = d.dept_id AND e.emp_name ='張飛');
emp_name
dept_name
劉備
[NULL]
關羽
[NULL]
張飛
行政管理部
諸葛亮
[NULL]
…
第二個查詢(ON):ON 是在 JOIN 過程中 決定「哪些資料可以匹配」。因為這是 LEFT JOIN,左表(employee)的所有資料都會被保留,但只有符合 e.emp_name = ‘張飛’ 的那一筆會配到右表(department)的資料,其他的員工因為不符合 ON 條件,右表的資料欄位就會是 NULL。
💼 一個電商平台想分析每個用戶的首單情況
1 2 3 4 5 6 7 8
SELECT user_id, MIN(order_date) as first_order_date, order_amount FROM orders GROUPBY user_id
雖然 MIN (order_date) 可以找出首單日期,但 order_amount 不在聚合函數內,且不在 GROUP BY 中,這會導致拿到的 order_amount 是不確定的,因此 SQL 會噴錯 SQL 的執行順序是先 GROUP BY,然後執行聚合函數,最後選擇非聚合列。要修正這個問題,可以使用子查詢 & Window Function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
SELECT user_id, first_order_date, order_amount FROM ( SELECT user_id, order_date, order_amount, ROW_NUMBER() OVER (PARTITIONBY user_id ORDERBY order_date) as rn FROM orders ) sub WHERE rn =1
💼 公司要給每個部門薪資排名前三的員工發放獎金
1 2 3 4 5 6 7 8 9
SELECT department, employee_name, salary, RANK() OVER (PARTITIONBY department ORDERBY salary DESC) as salary_rank FROM employees WHERE salary_rank <=3
WHERE 比 SELECT 早執行,因此在 WHERE 中不能引用在 SELECT 中定義的別名(這裡是 salary_rank)。 正確的方法是使用子查詢或 CTE
1 2 3 4 5 6 7 8 9 10 11 12
WITH RankedEmployees AS ( SELECT department, employee_name, salary, RANK() OVER (PARTITIONBY department ORDERBY salary DESC) as salary_rank FROM employees ) SELECT*FROM RankedEmployees WHERE salary_rank <=3
WITH TransactionSums AS ( SELECT user_id, transaction_date, amount, SUM(amount) OVER (PARTITIONBY user_id ORDERBY transaction_date) as cumulative_sum, CASE WHENSUM(amount) OVER (PARTITIONBY user_id ORDERBY transaction_date) >1000THEN1 ELSE0 ENDas exceed_1000 FROM transactions ), FirstExceed AS ( SELECT*, LAG(cumulative_sum, 1, 0) OVER (PARTITIONBY user_id ORDERBY transaction_date) as previous_cumulative_sum FROM TransactionSums ) SELECT* FROM FirstExceed WHERE exceed_1000 =1 AND previous_cumulative_sum <=1000;
WITH ByTotalPayTypeRank AS ( SELECT TradesOrderThirdPartyPayment_TypeDef, TradesOrderThirdPartyPayment_TotalPayment, SUM(TradesOrderThirdPartyPayment_TotalPayment) OVER (PARTITIONBY TradesOrderThirdPartyPayment_TypeDef) AS PayTypeSum, ROW_NUMBER() OVER (PARTITIONBY TradesOrderThirdPartyPayment_TypeDef ORDERBY TradesOrderThirdPartyPayment_TotalPayment DESC) AS SaleRankByPayType FROM TradesOrderThirdPartyPayment(NOLOCK) )
SELECT* FROM ByTotalPayTypeRank WHERE SaleRankByPayType <=3
SELECT TradesOrderThirdPartyPayment_TypeDef, SUM(TradesOrderThirdPartyPayment_TotalPayment) AS PayTypeSum FROM TradesOrderThirdPartyPayment(NOLOCK) GROUPBY TradesOrderThirdPartyPayment_TypeDef
WITH MovingAverages AS ( SELECT stock_id, trade_date, price, AVG(price) OVER ( PARTITIONBY stock_id ORDERBY trade_date ROWSBETWEEN6 PRECEDING ANDCURRENTROW ) AS moving_avg FROM stock_prices ) SELECT stock_id, trade_date, price, moving_avg FROM MovingAverages WHERE moving_avg > price;
🔹 HAVING 不能直接用 CASE 結果欄位名稱,要包成子查詢
sql 執行順序如下
1
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
HAVING 在 SELECT 之前執行,而 CASE WHEN … AS CustomerLevel 是在 SELECT 階段才產生的「暫時欄位名稱」,所以 HAVING 看不到 SELECT 的別名!
會出錯
1 2 3 4 5 6 7 8 9
SELECT CustomerId, SUM(Total) AS TotalSales, CASE WHENSUM(Total) <40THEN'Normal' WHENSUM(Total) >=40THEN'VIP' ELSE'Undefined' ENDAS CustomerLevel FROM invoices GROUPBY CustomerId HAVING CustomerLevel ='VIP'
包成子查詢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
SELECT CustomerLevel, COUNT(DISTINCT CustomerId) AS CustomerCnt FROM ( SELECT CustomerId, CASE WHENSUM(Total) <40THEN'Normal' WHENSUM(Total) >=40THEN'VIP' ELSE'Undefined' ENDAS CustomerLevel FROM invoices GROUPBY CustomerId ) AS SubQuery GROUPBY CustomerLevel