innodb_stats_method 是 InnoDB 控制索引列中 NULL 值统计方式的变量,影响 Cardinality 估算精度,进而导致优化器误判索引选择性:nulls_equal(默认)将所有 NULL 视为同一值,严重低估区分度,易使优化器弃用索引。

innodb_stats_method 是什么,为什么它会影响索引选择
innodb_stats_method 是 InnoDB 用来决定「如何统计索引列中 NULL 值的分布」的系统变量。它不控制索引是否能用,而是影响优化器对 Cardinality(索引区分度)的估算——而这个估算直接决定优化器会不会选这个索引。
比如一个 status 列有 100 万行,其中 95 万是 NULL,其余 5 万个值均匀分布在 A/B/C 上。如果优化器误以为这 95 万个 NULL 是同一个值,就会算出「平均每个值重复约 24 万次」,从而判定该索引区分度极低,大概率跳过它走全表扫描。
这个误判不是因为索引坏了,而是统计口径错了。
nulls_equal / nulls_unequal / nulls_ignored 的实际差异
三个取值对应三种 NULL 认知模型:
-
nulls_equal(MySQL 5.7 默认,且 5.7.22+ 强制固定):所有NULL算作一个值 →Cardinality被严重低估 → 优化器倾向不用索引 -
nulls_unequal:每个NULL都算独立值 →Cardinality被高估 → 优化器可能过度依赖索引,尤其在IS NULL查询时反而更准 -
nulls_ignored:完全跳过NULL行统计 →Cardinality只基于非空数据计算 → 对IS NOT NULL类查询更友好,但 MySQL 5.7.22+ 已禁用此选项
注意:SET GLOBAL innodb_stats_method = 'nulls_unequal' 在 MySQL 5.7.22 及之后版本无效,变量会被忽略并静默回退到 nulls_equal;8.0+ 同样写死为 nulls_equal。
为什么 is not null 很容易触发全表扫描
IS NOT NULL 本身可以走索引(例如 type: range),但前提是优化器认为它能过滤掉足够多的行。而 innodb_stats_method = nulls_equal 导致优化器把大量 NULL 当作“同一个高频值”,于是推断 IS NOT NULL 会命中绝大多数非空值(比如 5 万行),而这些值本身又可能很分散——此时优化器常判断「走索引回表成本 > 直接全表扫描」。
典型表现:
-
EXPLAIN SELECT * FROM t WHERE status IS NULL;→ 可能走ref(因 NULL 被当作单值,匹配明确) -
EXPLAIN SELECT * FROM t WHERE status IS NOT NULL;→ 常见type: ALL(因优化器预估要读取太多索引+回表行) - 即使
status上有索引,WHERE status != 'active'也大概率失效——NULL 不参与比较,但优化器仍受统计偏差拖累
真正可控的应对方式,而不是改 innodb_stats_method
既然 innodb_stats_method 在主流版本已不可调,就得从更实际的地方入手:
- 建表时尽量避免索引列允许
NULL:用NOT NULL DEFAULT ''或NOT NULL DEFAULT 0替代,业务层保证填充 - 对已有含大量 NULL 的索引列,执行
ANALYZE TABLE tbl_name;强制刷新统计信息(尤其在批量导入/删除后) - 观察
SHOW INDEX FROM tbl_name;中的Cardinality值是否明显偏低;若远小于实际非空值数量,说明统计失真已发生 - 必要时加覆盖索引:比如
INDEX (status, id),让IS NOT NULL查询能直接从索引中拿到所需字段,减少回表开销,提升优化器选索引意愿
最常被忽略的一点:统计信息不准的问题往往在数据量突增或 NULL 比例剧烈变化后才暴露,但监控项(如 Handler_read_key / Com_select 比值)和定期 ANALYZE 并未纳入运维 checklist。


















