mysql 8.0 的 in 嵌套子查询默认走嵌套循环,而 postgresql 15 默认用 hash semi-join,故性能差约3倍;需 mysql 显式提示 materialize 或改写为 join,pg 则需防 limit 导致退化为 nestloop。

MySQL 8.0 vs PostgreSQL 15 的 IN 嵌套子查询执行速度为什么差 3 倍?
因为 MySQL 在多数情况下无法对 IN (SELECT ...) 子查询做物化(materialization),默认走嵌套循环(NLJ),而 PostgreSQL 15 默认启用 hash semi-join,直接把子查询结果哈希化后一次匹配。尤其当外层表大、子查询结果小(比如 SELECT id FROM users WHERE status = 'active')时,PostgreSQL 能跳过大量扫描。
- MySQL 8.0 需显式加
/*+ MATERIALIZE */提示(仅限优化器提示支持的版本),或改写为JOIN+GROUP BY - PostgreSQL 中若子查询含
LIMIT或不可下推的表达式,hash semi-join可能退化为nestloop semi-join,用EXPLAIN (ANALYZE)看实际计划里的Join Filter是否出现 - 测试时务必关掉查询缓存(MySQL 的
query_cache_type=0;PG 无全局缓存,但注意shared_buffers预热状态)
SQLite 的 EXISTS 嵌套比 IN 快,但只在单表子查询成立
SQLite 的查询规划器对 EXISTS 更激进地尝试使用索引查找(index lookup),而 IN 在子查询返回多列或含表达式时容易触发全表扫描。不过这个优势只在子查询本身不带 JOIN 或聚合的前提下稳定存在。
- 如果子查询是
SELECT user_id FROM orders WHERE amount > 100,EXISTS大概率走user_id索引;换成IN就可能变成先执行子查询再哈希匹配,而 SQLite 的哈希表实现较轻量,但内存受限时反而更慢 - 一旦子查询含
GROUP BY或窗口函数,SQLite 会强制走临时表,此时EXISTS和IN性能趋同,甚至更差(因多一次 EXISTS 判定开销) - 实测中,
PRAGMA temp_store = MEMORY对嵌套查询提速明显,但超过temp_store_directory限制后会静默切回磁盘,导致性能陡降
ClickHouse 的 GLOBAL IN 网络放大问题怎么避免?
GLOBAL IN 会把右子查询结果广播到所有分片节点,如果子查询返回 10 万行,集群有 20 个分片,就产生 200 万行网络传输 —— 这不是延迟问题,是带宽和内存双重瓶颈。普通 IN(本地)则只在当前节点执行子查询,但要求数据分布键与子查询字段一致。
- 优先用
JOIN替代GLOBAL IN,尤其是右表可建Distributed表时;JOIN支持parallel_replicas,能摊平压力 - 若必须用
GLOBAL IN,子查询务必加LIMIT(如SELECT id FROM events WHERE dt = '2024-01-01' LIMIT 1000),ClickHouse 不会自动截断 - 检查
max_bytes_before_external_group_by,该参数影响子查询是否落磁盘;设太小会导致频繁 spill,设太大可能 OOM
Oracle 19c 的 WITH 子句嵌套被重写成视图,但统计信息没更新
Oracle 把 WITH 当作内联视图(inline view)处理,优化器基于基表统计信息估算中间结果集大小。如果子查询过滤了 95% 的数据,但优化器仍按原表行数估算,就会选错连接顺序或索引 —— 这不是 bug,是统计信息未覆盖中间逻辑的结果。
- 对关键
WITH子句加/*+ MATERIALIZE */提示,强制物化并收集其统计信息(需配合DBMS_STATS.GATHER_TABLE_STATS对物化临时表操作) - 避免在
WITH中用绑定变量(:v1),Oracle 无法为不同变量值生成不同执行计划,容易复用低效计划 - 用
DBMS_XPLAN.DISPLAY_CURSOR查看真实A-Rows(实际返回行数)与E-Rows(预估行数)差距,差 10 倍以上就说明统计信息严重失准
嵌套查询的性能拐点往往不在语法本身,而在引擎如何“理解”子查询的边界 —— 是当作一次性计算、可复用中间结果,还是必须每行都重跑。这点在跨引擎迁移时最容易被忽略,尤其当开发环境数据量小,压测时才暴露。










