当前位置:首页 > 文章列表 > 数据库 > MySQL > MySQL CTE 多次引用时为什么可能重复物化

MySQL CTE 多次引用时为什么可能重复物化

来源:17golang原创 2026-09-10 16:41:36 0浏览 收藏

把同一个 CTE 写在两个 JOIN 位置后,执行计划里出现两个引用,很容易得出“CTE 被物化了两次”的结论。这个判断通常不对:在同一条语句中,如果 MySQL 选择物化一个 CTE,物化结果只创建一次,后续引用复用它;真正可能增加成本的是每个引用需要的临时索引、重复写了两份等价查询,或某些引用被合并而另一些被物化。

官方地址:https://dev.mysql.com/doc/refman/8.4/en/

要点速览
  • 普通 CTE 的“多次引用”不等于“多次物化”,递归 CTE 则总会物化。
  • 多引用时,MySQL 可能为不同访问方式建立多个自动索引,但临时结果仍是一份。
  • 先用 EXPLAIN 和 optimizer_trace 看证据,再决定是否使用 NO_MERGE 或调整 SQL。

先别急着把多处引用等同于多次物化

CTE 是一条语句范围内的命名结果集,`daily_stats` 既可以被 `s1` 引用,也可以被 `s2` 引用。优化器面对 CTE 有两条路:把定义合并进外层查询,或生成内部临时表。带有聚合、DISTINCT、GROUP BY、HAVING、LIMIT、UNION 等结构的 CTE 通常失去合并机会,更容易进入物化路径。

关键在于“物化对象”和“引用节点”不是一回事。多次引用只会让同一份共享临时表被多处读取;官方手册还说明,MySQL 可能按引用的访问方式给这份表加多个索引。执行计划中看到两个 `s1`、`s2`,不能直接推导出底层聚合执行了两遍。

MySQL CTE daily_stats 从 orders 生成共享临时表并被 s1、s2 两个引用读取的静态查询结构图
图1:把 orders、daily_stats、共享临时表与 s1、s2 分开看,引用节点多不代表物化结果多。

为什么 EXPLAIN 看起来像有两个 CTE

准备一个带分组的查询,让 CTE 的行为更容易观察。下面的例子只描述结构,不依赖某个数据量或固定耗时:

WITH daily_stats AS (
    SELECT customer_id, COUNT(*) AS order_count
    FROM orders
    WHERE created_at >= '2026-01-01'
    GROUP BY customer_id
)
SELECT s1.customer_id, s1.order_count, s2.order_count AS order_count_copy
FROM daily_stats AS s1
JOIN daily_stats AS s2 ON s2.customer_id = s1.customer_id;
-- 中文说明:GROUP BY 使 daily_stats 更可能走物化路径;s1、s2 是同一 CTE 的两个引用。

先看计划树,再看优化器跟踪。`EXPLAIN FORMAT=JSON` 里的一个完整 `materialized_from_subquery` 节点描述 CTE 的来源计划,其他引用可能只显示精简节点。要判断是否真的重复生成,重点找的是 `creating_tmp_table` 与 `reusing_tmp_table` 的组合:前者表示创建,后者表示复用。不要把“引用出现两次”当成“创建出现两次”。

MySQL CTE 计划树与 optimizer_trace 中 materialized_from_subquery、creating_tmp_table、reusing_tmp_table 的静态证据关系图
图2:用计划树、物化节点和跟踪线索区分一次创建、多次复用与按引用增加的自动索引。
EXPLAIN FORMAT=JSON
WITH daily_stats AS (
    SELECT customer_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY customer_id
)
SELECT s1.customer_id
FROM daily_stats AS s1
JOIN daily_stats AS s2 ON s2.customer_id = s1.customer_id;

SET optimizer_trace = 'enabled=on';
WITH daily_stats AS (
    SELECT customer_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY customer_id
)
SELECT s1.customer_id
FROM daily_stats AS s1
JOIN daily_stats AS s2 ON s2.customer_id = s1.customer_id;
-- 中文说明:执行同一查询后读取跟踪,观察创建与复用线索,而不是猜测节点数量。
SELECT TRACE
FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;

哪些写法才会真正重复做工作

第一种是把相同聚合分别写成两个派生表:它们没有共享 CTE 名称,优化器只能分别规划。第二种是定义两个内容等价但名字不同的 CTE,再分别引用;可读性看似提高,物化边界却可能增加。第三种是对一个 CTE 的不同引用采用不同策略:一处被 `MERGE` 展开,另一处用 `NO_MERGE` 物化,这时底层表的访问路径可能不同。

还有一个容易误判的情况:多引用 CTE 可能拥有多个自动索引。它们服务于不同的连接键或过滤条件,属于同一临时结果上的访问优化,不是把 CTE 内容重新算一遍。递归 CTE 则始终物化,不能用“关闭合并”来改变这个基本事实。

看到的现象更准确的解释先做什么
两个 CTE 引用可能共享一份物化结果看 trace 的创建/复用线索
多个临时索引不同引用需要不同访问路径比较连接列和过滤条件
两段相同子查询没有共享定义,可能重复工作合并为一个 CTE 后再看计划

怎么控制合并与物化

默认不要全局关闭优化。需要做对照时,可以在单条语句上使用 `NO_MERGE`,或在当前会话临时调整 `derived_merge`。提示只是让优化器倾向某条路,技术约束仍可能阻止合并;改完后必须重新比较过滤下推、临时表大小、连接访问和总体耗时。

WITH daily_stats AS (
    SELECT customer_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY customer_id
)
SELECT /*+ NO_MERGE(daily_stats) */ s1.customer_id, s1.order_count
FROM daily_stats AS s1;

SET SESSION optimizer_switch = 'derived_merge=off';
-- 中文说明:只在当前会话做实验;对照完成后恢复默认值,避免影响其他连接。
SET SESSION optimizer_switch = 'derived_merge=default';

采用建议很简单:如果 CTE 只被引用一次且定义可合并,先让优化器自由选择;如果同一份聚合结果被多处使用,保留一个 CTE 并观察复用;如果 trace 真正显示多个创建线索,再回头检查是否写成了多个定义、多个查询块或混用了合并策略。不要因为计划树有两个引用就先改 SQL。

常见问题

MySQL CTE 被引用两次,一定只物化一次吗?

在同一条语句中,若该 CTE 选择物化,官方规则是只物化一次;但它可能被合并,或按不同引用建立多个自动索引。

看到两个 materialized_from_subquery 就是执行两遍吗?

不一定。多引用时只有一个节点通常包含完整来源计划,其他节点可能是精简引用。应结合 `creating_tmp_table` 和 `reusing_tmp_table` 判断。

什么时候应该拆掉 CTE?

当两个定义实际过滤条件、聚合粒度或生命周期不同,拆开比追求复用更清楚;如果只是同一结果的不同访问方式,应先保留一个 CTE 并比较计划。

参考:https://dev.mysql.com/doc/refman/8.4/en/derived-table-optimization.htmlhttps://dev.mysql.com/doc/refman/8.4/en/with.html

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
LiblibAI能做AI绘画吗?文生图、参考图和工作流能力怎么选LiblibAI能做AI绘画吗?文生图、参考图和工作流能力怎么选
上一篇
LiblibAI能做AI绘画吗?文生图、参考图和工作流能力怎么选
Go map lookup 读取不存在键为什么返回元素零值
下一篇
Go map lookup 读取不存在键为什么返回元素零值
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    63次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    224次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    148次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    81次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    58次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码