MySQL索引
索引是存储引擎实现的,用于快速找到一条记录的一种数据结构。
索引的目的是为了提高数据查询的效率,让服务器快速地定位到表的指定位置,就像书的目录一样。对于数据库的表而言,索引其实就是它的“目录 ”。
所以索引对于良好的性能来说非常关键,尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。
索引优点:
1、索引大大减少了服务器需要扫描的数据量
2、索引可以帮助服务器避免排序和临时表
3、索引可以将随机IO变为顺序IO
索引的结构
在 InnoDB 中,表都是根据主键顺序以索引的织表。InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+树中的。
每一个索引在 InnoDB 里面对应一棵 B+ 树
B+树
由二叉查找树演化而来的为磁盘等直接存取的辅助设备而设计的一种平衡多路查找树。
一个m叉B+树特点:
- 1、每个节点中子节点的个数不能超过 m,也不能小于 m/2;
- 2、根节点的子节点个数可以不超过 m/2,这是一个例外;
- 3、m 叉树只存储索引,并不真正存储数据,这个有点儿类似跳表;
- 4、通过双向链表将叶子节点串联在一起,这样可以方便按区间查找(每个页中的记录也是通过双向链表进行维护的);
- 5、一般情况,根节点会被存储在内存中,其他节点存储在磁盘中。
B+树是B树的一个变种,相比与B树有以下区别:
1、非叶子结点只存储键值信息
2、存在同一个键值,即出现在内部节点,又出现在叶子节点中的情况
3、所有的叶子结点之间都有指向下一个叶子结点的指针
4、数据记录都存放在叶子结点中,非叶子结点中不存储数据记录信息。
5、B+树存放相同的数据记录时高度大大低于B树,查询时的IO操作远比B树少,效率更高。
InnoDB存储引擎中有页(page)的概念,页是其磁盘管理的最小单位,在InnoDB存储引擎中默认每个页的大小为16K,B+树将每个节点的大小设置和页大小一样,都为16K,因此读取一个节点,只需要一次IO。
做一个推理:InnoDb表的存储引擎中一页的大小为16k,一般表的主键类型占用8个字节,指针占用8个字节,那么一页可以存放16kb/(8b*8b)=1000个键值(关键字),所以一个高度为3的B+树可以存储1000^3即10亿条记录。查询时只需要两次IO操作就可找到要查找的记录。大大节约了查询时间。(主键索引)
B-Tree索引意味着所有的值都是按顺序存储的,并且每一个叶子页到跟的距离相同。B-Tree索引能够加快访问数据的速度,因为存储索引不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找;通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页。
注意:B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据;若是联合索引,则索引对多个值进行排列的依据是创建索引时列的顺序,所以索引还可以用于查询中的ORDER BY操作。
可以使用B-Tree索引的查询类型:全键值、键值范围、键前缀查找(只适用于根据最左前缀的查找,最左原则),所以B-Tree对如下类型的查询有效:
(1):全值匹配
- 和索引中的所有列进行匹配,查询条件和索引的所有列相同且顺序相同,且查询条件为相等
(2):匹配最左前缀
- 只使用索引中的第一列
(3):匹配列前缀
也可以只匹配某一列的值的开头部分
(4):匹配范围值
对索引列进行范围匹配
(5):精准匹配某一列并范围匹配另外一列
但是如果查询中有某个列的范围查询,则其右边所有列都无法使用索引进行查询
(6):只访问索引的查询
即查询只需要访问索引,而无须访问数据行(覆盖索引)
聚集索引
聚集索引并不是一种单独的索引类型,而是一种数据存储方式。
InnoDB的聚集索引实际上就是在同一个结构中保存了BTree索引和数据行。
因为无法同时将数据行存放在两个不同的地方,所以一个表只能有一个聚集索引。也可以理解为聚集索引就是表。
当表有聚集索引时,它的数据行实际上存放在索引的叶子页中。聚集表示数据行和相邻的键值紧凑地存储在一起。
聚集索引的每一个叶子节点都包含了主键值,事务ID,用于事务和MVCC的回滚指针以及所有的剩余列。
InnoDB通过主键聚集数据,也就是说被索引的列是主键。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚集索引。
优点:
1、可以将相关数据保存在一起,通过
2、数据访问更快,聚集索引将索引和数据保存在同一个B+树中。
3、用主键值查询即可使用覆盖索引
4、减少了当出现行移动或者数据页分裂时二级索引的维护工作,在移动行时无需更新二级索引叶子节点保存中的主键值。
缺点:
1、数据插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。
2、更新聚集索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置
3、基于聚集索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能会面临 “页分裂” 的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳改行。
4、聚集索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候
5、二级索引可能比想要的要大,因为在二级索引的叶子节点包含了引用行的主键列。
二级索引需要两次索引查找,因为二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值(并以此作为指向行的“指针”)。所以通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对用的主键值,然后根据这个值去聚集索引中查找到对应的行。需要进行两次BTree查找。
与MyISAM存储引擎的区别:
MyISAM是非聚集存储引擎,其按照数据插入的是顺序存储在磁盘上。MyISAM的主键索引和其他索引在结构上没有什么不同。主键索引就是一个名为PRIMARY的唯一非空索引
聚集索引主键的选择:
1、推荐使用自增列作为主键,这样可以保证数据行是按顺序写入,对于主键做关联的性能也会更好。
在插入数据时将每条记录都存储在上一条记录的后面,当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入到新的页中。
2、避免随机的(不连续且值的分布范围大,如UUID)数作为主键,会导致聚集索引的插入变得完全随机,会导致页分裂和碎片,造成插入行的耗时更长;另外主键字段更长,会导致索引占用的空间也很大。
在插入数据时,因为新行的主键值不一定比之前的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置,如果是已有数据的中间位置。
缺点:
(1)、在插入之前要为主键寻找目标页,因此要先在磁盘找到并从磁盘读取目标页到内存中,会导致大量的随机IO
(2)、因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间,页分裂会导致移动大量数据,一次插入最少得修改三个页而不是一个页
(3)、由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。因此在把数据插入到聚集索引之后,也许需要做一次OPTIMIZE TABLE来重建表并优化页的填充。
主键索引数据分布结构:
age二级索引
一条SQL语句的执行流程:
select * from tableName where age between 17 and 66; age非主键,有单列索引
1、在age的二级索引树上查找到age为17的一个节点记录,取得叶子节点上的主键值id=5
2、再到主键索引树上查到id=5对应的数据行记录(回表一次)
3、接着在age的二级索引树上取下一个叶子节点,判断此叶子节点上age的值是否小于等于66,如果满足条件,拿到对应的主键值id=30
4、再到主键索引树上查到id=30对应的数据行记录(回表一次)
3、4循环,直到叶子节点上age的值不满足条件,将得到的数据行返回给MySQL服务器。可以看到每条记录都要回表查询一次。因为查询结果所需要的数据只有主键索引上有,所以不得不回表。
(这也是联合索引中如果非最右索引列范围查询,之后索引列无法使用索引的原因,因为会遍历叶子节点)索引下推可以避免回表优化。
前缀索引和索引选择性
索引选择性:
不重复的索引值和表记录总数的比值,最大为1,索引的选择性越高则查询效率越高。因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引和主键索引的选择度为1,所以唯一索引和主键索引的性能是最好的。
select count(distinct columnName)/count(*) from tableName; //查询索引列的选择
前缀索引:
为一个很长的字符串列创建索引,如果将此列字符串全部索引,那么索引占用的磁盘空间很大,每个索引节点能存放的键值变少,查询时IO次数变多,查询效率变低。通常的做法是索引开始的部分字符, 这样可以大大节约索引空间,从而提高索引效率。
对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引。对于这类列创建索引要选择足够长的前缀以保证较高的选择性,同时又不能太长。前缀索引是一种能使索引更小,更快的有效办法,但另一方面也有其缺点:
1、使用前缀索引可能会增加扫描行数,这会影响到性能
2、MySQL无法使用前缀索引做覆盖扫描,每次都需要回表查询索引列的完整值判断是否符合条件
2、MySQL无法使用前缀索引做ORDER BY和GROUP BY(因为索引列不全)。
如何确认创建前缀索引时,选择多长的前缀?
select count(distinct left(columnName,n))/count(*) from tableName;
一般大于95%即可。
创建前缀索引:alter table table_name add index idx_name(column_name(n));
倒序存储:遇到前缀的选择性不好,可以判断一下如果倒序存储的话前缀选择性较好,可以考虑倒序存储。查询时用reverse函数转为正序值。
索引合并(index merge)
MySQL5.0之前,一条语句中一个表只能使用一个索引,无法同时使用多个索引(由于和全表扫描/只使用一个索引的速度比起来,去分析两个多个索引B-Tree更加耗费时间,所以绝大多数情况下数据库都是只用一个索引)。但是从5.1开始,引入了 index merge 优化技术,对同一个表可以使用多个索引
官方文档:The Index Merge access method retrieves rows with multiple range scans and merges their results into one. This access method merges index scans from a single table only, not scans across multiple tables. The merge can produce unions, intersections, or unions-of-intersections of its underlying scans.
MySQL :: MySQL 8.0 Reference Manual :: 8.2.1.3 Index Merge Optimization
1、索引合并是把几个索引的范围扫描合并成一个索引。
2、索引合并的时候,会对索引进行并集,交集或者先交集再并集操作,以便合并成一个索引。
3、这些需要合并的索引只能是一个表的。不能对多表进行索引合并。
如果where中可能有多个条件(或者join)涉及到多个字段,它们之间进行AND或者OR,那么此时就有可能会使用到index merge技术。
简单来说,就是MySQL在分析执行计划时发现走单个索引的过滤效果都不是很好,于是对多个索引分别进行条件扫描,然后将多个索引单独扫描的结果进行合并的一种优化操作。
如果出现了索引合并,EXPLAIN关键字输出的执行计划type列会显示 index_merge,key列展示使用的所有的索引,key_len展示的用到的索引中长度最长的索引的长度。
索引合并的方式分为三种:intersection、union和sort_union
1、 intersection,Extra显示Using intersect(A,B);
- 索引合并交集算法对所有使用的索引执行同时扫描,并生成从合并索引扫描接收到的行序列的交集
- 注意:只有AND条件的每个索引列的选择性都很低时,才会使用索引合并交集算法优化
- 否则只会使用其中一个索引或者使用索引下推进行查询优化
- 适用条件:WHERE子句的多个条件之间的关系为AND,并且每个条件是以下之一时
- 1、多列之间AND关系,且每个条件列上都有单独的索引
- 2、主键上的任何范围条件
2、union,Extra显示Using union(A,B);
- 索引合并联合算法对所有使用的索引执行同时扫描,并生成从合并索引扫描接收到的行序列的并集
- 适用条件:WHERE子句的多个条件之间的关系为OR,并且每个条件是以下之一时:
1、多列之间AND关系,且每个条件列上都有单独的索引
- 2、主键上的任何范围条件
3、sort_union,Extra显示Using sort_union(A,B);
- 索引合并排序联合算法,适用于当WHERE条件转换为多个范围条件时,且以OR组合在一起,并且不适用于索引合并联合算法
- 测试,OR条件的索引列有使用联合索引时,会出现sort_union
- 索引合并排序联合算法与索引合并算法的区别:
- 索引合并排序联合算法必须首先获取所有行的行 ID,并在返回之前按照行ID对它们进行排序。
- 原因:归并排序进行归并的时候所有的需要归并的子集是需要排序好的,当OR两边有一边查询到的结果不是按照主键排序好的,就会需要进行排序
优点:
索引合并,让一条sql可以使用多个索引。对这些索引取交集,并集,或者先取交集再取并集。从而减少从数据表中取数据的次数,提高查询效率。
注意:
1、相同模式的sql语句,可能有时能使用索引,有时不能使用索引。是否能使用索引,取决于mysql查询优化器对统计数据分析后,是否认为使用索引更快。因此,单纯的讨论一条sql是否可以使用索引有点片面,还需要考虑数据。
2、index merge使得我们可以使用到多个索引同时进行扫描,然后将结果进行合并。听起来好像是很好的功能,但是如果出现了 index intersect merge,那么一般同时也意味着我们的索引设计得不太合理,因为 index intersect merge 是可以通过建立 复合索引进行更一步优化的。
3、另外index merge还会造成死锁的可能,如:事务1持有主键锁,等待二级索引idx_order上的锁,而事务2持有二级索引idx_order上的锁,等待主键锁,从而造成死锁。
联合索引
为每列单独创建索引,虽然MySQL会使用索引合并的策略来使用多个单列索引来定位执行的行,但是却没有一个单列索引时非常有效的。
当服务器出现多多个索引做相交操作时(多个AND条件),通常意味需要一个包含所有列的联合索引。
最左前缀原则:查询条件精确匹配索引的左边连续一列或几列,查询才能完全使用联合索引;
如果索引列为ABC,查询条件为AC,那么只有A条件会使用索引,C条件需要在服务器层进行where过滤。
联合索引的索引列的顺序问题?
在一个多列BTree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或降序进行扫描,以满足精确符合列顺序的ORDER BY,GROUP BY和DISTINCT等子句的查询需求。所以,联合索引的列顺序至关重要。
原则:将选择性最高的列放在索引最前列。但不是绝对的。可以通过跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。
另外 ,还要考虑Where子句中的排序、分组和范围条件等其他因素。如虽然某列选择性不是最高的,但是这列作为单独的条件出现的很频繁,可将此列作为联合索引的最前列。
如果查询条件中第一列的选择性接近1,或者等于1,那么where条件后的查询就不必建立联合索引了。
注意:如果MySQL使用某个索引进行范围查询(>,<,between),就会停止匹配范围列可以用到索引,但是范围列后面的列无法用到索引。如果是多个等值条件查询(in),则没有这个限制。
TODO,举例说明 in走了索引,可以使用覆盖索引证明?
对于联合索引,索引列中中间列使用了模糊查询,存储引擎会使用索引下推技术进行查询优化。
索引条件下推 (index condition pushdown )
即索引条件下推给存储引擎,在Mysql5.6的版本上推出,用于优化查询。
这个优化技术关键的操作就是将与索引相关的条件(部分WHERE条件能使用索引中的字段)由MySQL服务器向下传递至存储引擎,减少全行读取的次数,从而减少IO操作。
索引下推可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
当使用explan进行分析时,如果使用了索引条件下推,Extra会显示Using index condition
对于InnoDB表,ICP仅用于二级索引。
在不使用“索引条件下推”的情况下如何进行索引扫描:
1、获取下一行,首先读取二级索引叶子节点信息,然后通过叶子节点保存的主键回表查找并读取整个表行(每条记录回表一次)。
2、返回给MySQL服务器,服务器运用适用于此表的部分WHERE条件判断数据是否符合,符合将行放在结果集中
使用“索引条件下推”,扫描将像这样进行:
1、获取下一行,首先读取二级索引叶子节点信息
2、存储引擎通过判断索引叶子节点信息是否符合MySQL服务器传递的有关该索引的条件
3、如果满足条件,存储引擎才使用索引叶子节点保存的主键回表查找并读取整个表行(每条记录回表一次)并返回给MySQL服务器。如果不满足条件,往下扫描索引的下一个叶子节点
4、服务器运用适用于此表的其余WHERE条件判断数据是否符合,符合将行放在结果集中
索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
1、客户端发送一条查询给服务器
2、服务器先查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果。否则进入下一阶段
3、服务器进行SQL解析、预处理再由优化器(选择成本最小)生成对应的执行计划
4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询(执行器)
5、将结果返回给客户端
索引下推经常使用的场景:
- 联合索引,非最右字段范围查询,其余索引列使用索引下推减少回表
- where条件包含主键和索引列的查询,主键条件使用索引下推减少回表
索引条件下推的好处:
1. 数据copy
减少了InnoDB层返回给server层的数据量,减少了数据copy。
2. 随机读取
对于二级索引的扫描和过滤,减少了回primary key上进行随机读取的次数
3. 记录锁
记录锁是在InnoDB层完成的,比如如果是select for update语句,就会发现index_condition_pushdown会大大减少记录锁的个数。
默认情况下,索引条件下推处于启用状态,关闭索引条件下推:
SET optimizer_switch = 'index_condition_pushdown=off';
覆盖索引
如果一个索引包含(或者说覆盖)所有查询的字段的值,就称为覆盖索引。
MySQL查询的列是联合索引的索引列+主键列的子集。
即索引的叶子节点中包含了所有要查询的列,不需要再通过主键索引查询数据行(回到主键索引树搜索的过程,我们称为回表)。
优点(核心SQL优先考虑覆盖查询):
1、索引条目远小于数据行大小,如果只需要读取索引,那MySQL就会极大地减少数据访问量,减少缓存的负载和IO读取次数
2、因为索引是按照列值顺序存储的,范围查询会比随机从磁盘读取每一行的数据的IO要少得多
3、InnoDB的二级索引在叶子节点保存了行的主键值,所以如果二级索引能够覆盖查询,则可以避免对主键索引的二次查询。
当发起一个被索引覆盖的查询,在EXPLAIN的Extra列可以看到Using index。
覆盖索引只能覆盖那些访问索引中部分列(可以包含主键)的查询。
应用:利用延迟关联和主键覆盖索引解决分页查询页码很大查询的问题
分页如果翻页到比较靠后时查询也可能缓慢,如:LIMTI 100000,10; 因为随着偏移量的增加,MySQL需要花费大量的时间来扫描需要丢弃的数据。一个比较好的策略是使用延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联原表获得需要的行。这样可以减少MySQL扫描那些需要丢弃的行数。如:
select xxx from profiles INNER JOIN
select <primary key col> FROM profiles where x.sex='M' ORDER BY rating LIMIT 100000,10) as x USING (<primary key col>);
使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果:
1、通过排序操作 2、按索引顺序扫描(EXPLAIN的type列为 index)
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但是如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机IO,因此按索引读取数据的速度通常要比顺序地全表扫描慢。
所以希望MySQL可以使用一个索引既满足排序,又用于查找行。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。
如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时(MySQL优化器默认JOIN表为第一张表),才能使用索引做排序,ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL都需要执行文件排序(filesort)操作,而无法利用索引排序。
有一种情况下ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候,如果where子句或者ORDER BY子句中对这些列指定了常量,就可以使用索引进行排序。如:表rental表有组联合索引idx_rental(rental_date,inventory_id,customer_id),则sql:select xxx from rental where rental_date='2019-04-14' ORDER BY inventory_id,customer_id;可以使用索引idx_rental排序。即使ORDER BY子句不满足索引的最左前缀的要求,这是因为索引的第一列被指定为了一个常数。
常见索引失效场景
1、被索引字段使用了表达式计算或函数。所以被索引字段不能是表达式的一部分,也不能是函数的参数。
2、被索引字段发生了隐式类型转换。MySQL在SQL语句执行过程中会自动地将SQL语句中与原字段类型不匹配的值进行类型转换,如被索引字段是字符串类型,而查询时条件使用的是数字类型,MySQL的策略是将索引列字符串转换为数字之后再比较。函数作用于表字段,索引失效。
(亲测只有索引列是字符串,条件是数字类型时会导致索引失效;而索引列是数字类型,条件是字符串类型时索引没有失效)
原因:索引的使用依赖于对整个BTree索引数的遍历,而索引树的遍历依赖于索引数底层叶子节点的有序性;如果被索引字段使用了表达式计算,函数计算,隐式类型转换后,有可能这个字段新的排列顺序和原来在索引树的叶子节点层的排列顺序不一样了;当SQL语句被执行时,SQL执行器无法判断原来的索引树是否还能被检索使用,索引SQL执行器会放弃使用该索引。
3、在like关键字后使用了左模糊匹配,'%XXX'。
4、被使用的索引字段不是联合索引的最左字段。
原因:MySQL中索引树的检索遵循了最左匹配原则,BTree索引叶子节点的有序性也是建立在最左匹配的基础上的,如果直接使用索引键的中部或者后部进行SQL查询,就违背了最左匹配原则,导致SQL执行器无法使用索引树进行检索,索引SQL执行器会放弃使用该索引。
FAQ
1、唯一索引需要注意:
唯一索引是通过给索引列添加唯一约束来实现的,除此之外与其他二级索引并没有什么不同。
官方的文档中明确说明在唯一索引中是允许存在多行值为NULL的数据存在的。A UNIQUE index permits multiple NULL values for columns that can contain NULL.
因为在扫描唯一索引树来确定是否会发生冲突的时候,源码有这段注释:
If the secondary index is unique, but one of the fields in the n_unique first fields is NULL, a unique key violation cannot occur,since we define NULL != NULL in this case。
因此在创建属于唯一索引的列时,最好指定字段值不能为空
2、NOT IN和<>两类操作无法走索引?
当not in的数据记录很少或者查询记录总数时,会使用索引。
NOT IN或<>两类操作使用二级索引的成本远超于全表扫描的成本,查询优化器按照成本选择"最优执行计划",导致查询不走二级索引。但不能因此就彻底判断NOT IN或<>两类操作不能走索引
3、where条件列的顺序和联合索引列的顺序一致才会使用联合索引?
不必一致
4、不满足联合索引的最左前缀就不会使用索引?
不是绝对,如果MySQL 认为使用此索引进行查询成本最小,才会走索引,所以还是要满足最左前缀原则。
abc索引,在索引下推优化的时候,条件为a和c也会使用索引(如果满足a的记录很少时)
5、isNull走不走索引?
is null 走索引,is not null 不走索引
可以对存在null值的列创建索引,对此列做is null和=查询时都走索引,对联合索引一列为null时也走索引。
索引不会包含有 NULL 值的列?
只要列中包含有 NULL 值都将不会被包含在索引中,复合索引中只要有一列含有 NULL 值,那么这一列对于此复合索引就是无效的。所以在数据库设计时不要让字段的默认值为 NULL
官方文档:MySQL :: MySQL 5.7 Reference Manual :: B.3.4.3 Problems with NULL Values
索引列为NULL在索引树中的结构时什么样的?