知识卡片

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的实际执行计划,而不是凭直觉预判。

参考来源

- 位置:《高性能MySQL:第3版》第6章"查询性能优化"6.5.1节"关联子查询" (源文件:_epub-src/OEBPS/Text/part0013.xhtml) - 结论依据:原文明确"MySQL的子查询实现得非常糟糕。最糟糕的一类查询是 WHERE条件中包含IN()的子查询语句……MySQL会将相关的外层表压到子查询 中,它认为这样可以更高效率地查找到数据行……根据EXPLAIN的输出我们 可以看到,MySQL先选择对file表进行全表扫描,然后根据返回的film_id 逐个执行子查询",直接说明IN()子查询被改写为相关子查询的具体机制及 其执行代价。 - 原始内容:MySQL会将相关的外层表压到子查询中,它认为这样可以更高 效率地查找到数据行……MySQL先选择对film表进行全表扫描,然后根据返回 的film_id逐个执行子查询。