news 2026/9/24 19:51:48

MySQL递归CTE实战:层级表上级路径查询与优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL递归CTE实战:层级表上级路径查询与优化

1. 你大概率也遇到过:层级表查“上级路径”到底难在哪

先交代一下背景。做组织架构、商品分类、权限菜单、评论回复链这类业务时,数据表十有八九是“邻接表”设计:每一行只保存一个parent_id,指向父节点。这种结构特别符合人的直觉,插入数据也不用关心顺序问题,随手一行INSERT就完事。但真正用起来就麻烦了,最典型的需求就是标题里写的“上级ID路径查询”。

举个例子,分类表category,里面有上下级依赖关系,我想知道“苹果手机”这个节点的完整上级链,也就是“手机数码 / 手机 / 智能机 / 苹果”这串路径,或者至少拿到“1, 2, 3, 5”这样一串祖先ID。这个需求看起来简单,但层级不定,有的节点 3 层就到顶了,有的节点可能挂 10 层,你没办法提前知道该JOIN几次自己。

在我接手的老项目里,遇到这种需求最常干的事就是:先把所有数据查出来扔到应用层,用 Java/Python 写个递归方法去拼父子关系。业务小的项目这么玩没问题,数据量一大,光是N+1查询就能把接口拖死。后来换了 MySQL 8.0,有了递归 CTE,这类查询终于可以在一条 SQL 里解决干净。

这篇就围绕“上级ID路径查询”这个具体场景,把 CTE 怎么用、递归怎么跑、路径怎么拼、性能怎么优化、有哪些坑,一条条掰开讲清楚。不管你之前有没有用过WITH RECURSIVE,看完都能直接拿 SQL 去改业务。

1.1 邻接表:最直观也最磨人的一张树表

先把示例表建好,后面所有 SQL 都跑在这张表上:

CREATE TABLE `category` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `parent_id` INT NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_parent_id` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里parent_id = 0表示顶级节点,不需要NULL,避免后面递归判断时还得处理空值。插入几条示例数据:

INSERT INTO `category` (id, name, parent_id) VALUES (1, '手机数码', 0), (2, '手机', 1), (3, '智能机', 2), (4, '功能机', 2), (5, '苹果', 3), (6, '华为', 3), (7, '三星', 3), (8, '平板电脑', 1), (9, '旧款功能机', 4), (10, '诺基亚', 9);

如果我现在想查id = 9(旧款功能机)的祖先链,肉眼可以数出来是:9 -> 4 -> 2 -> 1,也就是路径“手机数码 / 手机 / 功能机 / 旧款功能机”。但程序不认识“肉眼”,它需要一条能自动沿着parent_id往上爬的逻辑。

1.2 没有递归的年代,大家是怎么凑合查的

MySQL 8.0 之前没有递归 CTE,处理这种需求大概有四种土办法,每一种都各有各的难受:

第一种,固定层级 JOIN。如果业务方说“最多不会超过 4 层”,那就反复LEFT JOIN自己 4 次,把每一层的节点都查出来。这种写法最好懂,但层级一改,SQL 就得跟着改,而且一旦数据里有第 5 层,结果就直接丢了。

第二种,存储过程 + 循环。在存储过程里逐层查询,把结果塞进临时表,直到找不到父节点为止。好处是通用,坏处是代码量大、调试麻烦,而且存储过程不方便和普通业务 SQL 组合,更没法直接嵌到报表查询里。

第三种,应用层递归。把所有分类一次性查出来,在内存里用 Map 组装树。在数据量不大、分类总数就几千条的场景下这个方案很好用,但如果你想“只查某一个子树”,就不得不把全表数据都捞出来,属于杀鸡用牛刀。

第四种,设计层面换方案。比如用“路径枚举”表,每行节点直接存ancestors字段,或者用“闭包表”专门存所有父子关系。这些方案查询效率确实高,但写入时维护成本极大,增删改一个节点可能要连带更新几十上百行,对大部分中小业务来说属于过度设计。

所以递归 CTE 的价值就很清楚了:既不用改表结构,也不用写存储过程,一条 SQL 拿捏任意层级。

1.3 其实你要的只是两句话:祖先链和全路径

“上级ID路径查询”这个标题,展开来看无非两种情况:

  • 给定节点 ID,查出它所有上级节点的 ID 列表,不管顺序。
  • 给定节点 ID,从根节点到当前节点,拼出完整的 ID 路径,比如1/2/4/9

后面第三、四、五节分别来解决这两个问题,外加一个“向下查所有子孙节点”的常用变体。不过在动手写 SQL 之前,得先把WITH RECURSIVE的运行逻辑搞清楚,不然很容易写出“看上去对、跑起来错”的 SQL。

2. WITH RECURSIVE 怎么“递归”:锚点、迭代与终止

WITH RECURSIVE的语法其实特别简单,核心就是两部分:锚点成员(anchor member)和递归成员(recursive member)。中间用UNION [ALL | DISTINCT]连起来。

WITH RECURSIVE cte_name (列名1, 列名2, ...) AS ( -- 锚点成员:查询的起点,不引用 CTE 自身 SELECT ... UNION ALL -- 递归成员:引用 cte_name 自身,反复迭代 SELECT ... ) SELECT * FROM cte_name;

很多人第一次写会懵,觉得“递归”这个词太抽象。换个角度理解就顺了:锚点决定了迭代从哪里开始,递归成员决定了每一次迭代怎么从结果集里再长出下一批数据,当某次迭代没有产生任何新行时,递归自动终止。

2.1 一段最简单的代码:从数字1加到5

先看 MySQL 官方文档里最经典的数列例子:

WITH RECURSIVE cte (n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM cte WHERE n < 5 ) SELECT n FROM cte;

执行结果:

n --- 1 2 3 4 5

拆开看它的执行过程:

  1. 先执行锚点SELECT 1,结果集里有一条数据n = 1
  2. 执行递归成员SELECT n + 1 FROM cte WHERE n < 5,注意这里cte代表的是上一步刚产生的数据,而不是全部历史数据。上一步结果是n = 1,所以这一步产出n = 2,放进最终结果集。
  3. 第三步迭代拿n = 2,通过n + 1产出n = 3
  4. 一直迭代到n = 5。此时递归成员还是被执行了一次的,它拿n = 5尝试生产n = 6,但WHERE n < 5过滤掉了这一行,没有产生新数据,迭代就此停止。

这个例子请多看两遍,它是后面所有层级查询的基础。关键点在于:递归成员每次读到的cte,只是上一轮新产生的行,不包含前几轮产生的历史行。所以如果我们想一层层往上爬父节点,就得在递归成员里把当前行cte.parent_id作为条件去关联原表。

2.2 递归成员和锚点成员之间的关系

锚点和递归成员之间有个硬性要求:两边查出来的列数必须一致。如果锚点查 3 列,递归成员也必须是 3 列,顺序一一对应。否则 MySQL 直接报错,错误号大概是ERROR 3504,实际上就是列数量对不上。

列名不用在锚点里指定类型,但递归成员里的字段类型要和锚点兼容。比如锚点里CAST(id AS CHAR(500))转成了字符串,递归成员里CONCAT(...)拼接出来的也是字符串,这样才能兼容。

还有一点容易被忽略:递归成员不能使用聚合函数、窗口函数,包括GROUP BYDISTINCT这类操作。MySQL 官方文档限制得很明确。如果有这种需要,必须把递归 CTE 当成一个“临时结果源”,在外层再GROUP BY

2.3 UNION ALL 还是 UNION DISTINCT

写递归 CTE 时,UNIONUNION ALL都能用。UNION默认带DISTINCT,会对最终结果去重;UNION ALL不去重。在层级树查询场景里,我建议直接用UNION ALL

原因有二。第一,树形结构只要数据本身没有环,从同一个节点出发沿着parent_id往上爬,路径是唯一的,不存在重复行。去重没意义,纯属白白增加计算成本。第二,在某些数据量大的场景,UNION DISTINCT会在每一轮迭代都做排序去重,性能明显变差。

当然,如果表里存在脏数据、循环引用,UNION DISTINCT有时能靠去重“糊弄”过去,但这属于掩盖问题,后面专门讲循环引用这个坑。

3. 自底向上查询:给出任意节点,找到它所有上级

现在进入正题,标题里的“上级ID路径查询”,核心 SQL 长这样:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, 1 AS lvl FROM category WHERE id = 9 UNION ALL SELECT c.id, c.parent_id, c.name, cte.lvl + 1 FROM category c INNER JOIN cte ON c.id = cte.parent_id ) SELECT id, parent_id, name, lvl FROM cte ORDER BY lvl;

执行结果:

id parent_id name lvl 9 4 旧款功能机 1 4 2 功能机 2 2 1 手机 3 1 0 手机数码 4

3.1 获取全部祖先ID(核心SQL)

如果你只需要 ID 列表,那SELECT里只取id就行。这条 SQL 里的INNER JOIN cte ON c.id = cte.parent_id是灵魂,它做的操作是:拿当前已找到的节点ID,去原表里找谁是它的父节点。

锚点WHERE id = 9把起点定为“旧款功能机”,它先进入结果集。然后递归成员拿着9这个值去关联原表category,找到c.id = 9这一行,读出它的parent_id = 4,于是4进入结果集。下一轮,拿着结果集里的行(id = 4)再关联一次,找到id = 4parent_id = 2,把2加进结果集。等拿到id = 1时它的parent_id = 0,原表里没有id = 0的行,INNER JOIN匹配不到,递归终止。

这里有个初学者容易搞反的细节:向上查是c.id = cte.parent_id,向下查是c.parent_id = cte.id,两者正好相反。如果写反了,查id = 9的上级,结果会变成查它的所有子孙,一脸懵。别问我是怎么知道的。

3.2 带名称、带层级深度,不迷路

上面 SQL 里加了lvl字段,表示“当前节点距起点隔了几层”。这个字段非常实用,尤其在展示树形表格时,能直接用来做缩进。比如前端拿到结果后,按lvl乘以固定像素做缩进,就是一个天然的多级列表。

如果你还想在结果里顺便带上“每次迭代的源节点 ID”,可以用一个start_id字段保留最初起点:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, id AS start_id, 1 AS lvl FROM category WHERE id = 9 UNION ALL SELECT c.id, c.parent_id, c.name, cte.start_id, cte.lvl + 1 FROM category c INNER JOIN cte ON c.id = cte.parent_id ) SELECT start_id, id, parent_id, name, lvl FROM cte ORDER BY lvl;

当你需要在一个 CTE 里同时查多个节点的祖先链时(比如传入一批 ID),锚点直接改成WHERE id IN (5, 6, 9),这个start_id就能帮你区分每一条链分别属于谁。

3.3 直接生成逗号分隔的完整路径

很多时候我们不光要“有哪些祖先”,还要“祖先按从根到叶的顺序连起来的一串”。有两种写法都可以做到。

写法一:递归过程中用CONCAT累积路径。起点是id = 9,先让它自己的路径是'9';往上递归找到id = 4时,路径变成'4,9';再往上找到id = 2时变成'2,4,9';最后到根变成'1,2,4,9'。SQL 如下:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, CAST(id AS CHAR(500)) AS id_path, CAST(name AS CHAR(500)) AS name_path FROM category WHERE id = 9 UNION ALL SELECT c.id, c.parent_id, c.name, CONCAT(c.id, ',', cte.id_path), CONCAT(c.name, '/', cte.name_path) FROM category c INNER JOIN cte ON c.id = cte.parent_id ) SELECT id, id_path, name_path FROM cte ORDER BY LENGTH(id_path) - LENGTH(REPLACE(id_path, ',', '')) DESC;

结果里层次最深的那一行就是完整路径:

id id_path name_path 9 9 旧款功能机 4 4,9 功能机/旧款功能机 2 2,4,9 手机/功能机/旧款功能机 1 1,2,4,9 手机数码/手机/功能机/旧款功能机

注意锚点里我写了CAST(id AS CHAR(500)),这一步不能省。如果不转类型,递归成员里CONCAT(c.id, ',', cte.id_path)算出来的可能是其他类型,后面迭代再拼接时容易出问题。转成字符串既保证类型一致,也避免隐式转换导致索引失效。

写法二:递归出祖先集合后,用GROUP_CONCAT聚合。个人更推荐这种,因为它思路更简单,不需要维护一个越来越长的字符串:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, 1 AS lvl FROM category WHERE id = 9 UNION ALL SELECT c.id, c.parent_id, c.name, cte.lvl + 1 FROM category c INNER JOIN cte ON c.id = cte.parent_id ) SELECT GROUP_CONCAT(id ORDER BY lvl DESC) AS ancestor_ids, GROUP_CONCAT(name ORDER BY lvl DESC SEPARATOR '/') AS ancestor_names FROM cte;

查询结果:

ancestor_ids ancestor_names 1,2,4,9 手机数码/手机/功能机/旧款功能机

这段 SQL 的思路是先把祖先链递归出来,每个节点都带一个lvl层级号,然后用GROUP_CONCAT ... ORDER BY lvl DESC把层级最浅的根节点排在最前面,正好形成“从根到当前节点”的路径。

GROUP_CONCAT有一个隐藏限制:它默认最大长度只有 1024 字节,如果树的层级深、路径长,会被静默截断。解决方法是先调大会话变量:

SET SESSION group_concat_max_len = 1000000;

建议只要是正式报表查询,都先执行这一句,避免线上出现“路径怎么少了一截”的诡异问题。

3.4 这个方向最容易踩的坑

有一种错误写法很有迷惑性:递归成员里用WHERE cte.parent_id = c.id,或者WHERE c.parent_id = cte.id,乍一看好像也在“找上级”,实际上查出来的全是子节点。判断方向对不对,最好的方法是拿一层数据手推一遍。拿id = 9为例,如果你发现结果里出现了id = 10,那肯定方向反了,因为109的子节点,而不是父节点。

另一个坑是锚点里忘了加WHERE条件,直接把全表所有节点都当起点。你以为自己写的“递归”,其实变成了“每一棵树都从上往下跑一遍”,结果集直接爆炸,轻则数量翻倍,重则卡死。

4. 自顶向下查询:给出任意节点,展开它的整棵子树

既然是层级表,除了“查上级路径”,还有一个同等的刚需:查某个节点下面挂了哪些子孙节点。虽然它不完全等于标题里的“上级ID路径查询”,但它是递归 CTE 最常见的另一半用法,而且原理互通,顺手写清楚。

思想就是把第三节的 JOIN 条件反过来——拿当前节点的id去匹配原表里的parent_id

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, 1 AS lvl FROM category WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name, cte.lvl + 1 FROM category c INNER JOIN cte ON c.parent_id = cte.id ) SELECT id, parent_id, name, lvl FROM cte ORDER BY lvl, id;

查询结果(节选):

id parent_id name lvl 1 0 手机数码 1 2 1 手机 2 8 1 平板电脑 2 3 2 智能机 3 4 2 功能机 3 5 3 苹果 4 6 3 华为 4 7 3 三星 4 9 4 旧款功能机 4 10 9 诺基亚 5

4.1 获取全部子孙节点及其层级

这个结果集就是一个扁平化的“整棵子树”,lvl字段从 1 开始往下递增。实际项目中,我会把这个结果交给前端组件渲染成目录树,因为分层信息已经完整,前端不用再做任何递归计算。

如果只想要某个指定层级以下的数据,比如只要两层,外层加WHERE lvl <= 2就行。想要每个父节点下面直接挂了多少子节点,可以配合GROUP BY统计各层节点数:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, 1 AS lvl FROM category WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name, cte.lvl + 1 FROM category c INNER JOIN cte ON c.parent_id = cte.id ) SELECT lvl, COUNT(*) AS node_count FROM cte GROUP BY lvl ORDER BY lvl;

结果:

lvl node_count 1 1 2 2 3 3 4 3 5 1

这种“按层级统计节点数”的查询在分析分类结构是否合理时特别有用,比如突然发现某一层挂了上百个节点,说明分类设计可能有问题。

4.2 统计每个分支的叶子/总数

如果想查“某个分支下一共有多少个叶子节点”,也就是没有子节点的节点,可以先递归出整棵子树,再用NOT EXISTS过滤出“在原表中不存在任何 parent_id = 自身 id 的节点”:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name, 1 AS lvl FROM category WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name, cte.lvl + 1 FROM category c INNER JOIN cte ON c.parent_id = cte.id ) SELECT COUNT(*) AS leaf_count FROM cte WHERE NOT EXISTS ( SELECT 1 FROM category sub WHERE sub.parent_id = cte.id );

在这个示例里,叶子节点是 5、6、7、8、10 这 5 个。这个统计对权限模块特别常见:比如“某个角色组下到底绑定了多少个最终权限点”。

4.3 常见错误“方向反了”

自顶向下查询最常出的错误,就是把递归成员里面的 JOIN 条件写成c.id = cte.parent_id。一旦写反,你查id = 1的子孙,结果会一路向上找id = 1的父节点,然后返回来一堆无关数据,甚至因为parent_id = 0导致结果集直接为空。

记住一句口诀:向上查父,JOIN 的连接键是c.id = cte.parent_id;向下查子,JOIN 的连接键是c.parent_id = cte.id。判断方法永远只有一个——拿一行数据手推一遍。

5. 性能、深度限制和“老方案”对比

光会写 SQL 不算真会用,CTE 递归查询在生产环境会遇到三个绕不开的问题:深度限制、死循环、性能。

5.1 cte_max_recursion_depth 与死循环防护

MySQL 对递归深度有一个默认上限:cte_max_recursion_depth,默认值是 1000。也就是说递归迭代超过 1000 轮,MySQL 直接报错:

ERROR 3636 (HY000): Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth to a larger value.

这个限制本质是保护机制,防止递归失控把数据库拖垮。但如果你确实有超深层级(比如某个分类套了 1500 层),可以临时调大:

SET SESSION cte_max_recursion_depth = 10000;

更稳妥的做法是在配置文件的[mysqld]段永久调整:

cte_max_recursion_depth = 10000

这里要强调一句:调参不是解决问题的根本办法,遇到超限先怀疑数据是不是有环。树形结构的数据最怕脏数据,比如两条记录互相把对方设为父节点,形成 A→B→A 的环。一旦有环,递归就会无限迭代,直到撞上深度上限。如果没有上限保护,服务直接卡死。

怎么查有没有环?可以用一条普通 SQL 自查:

SELECT a.id, b.id FROM category a INNER JOIN category b ON a.parent_id = b.id AND b.parent_id = a.id;

这种互指数据在业务上通常是垃圾数据,建议在应用层写入时增加校验,或者在数据定期清洗任务里跑一遍上面的 SQL 找出来。

5.2 索引建议:parent_id 上有没有索引差别巨大

递归 CTE 性能好不好,很大程度上取决于parent_id上有没有索引。递归的本质就是反复通过parent_id查原表,每一轮迭代都是一次INNER JOIN。如果parent_id没有索引,每次迭代都是全表扫描,数据量一大,一次查询可能要扫好几遍全表,时间直接指数级上涨。

所以在建表时就应该加索引:

ALTER TABLE category ADD KEY idx_parent_id (parent_id);

主键id有主键索引,不用管。有了这个索引,递归查询每一轮相当于走一次ref连接,速度快得多。

另外一个排查性能问题的技巧是使用EXPLAIN ANALYZE(MySQL 8.0.18+ 支持),它可以真实执行语句并输出每一轮迭代的开销信息:

EXPLAIN ANALYZE WITH RECURSIVE cte AS ( SELECT id, parent_id, name, 1 AS lvl FROM category WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name, cte.lvl + 1 FROM category c INNER JOIN cte ON c.parent_id = cte.id ) SELECT * FROM cte;

输出里会有类似actual time=0.05..0.2 rows=10 loops=7的信息,loops就是迭代轮数。如果发现loops特别大,但查询结果行数又不多,就要警惕:是不是路径上存在重复引用?是不是parent_id索引没走?

5.3 对比表:存储过程、多次JOIN、程序递归、闭包表

整理一张表,方便你评估什么时候该用 CTE:

方案层级不固定查询代码量维护成本可嵌入普通SQL适用场景
递归 CTE支持可以中小数据量、业务变动频繁的树查询
固定层级 JOIN不支持可以层级确定的极简场景
存储过程 + 临时表支持不行老系统历史包袱
应用层递归支持不行全表数据量小、需要完整树
闭包表支持高(写入复杂)可以读多写极少、层级很深的场景

从这张表能看出,CTE 不是万能的,但对绝大多数业务来说,它是在代码量和灵活性之间最平衡的方案。如果你遇到的是“写极其频繁,但查询很少”的团队,闭包表会更合适;如果数据量超过百万节点,递归 CTE 每轮迭代都要走索引访问,性能可能会吃紧,这时建议评估一下闭包表或者路径枚举方案。

6. 实战笔记:结合业务数据处理的一些体会

这一节写一些实际项目中的心得体会,属于那种不亲自跑一遍很难从文档里学到的经验。

6.1 从CTE结果到前端树组件的完整链路

很多人以为拿到递归结果就算完事,结果前端拿着一个扁平列表不会渲染。实际做法是:在后端用 CTE 查出带lvlparent_id的扁平列表,然后应用程序里用一次循环组装成树结构,再返回给前端。组装树的逻辑很简单:

  1. 建立一个map[id -> node]
  2. 遍历列表,把每个节点挂到map[node.parent_id].children下面。
  3. 找不到父节点的,说明是顶级节点,作为根列表返回。

这样做的好处是数据库只查一次,应用层最多做一次O(n)的循环,前端拿到直接递归渲染,整个链路性能稳定。这里有个小技巧:CTE 负责查出“哪些节点属于这棵树”,应用层负责“谁是谁的爹”。两者职责分离,比在 SQL 里硬拼路径字符串更清晰。

6.2 循环引用(脏数据)导致递归爆炸的排查

曾经在线上遇到过一个诡异问题:一个看起来没多少数据的分组表,递归查询竟然执行了几十秒。后来一查,发现有一条数据的parent_id指回了自己,等于说这个节点是自己的爹。递归没有任何出口条件,一路疯狂迭代,直到撞上cte_max_recursion_depth上限报错。

排查方法其实就一句话:凡是递归查询的表,必须有数据完整性保护。最稳妥的办法是在应用层禁止parent_id等于自身 ID,禁止成环引用。如果历史原因已经产生了脏数据,可以在查询前先跑诊断 SQL:

-- 查找自引用 SELECT * FROM category WHERE id = parent_id; -- 查找互指(两层环) SELECT a.id, a.name, a.parent_id, b.id AS parent_of_a FROM category a INNER JOIN category b ON a.parent_id = b.id AND b.parent_id = a.id;

还有更复杂的多节点环,比如 A→B→C→A,这种就需要用递归 CTE 再去查一遍“路径中重复出现的节点”。不过到这一步基本属于极少数情况,最实际的预防措施还是在写入入口做校验。

6.3 面试与面试题视角:这道题到底在考什么

“MySQL 怎么查询树形结构的所有上级/所有下级”算是一道高频面试题,尤其是问到 MySQL 8.0 新特性的时候。面试官真正想考察的点其实有三个:

第一,你知不知道 MySQL 8.0 引入了递归 CTE。在 8.0 之前只能用存储过程或应用层递归,而 8.0 开始有标准写法。

第二,你了不了解 CTE 的执行机制。能说清楚锚点成员和递归成员的区别,能说清楚“每一轮迭代拿到的只是上一轮新生成的集合,而不是全部结果集”。

第三,你会不会处理递归的终止条件和死循环风险。比如为什么INNER JOIN能天然终止递归,为什么parent_id = 0不会导致无限循环,以及深度上限参数怎么设置。

如果一个候选人能答到第三层,基本可以确定他真的在项目里用过递归 CTE,而不是背了几道面试题。反过来,如果你正在准备面试,这篇文章里的第三、四节内容已经足够应对所有常规追问,剩下的就是亲手在本地 MySQL 上跑一遍,把执行结果看熟。

最后分享一个我个人的使用习惯:遇到层级查询需求,我不会上来就写递归 CTE,而是先问三个问题——这棵树会不会乱改?数据量大概多少?查询链路里谁会用到结果?如果数据量小、结构稳定,有时候一次全量查询配应用层组装就足够了;但如果是“任意节点找祖先链”这种按点查询,递归 CTE 一定是第一选择。灵活性、可读性、扩展性都更好,而且它用的是标准 SQL 语法,未来换到 PostgreSQL、SQL Server,这套写法依然通用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 19:51:13

现场安全检查流程图PPT制作:目视化设计与闭环管理全拆解

前阵子帮一家制造企业的朋友做现场安全检查的流程图PPT&#xff0c;做到一半我发现&#xff0c;这活儿的难点根本不在PPT操作&#xff0c;而在于怎么把“现场安全检查”这件事想清楚、讲明白。很多企业手里有检查制度、有整改台账&#xff0c;但你要他把整个检查流程画成一张图…

作者头像 李华
网站建设 2026/9/24 19:51:08

SQL+AI双驱动:从建表语句到ER图的高效生成实战

课设和毕设做到数据库设计这一环&#xff0c;很多人的感受是一样的&#xff1a;需求分析勉强能写&#xff0c;ER图却画得头疼。手绘吧&#xff0c;关系一多就乱&#xff1b;用建模工具吧&#xff0c;安装配置比画图还费劲&#xff1b;好不容易画完&#xff0c;老师又说“ER图里…

作者头像 李华
网站建设 2026/9/24 19:50:53

C#调用ffmpeg image2pipe实现USB摄像头本地预览与RTMP推流

简介&#xff1a;面向需要同时完成USB摄像头本地预览与网络推流的C#开发者&#xff0c;该资料基于ffmpeg的image2pipe参数&#xff0c;给出突破单应用独占摄像头限制的完整实现思路与工程demo。压缩包共65个文件&#xff0c;含7个C#源码工程文件、2个exe可直接运行体验&#xf…

作者头像 李华
网站建设 2026/9/24 19:50:07

桌面运维面试题深度拆解:从故障排查到答题加分技巧

简介&#xff1a;桌面运维面试题参考答案PDF&#xff0c;面向企业IT支持与网络运维岗位的求职者、转岗人员及初级工程师&#xff0c;用于在有限时间内集中梳理高频考点和应答思路。内容以问答形式展开&#xff0c;涵盖网络故障定位经典分层排查、DNS从Hosts到根域名服务器的完整…

作者头像 李华
网站建设 2026/9/24 19:49:31

VC++运行库缺失全解决:从DLL报错到一键安装全家桶

1. 为什么你的电脑总在缺运行库&#xff1a;先从一次真实的报错说起有一次帮同事装一个工业仿真软件&#xff0c;双击安装包一切正常&#xff0c;结果软件装好一启动直接弹窗&#xff1a;“无法启动此程序&#xff0c;因为计算机中丢失 MSVCP140.dll。尝试重新安装该程序以解决…

作者头像 李华
网站建设 2026/9/24 19:49:25

MySQL与MongoDB选型、安装、操作及数据导入实战指南

数据库存储这件事&#xff0c;说大不大&#xff0c;说小不小。我做了这么多年后端和数据处理&#xff0c;MySQL和MongoDB是我用得最频繁、也最常被问到的一对组合。前者是关系型数据库的绝对主力&#xff0c;后者是文档型数据库里最流行的一个&#xff0c;很多刚接触数据库的同…

作者头像 李华