sql server将in子查询转为semi join是为了避免重复执行,因相关子查询默认不物化,主表每行都触发一次子查询扫描,优化器重写后可将时间复杂度从o(n×m)降至o(n+m),但是否触发取决于成本估算和执行计划中是否存在hash match等节点。

优化器展平嵌套查询,不是为了“看起来更简洁”,而是为了避免重复执行、控制中间结果规模、争取并行机会——它只在估算成本更低时才动手,否则宁可保持嵌套。
SQL Server 为什么把 IN 子查询转成 Semi Join?
因为默认情况下,WHERE id IN (SELECT user_id FROM logs WHERE action = 'login') 这类相关子查询不会物化,主表每行都触发一次子查询执行。10 万行主表 → 10 万次子查询扫描,哪怕 logs 有索引,也是 10 万次单点查找。
- 优化器检测到这种模式后,会尝试重写为
Semi Join或Hash Match,一次性加载右表数据,时间复杂度从O(N×M)压到O(N+M) - 但不相关子查询(如
(SELECT MAX(created_at) FROM config))通常被缓存,优化器一般不改写 - 是否真触发,得看执行计划:开
SET STATISTICS XML ON,查sys.dm_exec_query_plan,找Hash Match、Merge Join或带Predicate的Nested Loops节点
MySQL 的“上拉”(pull-up)是怎么回事?
MySQL 查询优化器会把子查询里的嵌套 JOIN 上拉成平坦的 JOIN 列表,目的是让优化器能对所有表统一做连接顺序决策和索引选择,而不是被困在子查询边界里各自为政。
- 典型场景:
SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers JOIN regions ON ... WHERE region = 'CN') - 优化器可能把它重写为
orders JOIN customers JOIN regions,再整体应用WHERE region = 'CN' - 但上拉是否发生,取决于谓词位置:如果过滤条件写在子查询的
ON子句里,而非WHERE,优化器可能不敢动——怕语义变化
手动改写 JOIN 时最容易漏掉的三个语义坑
即使优化器没自动展平,你手写 JOIN 也得小心——语法对了,结果可能错。
-
IN对NULL敏感,INNER JOIN天然过滤NULL;若子查询列允许NULL,直接替换会导致结果变少 - 子查询带
TOP 1或LIMIT 1(如WHERE dept_id = (SELECT TOP 1 id FROM dept WHERE ...)),手写JOIN必须加DISTINCT或用ROW_NUMBER()去重,否则一对多会放大行数 -
ON和WHERE不等价:外连接中,把原WHERE条件塞进ON子句,会改变关联逻辑,不是简单“提前过滤”
真正卡住性能的,往往不是“该不该展平”,而是展平之后没验证 NULL 行为、没核对结果行数、没确认执行计划里是否真出现了 Hash Match 或 Merge Join 节点——这些细节一漏,优化就变成劣化。











