知识卡片
MySQL把IN()子查询悄悄改写成逐行相关子查询
内容
WHERE film_id IN (SELECT film_id FROM ... WHERE actor_id=1)这类写法
看起来会先执行子查询拿到一份ID列表,再对外层表按这份列表做一次IN()
过滤(就像手工先查出ID列表、再拼进外层查询里那样高效)。但MySQL的
查询优化器实际做的事恰好相反:它会把外层表的条件”压”进子查询内部,把
整条语句改写成等价的WHERE EXISTS (SELECT ... WHERE film_actor.film_id
= film.film_id AND actor_id=1)——因为子查询依赖外层表的film_id才能
关联,优化器认定子查询不能先独立算出来,于是执行计划变成对外层表做
全表扫描,然后针对外层表的每一行都重新跑一次这个子查询(EXPLAIN里会
显示为DEPENDENT SUBQUERY)。外层表小的时候这个开销不明显,但外层表
一旦很大,这种”逐行触发一次子查询”的执行方式代价会急剧上升。这个陷阱
的根源不是”IN()加子查询”这种写法本身有语法缺陷,而是这个具体优化器
实现选择的重写策略和直觉预期不一致,规避办法是手动把它改写成等价的
JOIN(INNER JOIN ... USING(film_id) WHERE actor_id=1),把关联判断交
给优化器更擅长处理的JOIN路径,通常能显著提速;也可以用GROUP_CONCAT()
现算出逗号分隔的ID列表拼进IN()里。这个案例的更普遍教训是:SQL写法”看起
来该怎么执行”和数据库实际选择的执行计划可能完全不同,遇到子查询、
IN()这类语义上容易有多种等价改写的结构,判断该不该用某种写法之前,
最可靠的办法是直接看EXPLAIN的实际执行计划,而不是凭直觉预判。