mysql 5.6+对主键关联的in子查询自动启用半连接优化,性能与等价join基本一致,无需强行改写;若explain显示firstmatch或loosescan则已优化,带group by/相关子查询时才需改写为join。

IN子查询 vs JOIN:主键关联时别急着改写
MySQL 5.6+ 和 PostgreSQL 对 WHERE col IN (SELECT pk FROM t2) 这类简单子查询会自动启用半连接(semi-join)优化,执行效率和等价的 JOIN 基本一致。强行改写反而可能破坏可读性或引入重复行。
实操建议:
- 先用
EXPLAIN看执行计划:如果出现FirstMatch、LooseScan或<semi-join></semi-join>,说明已被优化,不用动 - 若子查询带
GROUP BY、ORDER BY或引用外层字段(相关子查询),优化器大概率退化为逐行执行——这时必须改写为JOIN -
IN后接常量列表(如IN (1,2,3))比子查询更快;IN (VALUES (1),(2),(3))在 PostgreSQL 中也优于子查询
NOT EXISTS 比 LEFT JOIN + IS NULL 更安全
用 LEFT JOIN ... WHERE right_table.id IS NULL 查“不存在”的记录,一旦右表的 id 字段允许 NULL,就会漏数据——这不是语法错误,而是逻辑陷阱。
实操建议:
- 存在性检查优先用
NOT EXISTS:它只关心子查询是否返回行,不依赖字段值,天然免疫NULL干扰 - 若坚持用
LEFT JOIN,必须确保ON条件中右表的关联字段是NOT NULL(如主键或明确设为非空的外键) - SQL Server 对
NOT EXISTS生成更优计划;MySQL 8.0 中两者性能差距已极小,但语义清晰度差很多
子查询当派生表(FROM 中)时索引大概率失效
把 (SELECT * FROM t1 WHERE cond) AS dt 当作临时表再 JOIN,MySQL 默认会物化该结果并生成无索引的临时表——哪怕原表 t1 上有完美索引,物化后也丢了。
实操建议:
- 优先把子查询逻辑上提,合并进主
JOIN链,例如把WHERE t1.x IN (SELECT y FROM t2)改成JOIN t2 ON t1.x = t2.y - MySQL 8.0+ 虽支持
MATERIALIZATION和DERIVED提示,但不如重写可靠;PostgreSQL 的WITH ... MATERIALIZED同理,需查执行计划确认是否有Materialize节点 - 如果子查询结果固定且高频使用,考虑建物化视图或缓存表,而非每次运行时物化
UPDATE/DELETE 中直接查同一张表会报错
MySQL 特有报错:ERROR 1093 (HY000): You can't specify target table 't' for update in FROM clause。这是语法限制,不是性能问题。
实操建议:
- 用派生表包装子查询:把
SELECT id FROM t WHERE ...套一层(SELECT ...) AS tmp,再用于WHERE id IN (SELECT id FROM tmp) - 用
JOIN改写:例如UPDATE t JOIN (SELECT id FROM t WHERE ...) AS tmp ON t.id = tmp.id SET ... - 避免在应用层拼接大量 ID(如
IN (1,2,...,1000)),超过 1000 项时建议分批或走临时表
子查询和 JOIN 的边界其实很薄,真正容易被忽略的是:执行计划里的 DEPENDENT SUBQUERY 和 IS NULL 判定对象是否真的非空——这两处不看 EXPLAIN 和表结构定义,光靠语法对错根本发现不了。











