物化(Materialize)是 MySQL 优化器处理 CTE 的两种策略之一:将 CTE 的查询结果计算出来并存入内部临时表,后续对该 CTE 的引用直接读取临时表,而非重新执行其定义。 它与“合并(Merge)”相对——后者把 CTE 的定义展开进外层查询,让优化器与外层条件一起优化。理解物化,关键在于搞清楚何时触发、有何代价、如何验证。
物化与合并:两种策略的分工
MySQL 优化器对派生表、视图引用和 CTE 采用相同的处理框架:优先尝试合并,以避免不必要的物化,从而把外层条件下推到内部,生成更高效的执行计划。合并相当于“文本展开”:CTE 定义直接融入外层查询块,优化器拥有全局视野来重新安排连接顺序和过滤时机。
物化则走向另一条路:把 CTE 当作一个独立的临时结果集先行算出,后续引用只读这份结果。当合并不可行或代价更高时,优化器选择物化。触发物化的典型情形包括:
- CTE 被多次引用:此时物化一次、复用多次,避免重复计算。这是物化最核心的价值场景。
- 递归 CTE:递归 CTE 在 MySQL 中总是物化的,无法合并。
- CTE 定义中包含阻止合并的构造:如聚合函数、窗口函数、
DISTINCT、GROUP BY、HAVING、LIMIT、选择列表中的子查询、用户变量赋值等。 - 合并会导致外层查询块引用的基表超过 61 张:此时优化器强制选择物化。
换句话说,物化不是“默认选项”,而是优化器在合并不可行或多次引用更划算时的退路与优化手段。
多次引用:物化的核心收益
MySQL 官方博客中有一个经典对比:用视图或派生表在查询中被引用两次时,MySQL 会物化两次,每次引用各执行一遍;而改用 CTE 后,只物化一次,两次引用共享同一份临时表结果。在该示例中,物化是执行耗时的主要部分,因此单次物化几乎把执行时间减半。
这揭示了 CTE 物化与视图/派生表物化的关键差异:CTE 在单次查询内的多次引用共享同一份物化结果,而普通派生表每次出现通常各自物化。MySQL 文档也明确:如果 CTE 被物化,即使查询中多次引用,该 CTE 也只对查询物化一次。
这一特性的实际意义在于:当一个较重的中间结果需要被多个后续步骤使用时,CTE 物化避免了重复计算。但如果 CTE 只被引用一次,物化就失去了“复用”的优势,优化器通常会优先选择合并,以便条件下推。
物化的代价与自动索引
物化并非没有成本。它需要把结果写入内部临时表,涉及临时表的存储开销。如果结果集较大,内存临时表(MEMORY 引擎)可能溢出到磁盘,性能会明显下降。对于递归 CTE,每轮迭代都涉及工作表(working table)的读写,规模失控时开销更为显著。
不过 MySQL 在物化时有一个补偿机制:优化器会自动为物化的 CTE 添加索引,以加速顶层语句对它的访问。这与派生表的自动索引类似,但 CTE 有一个额外优势:当 CTE 被多次引用且各引用的访问方式不同时,优化器可以创建多个索引,分别优化不同引用路径。这意味着物化并非“算完就扫”,而是有机会以索引查找的方式被消费。
如何验证与干预
验证一个 CTE 是否被物化,最直接的方式是查看 EXPLAIN 输出。在 EXPLAIN FORMAT=JSON 中,物化的 CTE 会以 materialized_from_subquery 节点呈现;被合并(内联)的 CTE 则不会出现独立的物化节点,其底层表直接出现在执行计划中。在 FORMAT=TREE 中,出现 Materialize 相关节点表示发生了物化。
MySQL 也提供了优化器提示来干预策略。/*+ MERGE(cte_name) */ 和 /*+ NO_MERGE(cte_name) */ 可以分别推动合并或阻止合并,从而间接影响是否物化。但提示并非万能:优化器在技术条件不允许时(如递归 CTE)仍会物化,且提示的实际效果需要以 EXPLAIN 验证为准。
小结
CTE 物化的本质是用一次性的临时表存储,换取多次引用时的计算复用。优化器默认尽量不物化,以保留条件下推和连接重排的空间;但当 CTE 被多次引用、包含聚合或递归结构、或合并代价过高时,物化成为更优或唯一的选择。对于写 SQL 的人而言,需要记住的是:CTE 的可读性不自动等于性能优势;一个只被引用一次的 CTE 如果被意外物化,反而可能比直接写子查询更慢。用 EXPLAIN 确认实际策略,比假设“CTE 会物化”或“CTE 会内联”都更可靠。




