MySQL IS NULL 会索引失效?深度解析 InnoDB 索引结构与优化器决策

陆景行
陆景行
发布时间:2026-10-16 | 4人浏览过
原创

很多开发者认为在 MySQL 中使用 IS NULL、IS NOT NULL 或 != 会导致索引失效并引发全表扫描,这其实是一个常见的误解。本文将深入 MySQL InnoDB 引擎内部,结合源码级工具与执行计划追踪,还原索引处理 NULL 值的真实逻辑,并解释优化器何时会真正选择全表扫描。

核心结论:IS NULL 本身不影响索引可用性

MySQL 官方文档明确指出:MySQL can perform the same optimization on col_name IS NULL that it can use for col_name = constant_value. 这意味着 IS NULL 和等值查询一样,完全可以使用索引进行快速查找。

导致“看似索引失效”的真正原因通常是回表成本过高。当 SQL 条件筛选出的数据量占总数据比例较大时(经验阈值约为 20%),MySQL 优化器会计算发现:通过二级索引定位主键后,再进行回表查询的数据页读取次数太多,总成本高于直接遍历全表。此时,优化器才会主动选择放弃索引,采用全表扫描。

案例验证:基础数据下的索引表现

首先构建一张测试表 user_info,包含自增主键、普通索引字段 name 以及冗余字段 age。

CREATE TABLE `user_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(11) DEFAULT NULL,
  `age` int(4) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `user_info` (`id`, `name`, `age`) VALUES 
('1', 'tom', '18'), 
('2', null, '19'), 
('3', 'cat', '20');

在上述少量数据下执行 SELECT * FROM user_info WHERE name IS NULL; 和 SELECT * FROM user_info WHERE name IS NOT NULL;,查看 EXPLAIN 结果:

可以观察到,即便使用了 IS NULL 和 IS NOT NULL,两行语句均使用了 index_name 索引,type 为 ref 或 range,并未出现全表扫描。

原理剖析:NULL 在索引树中的位置

为了理解为什么能用到索引,我们需要探究 NULL 值在 InnoDB B+ 树叶子节点是如何存储的。这里使用两种方式进行验证:工具解析与二进制文件分析。

使用 innodb_ruby 工具解析

安装并使用强大的 MySQL 底层分析工具 innodb_ruby 直接读取 .ibd 文件。

# 安装依赖
yum install -y rubygems ruby-devel
gem install innodb_ruby

# 解析表的主键索引(此处仅展示关键输出)
innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I PRIMARY index-recurse

解析 index_name 普通索引结构的输出如下:

$ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I index_name index-recurse
ROOT NODE #4: 3 records, 38 bytes
  RECORD: (name=:NULL) → (id=2)
  RECORD: (name="cat") → (id=3)
  RECORD: (name="tom") → (id=1)

关键发现:NULL 值确实被存储在索引树中。且在排序规则中,NULL 被处理为最小的值,位于索引链表的最左侧(B+ 树的最左端叶子节点)。

从二进制文件看索引记录

直接使用编辑器(如 UltraEdit)打开 user_info.ibd 文件,定位到对应的索引页数据页,可以看到索引记录的物理存储结构。展开其中几段关键字节:

  • ID 为 2 的记录(对应 name=NULL):其字段值部分为空,紧随其后的是主键 ID 0x00 00 00 02。
  • 双向链表指针:InnoDB 的叶子节点通过双向指针串联。ID 为 2 的记录头信息中包含指向前驱节点(最小记录 infimum)和后继节点(ID 为 3 的记录)的偏移量。

这证实了索引记录是通过指针以索引值排序串联在一起的,而 NULL 因其被定义为最小值,稳居索引起始位置。因此,优化器可以通过二分查找快速定位到最左侧,进而找到所有 NULL 值记录。

误区揭秘:为何会产生“索引失效”的错觉?

既然 NULL 存入了索引,为何大家常觉得 IS NOT NULL 会失效?

根本原因在于统计信息与成本估算。当表中存在大量非 NULL 数据时,WHERE name IS NOT NULL 可能覆盖了表中 80%~90% 的行。InnoDB 优化器在计算执行计划时,发现:

  1. 使用索引需要先从二级索引查出主键;
  2. 然后根据这些主键去聚簇索引(主键索引)中逐个取完整数据(即“回表”);
  3. 由于回表操作随机读取数据页较多,IO 成本激增。

当预估的回表总成本超过全表顺序扫描的成本时,优化器就会决定“不使用索引”。这不是索引功能本身的缺陷,而是基于成本的理性决策。

实验复现:大数量下的索引失效

为了直观看到这一过程,我们向表中插入 1000 条非 NULL 记录,使总行数达到 1003 行。此时,IS NULL 仅命中 1 行,而 IS NOT NULL 需返回 1002 行。

-- 批量插入 1000 条数据
DELIMITER //
CREATE PROCEDURE init_user_info() 
BEGIN 
	DECLARE indexNo INT;
	SET indexNo = 0;
	WHILE indexNo < 1000 DO
		START TRANSACTION; 
		insert into user_info(name,age) values (concat(floor(rand()*1000000000)),floor(rand()*100));
		SET indexNo = indexNo + 1;
	COMMIT; 
END WHILE;
END //
DELIMITER ;
CALL init_user_info();

现象观察:

  • name IS NULL:命中 1 行,远低于 20% 临界值,正常走索引。
  • name IS NOT NULL:命中 1002 行,覆盖率超 99%,触发全表扫描。

终极诊断:Optimizer Trace

我们可以通过 MySQL 5.6+ 提供的优化器追踪功能,清晰地看到优化器在做选择时的成本对比。

SET optimizer_trace="enabled=on";
EXPLAIN SELECT * FROM user_info WHERE name IS NOT NULL;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;

在输出的 JSON 追踪日志中,重点关注 range_scan_alternatives 部分:

{
    "range_analysis": {
        "table_scan": {
            "rows": 1004,
            "cost": 206.9   // 全表扫描成本较低
        },
        "analyzing_range_alternatives": {
            "range_scan_alternatives": [
                {
                    "index": "index_name",
                    "ranges": ["NULL < name"],
                    "rows": 1002,
                    "cost": 1203.4, // 索引+回表成本极高
                    "chosen": false,
                    "cause": "cost"   // 因为成本原因被舍弃
                }
            ]
        }
    }
}

结果显示,全表扫描成本仅为 206.9,而走索引预计耗时 1203.4。正是这个巨大的成本差异,导致了最终的“索引失效”现象。

总结

MySQL 中的 IS NULL 并不会破坏索引的有效性,NULL 值会被作为最小值存入 B+ 树中供快速检索。所谓的“索引失效”,本质是 MySQL 优化器在面对高覆盖率条件(如 IS NOT NULL 且非空数据占绝大多数)时,基于回表 IO 成本过高而做出的最优路径选择。

在实际开发与调优中,如果遇到这类情况,不要盲目怀疑索引机制,可尝试通过 EXPLAIN 或 optimizer_trace 分析具体成本构成。若必须强制使用索引(例如业务对延迟极度敏感且愿意承担额外 IO),可在 SQL 末尾添加 FORCE INDEX。

以上就是 MySQL IS NULL 使用索引及失效原理的详细内容,更多关于 MySQL 索引优化技巧的资料请关注本站其它相关文章!
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表