最近写了一系列关于MySQL索引相关的文章,帮大家系统全面地把索引这块的知识丰富串联起来,需要回顾或学习这方面的知识的朋友可以看看前面的文章。
今天这篇文章给大家分析和示例一下,MySQL中,当查询条件为IS NULL
或 IS NOT NULL
时,哪些情况会走索引,哪些情况下又不会走索引。最终结论可能与大家的直觉有所不同。
这篇文章会涉及到前面讲到的B+树相关的知识,请参考《MySQL之进阶:一篇文章搞懂MySQL索引之B+树》一文。
下面我们直接通过具体的实例来看看当查询条件为IS NULL
或 IS NOT NULL
时,索引的使用情况。
实例一:少量数据,使用索引
这里采用的MySQL数据库版本为8.0.18,后续实例均采用此版本。
创建一个test表,创建语句如下:
sql
CREATE TABLE test (
id INT PRIMARY KEY,
col1 INT,
col2 INT,
INDEX idx_col1 (col1)
);
-- 添加两条数据
insert into test values(1,null,1);
insert into test values(2,null,2);
其中在col1
列上创建了索引。
实例演示
此时,我们来看IS NULL
和 IS NOT NULL
是否走索引。
yaml
mysql> explain SELECT * FROM test WHERE col1 IS NULL \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ref
possible_keys: idx_col1
key: idx_col1
key_len: 5
ref: const
rows: 1
filtered: 100.00
Extra: Using index condition
通过上面的EXPLAIN
语句,我们可以看到,当查询条件为IS NULL
,且对应查询条件字段上有索引时,MySQL使用索引来处理IS NULL
查询条件。
再来看IS NOT NULL
查询条件:
yaml
mysql> explain SELECT * FROM test WHERE col1 IS NOT NULL \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: range
possible_keys: idx_col1
key: idx_col1
key_len: 5
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index condition
当查询条件为IS NOT NULL
时,同样使用了索引。
在上面的示例中,我们可以看到无论是IS NULL
或 IS NOT NULL
都使用索引。
实例二:大量数据,少量NULL值
该实例依旧采用上述表结构,初始化3万数据,其中col1
中的NULL值约占5%。
sql
mysql> select count(*) from test;
+----------+
| count(*) |
+----------+
| 30000 |
+----------+
在上述情况下,我们再来看看两个查询语句的索引使用情况。
IS NULL
查询条件:
yaml
mysql> explain SELECT * FROM test WHERE col1 IS NULL \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ref
possible_keys: idx_col1
key: idx_col1
key_len: 5
ref: const
rows: 1551
filtered: 100.00
Extra: Using index condition
可以看到,IS NULL
查询条件使用了索引。
IS NOT NULL
查询条件:
yaml
mysql> explain SELECT * FROM test WHERE col1 IS NOT NULL \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ALL
possible_keys: idx_col1
key: NULL
key_len: NULL
ref: NULL
rows: 30570
filtered: 50.00
Extra: Using where
此时,IS NOT NULL
查询条件在执行的过程中并没有使用索引,而是采用了全表扫描。
这是因为,当表中的大部分数据都满足 col1 IS NOT NULL
的条件(例如超过一半以上的记录符合条件,本实例中为95%),MySQL 的查询优化器可能会认为使用索引的代价高于全表扫描的代价,从而选择全表扫描。
实例三:大量数据,大量NULL值
该实例依旧采用上述表结构,初始化3万数据,其中col1
中的NULL值约占95%。
在上述情况下,我们再来看看两个查询语句的索引使用情况。
IS NULL
查询条件:
yaml
mysql> explain SELECT * FROM test WHERE col1 IS NULL \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ref
possible_keys: idx_col1
key: idx_col1
key_len: 5
ref: const
rows: 14957
filtered: 100.00
Extra: Using index condition
可以看到,IS NULL
查询条件使用了索引。
IS NOT NULL
查询条件:
yaml
mysql> explain SELECT * FROM test WHERE col1 IS NOT NULL \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: range
possible_keys: idx_col1
key: idx_col1
key_len: 5
ref: NULL
rows: 1558
filtered: 100.00
Extra: Using index condition
可以看到,IS NOT NULL
查询条件也使用了索引。
通过上面的三个实例,我们可以看到,无论是IS NULL
或IS NOT NULL
都是有可能使用索引的。这也证明了网络上一概而论并不正确。
下面,我们就具体分析一下IS NULL
和IS NOT NULL
是否走索引的核心决定性因素。
索引是如何存储NULL值的?
在分析MySQL是否使用索引的原因之前,我们先要了解一下针对NULL值,在索引中是如何存储的。
在MySQL的InnoDB引擎中,聚簇索引一般是以主键作为存储依据,主键列的值不能为NULL。所以,针对这种情况,不存在NULL值存储的问题。
对于非聚簇索引中的NULL值,在大多数数据库实现中,NULL值在索引结构中有以下特点:
- B+树的排序:NULL值通常被看作是最小值,因此索引存储中,NULL会排在树的最左边。
- 链表形式存储:B+树叶子节点存储数据的引用,而叶子节点之间是顺序链接的,也就是说,包含NULL的记录会集中在最左侧,沿着链表可以顺序读取这些记录。
- 查找NULL值:当查询列值为NULL时,数据库的索引机制可以通过扫描树的最左侧开始查找所有NULL值。
关于聚簇索引和非聚簇索引的相关知识,可以参考《学习MySQL绕不开的两个基础概念:聚集索引与非聚集索引》一文。
索引失效的原因
通过上面关于NULL值索引的存储,我们可以看到,在MySQL的InnoDB数据引擎中,NULL也是"有序"的,也可以通过索引(从最左侧开始)进行查找的。大多数关系型数据库也都支持对 NULL
值进行索引。
而真正决定IS NULL
或 IS NOT NULL
是否走索引的前提是索引能够显著降低执行成本。在MySQL中,查询优化器会根据执行成本决定是否使用索引,而不是单纯地因为 NULL
或 IS NULL
的条件导致索引失效。
例如:
- 如果通过索引查询的结果集非常大(例如大多数记录都为
NULL
),那么索引命中的数据需要大量回表才能获取完整的记录,这种情况下,优化器可能会选择全表扫描,因为全表扫描的代价会更低。 - 如果通过索引可以显著减少数据访问和回表的次数(例如查询结果集很小或者覆盖索引被使用),MySQL一般会选择使用索引。
关于回表的内容可参考《MySQL中,什么是回表查询,如何避免和优化?》一文。
因此,关于NULL值索引失效,有以下相关结论:
- NULL 值可以走索引,但是否使用索引取决于具体的执行成本。
IS NULL
和IS NOT NULL
本身不会直接导致索引失效,优化器会根据数据量和数据分布动态选择索引或全表扫描。- 非聚簇索引通常需要回表,回表成本过高时,优化器可能放弃索引。
- 查询性能优化需要结合具体的场景和数据分布分析,而不能简单地说某些条件下索引会失效。
小结
通过上面的示例我们可以看到,在MySQL中,查询条件使用IS NULL
或 IS NOT NULL
,理论上都会走索引。部分不走索引的情况也是因为优化器判断全表扫描的效率要高于使用索引,才导致放弃使用索引,而这与查询条件为IS NULL
或 IS NOT NULL
本身没有直接关系,只和执行成本有关。
所以,针对IS NULL
和 IS NOT NULL
是否走索引,不能一概而论,还是要回归到数据构成本身,当然,原则上MySQL是会选择走索引的。