《PostgreSQL 開發指南》 第 20 篇 通用表表達式

通用表表達式(Common Table Expression)是一個臨時的查詢結果或者臨時表,可以在其他SELECTINSERTUPDATE以及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語句,也可以是INSERTUPDATEDELETE語句;
  • sql_statement 是主查詢語句,可以引用前面定義的 CTE。該語句同樣可以是SELECTINSERTUPDATE或者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語句之外,INSERTUPDATE或者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|

歡迎關注❤️、點贊👍、轉發📣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章