-- 1) IN子查询
SELECT * FROM table WHERE id IN (SELECT tid FROM detail WHERE code = 1);
-- 2)EXISTS
SELECT * FROM table WHERE EXISTS (SELECT 1 FROM detail WHERE code = 1 AND tid = table.id);
-- 3)拆分
-- 步骤1:查出常量列表
SELECT tid FROM detail WHERE code = 1; -- 结果:(1,2,3,4,5,6,7)
-- 步骤2:常量IN查询
SELECT * FROM table WHERE id IN (1,2,3,4,5,6,7);
-- 4)JOIN写法
SELECT table.*
FROM table
INNER JOIN detail ON table.id = detail.tid
WHERE detail.code = 1;
① IN子查询(原生写法)
先查子查询,把结果集堆到一个临时表里,然后再拿外层表去匹配。缺陷是临时表可能没索引,匹配起来全表扫。而且MySQL优化器是个黑盒,版本不同执行计划天差地别,线上容易踩坑。
② 常量IN
先查出ID列表,再塞到第二条SQL的IN里。这时候ID列表被当成常量数组,MySQL直接走索引范围扫描(range),执行计划100%可控。代价是多一次网络请求,但换来了绝对的稳定。
③ EXISTS
外层每扫一行,就执行一次 子查询去判断存不存在。它吃索引,要求子查询表关联字段必须有索引;同时要求外层结果集本身很小(比如已经过滤到只剩几十条),否则外层全表扫是灾难。
④ JOIN
把两张表关联起来,可以返回两边的字段。但要注意一对多关系会产生重复数据,可能需要加DISTINCT去重。性能取决于驱动表选择和关联字段索引,可以用STRAIGHT_JOIN强制指定驱动顺序。
最终选型一句话:
· 列表小(<1000条)→ 用你的常量IN,最稳
· 需要查右表字段 → 用JOIN
· 外层已过滤到很少数据,且子查询是大表 → 用EXISTS
· 原生子查询IN → 能不用就不用,优化器不靠谱
缺陷
① 原生子查询IN的缺陷
· 物化临时表 无索引(5.6以前):子查询结果集堆到临时表里,外层每扫一行都要全表扫这个临时表,数据量一大直接炸。
· 优化器抽风:即使5.6后有半连接优化 ,但只要子查询里带GROUP BY、DISTINCT、UNION、聚合函数,优化器立马退化为DEPENDENT SUBQUERY,外层每行执行一次子查询,复杂度O(n²)。
· NULL值陷阱 :子查询结果里如果混进NULL,整个IN条件返回UNKNOWN,结果集直接变空,业务上极难排查。
· 执行计划版本波动:MySQL 5.5、5.6、5.7、8.0对子查询优化策略都不一样,升级MySQL版本可能导致SQL突然变慢。
② 常量IN(拆成两次查询)的缺陷
· SQL长度受限 :ID列表太长(超过max_allowed_packet,默认4MB)直接报错;超过eq_range_index_dive_limit(默认200)优化器会放弃索引走全表扫描 。
· 网络IO翻倍:原来是1条SQL变2条,网络延迟翻倍,高并发下RT会上升。
· 数据一致性风险:两次查询之间,子查询数据可能发生变化(比如ID被删除),导致两次结果逻辑不一致(除非在事务内或业务允许脏读)。
· 列表去重负担:如果子查询结果有重复ID,应用层要自己去重,不然IN里重复值会额外消耗 索引匹配次数。
· 分页不友好:如果外层要分页LIMIT,必须先查所有ID再分页,可能查了一万个ID最后只取10条,浪费严重。
③ EXISTS的缺陷
· 外层必须全表扫描 :EXISTS是外层驱动内层,如果外层没有其他过滤条件(如WHERE status=1),它会全扫外层整个表,每条都去子查询判断一次,外层表大就必死。
· 子查询索引要求严苛:子查询关联字段必须建索引,否则每行执行一次全表扫内层表,直接灾难。
· 依赖子查询陷阱:如果写成了SELECT * FROM table WHERE EXISTS (SELECT tid FROM detail WHERE table.id = detail.tid AND code = 1),EXPLAIN里看到DEPENDENT SUBQUERY就意味着外层每行执行一次,没法提前终止。
每次子查询的SQL文本都一样,但里面table.id的值每次都不同,所以没办法提前把子查询结果算好复用,必须一行一行地代入、执行。
DEPENDENT------子查询依赖外层的当前行值,无法独立执行。
如果外层table有10万条数据,子查询detail每次都能走索引(0.1毫秒查到),那10万次就是10秒。但如果把table先过滤到10条(比如加个WHERE table.status=1),那子查询只执行10次,0.01秒就完事。
执行次数完全被外层行数绑架。外层扫多少行,它就执行多少次。优化器没有任何办法"提前终止"或"批量合并"------因为每行的table.id都不一样,优化器没法预判。
④ JOIN的缺陷
· 一对多产生重复数据:如果右表有多条匹配,左表数据会被重复返回 ,业务必须用DISTINCT或GROUP BY去重,++增加排序和临时表开销++ 。
· 驱动表选错全盘皆输:优化器选错驱动表(该小表驱动大表时选反了),直接导致全表扫大表,性能暴跌。虽然可以用STRAIGHT_JOIN强制,但需要人工分析执行计划。
· 返回字段宽导致内存爆炸:JOIN会把两表字段都缓存到join_buffer,如果字段多、数据量大,内存占用飙升。
· 锁范围扩大:关联查询在RR隔离级别下,可能同时锁两个表的索引间隙,死锁概率成倍增加。
总结
没有银弹。
常量IN输在列表长度和两次查询的一致性;
原生IN子查询输在优化器黑盒;
EXISTS输在外层全表扫和索引依赖;
JOIN输在数据膨胀和驱动表选择。
选哪个取决于数据量级、索引设计和业务容忍度,底线是:绝对不用原生IN子查询,其他三种按场景选,但必须先用EXPLAIN看执行计划