☰
MySQL CTE实战:告别嵌套子查询,让SQL清爽可读
2026/10/8 9:03:08 网站建设 项目流程

如果你写SQL的时候还被三层嵌套子查询逼疯过,那这篇值得看完。我入行做数据分析和后端开发那几年,最怕接手那种 SELECT 里套 SELECT、WHERE 里再塞 EXISTS 的"千层饼"SQL。后来 MySQL 8.0 终于引入了 CTE(Common Table Expression),我才算真正感受到什么叫"SQL 清爽神器"。这篇我会从最基本的语法讲起,但重点放在真实业务场景、性能真相和踩坑记录上,目标很直接:让你看完就能把以前那种绕来绕去的嵌套子查询改成一眼能读懂的 CTE,而且知道什么时候该用、什么时候别硬用。

1. 嵌套子查询的三大痛点:为什么你读SQL会头疼

说实话,我刚开始干数据分析那会儿,最怕的就是从前辈手里接手带着四五层括号的SQL。那玩意儿第一眼看过去确实能跑,但你想改个指标、加一个过滤条件,整个人就像拆炸弹一样。你可能觉得我在夸张,但真正经历过线上报表数据对不上、凌晨还在扒拉一条三层子查询的人,一定懂我说的滋味。

1.1 可读性:越套越深,越看越懵

嵌套子查询的本质是把一个查询的结果当作另一个查询的输入。这本身是很好的逻辑抽象,但问题在于SQL的书写顺序和阅读顺序不是一回事。人在读SQL时习惯从上往下、从左往右,但子查询往往是"从里往外"读。三层嵌套的时候,你脑子里要维护三张虚拟表,还要记住每一层引用的是哪张表、哪个别名。我见过最夸张的一次,一个报表SQL嵌套了六层,中间还混着 EXISTS、IN、NOT IN,当时整个团队没人愿意动它,因为动一处错三处。

这里有一个容易被低估的点:缩进。很多写嵌套子查询的人不缩进,括号一多,连哪里开始哪里结束都分不清。MySQL 又没有像 Python 那样强制的缩进语法,所以等读到第30行时,你根本不知道这个右括号到底是哪个子查询的。我后来给团队定的规矩是:超过两层嵌套必须拆开写,要么用临时表,要么用视图,要么直接上 CTE。

1.2 调试:中间结果取不出来

嵌套子查询最大的硬伤是你没法单独验证中间结果。比如我要先算出每个部门的平均薪资,再找出平均薪资超过公司整体均线的部门。如果直接写一句话嵌套查询,我想看看"第二步之前的中间结果"怎么办?只能把子查询单独拆出来,复制到另一个查询编辑窗口,跑完确认无误,再塞回去。这个过程极度单调,而且拆的时候还容易漏条件、写错别名。

调试对于一个复杂的SQL太重要了。我相信很多人和我一样,面对一个结果不对的查询,第一反应不是猜,而是想"先看看中间数据长什么样"。嵌套子查询在这个需求面前基本是无能为力的,要么拆,要么干脆相信运气。运气不好时,你会陷入"改一行、跑一次、结果还是不对"的死循环,最后发现是子查询里某个字段类型隐式转换导致索引失效,这种消耗是最让人崩溃的。

1.3 复用:同样的逻辑只能复制粘贴

一个真实场景:报表里要计算"本月新增客户"这个指标,你在一处写了带有 DISTINCT 和聚合的子查询。过两天另一个报表也要这个口径,你没有别的办法,只能把这段子查询再复制一份。复制粘贴本身问题不大,可怕的是口径一改,你要改的地方可能不止两处,而是五处八处。漏改一处,数据对不上,又得花半天排查是不是缓存问题。

复用问题还有另一个变种:同一段SQL里,同一个子查询可能要用两次。比如既要在 WHERE 里用它过滤,又要在 SELECT 里用它做对比,那就真的只能重复两遍。代码长度翻倍,而且两遍写得不一致的风险也在翻倍。这种时候,你要的不是"能不能跑",而是"能不能干净地跑"。

2. CTE基础:一句话理解,一个骨架学会

CTE 的全称是 Common Table Expression,中文一般叫公共表表达式。它做的事情其实特别简单:把一个查询先起个名字,存在查询的"最前面",然后再去引用。用生活化的类比,它就像你做饭之前先把葱姜蒜切好,放在小碗里,后面炒菜的时候随时取用,而不是每次炒到一半再跑去切菜。理解这一点,你就已经理解了 CTE 百分之七十的价值。

2.1 什么是CTE,和派生表有什么区别

很多人第一次看 CTE 都觉得这玩意儿不就是派生表(Derived Table)吗?也就是 FROM 后面的括号子查询。确实在功能上有重叠,但使用体验差别很大。派生表必须嵌在 FROM 子句里,所以你不能先声明一个派生表再在 WHERE 或者 SELECT 里引用它;CTE 则是用 WITH 在语句最前面声明,后面的整条语句都能用,位置灵活得多。

还有一个很重要的差异:CTE 可以自我引用,也就是递归。这是派生表永远做不到的。递归在遍历树形结构、生成序列、展开层级数据时是刚需,后面我会展开讲。简单来说,派生表是一次性工具,CTE 是名正言顺的复用单元。

MySQL 是从 8.0 开始正式支持 CTE 的,8.0.1 之后语法就稳定了。如果你还在用 5.7,抱歉,CTE 用不了,那只能继续忍受子查询或者想别的办法。现在8.0已经是主流,该升级就升级。如果你负责老项目迁移,这也可以作为升级 MySQL 的重要理由之一。

2.2 基本语法与多个CTE

一个最简单的 CTE 长这样:

WITH avg_salary_cte AS ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ) SELECT * FROM avg_salary_cte;

注意几个细节。第一,WITH 后面跟的是 CTE 的名字,名字之后用 AS,括号里是完整的查询。第二,主查询可以放在 SELECT、INSERT、UPDATE、DELETE 前面,但最终结果还是由最外层语句决定。第三,如果你有多个 CTE,可以用逗号隔开,后面一个可以引用前面一个。这种"流水线"式的写法才是 CTE 的精髓:

WITH dept_avg AS ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ), high_dept AS ( SELECT dept_id FROM dept_avg WHERE avg_salary > 8000 ) SELECT e.emp_id, e.emp_name FROM employee e WHERE e.dept_id IN (SELECT dept_id FROM high_dept);

这种"后面的CTE引用前面的CTE"的写法,本质上就是在表达一个数据加工流水线。第一步算什么,第二步基于第一步继续算什么,每一步都有名字、有边界,读起来跟读一个流程文档差不多。我经常给团队打比方:这就像做菜谱,第1步切菜,第2步炒菜,第3步装盘,每一步都写清楚,而不是把所有动作都塞进一个括号里。

2.3 什么时候应该用CTE

我的个人标准是下面几条,满足任意一条就把子查询改成 CTE:

  • 子查询数量超过一个;
  • 嵌套层级超过两层;
  • 同一个中间结果要被引用两次以上;
  • 查询里需要递归遍历;
  • 你希望别人(包括三个月后的自己)能快速看懂这段SQL。

反过来,如果一个非常简单的标量子查询就能搞定,比如 WHERE salary > (SELECT AVG(salary) FROM employee),那也没必要强行套个 CTE。过度封装同样是可读性杀手,适度就好。我对"适度"的理解是:CTE 把复杂变简单,而不是把简单变复杂。如果一个查询十行以内能说明白,直接写就好,不需要为了用 CTE 而用 CTE。

3. 从嵌套子查询到CTE:一个改写案例看懂全部

前面讲理论可能还不够直观,我们直接来一个实际改造案例。我尽量模拟日常报表中会真实出现的需求,而不是教科书里的清新例子。

3.1 场景背景与原始SQL

需求是:找出"部门平均薪资高于全公司平均薪资的部门里,薪资最高的人是谁"。这个需求脱胎于绩效分析,实际工作中很常见。先看嵌套子查询版本:

SELECT e.emp_id, e.emp_name, e.dept_id, e.salary FROM employee e WHERE e.dept_id IN ( SELECT d.dept_id FROM ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ) d WHERE d.avg_salary > ( SELECT AVG(salary) FROM employee ) ) ORDER BY e.salary DESC LIMIT 1;

这段 SQL 只有三层嵌套,但你已经能感受到问题了:第一个子查询在算"部门平均薪资",第二个标量子查询在算"全公司平均薪资",最外层又负责找薪资最高的人。如果你字段一多、过滤条件再复杂一点,这玩意儿基本没法维护。

3.2 改写为CTE的完整过程

改成 CTE 之后,我们把中间步骤拆开。先说清楚:这个版本的改写重点在"拆层",至于"组内排名"的语义细节我下一章专门讲,这里先看框架。

WITH company_avg AS ( SELECT AVG(salary) AS avg_salary FROM employee ), dept_avg AS ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ), target_dept AS ( SELECT d.dept_id FROM dept_avg d CROSS JOIN company_avg c WHERE d.avg_salary > c.avg_salary ) SELECT e.emp_id, e.emp_name, e.dept_id, e.salary FROM employee e INNER JOIN target_dept t ON e.dept_id = t.dept_id ORDER BY e.salary DESC LIMIT 1;

这一步一步拆下来,逻辑清楚多了:先算公司平均线,再算部门平均线,再筛出高于公司平均线的部门,最后拿员工表和目标部门做 JOIN。每一步 CTE 的职责都很单一,一旦结果不对,你可以单独运行其中任何一段去检查。这就像查一个复杂 bug,你不需要盯着整个报错堆栈看,而是把每一层函数单独测一遍,很快就能定位问题。

这个版本还有个隐藏的进阶点:company_avg 只写了一次,但它出现在整个查询的开头,后面如果还要拿它和其他部门维度做对比,随时可以再用。你不再需要为了算平均值把同一段 AVG 语句复制三遍。

3.3 解读:为什么CTE版本更爽

说实话,光从性能看,这两个版本在 MySQL 优化器眼里很可能最终执行计划差别不大,因为优化器会做子查询展开。真正拉开差距的是人力成本。CTE 版本有几个实打实的好处:

第一,每一步都可以单独验证。我经常在 Navicat 里先把第一个 WITH 单独选中跑一遍,确认 avg_salary 对不对,再把第二个跑一遍。嵌套子查询做不到这件事,你只能小心翼翼地复制。

第二,条件逻辑从"括号嵌套关系"变成了"命名引用关系"。人的工作记忆是有限的,大概能同时记住七加减二件事。三个 CTE 名字调用彼此,比三层括号叠加轻松太多。

第三,改需求的时候命中了"单一改动点"。比如公司平均线的计算口径变了,从"所有员工"改成"在职员工",你只需要改 company_avg 这个 CTE 里的 WHERE 条件,其他部分一点不用动。如果是在嵌套子查询里改,你得先定位到第几行括号里的第几个小括号,这个定位本身就是事故高发区。

4. CTE进阶实战:树形递归、分组TopN、滚动汇总

如果 CTE 只是让代码好看一点,那还不足以被称为"神器"。真正让 CTE 不可替代的是递归能力,以及和窗口函数组合后的表现力。这一章我们从实际需求出发,把三种高频场景都过一遍。

4.1 递归CTE遍历组织架构

先看最常见的递归场景:组织架构树。假设 employee 表里有 manager_id 表示直属领导,现在要拿到从 CEO 开始往下每一层的完整路径。

WITH RECURSIVE org_tree AS ( SELECT emp_id, emp_name, manager_id, 1 AS depth FROM employee WHERE manager_id IS NULL UNION ALL SELECT e.emp_id, e.emp_name, e.manager_id, ot.depth + 1 FROM employee e INNER JOIN org_tree ot ON e.manager_id = ot.emp_id ) SELECT emp_id, emp_name, depth FROM org_tree ORDER BY depth;

递归 CTE 的语法分三块:初始查询(锚点)、递归查询、终止条件。锚点负责找到树根;递归部分通过 JOIN 自己来一层层往下走;终止条件由两个机制保证,一是递归查询的结果集不断增长但路径总会在某个点走到尽头,二是超出深度限制会报错。

需要注意的关键点是 UNION ALL 和 UNION DISTINCT 的选择。树形结构不会出现重复路径,用 UNION ALL 性能更好;但如果你的关系数据有环,比如 A 的上级是 B,B 的上级又是 A,那就必须做好环路检测,否则会陷入死循环直到触发递归深度上限。我实际做组织架构树的时候还会在递归里加一个 depth 字段,方便前端渲染成缩进结构。你也可以在递归里拼接 path 字段,比如 CONCAT(ot.path, ' -> ', e.emp_name),这样每个节点能直接看到自己的完整汇报链。

4.2 分组TopN:窗口函数加CTE组合

接着 3.2 遗留的问题:如果我要找"部门平均薪资高于公司平均线的每个部门中,薪资最高的前两名",单靠 CTE 拆分层级还不够,因为 LIMIT 2 只能作用于最终结果集,没法按部门分组取 TopN。解决办法是 CTE 加窗口函数:

WITH target_dept AS ( SELECT dept_id FROM employee GROUP BY dept_id HAVING AVG(salary) > ( SELECT AVG(salary) FROM employee ) ), ranked AS ( SELECT e.emp_id, e.emp_name, e.dept_id, e.salary, ROW_NUMBER() OVER (PARTITION BY e.dept_id ORDER BY e.salary DESC) AS rn FROM employee e INNER JOIN target_dept t ON e.dept_id = t.dept_id ) SELECT emp_id, emp_name, dept_id, salary FROM ranked WHERE rn <= 2;

这段已经把我要的"每个部门各自的前两名,并且这些部门还得满足平均薪资高于公司平均线"完全表达清楚了。先 HAVING 筛部门,再 ROW_NUMBER 在部门内排名,最后 WHERE rn <= 2 取前两。没有一层多余的嵌套,每一步都是独立的命名块。

窗口函数里的 PARTITION BY 和 ORDER BY 是关键。PARTITION BY 指定分组维度,ORDER BY 指定排序规则,ROW_NUMBER() 在这个窗口内从 1 开始递增。如果你希望并列的名次也占上榜名额,可以换成 RANK() 或 DENSE_RANK(),这是我踩过坑的地方:排行榜需求里"相同成绩并列"和"相同成绩顺延"是两种完全不同的口径。RANK() 会在并列后跳号,比如 1、1、3;DENSE_RANK() 不跳号,是 1、1、2。决定用哪个函数,必须和业务方确认清楚。

4.3 累计汇总与按月滚动统计

做经营分析时,另一个高频能力是滚动累计。比如按月份统计销售额,再算一个截至当月的历史累计值。CTE 可以先按月聚合,再在外面用窗口函数做累计:

WITH monthly AS ( SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) AS total_amount FROM orders WHERE order_date >= '2024-01-01' GROUP BY DATE_FORMAT(order_date, '%Y-%m') ) SELECT month, total_amount, SUM(total_amount) OVER (ORDER BY month) AS running_total FROM monthly ORDER BY month;

这里的关键是 SUM(...) OVER (ORDER BY month)。在没有 GROUP BY 的窗口语境下,它会在所有行之间做累计,ORDER BY 决定累加顺序。CTE 的作用是先把原始的明细订单流转成月度汇总流,让外层的窗口函数专注做"累计"这一件事。

你甚至可以用 CTE 构建一个完整的日期序列,然后用 LEFT JOIN 把缺失月份补零。生成日期序列本身就用到递归 CTE:

WITH RECURSIVE all_months AS ( SELECT DATE('2024-01-01') AS month_start UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM all_months WHERE month_start < DATE('2024-12-01') ) SELECT month_start FROM all_months;

这个技巧在没有日历表的环境里尤为好用。以前要先生成一张临时数字表,现在一个递归 CTE 就搞定,而且写在同一段 SQL 里,语义非常清晰。把日期主表和业务数据 LEFT JOIN,再配合 COALESCE 补齐缺失月份的值,一份连续的月度趋势图就出来了。

4.4 数据去重检查的实际案例

最后说一个我非常常用的场景:按某个业务键查重。登录日志表里同一个用户同一天可能因为各种原因产生多条记录,要去重只保留最新一条。先查出重复记录:

WITH ranked AS ( SELECT id, user_id, login_time, ROW_NUMBER() OVER ( PARTITION BY user_id, DATE(login_time) ORDER BY login_time DESC ) AS rn FROM user_login_log ) SELECT id, user_id, login_time FROM ranked WHERE rn > 1;

当你想删除这些重复记录时,稳妥做法是先用上面的 SELECT 确认影响范围,再执行 DELETE。我个人的经验是不要贸然直接在 DELETE 里写窗口函数,虽然 MySQL 8.0 语法上允许 WITH 和 DELETE 配合,但版本不同、行为细节有差异,线上谨慎为上。最稳妥的做法是把重复 id 集合捞出来,再配合条件删除。CTE 的价值同样体现在这里:你可以先在 SELECT 分支里反复验证 CTE 的逻辑,确认无误后再把整套 SQL 挪到 DELETE 场景,排查成本低很多。

5. CTE性能真相与常见坑:别把语法糖当银弹

看到这里,很多朋友会自然产生一个问题:CTE 这么好用,性能会不会有问题?我的回答是:CTE 并不是银弹,它主要解决的是"人读不懂"的问题,不是"数据库跑得慢"的问题。这一章我们聊聊你必须知道的性能真相和坑。

5.1 CTE性能和嵌套子查询比到底如何

先说结论:在绝大多数场景下,CTE 和嵌套子查询的执行计划是等价的。MySQL 优化器在生成执行计划时会把 CTE 当作派生表或者内联视图来处理,能合并的就合并,不能合并的就物化成临时表。所以如果你把一段嵌套子查询原样改写成 CTE,除非优化器做了不同选择,否则性能不会翻天覆地地变好,也不会明显变差。

那 CTE 有没有可能更慢?有。当 CTE 被引用多次时,MySQL 可能选择物化 CTE 的结果,也就是把中间结果落地到内存中的临时表。这个物化过程有成本,如果中间结果集特别大,物化成本反而可能高于重复扫描原表。早年我做过一个报表,把一个 CTE 引用了三次,数据量几百万行,结果物化开销明显。后来我把 CTE 拆成不同的查询分别跑,配合索引优化,总耗时才降下来。

所以正确的态度是:CTE 是"可读性工具",不是"性能优化工具"。很多慢 SQL 的根因在表结构、索引、JOIN 策略上,把这个锅甩给 CTE 是不公平的。

5.2 多次引用CTE的物化问题

关于多次引用,我再说细一些。如果你在一个查询里对同一个 CTE 引用了两次,MySQL 通常不会把这段 CTE 重复执行两遍,而是倾向于先物化成临时表再复用。这听起来是好事,但也带来两个隐患。

第一,物化临时表没有索引,除非 MySQL 自动建立索引,否则你在外层多次 JOIN 这个 CTE 的时候,可能要走全表扫描,速度比直接查原表慢。第二,物化需要临时空间,在内存不够时会写到磁盘,一旦写盘,性能下降成指数级。我自己观察过 behavior:一个 500 万行的 CTE,内存模式下可能几十毫秒,一旦转成磁盘临时表,直接秒级起步。

我自己的习惯是:同一个 CTE 如果要用两次以上,我就会认真看 EXPLAIN,看它到底是 merge 还是 materialize。如果物化了而且结果集很大,我会考虑能否把这段逻辑独立成一张正式表,或者用临时表手动物化并加索引。CTE 写起来清爽,不等于可以随便写个大的。

5.3 递归CTE的四个常见错误

递归 CTE 是写作事故的高发区,我总结四个高频错误。

第一个错误是递归部分忘了加终止条件,或者条件写错,导致无限递归。MySQL 有一个保护机制:cte_max_recursion_depth,默认是 1000,超过就报错。如果你确实需要递归很深,可以临时调大,但一般超过 1000 就要反思数据模型了。

SET SESSION cte_max_recursion_depth = 5000;

第二个错误是递归部分使用了聚合函数或窗口函数。CTE 递归部分的 SELECT 不允许使用聚合函数和窗口函数,这是语法层面的限制,不是优化层面的。假设你想在递归里统计每层人数,对不起,写不了,你得先递归出所有节点,再在外面做聚合。

第三个错误是 UNION 和 UNION ALL 用错。关系数据有环的时候,UNION ALL 会让递归无限延伸,必须做好环路检测或用去重语义。树形结构没有环才可以用 UNION ALL,而且在语义上"保留所有路径"才是你真正想要的,这时候贸然上 UNION DISTINCT 还可能意外丢数据。

第四个错误是搞不清锚点查询和递归查询的顺序。递归 CTE 的结构必须先是锚点查询,然后 UNION [ALL/DISTINCT],最后是递归查询。递归查询里一定要引用 CTE 本身,否则它不是递归,就只是一个普通查询。很多人把锚点和递归部分写反了,结果跑出来只有一行,怎么查都查不全。

5.4 版本支持与线上检查

版本支持这块很少有人写清楚。MySQL 8.0.1 开始支持 CTE,8.0.14 前后语法趋于完善,但如果你连接的是 MariaDB,版本支持情况又不一样,MariaDB 支持 WITH 但语法细节可能有差异。上线之前我习惯做三件事:

  • 确认生产环境 MySQL 大版本 >= 8.0;
  • 执行 EXPLAIN 看 CTE 是被 merge 还是 materialize;
  • 在测试环境造一份接近真实规模的数据跑一遍,观察临时表大小和耗时。

另外要提醒一个现实的点:很多公司有 SQL 审查工具或者团队规范,对线上查询能写什么、不能写什么有纪律性约定。动手重构之前先问一下团队约定,别把好工具变成了吵架的理由。毕竟工具只是手段,团队协作和线上稳定才是目的。

6. 常见问题与排查技巧实录

最后这部分,我把自己维护 SQL 过程中遇到的高频问题整理成速查形式,方便你直接照着排查。

6.1 报错速查表

报错信息常见原因解决思路
You have an error in your SQL syntax... near 'WITH'版本低于 8.0 或写错位置确认 MySQL 版本,把 WITH 放在整条语句最前面
Recursive query without termination递归部分缺少终止条件或条件不可达检查递归 WHERE 条件的表达式,确保能收敛
Recursive Common Table Expression can't contain aggregate function递归部分使用了聚合/窗口函数把聚合挪到外层,递归里只做行级展开
Can't create temporary table物化临时表过大,内存磁盘都撑不住该 CTE 逻辑物化太贵,考虑建实体表或重构
Exceeded max recursion depth超过 cte_max_recursion_depth 默认 1000先确认数据没有环,再按需调大会话参数

这个表并不能覆盖所有场景,但覆盖了我和团队同事踩过的八成问题。遇到没见过的报错,第一件事永远是单独运行 CTE 的各个子查询,缩小问题范围。千万别在一条几百行的 SQL 里从头到尾猜是哪里的括号没闭合。

6.2 优化与排查清单

排查复杂查询时,我习惯按下面顺序走,而不是直接抓瞎优化:

  1. 先看 EXPLAIN 的 type 列,是否存在全表扫描,重点关注被 CTE 引用的主表有没有走索引;
  2. 单独跑一遍 CTE 里的查询,确认中间结果量级,如果中间结果有几百万行,后续 JOIN 的性能就要警惕;
  3. 检查 JOIN 字段的类型和字符集是否一致,不一致会导致索引失效,这个坑常出在跨库 JOIN 上;
  4. 如果 CTE 被引用多次,观察临时表大小,必要时用优化器相关参数做微调;
  5. 最终用 profiling 或 performance_schema 对比改造前和改造后的耗时,用真实数据说话。

这里单独说一下 EXPLAIN。在 MySQL 8.0 里执行 EXPLAIN SELECT ... FROM 一个引用 CTE 的查询,你会看到 CTE 出现在 derived table 相关行里。如果你看到 Materialize 关键字,说明它物化了;如果看到 Using temporary 也不要慌,关键看数据量和是否走索引。只有把这些细节都掌握清楚,你才能真正做到又快又清。

6.3 我的一些真实体会

文章写到这里,最后分享一点我个人的操作习惯。我现在写任何查询,默认流程都是"先拆解业务逻辑,再画信息流,最后写 SQL"。信息流里的每一个中间节点,我基本都会写成 CTE。这么做的好处是,SQL 越来越像一份可以读给业务同事听的执行说明。有一次需求评审,我直接把 SQL 里的 CTE 名称和注释念给业务方听,对方居然能边听边对口径,这是嵌套子查询永远不可能实现的事。

CTE 也不是万能的。真遇到特别复杂的 ETL 逻辑,我依然会用临时表分步落库,而不是硬塞一个超级大的 WITH 语句。清洗了大量数据之后你会发现:工具的关键不在于"能不能用",而在于"在什么粒度上用"。CTE 的粒度适合把"一个查询内部"拆清楚,跨查询、跨任务的时候,就交给视图、临时表和存储过程。

如果你们团队还没用上 CTE,我强烈建议从重构一条模糊的子查询开始,看看跑完再给别人读的效果。清爽不仅是一种感觉,它直接决定了你半小时后开会时,能不能讲清楚这段数的逻辑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询