做后台系统的这几年,我遇到最多也最头疼的SQL,基本都和树形数据有关。组织架构、商品分类、权限菜单、数据字典,表面上是一张张表,拆开看全是层级结构,真正查询起来完全不是普通条件过滤那么简单。早年间我处理这种层级结构查询,要么一次性把全表捞出来丢给程序递归拼树,要么写一个存储过程循环插入临时表,数据量小还能将就,量一大、层级一深,各种性能问题就全冒出来了。后来我把递归SQL系统地过了一遍,用 WITH RECURSIVE 这类写法在数据库内部完成树形数据展开,很多之前绕来绕去的需求突然就清爽了。这篇文章我就用几个贴近真实业务的案例,把递归SQL的语法机制、适用场景,以及我在项目中踩过的坑从头到尾捋一遍。如果你正在做报表、写权限模块、维护分类树,这篇内容大概率能直接帮上忙。
1. 树形数据为什么这么难查:先理解建模方式
1.1 大多数项目用的都是“邻接表”模型
打开任意一套后台管理系统,基本都能看到这种表结构:
sql复制CREATE TABLE dept (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
parent_id INT NULL,
CONSTRAINT fk_dept_parent FOREIGN KEY (parent_id) REFERENCES dept(id)
);
每条记录里只保存一个 parent_id,指向自己的上级节点,这就是经典的邻接表模型。它最大的优点是直观,新增一个部门只需要填一行 parent_id,查询某个节点的直接上级和直接下级也很快,一条普通 SQL 就能完成。但问题在于,一旦你问的不是“直接下级”,而是“某个部门下面到底有所有多少层部门”,邻接表模型本身给不出任何路径信息,SQL 不知道从哪一层结束,这就非常尴尬。
业务需求不会因为你建模简单就手下留情。报表系统需要按下钻看每个区域的所有子门店,权限系统需要判断某个角色有没有某个三级菜单的权限,商品系统需要统计一个顶级分类下的全部商品……这些需求本质上都在问同一个问题:给我这个节点底下所有的子孙节点。而邻接表原生只支持查“一级关系”,要查任意深度,就必须引入递归。
1.2 别再“全表捞出,程序递归”了
我在早期项目里用过一段时间的笨办法:先把整张部门表全部查出来,放到内存里,然后用 Java 或者 PHP 写循环,从根节点一层层往下把子节点挂上去。这种做法在小规模数据下确实能跑,毕竟一张几千行的部门表全量也就几百 KB,程序递归几毫秒就完成。但当树的数据量变大、查询频率变高以后,问题就很明显了。
首先是全表传输成本。我只想查“技术中心”这一支的子部门,却要把整个集团几万条组织架构数据全部拉出来,网络开销、内存占用都不划算。其次是程序里拼树的逻辑很难维护,尤其是节点顺序、路径展示、面包屑这些需求叠上来以后,递归函数越写越复杂,最后很可能只有当初写代码的人敢改。更麻烦的是,这种“先全量再递归”的做法在数据库里完全没法做进一步优化,比如要在递归过程中顺便统计每个部门的商品数量,你只能先把树拼好再重新查一遍,多出来一堆 N+1 查询。
真正让我下定决心改用递归SQL,是有一次一张组织表的数据量到了百万级,程序全量捞 10 次就要把应用服务器的内存打满,接口响应直接超时。那次之后我得到一个非常朴素的结论:能在数据库里完成的下钻,就别把数据搬到应用层做;能用一条 SQL 说清楚的层级结构查询,就别写几十行循环。
1.3 递归SQL到底解决了什么问题
可以把递归SQL理解成数据库自己执行的“局部树遍历”。你只需要告诉它两件事:第一,从哪里开始;第二,每一层怎么由上一层的记录产生下一层的记录。数据库会在内部维护一个临时结果集,反复执行第二步,直到产生不了新行才停下来。这样一来,无论树有多深,一条 SQL 就能返回某个节点下面的完整子树。
我觉得递归SQL真正的价值不只是“查出所有子部门”,而是它能把树展开这个动作和后续的统计、聚合、路径拼接放在同一条 SQL 里完成。比如你需要统计一个顶级分类下所有叶子商品的销售额,用递归CTE先把整棵分类树展开,再 JOIN 商品表和订单表,最后 GROUP BY,整个过程一气呵成,不需要任何中间步骤。这也是为什么树形数据处理里,递归SQL几乎是性价比最高的通用方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 递归CTE的核心机制与数据库写法差异
2.1 锚点成员和递归成员分别是什么
递归SQL在主流关系型数据库里基本都以 CTE(Common Table Expression,公用表表达式)为基础。PostgreSQL 和 MySQL 8.0 的关键字是 WITH RECURSIVE,SQL Server 用的是 WITH + UNION ALL 的形式,Oracle 从 11gR2 开始也支持标准递归 CTE。
一个递归CTE的基本结构是:
sql复制WITH RECURSIVE cte_name AS (
-- 锚点成员:首轮查询,负责产生初始结果
SELECT ...
FROM ...
WHERE 初始条件
UNION ALL
-- 递归成员:引用 cte_name 自身,产生下一层结果
SELECT ...
FROM cte_name
JOIN ... ON 上下层关系
)
SELECT * FROM cte_name;
锚点成员是整个递归的入口,它决定了从哪些行开始展开;递归成员则是“如何根据上一层结果找到下一层结果”的规则。数据库内部大概是这样执行的:先跑锚点成员,得到第一批结果;然后拿着这批结果去执行递归成员,得到第二批结果;再拿着第二批结果去执行递归成员,如此往复,直到某一次执行递归成员后一行都没有产生,整个过程就结束了。最终返回的是所有批次结果的并集。
用一个通俗的比喻来说,这个流程就像“查户口”:先找到村里的一户人家,然后问这户人家的子女是谁,接着去挨个问子女的子女是谁,一直问到家里没有小孩为止。每一轮询问都以上一轮找到的家庭为起点,而不是从头把整条街重新扫一遍。
2.2 不同数据库的写法差异对照
我整理了一份不同数据库对递归CTE的写法差异表,方便你从一种数据库切到另一种时快速对照:
| 数据库 | 是否支持版本 | 递归写法 | 限制方式 | 注意事项 |
|---|---|---|---|---|
| PostgreSQL | 8.4+ | WITH RECURSIVE |
默认看系统栈,可用 statement_timeout | 递归列类型需一致 |
| MySQL | 8.0+ | WITH RECURSIVE |
默认 1000 层,受 cte_max_recursion_depth 控制 | 5.7 及以下不支持,需要升级或换思路 |
| SQL Server | 2005+ | WITH cte AS (...) |
默认 100 层,可用 OPTION (MAXRECURSION) | 递归CTE语法不带 RECURSIVE 关键字 |
| Oracle | 11gR2+ | WITH RECURSIVE |
无明显默认限制 | 11.2 前用 CONNECT BY 居多 |
PostgreSQL 和 MySQL 8.0 的写法几乎一致;SQL Server 的差异稍微大一点,它写 CTE 的时候不需要 RECURSIVE 关键字,但限制递归次数是在整个语句的最后加 OPTION (MAXRECURSION n),如果不去管它,默认递归 100 层就会报错中断,遇到层级较深的树要先想好要不要调大这个值。
Oracle 的情况比较特殊,传统 SQL 里大量使用 START WITH ... CONNECT BY PRIOR ... 这种专用语法,而标准递归 CTE 在 Oracle 里的支持时间相对晚,很多老项目还在用 CONNECT BY。如果是新项目或者做跨数据库迁移,我更建议优先掌握标准写法,因为 CONNECT BY 只能在 Oracle 里用,换到别的数据库就得重写。
2.3 UNION 和 UNION ALL 的选择可能是关键
在递归CTE中,UNION 和 UNION ALL 不仅仅影响去重,还会直接影响递归结果。大多数树的层级关系里,同一个节点只会通过一条路径到达,所以用 UNION ALL 就足够,效率也高;但如果你的数据并不是严格的一棵树,而是带有多父节点的“有向无环图”,同一个节点可能通过不同路径到达,这时候如果你用 UNION 去重,递归过程会丢掉第二次到达该节点的路径,最终结果不完整。
我第一次踩这个坑是在做一个标签继承关系时,同一个子标签可以从两个父标签继承下来。因为递归成员写的是 UNION,数据库自动把重复节点去掉了,结果用一条父标签查询时,子标签的某个来源分支直接消失。后来改成 UNION ALL 才看到全貌,代价是结果里会出现重复行,需要在最终查询里再按业务口径去重或聚合。这也是递归SQL里最容易踩、却最不容易察觉的问题之一。
2.4 为什么我建议至少先掌握标准递归CTE
技术圈里经常有人争论“既然 Oracle 有 CONNECT BY,为什么还要学标准 CTE”,我的看法很现实:你在一个项目里用到的数据库引擎大概率不止一种,尤其是接触过数据仓库和数据分析平台之后,你会发现 Hive、Flink SQL、Doris、ClickHouse 都在向标准 SQL 语法靠拢,递归CTE基本都是以 WITH RECURSIVE 为蓝本。与其把精力花在各家私有方言上,不如先把标准能力吃透,换平台的时候迁移成本会低很多。
另外,不同数据库对于递归CTE的限制虽然不一样,但核心执行逻辑是一致的。一旦你理解了“锚点成员 + 递归成员 + 逐层扩展”这套模型,换数据库的时候只需要查一下关键字和限制参数,不需要重新学一遍设计思想。这也是我认为递归SQL值得多花时间的原因:它是少有的“一通百通”的 SQL 高级能力。
3. 实战落地:组织架构下钻与上溯查询
3.1 先造一张典型的部门表
我们沿用文章开头那张 dept 表,插入一组贴近真实公司组织架构的数据:
sql复制INSERT INTO dept (id, name, parent_id) VALUES
(1, '集团总部', NULL),
(2, '技术中心', 1),
(3, '市场中心', 1),
(4, '后端研发部', 2),
(5, '前端研发部', 2),
(6, '运维部', 2),
(7, '品牌部', 3),
(8, '华南分公司', 1);
此刻树的结构是这样的:集团总部下面挂着技术中心、市场中心、华南分公司;技术中心下面有后端研发部、前端研发部、运维部;市场中心下面有品牌部。虽然例子很小,但足以把递归SQL的两种典型方向讲清楚。
建议在建表时顺手给 parent_id 建一个索引。递归执行过程中每一层都要用“子记录的 parent_id 等于父记录的 id”这个条件去查表,如果 parent_id 列上没有索引,树一深,数据库每一层都需要全表扫描,性能会陡然下降。别小看这个细节,很多递归SQL慢的根源根本不是逻辑问题,而是漏了索引。
3.2 向下查:找技术中心下面的所有子部门
现在需要查询“技术中心”及其所有下级部门,SQL 长这样:
sql复制WITH RECURSIVE dept_tree AS (
SELECT id, name, parent_id, 1 AS depth
FROM dept
WHERE id = 2
UNION ALL
SELECT d.id, d.name, d.parent_id, dt.depth + 1
FROM dept d
JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT id, name, parent_id, depth
FROM dept_tree
ORDER BY depth, id;
执行结果:
| id | name | parent_id | depth |
|---|---|---|---|
| 2 | 技术中心 | 1 | 1 |
| 4 | 后端研发部 | 2 | 2 |
| 5 | 前端研发部 | 2 | 2 |
| 6 | 运维部 | 2 | 2 |
这段SQL的关键就是递归成员里的 JOIN 条件:d.parent_id = dt.id。它的含义是把“当前已经找到的部门”作为父节点,再去部门表里找 parent_id 等于这些 id 的新部门。跑完第一轮递归,dt 里是技术中心这一行;第二轮用技术中心的 id=2 去查,就把三个下级部门都找到了;第三轮拿这三个部门的 id 去查,没有任何部门再把它们当 parent_id,所以递归在第四轮自然结束。
3.3 带着层级、路径和缩进展示的一体化查询
真实业务里很少只要 id 和 name,通常还要求能拼出完整路径,比如“集团总部 / 技术中心 / 后端研发部”,或者在页面上按层级缩进展示。只要在递归的过程中顺带维护一个 path 字段和缩进字段,就能一条 SQL 完成:
sql复制WITH RECURSIVE dept_tree AS (
SELECT
id,
name,
parent_id,
1 AS depth,
CAST(name AS VARCHAR(500)) AS path,
name AS display_name
FROM dept
WHERE parent_id IS NULL
UNION ALL
SELECT
d.id,
d.name,
d.parent_id,
dt.depth + 1,
CAST(dt.path || ' / ' || d.name AS VARCHAR(500)),
REPEAT(' ', dt.depth) || d.name
FROM dept d
JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT id, name, depth, path, display_name
FROM dept_tree
ORDER BY depth, id;
这里有两个稍微值得注意的地方。第一,path 字段在锚点成员里必须做类型转换,比如写成 CAST(name AS VARCHAR(500)),否则 PostgreSQL 可能报“递归查询列类型不一致”的错误。第二,字符串拼接不同数据库语法不同,PostgreSQL 用 ||,MySQL 用 CONCAT(),SQL Server 用 +,写跨库脚本时要留意。display_name 里我用 REPEAT(' ', dt.depth - 1) 做缩进,输出到报表里可以直接看出层级关系。
3.4 向上溯:做面包屑导航还是查根信息
反过来,如果我知道了某个部门 id,想知道它向上一直到根节点的路径,递归的方向就要换一下。用“运维部”id=6 做例子:
sql复制WITH RECURSIVE dept_path AS (
SELECT id, name, parent_id, 1 AS depth
FROM dept
WHERE id = 6
UNION ALL
SELECT d.id, d.name, d.parent_id, dp.depth + 1
FROM dept d
JOIN dept_path dp ON d.id = dp.parent_id
)
SELECT id, name, parent_id, depth
FROM dept_path
ORDER BY depth;
这次 JOIN 条件写的是 d.id = dp.parent_id,含义是“拿当前已找到部门的 parent_id 回到部门表里查它的父部门记录”。每轮递归都会向上走一层,直到查到集团总部,它的 parent_id 是 NULL,没有再上一级,递归结束。结果如下:
| id | name | parent_id | depth |
|---|---|---|---|
| 6 | 运维部 | 2 | 1 |
| 2 | 技术中心 | 1 | 2 |
| 1 | 集团总部 | NULL | 3 |
这种向上收集祖先链路的写法在后台系统里非常常见,比如点击一条订单数据后,要展示“这条订单所属的完整组织机构”,或者在权限校验时反查用户角色挂在哪棵权限树的哪个分支下面,都属于这一类问题。
4. BOM物料清单里的递归用量汇总
4.1 一个典型的BOM数据结构
组织架构之外,我打交道最多的树形数据是制造业的 BOM(Bill of Materials,物料清单)。需求通常是这样的:生产一台电钻,需要用到哪些原材料,每个原材料需要多少个。BOM 里既有成品和半成品,也有最底层的原材料,数据天然是一棵树。
sql复制CREATE TABLE product (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE bom (
parent_id INT NOT NULL,
child_id INT NOT NULL,
qty INT NOT NULL,
PRIMARY KEY (parent_id, child_id)
);
product 表记录所有物料名称,bom 表记录装配关系。一行 (parent_id=1, child_id=2, qty=1) 表示“生产 1 台电钻需要 1 个电机组件”。这里把数量直接放在关系表里,比单独建一个数量字段更符合实体关系设计。我先插入一组最小可用的数据:
sql复制INSERT INTO product (id, name) VALUES
(1, '电钻'),
(2, '电机组件'),
(3, '外壳'),
(4, '定子'),
(5, '转子'),
(6, '轴承');
INSERT INTO bom (parent_id, child_id, qty) VALUES
(1, 2, 1),
(1, 3, 1),
(2, 4, 1),
(2, 5, 1),
(5, 6, 2);
这棵树的语义是:生产 1 台电钻需要 1 个电机组件和 1 个外壳;一个电机组件需要 1 个定子、1 个转子;一个转子需要 2 个轴承。你可能会发现,从电钻到轴承中间隔了电机组件、转子两层,用普通 JOIN 根本查不出轴承的需求量,因为根本不确定要 JOIN 几次。
4.2 用递归SQL逐层累加数量
要算总需求量,核心思路是在递归过程中把每一层的单个需求量往下乘:
sql复制WITH RECURSIVE bom_tree AS (
SELECT
child_id,
qty AS total_qty,
1 AS depth
FROM bom
WHERE parent_id = 1
UNION ALL
SELECT
b.child_id,
bt.total_qty * b.qty AS total_qty,
bt.depth + 1
FROM bom b
JOIN bom_tree bt ON b.parent_id = bt.child_id
)
SELECT
child_id,
SUM(total_qty) AS need_qty
FROM bom_tree
GROUP BY child_id
ORDER BY child_id;
递归过程并不神秘:第一轮先找到电钻的直接子件,也就是电机组件和外壳,此时 total_qty 都是 1。第二轮拿着电机组件的 id=2 去 bom 表里找 parent_id=2 的记录,找到定子和转子,然后把上一层的 total_qty=1 乘上当前行的 qty,得到每个物料总需求量是 1。第三轮拿着转子的 id=5 去找,找到轴承,用上一层的 total_qty=1 乘以关系表里的 qty=2,得到轴承总需求量为 2。
最终结果应该是定子 1 个、外壳 1 个、电机组件 1 个、转子 1 个、轴承 2 个。如果我用的是查询全部底层物料的口径,这个结果基本就是一张物料需求清单。
4.3 同一物料出现多条路径时的去重汇总
BOM 和部门树有个非常重要的区别:同一个物料经常会出现在不同的上级节点下面。比如同一款轴承既用在后轮组件里,也用在前轮组件里,递归展开时轴承会出现多行,每行的 total_qty 是不同分支贡献的数量,不能直接用普通查询去掉重复行。
这时候处理办法是等递归全部结束后再做一次聚合,也就是上面的 SQL 里先用 GROUP BY child_id 再 SUM(total_qty)。我遇到过不少初学者试图在递归成员内部去重或者分组,结果语法报错,或者算出来的数量完全对不上。正确姿势应该是让递归CTE把每条路径的需求量都暴露出来,最外层再做汇总,递归层只负责“逐层展开和乘法累加”,别在递归成员里做聚合操作。
4.4 递归CTE关联多表的注意事项
BOM 查询往往还需要把 product 表的物料名称带到结果里。递归成员里也可以继续写 JOIN,比如把 product 表 JOIN 进来取 name,完全没有问题。但有几个容易踩的坑值得单独提醒。
第一个是不要在递归成员里写 ORDER BY 或 LIMIT。多数数据库不允许在递归成员中使用这些子句,因为每一层的“行数”和“顺序”都不是最终结果,只有最外层的 ORDER BY 是有效的。第二个是确保 JOIN 的关联字段上有索引,尤其是 bom 表里的 parent_id,否则每一轮递归都是全表扫描,层次稍微深一点整个查询就能卡死。第三个是列类型一致性,递归成员里 SELECT 出来的列必须和锚点成员保持相同类型,特别是需要拼接生成路径字段时,最好都显式 CAST 成同一个长度足够的字符串类型。
5. 递归SQL常见问题与调优经验
5.1 死循环:数据脏了怎么办
递归SQL最大的隐患就是无限递归。比如一张部门表里出现了一条异常数据,把 1 号部门的 parent_id 改成 2,同时 2 号部门还是 1 号部门的子部门,两个节点互相指向,递归查询就会在它们之间反复横跳,永远停不下来。
MySQL 遇到这种情况会默认在递归 1000 次后报错中断,SQL Server 会在默认递归 100 次时报错,所以实际表现往往不是“卡死”,而是报错提示“递归次数超过限制”。但从工程角度看,不应该把希望寄托在数据库的兜底参数上,最好在递归成员里加一个深度保护,比如 WHERE depth < 20,或者通过路径字段判断当前节点是否已经出现过。
如果树的数据来源不可控,我更建议在应用层对写入的 parent_id 做一次祖先链路校验,坚决不允许把 parent_id 设置成自己的直系子孙。我接手过一个客户主数据系统,就因为没有校验“父节点不能是自己的子孙节点”,上线三个月后出现环路,所有递归查询全部失败,最终还是写了一个专门的数据修复 SQL 才把脏数据清掉。
5.2 不同数据库的递归深度限制怎么调
| 数据库 | 默认限制 | 调整方式 |
|---|---|---|
| MySQL 8.0 | 1000 层 | SET SESSION cte_max_recursion_depth = 10000; |
| SQL Server | 100 层 | 语句末尾加 OPTION (MAXRECURSION 1000),0 表示不限制 |
| PostgreSQL | 没有显式默认限制,但受系统栈和 statement_timeout 影响 | 可用 SET statement_timeout 兜底 |
| Oracle | 无常见限制,但有函数递归层级限制 | 关注 CONNECT BY 里 LEVEL 和 NOCYCLE |
SQL Server 里调整递归上限不是在开头设置,而是整个查询语句最后加一行:
sql复制WITH dept_tree AS (...)
SELECT * FROM dept_tree
OPTION (MAXRECURSION 0);
这里 0 代表不限制次数。生产环境我一般不建议直接设成 0,因为一旦数据出现环路,不限制次数的递归会把数据库资源耗光,更稳妥的做法是把上限设成一个略大于业务最大层级的数值,比如 500。
5.3 递归越来越慢,先检查扇出和索引
递归SQL的性能问题首先是扇出问题。所谓扇出,是指每一层平均能扩展出多少个子节点。如果一棵树每个节点平均有 10 个子节点,那么第 1 层有 10 行,第 2 层 100 行,第 3 层 1000 行,到第 6 层就已经是 100 万行。你越往下查,SQL 要处理的中间行数就越多,这个膨胀速度一旦失控,加什么索引都没用。
遇到过两次典型的慢递归:一次是查询部门树的时候没有给 parent_id 加索引,导致每一轮都要全表扫描一次部门表;另一次是从某个顶级分类查询全部商品分类下的商品,因为分类树本身很深,每一层都会产生海量中间结果,再去 JOIN 商品表自然慢得离谱。解决前者很简单,加索引即可;解决后者则需要调整思路,比如先查目标节点的小范围子树,再 JOIN 商品表,而不是从根节点把整棵大树全部展开。
5.4 数据量极大时的替代方案:闭包表和物化路径
递归SQL虽然通用,但并非万能。在数据量极大、查询极频繁的线上场景,递归展开的计算成本没有办法用索引完全抵消,因为中间结果的行数天然是膨胀的。真到这一步就需要考虑用空间换时间。
闭包表是常见的一种替代方案,专门建一张关系表,比如 dept_closure(ancestor_id, descendant_id, depth),在数据写入时就把所有祖先和后代关系一次性补齐。查询某部门所有子部门就变成对这张关系表做等值查询,性能非常稳定。代价是写入成本高,每次增加或移动一个节点,都要维护很多条关系数据。物化路径则是在原表加一个 path 字段,比如 "001.002.004",查询子节点时用 WHERE path LIKE '001.%',实现简单但需要保证编码格式规范,移动节点时改 path 也很麻烦。
对大多数中小型系统,邻接表加递归CTE已经足够;只有树的规模大到递归明显拖垮接口时,才值得引入更重的模型。我的建议是先用递归CTE快速跑通业务,通过监控接口耗时确定是否真正需要重构。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 查询报错“recursion aborted after 1001 iterations” | MySQL 递归超限 | 执行 SET SESSION cte_max_recursion_depth = 更高值 |
| SQL Server 报“maximum recursion 100 has been exhausted” | 默认递归 100 次不够 | 在语句末尾加 OPTION (MAXRECURSION 500) |
| 查询卡死或 CPU 居高不下 | 数据存在环路 | 在递归成员加 WHERE depth < 20,并修复脏数据 |
| 递归结果缺少部分子节点 | 错误使用了 UNION 去重,或存在孤儿数据 | 改为 UNION ALL,并检查父节点数据一致性 |
| PostgreSQL 报“recursive query column type mismatch” | 锚点和递归成员列类型不一致 | 对拼接字段做 CAST(... AS VARCHAR(...)) |
| SQL Server 报列类型不一致 | 锚点和递归成员列类型不一致 | 同样对字段做类型转换 |
| 结果顺序混乱 | 递归内部排序列被忽略 | 只在最外层加 ORDER BY |
| 递归速度越来越慢 | parent_id 没有索引,或扇出过大 | 建索引,或先缩小递归起点范围 |
| 需要在递归过程中统计数量 | 误在递归成员里做 GROUP BY | 递归只负责展开,末尾再用 GROUP BY/SUM |
最后再分享一个我自己的使用习惯。递归SQL和闭包表不是二选一的对立关系,我现在做业务时,会把“频繁使用的稳定树”维护成闭包表,而把所有临时分析、手工下钻、运维排查场景交给递归CTE。闭包表维护成本高,适合固定的核心路径;递归CTE写起来快,适合随时变化的探索性查询。根据个人经验,这样组合使用,比单一方案顺手得多。如果你正在做一个从零开始的层级结构查询需求,先别急着建冗余字段,老老实实写一条递归SQL跑一下,观察数据规模和响应时间,再决定要不要上更重的模型。很多看起来吓人的树形数据问题,一条递归SQL就解决了。
