mysql 5.6前in子查询重复执行致性能差,5.7+默认启用semijoin优化但需满足无关联、无group by等条件;未触发时应改写为join或exists,并注意NULL、去重及索引。

子查询被重复执行怎么办
MySQL 5.6 以前,IN 子查询常被物化为临时表并反复扫描,尤其在外层结果集大时性能急剧下降。5.7+ 默认启用 semijoin 优化,但仅对特定结构生效——必须是无关联子查询(即子查询不依赖外层表),且外层不能是 GROUP BY 或含聚合函数。
- 检查是否触发了 semijoin:执行
EXPLAIN,看select_type是否为SEMISPACE或DEPENDENT SUBQUERY - 若仍是
DEPENDENT SUBQUERY,说明子查询含外层引用(如WHERE id IN (SELECT user_id FROM logs WHERE logs.uid = users.id)),此时无法走 semijoin,应改写为JOIN - 强制关闭 semijoin 测试对比:加
/*+ NO_SEMIJOIN() */提示,观察执行计划变化
用 JOIN 替换 IN 子查询的边界条件
并非所有 IN 子查询都适合改 JOIN。关键看语义是否等价、去重逻辑是否一致、NULL 处理是否可接受。
-
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE status = 'active')→ 可安全转为INNER JOIN,且通常更快 - 若子查询可能返回
NULL(如SELECT id FROM users WHERE deleted_at IS NULL),而外层IN遇到NULL会整体返回空结果,JOIN则直接过滤掉,行为不同,需加IS NOT NULL显式约束 - 子查询含
DISTINCT或GROUP BY?JOIN 后需手动GROUP BY或DISTINCT去重,否则结果行数可能膨胀
EXISTS 比 IN 快的典型场景
当子查询结果集大、外层小,且只需判断存在性时,EXISTS 往往更优——它能在找到第一条匹配后立即短路退出,而 IN 通常需生成完整结果集。
- 适用前提:子查询带关联条件(如
EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid')) - 注意索引:确保子查询中关联字段(如
o.user_id)和过滤字段(如o.status)有联合索引,否则EXISTS也会全表扫描 - 避免
SELECT *在子查询中:写SELECT 1即可,MySQL 不关心返回值内容,只判断是否存在
临时表 + 索引人工干预
当子查询逻辑复杂、无法改写或优化器始终选错执行路径时,显式创建临时表并建索引是最可控的兜底方案。
- 用
CREATE TEMPORARY table tmp_ids AS SELECT DISTINCT user_id FROM events WHERE ts > '2024-01-01'提前物化结果 - 立刻在
tmp_ids上建索引:CREATE INDEX idx_user ON tmp_ids(user_id) - 外层查询改用
JOIN tmp_ids ON t.user_id = tmp_ids.user_id - 注意:临时表生命周期仅限当前连接,高并发下需考虑命名冲突,可用
CONNECTION_ID()动态构造表名
真正难处理的是子查询嵌套三层以上、又混用聚合与窗口函数的场景——这时候优化器基本放弃推导,人工拆解+物化几乎是唯一稳定手段。