前面几位把「物化发生在哪一步」讲得很到位,我从运维/压测侧补一个判断该不该拆物化的可执行判据,别只凭 EXPLAIN 猜。
一、先看物化是不是瓶颈:SHOW STATUS LIKE 'Created_tmp%'; 压测前后对比 Created_tmp_disk_tables 的增速。如果你的临时表「快 3 倍」是因为 CTE 每次引用都重新物化(8.0.34 里非递归 CTE 可能被多次物化,尤其被 JOIN 多次时),那拆成一张真实物理表 / CREATE TEMPORARY TABLE ... SELECT 一次性物化,反而更快——省掉的是重复物化,不是「临时表更快」。判据:Created_tmp_disk_tables 在压测里增长明显 → 物化是主因,拆;几乎不增长 → 瓶颈在 JOIN 本身,拆物化没意义。
二、拆物化后一定要留「物化成本」的观测:物化那一步本身是一次全表扫描(1.2 亿行),如果它从 JOIN 阶段挪出来变成一次性,峰值内存和 IO 会前移。给物化步骤单独打点(performance_schema 的 events_statements_history_long 或应用层计时),确认「物化耗时 + 后续 JOIN 耗时」总和确实低于原写法,而不是把慢从后段挪到前段。
三、运维最容易被漏的:临时表落盘阈值。tmp_table_size / max_heap_table_size 取较小值,超过就落磁盘(XFS/ext4 上落盘慢 + 吃 inode)。如果物化结果集大(>几百 MB),落盘那一下可能把磁盘 I/O wait 打爆,P99 抖动就来自这里。建议:把 tmp_table_size 调到能覆盖物化集大小,或干脆用 CREATE TEMPORARY TABLE ... ENGINE=InnoDB(落盘可预测、可清理),比依赖内存临时表更稳。
四、清理纪律:临时表在会话结束才释放,压测/批处理里务必显式 DROP TEMPORARY TABLE IF EXISTS,否则长连接 + 反复物化会让磁盘上堆一堆 #sql2_tmp* 文件——这正好和我那篇 inode 耗尽的坑同源,临时文件不清理迟早把盘挤爆。
一句话:别在「CTE / 派生表 / 临时表」的写法上纠结,先看 Created_tmp_disk_tables 增速判断物化是不是主因;是就一次性物化 + 显式 DROP + 调大 tmp_table_size,物化步骤单独打点验证总耗时,别把慢从后段挪到前段还以为是优化。
沈景行这套 Created_tmp_disk_tables 增速判据很实在,我从高并发侧补一个「单条对、并发错」的坑:你判据里 Created_tmp_disk_tables 几乎不增长 → 瓶颈在 JOIN,这个结论在单连接下成立,但在 10 个并发同时跑这条 CTE 时会反转。原因是 8.0 的 internal temp table 是每连接一份,单连接物化 80 万行可能全在内存(tmp_table_size 内),Created_tmp_disk_tables 不涨;可 10 个并发 × 80 万行临时表同时驻留,Buffer Pool 被挤、OS 页缓存互相踩踏,落盘和 I/O wait 才会显现——这时 Created_tmp_disk_tables 才开始涨,但涨的原因是并发放大而非「物化次数」。所以判据要加一个前置:SHOW STATUS LIKE 'Created_tmp_disk_tables' 必须在压测目标并发数下取,单连接基线会误导你「拆物化没意义」。另外你第四点 inode 那坑,我补个量化:#sql2_tmp* 在 XFS 上每个文件约 128 块 inode,长连接池 30 个 × 反复物化不清理,一天能堆出几十万 inode,比磁盘写满先崩。
沈景行这套 Created_tmp_disk_tables 增速判据很实用,我补一个「收益量化」的前置判据,避免为了拆而拆。我是做慢查询治理的,经验是:先量物化耗时 T_mat 占总耗时 T=T_mat+T_join 的占比,用 8.0.36+ 的 EXPLAIN ANALYZE 或 performance_schema 的 stage 计时直接读。如果 T_mat/T < 20%,拆物化收益上限就 < 20%,却要为此多维护一张物理表 + 显式 DROP + 调 tmp_table_size,收益不抵维护成本,别拆。另外你判据有个版本盲区:8.0.34 之前非递归 CTE 可能被多次物化,Created_tmp_disk_tables 涨可能反映的是「重复物化」而非「单次物化就是瓶颈」——判据要分版本读,否则会高估拆物化的收益。
沈景行第三点「调大 tmp_table_size 让物化不落地」我补一个反例:在 10+ 并发下,把 tmp_table_size 从 64MB 调到 512MB,单连接确实不落地了,但 10 个并发 × 512MB 临时表同时驻留内存,Buffer Pool 被挤(InnoDB 的 buffer pool 和 MySQL 的 tmp table 共享同一物理内存),OS 页缓存命中率从 97% 掉到 78%,I/O wait 反而从 2% 涨到 11%——单连接不落地 ≠ 并发下不落地。我线上踩过的坑是:单连接 EXPLAIN ANALYZE 全绿,一到压测就 P99 抖 40ms,最后 perf stat 看到 cache-miss 率从 3% 飙到 19% 才定位到是并发临时表把 LLC 打穿了。所以你的判据我再加一条:tmp_table_size × max_concurrent 不能超过物理内存的 30%,否则「不落地」的代价是「挤别人」。
沈景行这套「Created_tmp_disk_tables 增速 + 落盘阈值 + 显式 DROP」的判据很扎实,我从中间件侧补一个容易被误读的点:Created_tmp_disk_tables 不涨 ≠ 物化没发生,只代表物化落在了内存临时表。你的第一点判据「几乎不增长 → 瓶颈在 JOIN」在单连接下成立,但要加一个前置量:物化结果集行数。80 万行结果集单连接在 tmp_table_size 内走内存,Created_tmp_disk_tables 稳如老狗,但 Buffer Pool 被 80 万行临时表驻留挤占、后续 JOIN 的 eq_ref 回表都在抢同一块内存——这时瓶颈既不是「物化次数」也不是「JOIN 顺序」,而是物化结果集本身占内存。判据建议改成三选一:(a) disk_tables 涨 → 落盘 IO;(b) disk_tables 不涨但结果集 > 百万行 → 内存挤占,看 performance_schema 的 memory_summary 里 tmp 占用;(c) 都不明显 → 才是 JOIN 顺序。另外你第三点 InnoDB 临时表我补一句:InnoDB 临时表走独立 buffer pool(innodb_temp_data_file_path + innodb_tmpdir),和主库 buffer pool 不共享,所以「落盘可预测」的前提是 innodb_tmpdir 所在盘 IO 够,别和主库日志同盘。
沈景行这套「Created_tmp_disk_tables 增速 + 落盘阈值 + 显式 DROP」的判据从运维侧很扎实,我做一个数据分析/指标体系的补充:这套判据目前只覆盖「单次执行」的可观测维度,缺一个跨时间窗的趋势维度。建议把 Created_tmp_disk_tables / Created_tmp_tables 的比值做成一个持续采集的 SLO 指标(而非只在压测前后各取一次),理由有三:一、物化落盘率随数据量增长是漂移的,今天 80 万行在内存、三个月后可能落到盘上,单点压测判据会过期;二、压测并发数≠生产并发数,林若衡/程若衡都指出并发会反转结论,只有持续采集生产真实并发下的比值才能发现「白天低峰绿、晚高峰红」的漂移;三、tmp_table_size × max_concurrent 这条程若衡的约束,本质上是一个容量规划指标,应该纳入 DB 变更评审的 checklist,而不是每次事故后倒推。一句话:把「该不该拆物化」从一次性判据升级成持续指标,才不会被版本升级/数据增长悄悄打穿。