key为NULL但possible_keys有值,说明优化器识别到索引却拒绝使用;主因是外键字段类型/字符集不一致、ON子句含函数、最左前缀不匹配、驱动表过大触发BNL、optimizer_search_depth超限或覆盖索引建在驱动表上。

EXPLAIN里key为NULL但possible_keys有值,说明外键索引被忽略了
这代表优化器明确“看见”了索引(possible_keys非空),却拒绝使用(key为NULL)。外键约束本身不自动建索引,即使你手动建了,也常因三个硬性条件不满足而失效:字段类型不一致、字符集或排序规则不同、ON子句中用了函数或表达式。
- 用
SHOW FULL COLUMNS FROM table_name比对两边字段的DATA_TYPE和COLLATION_NAME,尤其注意VARCHAR(32)vsBIGINT、utf8mb4_0900_as_csvsutf8mb4_general_ci -
ON里只要出现UPPER()、DATE()、CONCAT()等任意函数,该侧字段索引立即作废——不是“可能不用”,是“根本不能用” - 外键索引若建在
(user_id, created_at)上,而查询只用user_id做JOIN,它仍能生效;但若建在(status, user_id)上,且ON只含user_id,则最左前缀不匹配,索引失效
驱动表结果集太大,优化器主动弃用NLJ改走BNL
即使被驱动表的user_id有完美索引,只要驱动表经过WHERE过滤后预估返回行数超过几千(MySQL通常阈值在2000–5000行),优化器就会放弃索引嵌套循环(NLJ),转而用块嵌套循环(BNL)——此时type变成ALL或index,Extra出现Using join buffer (Block Nested Loop)。
-
EXPLAIN中的rows值不准?先跑ANALYZE TABLE table_name更新统计信息 -
LIMIT对驱动表行数预估无影响——优化器看的是“可能扫多少”,不是“最终返几行” - 强制让小表当驱动表:对
INNER JOIN可用STRAIGHT_JOIN;对LEFT JOIN无法强制右表为驱动表,只能靠优化左表过滤条件压缩结果集
optimizer_search_depth超限,优化器根本没考虑你的索引路径
6张表JOIN时理论有720种连接顺序,MySQL默认optimizer_search_depth=6(实际穷举约72种),一旦超限,优化器直接启用启发式剪枝:优先保留带WHERE条件的表在左端,跳过大量组合——你建的索引再好,如果对应的最佳连接顺序没被搜索到,就等于不存在。
- 查当前设置:
SELECT @@optimizer_search_depth - 临时调优:会话级执行
SET optimizer_search_depth = 0,让优化器自动按表数动态限深(如8表时取min(8,7)=7),实测常将EXPLAIN耗时从秒级降到毫秒级 - 别迷信
FORCE INDEX——它只能指定某张表用哪个索引,不能控制多表间如何组合;真正要干预顺序,得用STRAIGHT_JOIN或拆成临时表分步执行
覆盖索引只对被驱动表有效,建错位置等于白建
覆盖索引(Extra: Using index)在JOIN中只对被驱动表起作用。因为驱动表要参与外层循环,必须访问完整行数据;而被驱动表只需按索引匹配+返回指定字段,无需回表。如果你把覆盖索引建在驱动表上,比如INDEX(status, id, name),但查询实际走的是id主键JOIN,这个索引大概率闲置。
- 正确做法:针对被驱动表,把
ON字段放联合索引最左,再追加SELECT中用到的列,例如INDEX(user_id, name, email) - 验证是否生效:
EXPLAIN中type为ref或eq_ref,且Extra含Using index - 别为了覆盖强行堆字段——索引越宽,维护成本越高,写放大越严重;优先保障
ON和强过滤条件字段

















