postgresql 15 中子查询常比 cte 快,因cte默认不物化且禁用内联优化,而子查询更易触发谓词下推、join重写等优化;cte仅提升可读性,非性能优化手段。

PostgreSQL 15 中,子查询在很多场景下确实比 CTE 执行得更快——不是因为子查询本身更“快”,而是因为 CTE 默认不物化,且优化器常被迫放弃内联优化,导致额外开销。
CTE 在 PostgreSQL 15 中根本不会强制物化
很多人误以为 WITH 就等于“缓存一次、复用多次”,但在 PostgreSQL 15 中:
- WITH MATERIALIZED cte AS (...) 是语法错误,直接报 ERROR: syntax error at or near "MATERIALIZED";
- 所谓“默认物化”是过时认知:12+ 版本起,非递归 CTE 默认采用 inline 展开策略(即复制逻辑到每个引用处);
- 优化器只在它估算 inline 成本更高时才可能物化,但这个决策不可控、不透明、也不可预测。
常见错误现象:
- 同一个 CTE 被引用两次,EXPLAIN ANALYZE 显示两个独立的 Seq Scan 或 SubPlan,actual time 和 rows 高度重复;
- 表数据量不大,但 CTE 内含复杂 JOIN 或函数调用,结果被算两次,CPU 和 IO 白白翻倍。
子查询反而更容易触发优化器融合
非相关子查询(即不依赖外层字段的子查询)会被 PostgreSQL 优化器积极重写为 JOIN 或下推过滤条件,尤其当外层有 WHERE 或 ORDER BY ... LIMIT 时:
- 子查询中的 WHERE 可能和外层条件合并,提前过滤;
- LIMIT 可以下推到子查询内部,大幅减少中间行数;
- CTE 因“黑盒”隔离特性,这些优化通常被阻断。
实操建议:
- 把 WITH base AS (SELECT * FROM orders WHERE status = 'paid') 改成内联子查询 (SELECT * FROM orders WHERE status = 'paid'),再在外层加 WHERE created_at > '2026-08-01' → 很可能触发谓词下推;
- 若子查询带 GROUP BY + LIMIT,而 CTE 中也这么写,CTE 很可能先聚合全量再 LIMIT,子查询则可能先 LIMIT 再聚合(取决于优化器判断)。
真正影响性能的关键往往不在 CTE/子查询选型上
很多“CTE 比子查询慢”的案例,根因其实是:
- 基表缺索引,导致 CTE 内扫描全表,还被算两次;
- work_mem 设置过低,CTE 物化过程(哪怕只发生一次)触发磁盘排序,I/O 暴增;
- 用了 UNNEST + LATERAL 却没控制数组长度,行数爆炸,误判为 CTE 慢;
- 把本该用窗口函数的逻辑硬套进 CTE(比如“每个用户最新订单”),结果 CTE + 自连接 + NOT EXISTS,远不如 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)。
验证方法:
- 先对 CTE 内部查询单独跑 EXPLAIN ANALYZE,看单次执行耗时;
- 再对比完整语句的 EXPLAIN ANALYZE,确认 CTE 是否真被重复执行;
- 如果单次就慢,优先加索引或改写逻辑,别急着换语法。
真正需要警惕的是:把性能问题归咎于“CTE 不够物化”,却忽略 work_mem、索引缺失或错误使用 LATERAL 这些更底层、更易修复的点。CTE 的价值始终在可读性与调试便利性,而不是性能魔法棒。










