子查询被重复执行怎么办
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()动态构造表名
真正难处理的是子查询嵌套三层以上、又混用聚合与窗口函数的场景——这时候优化器基本放弃推导,人工拆解+物化几乎是唯一稳定手段。
