通用表表达式(Common Table Expression、CTE)是一个临时的查询结果或者临时表,可以
在其他 SELECT、INSERT、UPDATE 以及 DELETE 语句中使用。通用表表达式只在当前语句中
有效,类似于子查询。
使用 CTE 的主要好处包括:
通用表表达式的定义如下:
WITH cte_name (col1, col2, ...) AS (
cte_query_definition
)
sql_statement;
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_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;
以上示例中定义了两个 CTE,其中 cte2 引用了 cte1。最后的查询使用两者进行连接查询。
递归 CTE 允许在它的定义中进行自引用,理论上来说可以实现任何复杂的计算功能,最常
用的场景就是遍历层次结构的数据和图结构数据。
WITH RECURSIVE cte_name AS(
cte_query_initial -- 初始化部分
UNION [ALL]
cte_query_iterative -- 递归部分
) SELECT * FROM cte_name;
RECURSIVE
表示递归;
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;
如果没有指定终止条件,上面的查询将会进入死循环
递归 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 employee_name, path
from employee_path
order by employee_id;
其中,初始化查询语句返回了公司最高层的领导(manager_id IS NULL),也就是
“Steven,King”;递归查询将员工表的 manager_id 与已有结果集中的 employee_id 关联,获取每个
员工的下一级员工,直到无法找到新的数据;path 字段存储了每个员工从上至下的管理路径
更多关于递归 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;
我们首先创建了一个记录员工历史信息的 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;
returning 在 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;