通用表表達式(Common Table Expression)是一個臨時的查詢結果或者臨時表,可以在其他SELECT
、INSERT
、UPDATE
以及DELETE
語句中使用。通用表表達式只在當前語句中有效,類似於子查詢。
使用 CTE 的主要好處包括:
- 提高複雜查詢的可讀性。CTE 可以將複雜查詢模塊化,組織成容易理解的結構。
- 支持遞歸查詢。CTE 通過引用自身實現遞歸,可以方便地處理層次結構數據和圖結構數據。
簡單 CTE
通用表表達式的定義如下:
WITH cte_name (col1, col2, ...) AS (
cte_query_definition
)
sql_statement;
其中,
WITH
表示定義 CTE,因此 CTE 也稱爲WITH
查詢;- cte_name 指定了 CTE 的名稱,後面是可選的字段名;
- 括號內是 CTE 的內容,可以是
SELECT
語句,也可以是INSERT
、UPDATE
、DELETE
語句; - sql_statement 是主查詢語句,可以引用前面定義的 CTE。該語句同樣可以是
SELECT
、INSERT
、UPDATE
或者DELETE
。
PostgreSQL 中的 CTE 通常用於簡化複雜的連接查詢或子查詢。例如:
WITH department_avg(department_id, avg_salary) AS (
SELECT department_id,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT d.department_name,
da.avg_salary
FROM departments d
JOIN department_avg da
ON (d.department_id = da.department_id)
ORDER BY d.department_name;
department_name |avg_salary |
----------------|----------------------|
Accounting |10154.0000000000000000|
Administration | 4400.0000000000000000|
Executive | 19333.333333333333|
...
首先,我們定義了一個名爲 department_avg 的 CTE,表示每個部門的平均月薪;然後和 departments 表進行連接查詢。雖然用其他方式也可以實現相同的功能,但是 CTE 讓代碼顯得更加清晰易懂。
一個WITH
關鍵字可以定義多個 CTE,而且後面的 CTE 可以引用前面的 CTE。例如:
WITH cte1(n) AS (
SELECT 1
),
cte2(m) as (
select n+1 from cte1
)
SELECT *
FROM cte1, cte2;
n|m|
-|-|
1|2|
以上示例中定義了兩個 CTE,其中 cte2 引用了 cte1。最後的查詢使用兩者進行連接查詢。
遞歸 CTE
遞歸 CTE 允許在它的定義中進行自引用,理論上來說可以實現任何複雜的計算功能,最常用的場景就是遍歷層次結構的數據和圖結構數據。
WITH RECURSIVE cte_name AS(
cte_query_initial -- 初始化部分
UNION [ALL]
cte_query_iterative -- 遞歸部分
) SELECT * FROM cte_name;
其中,
RECURSIVE
表示遞歸 CTE;- cte_query_initial 是初始化查詢,用於創建初始結果集;
- cte_query_iterative 是遞歸部分,可以引用 cte_name;
- 如果遞歸查詢無法從上一次迭代中返回更多的數據,將會終止遞歸併返回結果。
一個經典的遞歸 CTE 案例就是生成數字序列:
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 10
)
SELECT n FROM t;
n |
--|
1|
2|
3|
4|
5|
6|
7|
8|
9|
10|
以上語句執行過程如下:
- 執行 CTE 中的初始化查詢,生成一行數據(1);
- 第一次執行遞歸查詢,判斷 n < 10,生成一行數據 2(n+1);
- 重複執行遞歸查詢,生成更多的數據;直到 n = 10 終止;此時臨時表 t 中包含 10 條數據;
- 執行主查詢,返回所有的數據。
注意,如果沒有指定終止條件,上面的查詢將會進入死循環。
接下來我們看一個更實用的案例,通過遞歸 CTE 遍歷組織結構。
WITH RECURSIVE employee_path (employee_id, employee_name, path) AS
(
SELECT employee_id, CONCAT(first_name, ',', last_name), CONCAT(first_name, ',', last_name) AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, CONCAT(e.first_name, ',', e.last_name), CONCAT(ep.path, '->', e.first_name, ',', e.last_name)
FROM employee_path ep
JOIN employees e ON ep.employee_id = e.manager_id
)
SELECT * FROM employee_path ORDER BY employee_id;
employee_id|employee_name |path |
-----------|-----------------|--------------------------------------------------------------|
100|Steven,King |Steven,King |
101|Neena,Kochhar |Steven,King->Neena,Kochhar |
102|Lex,De Haan |Steven,King->Lex,De Haan |
103|Alexander,Hunold |Steven,King->Lex,De Haan->Alexander, |
104|Bruce,Ernst |Steven,King->Lex,De Haan->Alexander,Hunold->Bruce,Ernst |
105|David,Austin |Steven,King->Lex,De Haan->Alexander,Hunold->David,Austin |
106|Valli,Pataballa |Steven,King->Lex,De Haan->Alexander,Hunold->Valli,Pataballa |
...
其中,初始化查詢語句返回了公司最高層的領導(manager_id IS NULL),也就是“Steven,King”;遞歸查詢將員工表的 manager_id 與已有結果集中的 employee_id 關聯,獲取每個員工的下一級員工,直到無法找到新的數據;path 字段存儲了每個員工從上至下的管理路徑。
當然,我們也可以對組織結構從下至上進行遍歷。更多關於遞歸 CTE 的實際應用場景,可以參考這篇文章。
DML 語句與 CTE
除了SELECT
語句之外,INSERT
、UPDATE
或者DELETE
語句也可以與 CTE 一起使用。我們可以在 CTE 中使用 DML 語句,也可以將 CTE 用於 DML 語句。
如果在 CTE 中使用 DML 語句,我們可以將數據修改操作影響的結果作爲一個臨時表,然後在其他語句中使用。例如:
-- 創建一個員工歷史表
CREATE TABLE employees_history
AS SELECT * FROM employees WHERE 1 = 0;
with deletes as (
delete from employees
where employee_id = 206
returning *
)
insert into employees_history
select * from deletes;
select employee_id, first_name, last_name
from employees_history;
employee_id|first_name|last_name|
-----------|----------|---------|
206|William |Gietz |
我們首先創建了一個記錄員工歷史信息的 employees_history 表;然後使用DELETE
語句定義了一個 CTE,returning *
返回了被刪除的數據,構成了結果集 deletes;然後使用INSERT
語句記錄被刪除的員工信息。
接下來我們將該員工添加回員工表:
with inserts as (
insert into employees
values (206,'William','Gietz','WGIETZ','515.123.8181','2002-06-07','AC_ACCOUNT',8800.00,NULL,205,110)
returning *
)
insert into employees_history
select * from inserts;
除了插入數據到 employees 表之外,我們還利用 CTE 在表 employees_history 中增加了一條歷史記錄,現在該表中有兩條數據。
CTE 中的UPDATE
語句有些不同,因爲更新的數據分爲更新之前的狀態和更新之後的狀態。例如:
delete from employees_history;-- 清除歷史記錄
with updates as (
update employees
set salary = salary + 500
where employee_id = 206
returning *
)
insert into employees_history
select * from employees where employee_id = 206;
select employee_id, salary from employees_history;
employee_id|salary |
-----------|-------|
206|8300.00|
在 CTE 中,UPDATE
語句修改了一個員工的月薪;但是爲了記錄修改之前的數據,我們插入 employees_history 的數據仍然來自 employees 表。因爲在一個語句中,所有的操作都在一個事務中,所以主查詢中的 employees 是修改之前的狀態。
如果想要獲取更新之後的數據,直接使用 updates 即可:
with updates as (
update employees
set salary = salary - 500
where employee_id = 206
returning *
)
select employee_id,first_name, last_name, salary
from updates;
employee_id|first_name|last_name|salary |
-----------|----------|---------|-------|
206|William |Gietz |8300.00|
歡迎關注❤️、點贊👍、轉發📣