mysql 5.6+ 默认启用半连接优化,in和exists在满足条件时均可转为半连接执行;但实际性能取决于索引、子查询结构及优化器是否正确选择firstmatch/loosescan等策略,not in/not exists则触发反半连接,逻辑与性能均不同。

MySQL 5.6+ 默认启用 semijoin 优化,只要满足条件,IN 和 EXISTS(8.0.16+)都会被自动转成半连接执行——但这个“自动”不等于“无脑高效”,实际性能差异往往取决于索引、子查询结构和优化器是否真能走对策略。
哪些 IN/EXISTS 查询能触发 SemiJoin 转换
优化器不会对所有子查询都做半连接处理。必须同时满足以下硬性条件:
-
IN或= ANY必须出现在WHERE或ON子句的最外层,不能嵌套在AND表达式深层(如WHERE a = 1 AND (id IN (SELECT ...))是 OK 的;但WHERE (id IN (SELECT ...)) OR b = 2就可能失效) - 子查询必须是单个
SELECT,不能含UNION、GROUP BY、HAVING、聚合函数或ORDER BY ... LIMIT - 外部查询不能使用
STRAIGHT_JOIN,否则整个半连接逻辑被禁用 - 子查询中引用的列,最好在内表上有可用索引(比如
roster.class_num上有索引),否则即使转成半连接,仍要全表扫描
EXISTS 与 IN 在 8.0.16+ 的行为差异几乎消失
MySQL 8.0.16 起,优化器对 EXISTS 子查询也尝试做 semijoin 转换,语义等价性大幅提升。例如这两条语句:
SELECT * FROM class WHERE EXISTS (SELECT 1 FROM roster WHERE roster.class_num = class.class_num);
SELECT * FROM class WHERE class_num IN (SELECT class_num FROM roster);
在满足前述条件时,EXPLAIN 显示的执行计划很可能完全一致,都走 FirstMatch 或 DuplicateWeedout 策略。但注意:NOT EXISTS 和 NOT IN 触发的是反半连接(antijoin),逻辑和性能表现完全不同,不能混为一谈。
看懂 EXPLAIN 中的 SemiJoin 策略标识
执行 EXPLAIN FORMAT=TRADITIONAL 后,关键要看 Extra 列是否出现半连接相关提示:
-
Using where; FirstMatch(class):表示对内表做了“首次匹配即退出”的短路优化,适合内表小、匹配率低的场景 -
Using where; LooseScan(class_num):利用索引去重扫描,要求内表关联字段有唯一索引或主键 -
Using where; DuplicateWeedout:先当普通 JOIN 执行,再用临时表去重——这是兜底策略,性能通常最差,说明优化器没找到更优路径 - 如果
Extra里只有Using index condition或空,但没出现上述关键词,大概率半连接根本没生效,得回头检查子查询结构或索引
手动干预半连接行为的实用手段
当发现优化器选了低效策略(比如 DuplicateWeedout),可有限度地引导它:
- 给子查询中的关联字段加索引——这是最直接有效的操作,比调参数管用十倍
- 用
/*+ SEMIJOIN(FIRSTMATCH, LOOSESCAN) */提示优化器优先尝试某策略(MySQL 8.0.22+ 支持) - 把
IN改写为显式INNER JOIN+DISTINCT,虽然多写几行,但执行路径完全可控,且能避免NULL值导致IN返回空结果的陷阱 - 禁用半连接仅作诊断用:
SET optimizer_switch='semijoin=off';,对比执行计划变化,确认是否真是半连接在起作用
真正容易被忽略的点是:半连接优化只解决“是否存在匹配”这一层问题,它不改变主表扫描范围。如果外层 WHERE 条件本身没走索引,再好的半连接也救不了慢查询。











