exists子查询必须带关联条件才能走索引,否则退化为全表扫描;正确写法需在子查询where中显式关联外层字段(如c.id = o.customer_id),且内层表须有以关联字段开头的复合索引(如index idx_id_status (id, status)),并用explain format=tree验证索引是否生效。

EXISTS子查询必须带关联条件才能走索引
不写关联字段的EXISTS,比如WHERE EXISTS (SELECT 1 FROM customers WHERE status = 'active'),本质是“只要customers表里有一条active就全表返回”,根本不会用到customers.status上的索引——因为不需要查具体哪一行,只判是否存在。真正能触发索引的是相关子查询,即子查询WHERE里必须出现外层表字段,例如c.id = o.customer_id。
常见错误是列名写错或表别名混淆,比如把o.customer_id写成o.id,或误以为customers表有customer_id字段而实际主键叫id。一旦关联失效,优化器会退化为物化临时表或全表扫描。
子查询表的索引要覆盖关联字段 + 过滤条件
EXISTS能否走索引,取决于子查询表上有没有能同时满足「关联匹配」和「条件过滤」的索引。例如:
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM customers c
WHERE c.id = o.customer_id AND c.status = 'active');
这里需要的是以c.id开头的联合索引,比如INDEX idx_id_status (id, status),而不是单列INDEX idx_status (status)——后者无法加速c.id = o.customer_id这一步关联查找。
- 如果
customers只有status索引,执行计划里type会是ALL或index,不是ref/eq_ref - 若
id是主键,那PRIMARY KEY(id)本身就能支撑c.id = o.customer_id,此时只需确保status在索引中靠后(如(id, status)),避免额外回表 - 不要建
(status, id)——最左前缀原则下,c.id = ...无法使用该索引
EXPLAIN FORMAT=TREE才是验证索引是否生效的唯一可靠方式
传统EXPLAIN对子查询只显示<subquery2></subquery2>,看不出它到底走了哪个索引、访问类型是什么。MySQL 8.0+ 必须用EXPLAIN FORMAT=TREE,才能看到类似这样的输出:
-> Index lookup on c using idx_id_status (id=o.customer_id) (cost=0.35 rows=1)
如果看到的是Table scan on c或Using temporary; Using filesort出现在子查询分支下,基本确认索引没生效。
注意:ANALYZE SELECT ...比EXPLAIN更准,它实际执行并统计真实I/O和行数,但会带来副作用,生产环境慎用。
NULL值会让NOT EXISTS逻辑出错,且无法用索引规避
当关联字段(如o.customer_id)可能为NULL时,NOT EXISTS行为会反直觉:因为NULL = c.id永远为UNKNOWN,子查询查不到匹配行,导致NOT EXISTS返回TRUE——相当于把所有customer_id IS NULL的订单都包含进结果,即使业务上它们不该被选中。
这种问题跟索引无关,加再多索引也解决不了。必须显式排除:AND o.customer_id IS NOT NULL,或者改用LEFT JOIN ... WHERE c.id IS NULL写法。
容易被忽略的是:即使customers.id有非空约束,只要orders.customer_id允许NULL,这个陷阱就存在。











