文章目录
- 概述
- MySQL 索引类型
- MySQL 索引方法
- BTREE 方法
- HASH 方法
- 主键构成的索引结构
- 主键索引的优点
- 主键索引的缺点
- 依赖顺序插入
- 更新代价高
- 索引使用指南
- 索引树回顾
- 索引树排序规则
- 最左前缀法则
- 最左前缀法则的产生依据
- 最左前缀法则延申
- 字段书写顺序不影响最左前缀法则
- 最左前缀法则总结
- 索引字段匹配时不要进行操作
- 大于小于符号范围查询对于索引后面字段的影响
- 其他的规则
- 综合评估索引是否使用
- 值辨别度低的字段不使用索引
- 通配符开头的模糊查询不使用索引
- 不等于操作不使用索引
概述
在日常开发过程中,为 MySQL
表中的字段建立索引是我们常用的性能优化手段。使用开发工具添加索引也很方便,使用次数多了闭着眼睛都能完成操作。
但是细心一点的小伙伴会发现,索引并不是那么简单:
- 除了普通索引或者唯一索引,还有哪些类型的索引,它们的作用分别是什么呢?
BTREE
,HASH
索引方法的适用场景分别是什么呢?- 为什么字段上已经建立了索引,但是实际执行包含该字段的
SQL
时却没有走索引? - 索引有哪些使用规则呢?
下面请跟随着这篇文章,一起得到这些问题对应的答案。
MySQL 索引类型
MySQL
的索引类型,主要包括:
NORMAL
:普通索引,普通索引使用方法没有做特殊限制,因此应用范围比较广UNIQUE
:唯一索引,在普通索引的基础上,增加了唯一约束功能。唯一约束表示建立在这种索引上的字段值将会在表中唯一(一个或多个字段联合唯一)FULLTEXT
:全文索引,必须建立在**字符或文本类型(如varchar
)**的列上,可以实现在大文本中搜索指定关键词的功能- 注意,使用全文索引时,必须使用
match
和against
关键字进行操作
- 注意,使用全文索引时,必须使用
SPATIAL
:空间索引,必须建立在空间数据类型(如geometry
)且非空的列上,可以实现空间数据查询的功能
上面就是 MySQL
所有的索引类型了,在实际使用中,需要结合实际情况需要来选择合适的索引类型。
MySQL 索引方法
MySQL
的索引方法,主要包括 BTREE
和 HASH
。
顾名思义,BTREE
方法,就是通过构建 B+
树的方法来组织索引结构;而 HASH
方法,就是通过构建哈希表的方法来组织索引结构。
BTREE 方法
BTREE
方法,是通过构建 B+
树的方法来组织索引结构的。例如有这样一张表:
CREATE TABLE `user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',`name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',`age` smallint NOT NULL COMMENT '年龄',`register_date` date NOT NULL COMMENT '注册日期',`sex` tinyint NOT NULL COMMENT '性别',`address` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '地址',`phone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '电话',PRIMARY KEY (`id`),KEY `idx_name_age_registerdate` (`name`,`age`,`register_date`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
可以看到,在 user
表中,有一个由 name、age、register_date
三个字段联合组成的普通索引。现在假设表中有一些这样的数据:
那么由 BTREE
方法组织的这个索引的结构示意(图只为示意,请不要较真名字的真实字典序和实际的 B+
树插入逻辑)图为:
可以看到,这就是一颗 B+
树,且索引关键字之间的字段值与其在数中的实际顺序的对应关系一目了然,这张图在后面讲解索引规则的时候也还会用到。
HASH 方法
HASH
方法(下面简称哈希索引)是通过构建哈希表的方法来组织索引结构的,解决哈希冲突的方法为链地址法。
既然哈希索引是基于哈希表实现的,那么它就继承了哈希表的所有优点和缺点。那么我们可以得出:
- 哈希索引只在等值查询时才有效,无法在范围查询时生效,也不能应用于排序
- 哈希索引不支持部分索引列匹配查找,通俗地说就是,如果想要用哈希索引,就要用全,而不是只用一部分字段
- 当哈希碰撞的情况很严重时,对哈希索引的维护和查询操作性能都会降低
主键构成的索引结构
InnoDB
存储引擎主键字段构成的索引结构,是在 UNIQUE
索引类型 + BTREE
索引方法基础上,将实际数据存储在了叶子节点上。这样的索引结构,我们称为主键索引,或者聚簇索引。
聚簇,这个词的含义,是指为了提高某个属性(或属性组)的查询速度,把这个或这些索引上具有相同值的元组几种存放在连续的物理块。
在这里,是指索引与数据保存在同一个结构中,因此这里的聚簇,指的就是索引与数据的聚簇。
请注意,主键索引(或者说聚簇索引)并不是一种索引类型,而是一种将索引与数据保存在一个结构中的存储方式。
使用 InnoDB
作为存储引擎的表,都是基于主键索引来构建的,所以这就是为什么在使用 InnoDB
存储引擎时,一定要定义一个主键的原因。
如果没有定义主键,那么 InnoDB
会选择一个 UNIQUE
索引类型的索引作为主键索引,如果没有,InnoDB
会定义一个隐式的主键来作为主键索引。
总之,使用 InnoDB
作为存储引擎的表,一定会有一个主键,如果开发者没有定义,那么 MySQL
将会自己指定或者创建一个隐式列来作为主键。
这一小节的标题是主键构成的索引结构,其实这个标题的名字可能不太好理解,可能换成主键索引结构是大家普遍能接受的说法。
但是我并没有这样做,因为主键索引是一个很突兀的概念,它不是一种索引类型,也不是一种索引方法,而是一种索引与数据共存的存储方式。
主键索引的优点
主键索引的优点,通常是与非主键索引进行对比得出的。最主要的一点就是在查询数据行时,主键索引在大多数场景下比非主键索引高效。
因为如果使用了主键索引,那么在 B+
树中搜索到对应的叶子节点后,可以直接获得该索引值对应的数据行。
而如果使用的是非主键索引,那么在 B+
树中搜索到对应的叶子节点后,还需要通过叶子节点上存储的数据指针(或者主键值)再进行一次寻址(或者根据主键在主键索引中再进行一次搜索),才能得到真正的数据行。
总的来说,就是在通常情况下,主键索引会比非主键索引少一次查找数据行的过程。
主键索引的缺点
凡事都有两面性,主键索引也不例外。
依赖顺序插入
主键索引的插入需要依赖顺序插入,否则会发生页分裂而严重影响性能。在往 InnoDB
主键索引中插入数据时,有以下三个规则:
- 根据
B+
树的性质,主键值最终都会写入到叶子节点中,而InnoDB
将会把叶子节点写入到数据页中 InnoDB
页与页之间的顺序是由主键的大小顺序决定的,新创建的页中的主键值应该比之前创建页中的主键值都大,且页中每条新写入的主键值应该比已经写入的主键值都大InnoDB
默认会将新写入的数据放在最新创建的页中
所以结合上面的规则可以得出,InnoDB
隐式地约定了最新写入的主键需要比之前写入的所有主键值都要大。
那么如果新写入的数据的主键值不是当前最大的主键值,那么InnoDB
为了维护上面的规则,将会在之前创建的页中寻找这个主键值应该存放的位置。那么如果旧页已经满了的情况下,将会把这一页中的所有数据拆分为两个页,或者合并临近的未满的页中,这种拆分-合并页的现象就称之为页分裂。
在发生页分裂时,InnoDB
将会把当前表锁住,等页分裂完成后,再释放锁,所以页分裂是一种比较影响性能的现象。
更新代价高
当我们需要更新主键值时,就可能涉及到主键值存放位置的变动,就有可能出现页的拆分和合并,即有可能发生页分裂。所以主键的更新代价是比较高的,在日常开发中一般也会禁止更新主键值。
索引使用指南
在实际开发过程中,主键索引的用法相对固定,除主键索引外用的比较多的是由 BTREE
方法构建的索引,所以我们接下来的讨论内容是由 BTREE
方法构建的非主键索引。在此章节中出现的索引,如无特殊说明,都指代由 BTREE
方法构建的非主键索引。
索引树回顾
让我们再回顾一下索引的数据存储示意图:
基于上面这张图,我们来梳理一下索引的使用规则
索引树排序规则
在介绍索引的使用规则之前,我们先了解一下索引树的排序规则。索引树的排序规则,主要指的是索引树节点的关键字的排序规则,而在这里,关键字代表的就是索引字段值,那么索引树的排序规则实际上指的就是索引字段值的排序规则。
上面是由 name
、age
、register_date
三个字段组成的索引树,那么这颗索引树的排序规则是什么样的呢?
例如,我们现在有如下四条数据:
那么,排序的规则为:
- 首先按照
name
进行排序,由于**“小美”** = “小美” = “小美”<
“美美”,则有:
- 当
name
相同时,再按照age
进行排序,由于22
<48
=48
,则有:
- 当
name
和age
都相等时,再按照register_date
进行排序,由于2018-07-10
<2018-10-01
,则有:
- 整合上面的所有顺序,则最后的顺序为:
由上面的流程我们可以得知,索引字段值的顺序,正是索引字段依次比较的结果。有了这个概念,那么理解后面的索引使用规则就会容易得多。
最左前缀法则
最左前缀法则,可能是大家最耳熟能详的索引使用规则了,规则大家也都很清楚,就是使用同一个索引的多个索引字段作为查询条件时,必须从索引字段顺序的左边开始依次(且不能跳过字段)进行值匹配,如果从左边开始有哪个字段没有出现在查询条件中,那么从这个字段开始,后面的字段查询都不能使用索引。
最左前缀法则的产生依据
为什么会有最左前缀法则,它产生的依据是什么呢?例如,我想跳过 name
字段,直接使用 age
字段作为索引树的查询条件,这样为什么不能使用索引呢?
我们知道,如果数据集有序,那么我们只要使用二分查找就能轻松把查询性能提升几个数量级;反之,如果在无序的数据集中进行查询,那就只能把所有数据都遍历一遍了。利用索引树中的数据有序的性质,才叫真正地使用索引。
那么我们仔细观察一下,在跳过 name
字段后,剩下两个字段还有序吗?很显然,排除掉 name
字段后的剩下两个字段值并不是有序的,即整个索引树在排除掉 name
字段后,并不是一个有序的数据集,所以索引就不能提升查询性能了。
最左前缀法则延申
最左前缀法则,其实不仅仅在索引字段之间有效,在同一个字段的前缀查询中也是生效的,即字段的前缀查询也满足最左前缀法则,也可以使用索引。当然前提是得满足字段间的最左前缀
例如,在本文讨论的索引结构中,我们使用如下的几个 SQL
也是满足最左前缀法则的:
-- 单独对 name 进行前缀查询
select * from user where name like '小%';-- 对于 name 进行等值查询,对 age 进行前缀查询
select * from user where name = '小美' and age like '2%';-- 对于 name 和 age 进行等值查询,对 register_date 进行前缀查询
select * from user where name = '小美' and age = '22' and register_date like '2015%';
但是在如下几个 SQL
中是不满足最左前缀法则的,即不能(或只能使用一部分)使用索引:
-- 对于 name 进行后缀查询,完全不能使用索引
select * from user where name like '%美';-- 对于 name 进行等值查询,但是 age 进行后缀查询,那么只能用到 name 的部分
select * from user where name = '小美' and age like '%2';-- 对于 name 和 age 进行等值查询,但是 register_date 进行后缀查询,那么只能用到 name + age 的部分
select * from user where name = '小美' and age = '22' and register_date like '%05';
当然,这里的最左前缀,是建立在索引前面的字段是进行的等值查询(=
)的前提下进行讨论的。索引前面的字段不是等值查询的情况将在后面的章节中进行讨论。
字段书写顺序不影响最左前缀法则
在我初学 MySQL
的时候,有人曾经问过我下面的这条 SQL
会走索引吗?(还是以本文中的索引为例):
-- 查询条件
select * from user where age = '22' and name = '小美' and register_date = '2015-03-05'
由于当时对于 MySQL
知识的一知半解,我回答了一个不能,现在想想仍为当时的自己感到尴尬。因为当时虽然知道最左前缀匹配,但是却不知道 MySQL
的架构中有分析器和优化器的存在,也就不知道其实查询条件中的字段书写顺序其实并不会影响最左前缀法则。
写到这里回忆起了这件事情,所以把它写下来,也算是记录一下在变强这条道路上踩过的一个脚印吧!
最左前缀法则总结
最左前缀法则,是索引最重要的使用规则之一,也是制定高性能索引策略的基础。在日常开发过程中,我们也要遵守最左前缀法则,来尽量符合索引的使用条件。
这里需要划重点:利用索引树中数据有序的性质,才叫真正地使用索引。这是判断索引能不能生效的关键条件,这个结论也会在后面的章节中反复用到。
索引字段匹配时不要进行操作
再使用索引字段进行匹配时,不要在索引字段上进行操作。操作包含了一切需要计算才能得到结果的动作,包括表达式计算、函数、类型转换等。原因也很简单,因为包含了计算,那么每条数据的索引字段值都需要计算才能得到结果,那么就只能每条数据都遍历一遍了。
例如下面的操作是不能(或只能使用一部分)使用索引的:
-- 在等值匹配 name + age 时,age 上进行了一个数学运算,此时只能使用索引的 name 部分
select * from user where name = '小美' and age -1 = 21;-- 在等值匹配 name 时,name 上进行了一个字符串拼接的操作,此时是不能走索引的
select * from user where CONCAT(name, '01') = '小美01'
--
要想改写包含操作的 SQL
来实现对应的功能,有两种做法:
- 把操作从索引字段侧转移到常量侧:在第一个
SQL
中,把-1
操作转移到右侧来,即age = 21+1
- 建立函数索引:在第二个
SQL
中,先执行创建函数索引的语句,添加函数索引
-- 在 name 列上增加一个函数索引,索引的字段值为 concat(`name`, '01')
ALTER TABLE `user` ADD INDEX `idx_name_concat_01` ((concat(`name`, '01'))) USING BTREE;
再执行第二个 SQL
,就可以使用上面创建的函数索引了。
大于小于符号范围查询对于索引后面字段的影响
前面我们在讨论最左前缀法则时,特意提到讨论的前提是建立在索引前面的字段是进行的等值查询(=
)下的。那么这一部分中将会讨论建立在索引前面的字段进行范围查询时,对后面的索引部分的影响。
在使用索引前面的字段进行大于(>
)、小于(<
)符号的范围查询时,该字段后面的索引部分将不会被使用。
以这个叶子节点举例,假设我有以下 SQL
语句:
-- name 进行等值查询,age 进行大于符号的范围查询,register_date 使用等值查询时,age 后面的索引部分将不会被使用
select * from user where name = '王小美' and age > 25 and register_date = '2014-04-25'-- name 进行等值查询,age 进行小于符号的范围查询,register_date 使用等值查询时,age 后面的索引部分将不会被使用
select * from user where name = '王小美' and age < 30 and register_date = '2014-04-25'
上面两个 SQL
,索引并没有使用完全,原因就是 age
字段使用了大于(>
)、小于(<
)符号的范围查询。如果将大于(>
)、小于(<
)符号改成 between...and...
就可以使用索引了:
-- 使用 between...and... 可以使用索引
select * from user where name = '王小美' and age between 1 and 30 and register_date = '2014-04-25'
其他的规则
综合评估索引是否使用
划重点,MySQL
其实并不会按照死板的规则来约定走或者不走索引,而是要根据检索比例,表数据量等因素综合评估是否走索引,常见的需要综合评估的查询操作有 >
、<
、>=
、<=
、in
、or
。
例如下面两个 SQL
:
-- 这个查询条件,从最左前缀法则来看,应该是会走索引的,但是实际情况是它走的全表扫描
-- 原因就是这个查询条件对于过滤数据几乎没有帮助,因为表中所有的数据都符合条件
-- 所以在这种情况下,由于使用索引还会带来回表操作的开销,所以性能还不如直接全表扫描
select * from user where name > '0' ;-- 这个查询条件是会走索引的,因为符合这个查询条件的数据比较少,检索比例比较低
select * from user where name > '王' ;
所以,日常开发过程中在对 SQL
进行性能分析时,不能一味地套用各种规则,而是要根据实际情况(如符合条件的数据量大小)来综合评估索引的使用情况。
值辨别度低的字段不使用索引
在一个数据列中,如果数据值的取值范围非常有限的情况下,重复度就会很高,这也就造成了只使用这一列作为数据的辨别条件时,数据与数据之间的辨别度很低,这样的列就称为值辨别度低的字段。
这样的列在日常开发中也非常常见,例如用户表中的性别字段,一般来说只有 2
个或者 3
个取值,那么性别字段就可以称为值辨别度低的字段。在这样的字段上建立索引,一般来说是得不偿失的,因为在大多数情况下来说,MySQL
都不会选择使用该字段对应的索引。
原因也很简单,这样的字段的筛选条件过于简单,在表数据量大的情况下符合条件的数据量通常也会很大(例如性别字段中,男女两种取值的数据一般各占 50%
),那么与其使用索引还会带来回表操作的开销,不如直接全表扫描还能获得更高的性能。
is null
或者 is not null
查询条件也是一样的道理,一般来说也不会使用索引。
但是这也不绝对,因为如果在某个取值的数据在表中占比很少(假设在某个系统中,女性用户只占 1%
),且这个字段符合使用索引的条件时,还是会使用索引的。
通配符开头的模糊查询不使用索引
当使用通配符开头的模糊查询时,例如 name like '%小美'
或者 name like '%小%
这样的条件时,是不会使用索引的。原因很简单,违反了最左前缀法则。
不等于操作不使用索引
像常见的 !=
、<>
、not in
、not like
、not exists
操作都不会使用索引。个人猜测原因主要是:
MySQL
认为符合不等于条件的数据量比较大- 索引树中如果使用不等于条件,那么将无法使用索引树有序的性质