真正可行的路径是用WITH RECURSIVE(新版本)或应用层迭代(旧版MySQL),而非子查询;因子查询仅静态执行一次,无法实现多级路径追踪,且易导致性能崩盘、结果错乱或无限循环。

直接用子查询做供应链路径追踪,90% 的 case 会卡在递归深度、性能崩盘或结果错乱上——真正可行的路径是:先判断是否需递归,再选 WITH RECURSIVE(PostgreSQL/MySQL 8.0+/SQL Server)或自连接 + 迭代模拟(旧版 MySQL),别硬套普通嵌套查询。
WHERE 子句里的子查询根本没法追踪多级供应关系
常见错误是写成 SELECT * FROM suppliers WHERE parent_id IN (SELECT supplier_id FROM suppliers WHERE region = 'Shenzhen')。这只能查一级上游,而供应链里“A → B → C → D”这种 4 层路径,它连 C 都摸不到。
原因很实在:子查询只执行一次,返回一个静态集合,不随外层行变化而重算;它不是“对每条记录展开找它的上家”,而是“先找出所有深圳供应商,再筛出 parent_id 在这个集合里的记录”。没有上下文绑定,就没有路径延续性。
- 适用场景仅限单跳关系,比如“查所有直属上级是某类供应商的厂商”
- 若强行嵌套多层(如
WHERE parent_id IN (SELECT supplier_id FROM ... WHERE parent_id IN (...))),可读性爆炸,且 SQL Server 会报错“嵌套层级超限”,MySQL 7.x 直接拒绝解析 - 空值处理极脆弱:任一环节
parent_id IS NULL,整条路径就断在那,不会跳过继续找
必须用 WITH RECURSIVE 构建路径栈,否则拿不到完整链条
WITH RECURSIVE 是唯一能自然表达“从起点出发,逐层向上/向下延伸”的语法。核心在于两部分:锚点(anchor)定义起始节点,递归成员(recursive term)定义如何从当前层生成下一层,并用 UNION ALL 合并结果。
以追踪零件供应商源头为例:
WITH RECURSIVE supply_chain AS ( -- 锚点:从指定零件开始 SELECT id, name, parent_id, 1 AS level, CAST(name AS VARCHAR(500)) AS path FROM suppliers WHERE id = 123 <p>UNION ALL</p><p>-- 递归:找每个节点的 parent SELECT s.id, s.name, s.parent_id, sc.level + 1, sc.path || ' ← ' || s.name FROM suppliers s INNER JOIN supply_chain sc ON s.id = sc.parent_id WHERE s.parent_id IS NOT NULL AND sc.level </p>
-
level和path字段必须显式定义并传递,否则递归中无法累积信息 -
WHERE s.parent_id IS NOT NULL是终止条件之一,防无限循环;sc.level 是安全兜底,避免数据异常导致栈溢出 - PostgreSQL 用
||拼接字符串,SQL Server 要改用CONCAT()或+,MySQL 8.0+ 支持CONCAT() - 如果要反向查下游(即谁用了我的零件),把
ON s.id = sc.parent_id改成ON s.parent_id = sc.id即可
旧版 MySQL(5.7 及以下)只能靠自连接硬凑,但最多撑 4 层
没有 WITH RECURSIVE 时,有人用 LEFT JOIN 多次关联同一张表模拟层级,例如 s1 LEFT JOIN suppliers s2 ON s2.id = s1.parent_id LEFT JOIN suppliers s3 ON s3.id = s2.parent_id...。这不是真递归,是固定深度展开。
- 每加一层 JOIN,结果集行数可能指数增长(尤其有多个并列上游时),1000 行原始数据 + 4 层 JOIN 容易产出百万级中间结果
- 字段命名必须手动区分:不能都叫
name,得是s1.name AS level1_name,s2.name AS level2_name……维护成本高 - 无法动态截断:即使某条路径实际只有 2 层,也会填满 4 层字段,空值一堆,后续清洗麻烦
- 真正该用的替代方案是应用层迭代查询:先查 ID=123 的 parent_id,再用该值查它的 parent_id,循环至 NULL——虽然多几次 round-trip,但可控、可 debug、不爆内存
递归查询最易被忽略的不是语法,而是终止条件设计和索引覆盖。没给 parent_id 加索引,WITH RECURSIVE 执行起来比全表扫描还慢;而漏写 level 限制,线上跑着跑着就触发服务器 max_recursive_depth 退出。路径追踪的本质是图遍历,别把它当成普通过滤来写。











