cte 的“简洁”是表象,临时表才是性能更稳的选择;真实开销需看执行计划:mysql 查 materialized、postgresql 看 cte scan 次数、sql server 找重复 nodeid 或 spool;超10万行或需多次 join/过滤/排序时,应优先建带索引的临时表。

看执行计划,别信“语法看起来更干净”
CTE 写起来像分步说明书,但 MySQL 8.0.31 之前默认不物化,WITH cte AS (SELECT ...) 被多次引用时,可能真被算两次;PostgreSQL 默认物化,但若执行计划里出现重复的 CTE Scan 节点,说明没生效;而子查询在 WHERE 里套三层 (SELECT ...),执行计划里大概率是 DEPENDENT SUBQUERY 或 MATERIALIZED 标记混乱——这些才是真实开销信号。
实操建议:
- MySQL 中用
EXPLAIN FORMAT=TREE查看是否含MATERIALIZED;没加MATERIALIZED提示就别假设它缓存了 - PostgreSQL 中用
EXPLAIN (ANALYZE, BUFFERS),重点看CTE Scan出现几次、Shared Hit是否高 - SQL Server 里查执行计划 XML,找
RelOp NodeId是否重复,或看是否有Spool算子 - 别只看“rows”估算值——临时表的统计信息更准,大表上
#tmp的Actual Rows往往比 CTE 的Estimate Rows可靠得多
数据量 > 10 万行且要 JOIN 多次时,临时表不是更重,是更稳
CTE 在跨 JOIN 场景下容易让优化器误判:比如 WITH a AS (SELECT id, SUM(val) FROM big_table GROUP BY id),再 JOIN a ON ... JOIN a ON ...,MySQL 可能对每个 JOIN 都重跑聚合;而临时表建完立刻有准确行数和分布,CREATE INDEX 后还能加速关联。
实操建议:
- 中间结果超 10 万行,且后续要
JOIN、WHERE过滤、ORDER BY排序,直接上CREATE TEMPORARY TABLE tmp_xxx AS ... - 建完立刻
CREATE INDEX,尤其对 JOIN 列或 WHERE 条件列 - 避免在存储过程中反复
DROP + CREATE TEMPORARY TABLE,一次建好复用全程 - 命名加唯一后缀,如
tmp_user_agg_20260929_12345,防连接池中残留冲突
相关子查询是性能黑洞,优先拆成 CTE 或临时表
像 WHERE t1.id IN (SELECT t2.ref_id FROM t2 WHERE t2.status = t1.status) 这种,t1 每行都触发一次 t2 扫描,O(n²) 不是吓唬人。这时候 CTE 至少能强制先算一遍 t2 结果集;临时表还能加索引加速匹配。
实操建议:
- 把相关子查询里的内层逻辑抽出来,定义为 CTE,主查询改用
JOIN替代IN或EXISTS - 如果 CTE 抽出来后仍慢(比如
t2行数太大),立刻转CREATE TEMPORARY TABLE并对ref_id和status建联合索引 - 别试图用
/*+ MATERIALIZE() */强撑——MySQL 8.0.31+ 才支持,老版本无效 - SQL Server 上可试
OPTION (RECOMPILE),但不如物理临时表稳定
递归和多步加工是分水岭,别硬套 CTE
CTE 真正不可替代的只有两个场景:递归(组织树、路径展开)和明确依赖链(A ← B ← C)。除此之外,只要涉及“INSERT → UPDATE → SELECT”三步走,或者中间结果要加索引、要统计采样、要人工校验,CTE 就不是选项,是障碍。
实操建议:
- 需要递归?必须用
WITH RECURSIVE,子查询和临时表都搞不定 - 要对中间结果做多次不同过滤?CTE 不行,临时表可以
SELECT * FROM #tmp WHERE ...、UPDATE #tmp SET ...自由操作 - 想调试中间数据?CTE 查不了,临时表直接
SELECT TOP 100 * FROM #tmp - 跨语句复用?CTE 生命周期只到当前
SELECT结束,临时表活到会话结束
CTE 的“简洁”是给眼睛看的,临时表的“啰嗦”是给优化器听的。真正卡住性能的,往往不是语法选择本身,而是你没看清哪一步该固化、哪一步该放开让优化器推导。










