老周家的祠堂里挂着一幅巨大的族谱,从三百年前的老祖宗往下,枝枝杈杈开出上百个人名。每逢祭祖,族老都要回答同一个问题:“三房一共多少人?五世祖名下的直系子孙有哪些?”翻族谱翻到眼花是常有的事。其实,族谱就是一棵树:每个人是一个结点,箭头指向“谁是谁的父辈”。而我们要做的动作只有两个——先找到起点(老祖宗),然后一遍遍执行“查这个人有哪些子女”,直到整棵树被点亮。这个“一遍遍重复同一个查询动作”的过程,在SQL世界里叫作递归查询。CTE(公共表表达式,Common Table Expression)是承载递归的载体,像一张随写随用的备忘纸。CTE递归,就是让数据库自己学会翻族谱。

岗位关联

对AI从业者而言,CTE递归是与数据打交道的必修课——无论是知识图谱的层级检索、特征工程里的父子关系展开,还是AI应用背后的数据血缘追溯,递归SQL都能让你在数据侧省下大量手写代码。

目录
  1. 可视化演示
  2. 递归CTE入门:锚点与扩展
  3. 终止条件设计
  4. 层级深度追踪
  5. 树形结构遍历
  6. 组织层级查询
  7. 图状数据处理
  8. 性能优化考量
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示递归CTE锚点+递归部分结构、树形层级遍历深度追踪、递归终止条件与性能优化。切换模式掌握SQL递归查询处理层级数据的核心技术。

本节配有交互动效(进入视口后自动加载,约 3KB)。

递归CTE结构:锚点+递归部分 WITH子句定义CTE,锚点查询提供初始行集,递归部分通过UNION ALL自引用迭代扩展 每次递归迭代基于上次结果集扩展,直到递归查询返回空行集自然终止 WITH RECURSIVE cte AS (anchor UNION ALL recursive_join) 锚点+UNION ALL+自引用是递归CTE的三大核心要素

递归CTE入门:锚点与扩展

老周翻族谱时总有一个固定动作:先找到写有“一世祖周元公”的那一行,把它单独抄在纸上,然后看这一行下面挂着哪些名字。这张抄着老祖宗的备忘纸,就是递归CTE的“锚点”;接下来“找到每个人名下的子女”这个重复动作,就是“递归部分”。

在SQL里,递归CTE的语法像一份填好的表格:WITH RECURSIVE 名字 AS ( 锚点查询 UNION ALL 递归查询 ) SELECT ...。锚点查询返回第一批种子行;递归查询拿着上一轮的结果去表里再查一遍,查出的新行与旧结果用 UNION ALL 拼在一起,然后进入下一轮。数据库会重复执行“拿着上一轮结果继续查”这个动作,直到某轮查不出任何新行为止。整个过程没有花哨的高科技,就和你翻族谱一样朴素。

老周盯着这张不断变长的备忘纸,发现“翻族谱”这件事被拆成了两行字:先写谁,再写怎么找。SQL 里的递归CTE也正是这么两句——一句锚点,一句递归,数据库就替你把整棵族谱翻完了。

R0=A,Rn+1=Rn∪F(Rn),R∞=⋃i=0∞Ri\displaystyle R_{0} = A, \quad R_{n+1} = R_n \cup F(R_n), \quad R_{\infty} = \bigcup_{i=0}^{\infty} R_i
整个递归CTE可以概括为:一个种子、一个重复扩展动作、一个最终并集。
  1. $R_0 = A$(锚点种子)$R_0$ 表示第 0 轮迭代产生的初始结果集,$A$ 是锚点查询的返回结果。锚点就是“一世祖周元公”那一行,是递归的出发点。
    R0=AR_{0} = A
  2. $R_{n+1} = R_n \cup F(R_n)$(迭代扩展)每执行一轮递归,旧结果 $R_n$ 会与新增行 $F(R_n)$ 求并集,形成下一轮结果 $R_{n+1}$。$F$ 就是“查子女”这个递归查询。
    Rn+1=Rn∪F(Rn)R_{n+1} = R_n \cup F(R_n)
  3. $R_{\infty}$(最终结果)迭代到没有新行时,所有轮次的记录合并起来,就是完整的族谱展开。$\bigcup$ 表示把所有轮次并到一起。
    R∞=⋃i=0∞RiR_{\infty} = \bigcup_{i=0}^{\infty} R_i

终止条件设计

族谱里总有几个名字下孤零零写着“无后”。老周每次捋到这种人,这一支就自然到头了,不用再往下翻。递归CTE的终止逻辑也是这个道理:当一轮迭代执行完,UNION ALL 没有并入任何新行,数据库就认为“这页族谱没有新人了”,自动收笔。

但真实的表里会有防不胜防的“幽灵亲属”:比如某条记录的上级ID指向了自己,或者两条记录互相指认对方是上级,形成环路。这就像族谱里有人乱写“我爹是我儿子”,顺着查下去会没完没了。因此终止条件设计通常有三道防线:第一是依赖“无新行自然终止”的天然边界;第二是给递归加上深度上限,比如 WHERE depth <= 20;第三是用路径字符串记录已访问的节点,发现重复就剪枝。

老周后来给族谱加了一条规矩:每页都标注“世数”,超过三十世就不再往下写。他说,有边界才能走得远,递归查询也一样——要么靠数据本身到头,要么靠深度限制硬刹车,否则再好的查询也会变成无底洞。

Stop  ⟺  F(Rn)⊆Rn  ⟺  Rn+1=Rn\displaystyle \text{Stop} \iff F(R_n) \subseteq R_n \iff R_{n+1} = R_n
终止的本质是结果集到达“固定点”:新扩展的内容全部是旧内容,集合不再变化。
  1. Stop(终止判定)当递归部分的输出已经被之前的结果集完全包含,继续迭代没有意义,递归应该停止。
    Stop  ⟺  F(Rn)⊆Rn\text{Stop} \iff F(R_n) \subseteq R_n
  2. $F(R_n) \subseteq R_n$(新行不增加)$F(R_n)$ 是这一轮查出的所有子节点,$\subseteq$ 表示它们全都已经在 $R_n$ 里了。族谱上“无后”的人就属于这种情况。
    F(Rn)⊆RnF(R_n) \subseteq R_n
  3. $R_{n+1} = R_n$(固定点)当新结果等于旧结果,说明集合再也长不大,数据库自动结束递归。
    Rn+1=RnR_{n+1} = R_n

层级深度追踪

族谱上每个名字旁边都会盖一个“第几世孙”的小章。老周从小周那一辈往前数,每一代就多一横,到老祖宗正好十世。数据库里的分层数据同样需要回答“这条记录在第几层”,而答案不在表里,要靠递归过程自己算出来。

做法很简单:锚点部分把初始记录的深度记为 0;递归部分在拼出新行时,把“上一轮深度 + 1”作为新行的深度带下去。顺着这条深度字段,你不仅能知道谁在第几层,还能拼出一条从根到当前节点的完整路径,比如“周元公 / 三房 / 周传福”。这和族谱上从一世写到十世的辈分层级完全一一对应。

老周把这种“每传一代盖个章”的智慧教给了数据库,于是查询结果里每一行都自带辈分。以后族老再问“五世祖以下有哪些人”,直接按深度过滤就能答上来。

depth(u)=depth(p)+1,path(u)=path(p)∥′/′∥u.name\displaystyle \text{depth}(u) = \text{depth}(p) + 1, \qquad \text{path}(u) = \text{path}(p) \parallel '/' \parallel u.\text{name}
层级深度的公式本质是:子结点的深度比父结点多一,路径是在父路径末尾补上当前结点名字。
  1. $\text{depth}(u)$(当前结点深度)$u$ 是新递归出来的结点,深度是它相对根结点的层数。根结点深度为 0,其直接子女深度为 1。
    depth(u)=depth(p)+1\text{depth}(u) = \text{depth}(p) + 1
  2. $p$(父结点)$u$ 是通过父结点 $p$ 的递归 JOIN 找到的,所以 $u$ 的深度等于 $p$ 的深度再加一。
    p∈Rn,u∈F(p)p \in R_n, \quad u \in F(p)
  3. $\text{path}(u)$(路径拼接)用 $\parallel$ 把父路径和当前结点名拼起来,中间加一个 / 分隔符,就像从根目录走到当前文件的文件路径。
    path(u)=path(p)∥′/′∥u.name\text{path}(u) = \text{path}(p) \parallel '/' \parallel u.\text{name}

树形结构遍历

老周查族谱有两种姿势:从老祖宗往下翻,看某支繁衍成了多少人;或者从自己往上倒,看这十世之间究竟经过哪些人。前者叫自顶向下,后者叫自底向上。递归CTE同样支持这两种方向,区别只在于锚点和递归部分的写法。

自顶向下的锚点是根节点,递归部分不断找子节点;自底向上的锚点是目标叶子节点,递归部分不断找父节点。两者会合之处就是整棵树的完整路径。除了遍历方向,树形结构还常涉及“叶子查询”(没有子女的结点)和“子树聚合”(把某个子树所有后代的值汇总)。聚合的写法很特别:递归部分只负责把树展开成行,SUM、COUNT 这类统计要放在递归结束后的外层 SELECT 里做,好比先把整支族人列出来,再挨个点人头。

老周发现,族谱无论从上往下翻还是从下往上翻,只要方向定清楚,树就乱不了。递归CTE也是一样:锚点定了方向,递归部分定了步伐,整棵树就规规矩矩地摊在你面前。

Total(u)=val(u)+∑c∈children(u)Total(c)\displaystyle \text{Total}(u) = \text{val}(u) + \sum_{c \in \text{children}(u)} \text{Total}(c)
子树的汇总等于自身值加上所有子结点子树的汇总,这是一种典型的“分而治之”递归定义。
  1. $\text{Total}(u)$(子树汇总)$u$ 是子树的根,$\text{Total}(u)$ 表示这一整支人合起来的值,比如总人数。
    Total(u)=val(u)+∑c∈children(u)Total(c)\text{Total}(u) = \text{val}(u) + \sum_{c \in \text{children}(u)} \text{Total}(c)
  2. $\text{val}(u)$(自身值)$u$ 自己的贡献,不能漏掉,就像统计三房人数时三房家长本人也算一口人。
    val(u)\text{val}(u)
  3. $\sum_{c \in \text{children}(u)}$(累加子树)对 $u$ 的每个子结点 $c$ 递归求其子树汇总,再全部加到一起。$\sum$ 表示求和,遍历的方向是“先从根往下拆,再自底向上归并”。
    ∑c∈children(u)Total(c)\sum_{c \in \text{children}(u)} \text{Total}(c)

组织层级查询

周氏集团三房的大管家想考考小周:“你帮我查查三房下面一共挂着多少员工,再把张三到总经理之间经过的所有人列出来。”这几乎是递归CTE最经典的实战场景:一张员工表,两个字段 id 和 manager_id,就是一棵组织树。

组织层级查询有四种常见诉求:上下级关系展开(树形目录)、全链路追溯(找所有上级)、汇总聚合(统计部门人数)、权限范围(某经理能看的数据范围)。它们的 SQL 骨架高度相似,变化的只是锚点和 SELECT 列表。值得注意的是权限范围查询:如果每个经理只能看自己和所有下级的数据,那么“范围”就是一棵子树——这正是用 Scope(u) 这个集合表达的概念,也是后台系统做数据权限隔离的常用手段。

小周写完这个查询,三房大管家非常满意。过去要写四五层自连接才能勉强完成的事,现在一段递归CTE就干净利落地解决了——组织越深,递归CTE的优势越明显。

Scope(u)={x∈Employee∣ancestor(x)=u}\displaystyle \text{Scope}(u) = \{ x \in \text{Employee} \mid \text{ancestor}(x) = u \}
权限范围是“以 u 为根的整棵子树”:凡是祖先中出现了 u 的员工,都属于 u 的可见范围。
  1. $\text{Scope}(u)$(可见范围)集合记号 $\{ \mid \}$ 表示“所有满足条件的员工 $x$ 组成的集合”。Scope 是数据权限的核心。
    Scope(u)={x∈Employee∣ancestor(x)=u}\text{Scope}(u) = \{ x \in \text{Employee} \mid \text{ancestor}(x) = u \}
  2. $x \in \text{Employee}$(员工集合)$x$ 是员工表中的一条记录。
    x∈Employeex \in \text{Employee}
  3. $\text{ancestor}(x) = u$(祖先是 u)$x$ 沿着 manager_id 一路往上走,最终到达的根结点就是 $u$。这样 $u$ 的所有下级都会落进这个集合。
    ancestor(x)=u\text{ancestor}(x) = u

图状数据处理

周家和刘家联姻之后,族谱不再是一棵纯粹的树:周家的小女儿嫁进刘家,两家的人互相都有往来,谱上出现了“两条路都能走到同一个人”的交叉。老周摇头说,这哪还是树,分明是一张网。数据库里的图状数据正是如此:一个节点可能有多个父节点,也可能通过不同路径回到原来的节点。

遍历图比遍历树多一个麻烦:环路。如果图里有 A -> B -> A 这样的环,递归CTE会无限循环下去。解决办法是在每一轮带上“走过的路径”字段,查到新节点时检查它是否已经在路径里;如果已经访问过,就跳过这一支。图数据的递归查询还能做更多事:有向图遍历、环检测、最短路径搜索、传递可达性分析。比如判断“周家三房的人能否通过联姻关系联系上刘家五房”,就是一次带防环的递归展开。

老周把防环这个规矩看得极重:“族谱上记了谁,就不要再绕回去。”写图数据的递归CTE时,路径记录就是这张“拜访清单”,它保证递归在复杂网络中也能走到头。

Reach(u,v)=Edge(u,v)∨∃w(Edge(u,w)∧Reach(w,v))\displaystyle \text{Reach}(u,v) = \text{Edge}(u,v) \lor \exists w \big( \text{Edge}(u,w) \land \text{Reach}(w,v) \big)
可达性是一个递归定义:能直接到达,或者先到某个中间点再继续递归到达。
  1. $\text{Reach}(u,v)$(可达关系)表示从节点 $u$ 出发,沿着边能否走到 $v$。这是图查询的通用语言。
    Reach(u,v)\text{Reach}(u,v)
  2. $\text{Edge}(u,v)$(直接边)$u$ 到 $v$ 有一条直接相连的边,一步就能到。这是递归的“锚点”。
    Edge(u,v)\text{Edge}(u,v)
  3. $\exists w$(存在中间点)存在某个中间节点 $w$,说明 $u$ 不是一步到 $v$,而是先到 $w$ 再继续走。
    ∃w(Edge(u,w)∧Reach(w,v))\exists w \big( \text{Edge}(u,w) \land \text{Reach}(w,v) \big)
  4. 递归下降$\text{Reach}(w,v)$ 重复同样的定义,每走一步就缩短一段距离,直到命中直接边的锚点。
    Reach(w,v)\text{Reach}(w,v)

性能优化考量

族谱越修越厚,老周翻起来越来越慢。后来他想了个办法:在谱盒里放一个按名字笔画排的“索引卷”,查谁先翻索引,直接定位到第几页。数据库的索引干的是同一件事。递归CTE每一轮都要用父键去 JOIN 表,如果 parent_id / manager_id 字段没有索引,每一轮都会全表扫一遍,深度一深性能就崩。

性能优化有四板斧:第一,为父键字段建索引,让每轮 JOIN 快速定位;第二,控制递归最大深度,必要时在递归部分加 WHERE depth <= N;第三,中间结果物化,把每轮都要用到的数据先落到临时表,减少重复扫描;第四,评估替代方案,对层级很浅(比如 2-3 层)的数据,普通自连接或应用层循环可能更快。递归CTE不是银弹,但它把“复杂度”从应用代码移到了数据库里,用得好能省下大把传输开销。

老周把索引卷放进谱盒后,翻谱速度快得惊人。递归CTE的性能密码也藏在这句话里:深度的轮次省不掉,但每一轮的查找成本可以用索引压到最低。

Ttotal=∑i=1DO(scan(Ri−1)×join_cost)=O(D×M)\displaystyle T_{\text{total}} = \sum_{i=1}^{D} O\big( \text{scan}(R_{i-1}) \times \text{join\_cost} \big) = O(D \times M)
总代价约等于“迭代轮数 × 每轮扫描扩展的记录量”,所以深度和中间结果大小是两个关键变量。
  1. $T_{\text{total}}$(总代价)递归CTE的耗时由每一轮迭代的开销累加而成。
    Ttotal=∑i=1DO(scan(Ri−1)×join_cost)T_{\text{total}} = \sum_{i=1}^{D} O\big( \text{scan}(R_{i-1}) \times \text{join\_cost} \big)
  2. $D$(最大深度)递归轮数等于树的深度。族谱从一世到十世,$D$ 就是 10;每多一轮就多一次全表访问。
    D=max⁡depth(u)D = \max \text{depth}(u)
  3. $O(D \times M)$(简化估算)当每轮扩展的平均记录数是 $M$,总代价近似为轮数乘以每轮规模。索引的作用在于把 $\text{join\_cost}$ 从全表扫描降为 O(1) 的索引查找。
    Ttotal=O(D×M)T_{\text{total}} = O(D \times M)

AI 应用场景

社交关系链的逐层扩展

阶段:数据准备与特征工程

解决的问题:推荐系统需要知道“好友的好友”甚至“三层关系内的潜在兴趣节点”。传统做法是在应用层循环发多次 SQL,每次都要往返数据库,网络开销巨大,代码也难以维护。用递归CTE可以在一条 SQL 里完成整条关系链的展开,把多轮查询合并成一次数据库内部迭代。

具体案例:某社交App做“你可能认识的人”推荐。用户表 user(id) 和朋友关系表 friendship(user_id, friend_id),用递归CTE从目标用户出发做三层广度扩展,过滤掉已是好友的节点,得到候选推荐集合,再交给排序模型打分。整个过程数据获取一条SQL搞定。

知识图谱的概念层级加载

阶段:知识表征与检索增强

解决的问题:AI助手回答复杂问题时需要把知识库里的父子概念一次性展开。人工逐个查询效率低、易遗漏子节点。递归CTE可以从根概念出发,把整棵概念子树拉出来,保证检索的完整性。

具体案例:某企业知识库用 concept(id, parent_id, name) 存着“机器学习 > 深度学习 > 神经网络 > Transformer”这样的概念树。当用户问“深度学习有哪些子领域”时,系统先递归CTE取出整棵子树,再送给向量检索模块做相关段落召回,回答的覆盖面明显提升。

组织架构数据权限隔离

阶段:数据治理与安全管控

解决的问题:AI 报表平台需要让每个经理只能看到自己和下级的数据。传统写法要在 SQL 里拼一长串 IN 子句,数据一多就超出数据库参数上限。递归CTE能把“可见范围”变成一棵子树,直接作为权限过滤子查询。

具体案例:某集团的 BI 系统里有 8000 名员工,层级深达 7 层。每次生成报表时,先递归CTE算出当前经理的可见员工 ID 集合,再与外层聚合查询 JOIN。权限逻辑收敛在一段 SQL 里,审计时也能清楚解释每行数据的来源。

Python 演示

python57 行
import sqlite3 conn = sqlite3.connect(":memory:")cur = conn.cursor() cur.execute("""CREATE TABLE employee (    id INTEGER PRIMARY KEY,    name TEXT NOT NULL,    manager_id INTEGER);""") employees = [    (1, "周元公", None),    (2, "周长房", 1),    (3, "周三房", 1),    (4, "周传福", 3),    (5, "周传禄", 3),    (6, "周小周", 4),]cur.executemany("INSERT INTO employee VALUES (?,?,?)", employees)conn.commit() rows = cur.execute("""WITH RECURSIVE org_tree AS (    SELECT id, name, manager_id, 0 AS depth    FROM employee    WHERE manager_id IS NULL    UNION ALL    SELECT e.id, e.name, e.manager_id, ot.depth + 1    FROM employee e    JOIN org_tree ot ON e.manager_id = ot.id)SELECT id, name, depth FROM org_tree ORDER BY depth, id;""").fetchall() print("全公司组织树:")for row in rows:    print("  " * row[2] + f"- {row[1]} (id={row[0]})") ancestors = cur.execute("""WITH RECURSIVE chain AS (    SELECT id, name, manager_id, 1 AS level    FROM employee WHERE id = 6    UNION ALL    SELECT p.id, p.name, p.manager_id, c.level + 1    FROM employee p    JOIN chain c ON p.id = c.manager_id)SELECT name FROM chain ORDER BY level DESC;""").fetchall() names = [name for name, in ancestors]print("周小周的上级链路:", " > ".join(names)) conn.close()
目的:用 Python 内置 sqlite3 驱动,在内存数据库中演示递归CTE的两种典型用法——自顶向下展开组织树、自底向上回溯上级链路。
  1. WITH RECURSIVE org_tree AS (定义递归CTE,语义是“我要开始一遍遍翻族谱了”
  2. WHERE manager_id IS NULL锚点部分:没有上级的人就是老板,相当于族谱的老祖宗
  3. UNION ALL把每轮新找出的员工追加到结果集,保留重复也允许
  4. JOIN org_tree ot ON e.manager_id = ot.id递归部分的核心:拿上一轮的人当“父亲”,查出他们的直属下级
  5. ot.depth + 1每往下一层,深度加一,像族谱上盖辈分的章
  6. WHERE id = 6锚点变成“从我自己开始”,方向换成自底向上追溯
  7. ORDER BY level DESC链路查询里把辈分最高的祖先排在最前面,得到从根到我的顺序

常见误区

误区写递归CTE时总要像写 while 循环一样,手动加一个“如果满足某条件就退出”的判断,否则数据库会死循环。

正解数据库不需要你写显式终止条件。每轮迭代执行完后,如果 UNION ALL 没有产生任何新行,迭代自动结束。比如族谱里写着“无后”的人,查不出子女就自然到头了。真正要防的是环路导致每轮都在出新行,这时要加深度限制或已访问路径来剪枝。

为何常见:编程语言里的 while/for 都要自己写退出条件,人容易把这种控制流惯性迁移到 SQL 上。

误区CTE递归是围着“父子关系”转的,一旦数据出现多父节点或者环状引用,递归就没法用了。

正解递归CTE完全能处理图。有向图遍历、传递闭包、环检测都是递归CTE的典型场景,唯一多做的事情是记录已访问路径,防止环路导致无限迭代。联姻后的族谱是张网,但配上“拜访清单”照样能捋清楚。

为何常见:大多数教材用组织架构这种树形数据举例,导致初学者以为树是递归CTE的边界。

误区我可以在递归部分的 SELECT 里写 GROUP BY,顺手统计每一层有多少人,这样一条SQL全搞定。

正解递归部分的语义是“逐行向外扩展”,聚合会破坏每一轮的行身份,导致递归无法继续推进,很多数据库会直接报错或返回错误结果。正确做法是递归CTE只负责把树展开,聚合放到外层 SELECT 里做——先把全家人都叫到祠堂,再点人数。

为何常见:写惯了普通 SQL 的人总想在一个 SELECT 里完成所有事,忽略了递归部分特殊的“逐行追加”语义。

误区递归CTE写法优雅,不管数据多浅多深,一律用递归就对了。

正解当树的深度只有两三层时,几次自连接或应用层循环的开销远小于递归CTE的迭代管理。老周查族谱,如果只查“我爹和我爷爷”,直接翻两页就行,没必要把整本谱摊开。递归CTE适合深度未知或层级较深的数据。

为何常见:学会新工具后容易“手里有锤子看什么都是钉子”,忽略了不同层级的成本差异。

误区递归CTE是 Oracle 或者 PostgreSQL 专属的,MySQL 用不了。

正解MySQL 从 8.0 开始已经完整支持 WITH RECURSIVE 语法,MariaDB 10.2+ 也支持。8.0 之前的版本确实只能用自连接或临时表模拟,但这已经是过去式。老周的谱盒在 8.0 版本之后多了一本自动翻谱的机器说明。

为何常见:很多教程和博客写作时间早于 MySQL 8.0 发布,旧资料的错误认知被一路转发到现在。

面试题

Q1. 递归CTE中“锚点”(Anchor)的作用是什么?

单选题 · easy

  • A.定义递归的终止条件
  • B.提供迭代的初始数据行← 正确答案
  • C.合并递归的中间结果
  • D.指定父子表的连接字段

答案:B。锚点是递归的起始种子,相当于族谱上那位老祖宗,提供第一轮迭代的数据。A 是终止条件管的事,C 是 UNION ALL 的职责,D 是递归 JOIN 的 ON 条件。

Q2. 连接锚点部分与递归部分的关键字是?

单选题 · easy

  • A.UNION 自动去重
  • B.UNION ALL 追加行← 正确答案
  • C.JOIN 横向拼表
  • D.LIMIT 限制行数

答案:B。递归CTE用 UNION ALL 把每轮新行简单追加到结果集。UNION 会去重且开销更大,JOIN 和 LIMIT 都不负责拼接递归轮次。每一轮就像在族谱上新增一页人名,直接贴在后面即可。

Q3. 递归CTE某一轮没有产生新行时会发生什么?

单选题 · easy

  • A.报错终止整个查询
  • B.自动结束迭代返回结果← 正确答案
  • C.无限循环直到内存耗尽
  • D.忽略本轮继续下一轮

答案:B。递归的自然终止条件就是“没有新行产生”。数据库检测到 UNION ALL 没有并入任何记录,就认为族谱翻到底了,自动返回结果。它是自然结束,不是报错,更不会无限循环。

Q4. 想在递归结果中记录每个节点的层级深度,正确做法是?

单选题 · mid

  • A.在锚点中写死一个零值
  • B.在递归部分深度加一← 正确答案
  • C.在外部查询用计数
  • D.在排序字段中计算

答案:B。锚点把根节点深度置为 0,递归部分每扩展一层就 depth + 1,把深度像族谱世数一样一路带下去。外部 COUNT 是统计次数,无法表达树中的相对层级。

Q5. 递归查询某员工的全部上级链路,正确的方向是?

单选题 · mid

  • A.从叶子向根逐级向上← 正确答案
  • B.从根向叶子逐级向下
  • C.同一层级横向展开
  • D.随机顺序遍历全表

答案:A。从目标员工出发,锚点是“我自己”,递归部分不断找 manager_id = 当前 id 的上级,一路走到没有上级的根节点为止。方向是叶子到根,和“揪着衣领问谁是你爹”是一个道理。

Q6. 图状数据中环路会让递归CTE出现什么问题?

单选题 · mid

  • A.所有边都被自动删除
  • B.迭代无法自然终止← 正确答案
  • C.查询并行度显著提高
  • D.索引优化完全失效

答案:B。环路上每一轮都能产生“新”行,UNION ALL 永远不空,迭代停不下来。就像族谱里“我爹是我儿子”的死循环。解决办法是记录已访问路径,发现回头路就剪枝。

Q7. 下列哪些是递归CTE的典型适用场景?

多选题 · mid

  • A.组织架构的层级展开← 正确答案
  • B.物料清单逐层展开← 正确答案
  • C.简单二维表的等值连接
  • D.社交关系链逐层扩展← 正确答案

答案:ABD。组织树、BOM 物料、社交关系链都是典型的层级或图状数据,适合递归CTE。C 是普通两表 JOIN 的活,硬上递归反而多此一举。

Q8. 如何防止递归CTE查询出现死循环?

多选题 · hard

  • A.在递归部分限制最大深度← 正确答案
  • B.存储已访问节点路径← 正确答案
  • C.用 UNION 替代 UNION ALL
  • D.彻底不使用递归查询

答案:AB。深度限制像族谱规定“写到三十世为止”,路径记录像“拜访过的人不再拜访”,两者都能断环。UNION 不能解决环路,反而会丢语义;D 是回避问题而非解决问题。

Q9. 哪些手段能明显提升递归CTE的查询性能?

多选题 · hard

  • A.为父键字段建立索引← 正确答案
  • B.限制递归的最大深度← 正确答案
  • C.物化每轮中间结果← 正确答案
  • D.在递归部分使用聚合

答案:ABC。父键索引让每轮 JOIN 快速定位;深度限制减少迭代轮数;物化中间结果避免重复扫描。D 是错误的:递归部分用聚合会破坏逐行扩展语义。

Q10. 递归部分返回了与锚点完全相同的行,最可能的结果是?

单选题 · extreme

  • A.UNION ALL 自动去重此数据
  • B.结果集中该行出现多次← 正确答案
  • C.数据库编译直接报错
  • D.递归提前终止本轮迭代

答案:B。UNION ALL 本身不去重,重复行会原样保留,集合中同一记录可能出现多次。这和“族谱上有两个重名的人”类似,数据库不会帮你判断它们是不是同一个。