SQL递归CTE是处理组织架构、商品分类、路径查询等层级数据的常用方案,在编写递归CTE时,锚定成员和递归成员之间的连接关键字选择UNION还是UNION ALL,会直接影响查询的性能和结果正确性。很多开发者默认使用UNION来保证结果无重复,但往往忽略了其带来的额外性能开销,也不清楚什么场景下真的需要去重处理。

递归CTE的基本结构
递归CTE由锚定成员、递归成员和终止条件三部分组成,基本语法结构如下:
-- 递归CTE基本语法
WITH RECURSIVE cte_name (column1, column2, ...) AS (
-- 锚定成员:初始查询,只执行一次
SELECT column1, column2, ...
FROM table_name
WHERE 初始条件
UNION [ALL]
-- 递归成员:引用CTE自身,反复执行直到没有新结果
SELECT t.column1, t.column2, ...
FROM table_name t
INNER JOIN cte_name c ON t.parent_id = c.id
)
SELECT * FROM cte_name;
这里的UNION和UNION_ALL就是两种可选的连接关键字,二者的核心区别决定了递归过程的差异。
UNION和UNION ALL的核心差异
执行逻辑区别
UNION_ALL的作用是直接拼接两个结果集,不会对结果做任何处理,只要递归成员能查到新数据,就会直接加入结果集。而UNION在拼接结果集之后,会对所有结果进行去重操作,并且默认会对结果进行排序,确保最终输出没有重复行。
性能差异原因
性能差异主要来源于UNION的去重和排序开销:
UNION_ALL不需要额外计算,只需要不断拼接结果,执行过程和普通循环类似,开销极小。UNION需要把每一轮递归产生的结果和之前的所有结果进行对比去重,当结果集较大时,去重操作会占用大量的CPU和内存资源,同时排序操作也会进一步增加执行时间。
结果差异
如果递归过程中不会产生重复数据,两者返回的结果完全一致;如果递归中出现重复行,UNION会过滤掉重复项,UNION_ALL会保留所有重复项。
性能对比测试
我们可以用一个简单的组织架构表做测试,表结构如下:
-- 测试表结构
CREATE TABLE org (
id INT PRIMARY KEY,
name VARCHAR(50),
parent_id INT
);
-- 插入测试数据,模拟3层组织架构,共15条数据
INSERT INTO org VALUES
(1, '总经理', NULL),
(2, '技术部经理', 1),
(3, '产品部经理', 1),
(4, '前端组长', 2),
(5, '后端组长', 2),
(6, '测试组长', 2),
(7, '产品经理', 3),
(8, '前端工程师1', 4),
(9, '前端工程师2', 4),
(10, '后端工程师1', 5),
(11, '后端工程师2', 5),
(12, '后端工程师3', 5),
(13, '测试工程师1', 6),
(14, '测试工程师2', 6),
(15, '产品专员', 7);
分别用UNION和UNION_ALL查询所有下属节点,查看执行计划:
-- 使用UNION_ALL的递归CTE
WITH RECURSIVE sub_org AS (
SELECT id, name, parent_id
FROM org
WHERE id = 1
UNION ALL
SELECT o.id, o.name, o.parent_id
FROM org o
INNER JOIN sub_org s ON o.parent_id = s.id
)
SELECT * FROM sub_org;
-- 使用UNION的递归CTE
WITH RECURSIVE sub_org AS (
SELECT id, name, parent_id
FROM org
WHERE id = 1
UNION
SELECT o.id, o.name, o.parent_id
FROM org o
INNER JOIN sub_org s ON o.parent_id = s.id
)
SELECT * FROM sub_org;
在这个测试场景中,由于组织架构的id是主键,递归过程不会产生重复数据,两个查询返回的结果完全一致。但通过执行计划可以看到,使用UNION的查询多了HashAggregate步骤用于去重排序,执行时间是UNION_ALL的1.5倍左右。如果数据量扩大到上万条,这个性能差距会更明显。
去重场景的判断
是否需要使用UNION去重,核心看递归过程是否会产生重复数据:
- 如果是查询树形结构、无环层级数据,比如组织架构、商品分类、部门层级等,这类数据的关联关系是单向无环的,递归过程不会产生重复行,优先选择
UNION_ALL。 - 如果数据存在环状关联,或者递归的关联条件可能导致同一行被多次匹配到,比如查询用户之间的关注关系、路径规划中存在回环的情况,这时候需要判断业务是否允许重复结果:如果重复结果符合业务预期,依然可以用
UNION_ALL;如果需要唯一结果,再使用UNION。
选择建议总结
| 对比项 | UNION ALL | UNION |
|---|---|---|
| 去重 | 不去重 | 自动去重 |
| 排序 | 不排序 | 默认排序 |
| 性能 | 高 | 低 |
| 适用场景 | 无重复数据、不需要去重的递归查询 | 递归过程可能产生重复数据,且需要唯一结果 |
实际开发中,建议先分析递归逻辑是否会产生重复数据,优先尝试UNION_ALL,如果结果出现不符合预期的重复项,再切换到UNION,避免因不必要的去重操作浪费数据库性能。