innodb_stats_method默认值nulls_equal将所有null视为同一值,大幅拉低索引列基数估算,导致优化器误判索引低效而倾向全表扫描;5.7.22+版本已禁用nulls_unequal和nulls_ignored,只能通过analyze table、强制索引或改用not null default设计来补救。

innodb_stats_method 怎么让优化器误判索引价值
MySQL 优化器决定是否走索引,核心依据是 Cardinality(基数)估算值——它反映索引列的“区分度”。而这个值由 innodb_stats_method 控制怎么数 NULL。默认值 nulls_equal 把所有 NULL 当作同一个值,直接拉低基数估算。
比如一个 status 列有 100 万行,其中 95 万是 NULL,剩下 5 万均匀分布在 A/B/C。优化器按 nulls_equal 算:总共就 4 个“不同值”(NULL、A、B、C),平均每个值重复 25 万次 → 判定该索引低效 → 跳过索引选全表扫描。
-
nulls_unequal(每个 NULL 算独立值):基数被高估,IS NULL 查询反而更可能走索引,但 MySQL 5.7.22+ 已禁用该设置,执行SET GLOBAL innodb_stats_method = 'nulls_unequal'会静默失败 -
nulls_ignored(完全跳过 NULL 统计):对IS NOT NULL类查询友好,但同样在 5.7.22+ 被移除 - 你只能接受
nulls_equal的现实,所以必须靠其他手段补救
EXPLAIN 显示 key=NULL 不代表索引不能用
EXPLAIN 中 key 为 NULL 只说明优化器这次没选它,不是物理上走不了。真正卡住的是成本估算逻辑:当 NULL 占比超过约 20%,优化器常认为全表扫描比索引回表更快。
验证方法很简单:
- 先查索引定义:
SHOW INDEX FROM t WHERE Key_name = 'idx_status';,确认Null列值为YES - 再强制走索引:
EXPLAIN SELECT * FROM t FORCE INDEX (idx_status) WHERE status IS NULL;,对比rows是否明显下降 - 如果强制后
rows大幅减少,说明索引本身可用,只是被低估了;此时运行ANALYZE TABLE t;更新统计信息,有时就能让优化器自动改选
联合索引里只要最左列是 NULL,整个前缀匹配就断了
InnoDB 的 B+ 树依赖有序性,而 NULL 不参与排序,也不写入键值序列。联合索引 (a, b) 要求从左开始提供可定位的非空值;一旦 a IS NULL,B+ 树就找不到起始页位置,后续 b 的有序性彻底失效。
典型表现:
-
WHERE a IS NULL AND b = 1→ 基本不走索引,或只扫所有a IS NULL的叶子节点再逐行过滤b -
WHERE a = 1 AND b IS NULL→ 可走,a = 1定位子树后,在该子树内线性检查b IS NULL -
WHERE a IS NULL OR b = 1→ 几乎必然退化为全表扫描,OR 加 IS NULL 是双重否定
NOT NULL + 默认值能绕过大部分 NULL 带来的开销
允许 NULL 的索引列,每条记录多占 1 字节空值标记位,导致 key_len 增大、单页存的索引项变少、B+ 树层级可能升高。这不是理论开销,而是真实影响 I/O 次数。
更关键的是语义清晰:
- 避免
!=、NOT IN因 UNKNOWN 导致结果为空 -
COUNT(col)和COUNT(*)行为不再割裂 - 联合索引中不会因某列 NULL 就触发“半失效”逻辑
- 用
DEFAULT ''或DEFAULT 0替代 NULL,既保持业务表达能力,又让索引统计和查询路径回归可预测
真正难处理的从来不是 NULL 本身,而是它在统计模型、B+ 树结构、SQL 语义三层同时引入的不确定性。修复往往不在查询层,而在建表时那一句 NOT NULL DEFAULT ...。











