CTE 簡介 (SQL CTE)
這篇 SQL CTE 教學將帶你認識 CTE(Common Table Expression,通用表格運算式)的基本概念與用法。CTE 透過 WITH 關鍵字定義一個暫時的結果集,讓你可以在後續的查詢中直接引用,大幅提升複雜查詢的可讀性與維護性。
你可以把 CTE 想像成一個「暫時的虛擬表」,它只在當次查詢中有效,不會真正儲存在資料庫中。相較於 Subquery(子查詢),CTE 將複雜的子查詢抽出來獨立命名,不僅更容易閱讀,還能在同一查詢中被多次引用。
以下是 Subquery 與 CTE 的主要差異:
| 比較項目 | Subquery | CTE |
|---|---|---|
| 定義位置 | 嵌套在查詢內部 | 用 WITH 定義在查詢之前 |
| 可讀性 | 複雜時較難閱讀 | 結構清晰,易於理解 |
| 可重複引用 | 不行,需重複寫 | 可以在同一查詢中多次引用 |
| 遞迴支援 | 不支援 | 支援遞迴 CTE |
CTE 語法 (SQL CTE Syntax)
WITH cte_name AS (
SELECT column1, column2, ...
FROM table_name
WHERE condition
)
SELECT column1, column2, ...
FROM cte_name;
說明:
| 關鍵字 | 說明 |
|---|---|
| WITH | 用來定義 CTE 的開頭關鍵字 |
| cte_name | 自訂的 CTE 名稱,可以在後續查詢中引用 |
| AS (…) | CTE 的查詢定義,括號內放入 SELECT 查詢 |
| SELECT … FROM cte_name | 主查詢,引用 CTE 的結果集進行後續操作 |
CTE 用法 (Example)
假設我們有一個員工資料表 employees 如下(若需了解如何建立資料表,請參考 CREATE TABLE 教學):
| EmployeeID | Name | Department | Salary |
|---|---|---|---|
| 1 | 張一 | 業務部 | 35000 |
| 2 | 王二 | 資訊部 | 42000 |
| 3 | 李三 | 業務部 | 38000 |
| 4 | 趙四 | 資訊部 | 45000 |
| 5 | 陳五 | 人資部 | 40000 |
| 6 | 林六 | 業務部 | 36000 |
| 7 | 黃七 | 資訊部 | 48000 |
基本 CTE 用法
使用 CTE 查詢各部門的平均薪資,並篩選出平均薪資超過 40000 的部門。
WITH dept_avg AS (
SELECT Department, AVG(Salary) AS avg_salary
FROM employees
GROUP BY Department
)
SELECT Department, avg_salary
FROM dept_avg
WHERE avg_salary > 40000;
查詢結果如下:
| Department | avg_salary |
|---|---|
| 資訊部 | 45000 |
在這個範例中,我們先用 CTE 定義了 dept_avg,計算各部門的平均薪資,然後在主查詢中直接引用 dept_avg 來篩選結果。這樣的寫法比嵌套子查詢更容易理解。
CTE 搭配 JOIN
CTE 的結果集可以像一般資料表一樣與其他表進行 JOIN。例如,先用 CTE 計算各部門的平均薪資,再與原始資料表 JOIN,找出薪資高於部門平均的員工。
WITH dept_avg AS (
SELECT Department, AVG(Salary) AS avg_salary
FROM employees
GROUP BY Department
)
SELECT e.Name, e.Department, e.Salary, d.avg_salary
FROM employees e
JOIN dept_avg d ON e.Department = d.Department
WHERE e.Salary > d.avg_salary;
查詢結果如下:
| Name | Department | Salary | avg_salary |
|---|---|---|---|
| 李三 | 業務部 | 38000 | 36333 |
| 趙四 | 資訊部 | 45000 | 45000 |
| 黃七 | 資訊部 | 48000 | 45000 |
透過 CTE,我們將「計算部門平均薪資」的邏輯抽出來獨立命名,主查詢只需專注在篩選條件,程式碼更加清晰。
多個 CTE 定義
你可以在同一個 WITH 子句中定義多個 CTE,各個 CTE 之間用逗號分隔。後方的 CTE 可以引用前方已定義的 CTE。
WITH dept_stats AS (
SELECT Department,
COUNT(*) AS headcount,
AVG(Salary) AS avg_salary
FROM employees
GROUP BY Department
),
high_salary_depts AS (
SELECT Department, avg_salary
FROM dept_stats
WHERE avg_salary > 38000
)
SELECT * FROM high_salary_depts;
查詢結果如下:
| Department | avg_salary |
|---|---|
| 資訊部 | 45000 |
| 人資部 | 40000 |
第一個 CTE dept_stats 計算各部門的人數與平均薪資,第二個 CTE high_salary_depts 引用前者的結果來篩選平均薪資超過 38000 的部門。這種層層拆解的方式讓查詢邏輯更容易理解。
遞迴 CTE (Recursive CTE)
CTE 最強大的功能之一就是支援遞迴查詢。遞迴 CTE 可以在定義中引用自己,適合用於處理階層式資料(如組織架構、樹狀結構)或產生序列資料。
遞迴 CTE 的基本結構包含兩個部分:
| 部分 | 說明 |
|---|---|
| 錨點查詢(Anchor) | 遞迴的起始條件,不引用 CTE 自身 |
| 遞迴查詢(Recursive) | 引用 CTE 自身,每次執行會產生新的資料列 |
| UNION ALL | 將錨點查詢與遞迴查詢的結果合併 |
WITH RECURSIVE cte_name AS (
-- 錨點查詢(起始條件)
SELECT ...
UNION ALL
-- 遞迴查詢(引用自己)
SELECT ...
FROM cte_name
WHERE 終止條件
)
SELECT * FROM cte_name;
注意:在 MySQL 中使用 WITH RECURSIVE,而 SQL Server 只需要 WITH 即可支援遞迴。
產生數字序列
使用遞迴 CTE 產生 1 到 5 的數字序列:
WITH RECURSIVE numbers AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1
FROM numbers
WHERE n < 5
)
SELECT n FROM numbers;
查詢結果如下:
| n |
|---|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
錨點查詢 SELECT 1 AS n 產生第一筆資料 n=1,遞迴查詢每次將 n 加 1,直到 n < 5 條件不成立時停止。
階層式資料查詢
假設我們有一個員工與主管的階層資料表 staff:
| EmployeeID | Name | ManagerID |
|---|---|---|
| 1 | 王總 | NULL |
| 2 | 李經理 | 1 |
| 3 | 張組長 | 2 |
| 4 | 陳員工 | 3 |
| 5 | 林員工 | 2 |
使用遞迴 CTE 查詢王總下屬的所有員工及其層級:
WITH RECURSIVE org_tree AS (
-- 錨點:從王總開始
SELECT EmployeeID, Name, ManagerID, 0 AS level
FROM staff
WHERE ManagerID IS NULL
UNION ALL
-- 遞迴:找出下屬員工
SELECT s.EmployeeID, s.Name, s.ManagerID, ot.level + 1
FROM staff s
JOIN org_tree ot ON s.ManagerID = ot.EmployeeID
)
SELECT EmployeeID, Name, level
FROM org_tree
ORDER BY level, EmployeeID;
查詢結果如下:
| EmployeeID | Name | level |
|---|---|---|
| 1 | 王總 | 0 |
| 2 | 李經理 | 1 |
| 3 | 張組長 | 2 |
| 5 | 林員工 | 2 |
| 4 | 陳員工 | 3 |
遞迴 CTE 從最高層的王總(level=0)開始,每次遞迴找出下一層的員工,直到沒有更多下屬為止。這種查詢在處理組織架構、分類階層等情境非常實用。
CTE 與 Subquery 的比較
同一個查詢通常可以用 CTE 或 Subquery 實現。以下用「查詢平均薪資超過 40000 的部門」來比較兩種寫法。
使用 Subquery:
SELECT Department, avg_salary
FROM (
SELECT Department, AVG(Salary) AS avg_salary
FROM employees
GROUP BY Department
) AS dept_avg
WHERE avg_salary > 40000;
使用 CTE:
WITH dept_avg AS (
SELECT Department, AVG(Salary) AS avg_salary
FROM employees
GROUP BY Department
)
SELECT Department, avg_salary
FROM dept_avg
WHERE avg_salary > 40000;
兩種寫法的結果完全相同,但 CTE 將子查詢抽出來獨立命名,當查詢變得複雜時,CTE 的可讀性明顯更好。特別是當同一個子查詢需要被多次引用時,CTE 可以避免重複撰寫相同的查詢。
CTE 使用注意事項
| 注意事項 | 說明 |
|---|---|
| 作用範圍 | CTE 只在緊接其後的單一 SQL 敘述中有效 |
| 不儲存資料 | CTE 不會建立實際的資料表,只是暫時的結果集 |
| 遞迴限制 | 遞迴 CTE 需設定終止條件,否則可能無限迴圈。各資料庫預設遞迴上限不同 |
| 資料庫支援 | MySQL 8.0+、PostgreSQL、SQL Server、Oracle、SQLite 3.8.3+ 等皆支援 |
延伸閱讀
- SQL Subquery
- SQL GROUP BY
- SQL 聚合函數
- SQL JOIN
- SQL HAVING
- SQL SELECT
- SQL WHERE
- SQL VIEW
- SQL ORDER BY
- SQL CASE