当某一个SQL查询比较慢,分析后你可能就会说“给某个字段加个索引吧”。 到底什么是索引,索引又是如何工作的呢?
- 索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。一本500页的书,如果你想快速找到其中的某一个知识点,在不借助目录的情况下,那我估计你可得找一会儿。同样,对于数据库的表而言,索引其实就是它的“目录”。
索引的常见模型
索引的出现是为了提高查询效率,但是实现索引的方式却有很多种。
提高读写效率的数据结构很多,先介绍三种常见/较简单的数据结构:哈希表、有序数组和搜索树。
下面我主要从使用的角度,为你简单分析一下这三种模型的区别。
- 哈希表是一种以键-值(key-value)存储数据的结构。哈希的思路很简单,把值放在数组里,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置。
- 处理冲突的一种方法:拉出一个链表。
- 哈希表这种结构适用于只有等值查询的场景,比如Memcached及其他一些NoSQL引擎。(无序)
- 有序数组在等值查询 / 范围查询的场景中性能优秀。(如利用二分查找)
- 有序数组索引只适用于静态存储引擎,比如你要保存的是2017年某个城市的所有人口信息,这类不会再修改的数据。(更新不方便)
- 二叉搜索树也是课本里的经典数据结构了。
二叉树搜索效率最高,但实际大多数据库存储不使用二叉树。原因:索引既存在内存,还写到磁盘。
为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N叉”树。这里,“N叉”树中的“N”取决于数据块的大小。
以InnoDB的一个整数字段索引为例,这个N差不多是1200。这棵树高是4的时候,就可以存1200的3次方个值,这已经17亿了。考虑到树根的数据块总是在内存中的,一个10亿行的表上一个整数字段的索引,查找一个值最多只需要访问3次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
N叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。
- 数据库技术发展到今天,跳表、LSM树等数据结构也被用于引擎设计中。
你心里要有个概念,数据库底层存储的核心就是基于这些数据模型的。每碰到一个新数据库,我们需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景。
现在,我们一起进入相对偏实战的内容吧。
在MySQL中,索引是在存储引擎层实现的,所以不同存储引擎的索引的工作方式并不一样。而即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。
由于InnoDB存储引擎在MySQL数据库中使用最广泛,所以下面以InnoDB为例分析其中的索引模型。
InnoDB 的索引模型
在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。
InnoDB使用了B+树索引模型,数据都存储在B+树中。每一个索引在InnoDB里面对应一棵B+树。
假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引。
这个表的建表语句是:create table ``T(
**id** int primary key,
**k **int not null,
name varchar(16),
index (k))engine=InnoDB;
表中R1~R5的(ID,k)值:(100,1)、(200,2)、(300,3)、(500,5)和(600,6),两棵树的示例示意图如下:

图4 InnoDB的索引组织结构
从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
- 主键索引的叶子节点存的是整行数据。在InnoDB里,主键索引也被称为聚簇索引。
- 非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引。
根据上面的索引结构说明,我们来讨论一个问题:基于主键索引和普通索引的查询有什么区别?
- 如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
- 如果语句是select from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为*回表。
总结:基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
索引维护
B+树为了维护索引有序性,在插入新值的时候需要做必要的维护。
以上面这个图为例,
- 如果插入新的行ID值为700,则只需要在R5的记录后面插入一个新记录。
- 如果新插入的ID值为400,就相对麻烦了:需要逻辑上挪动后面的数据,空出位置。
- 而更糟的情况是,如果R5所在的数据页已经满了,根据B+树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。
**除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约50%。**基于上面的索引维护过程,我们来分析一下哪些场景下应该使用自增主键,而哪些场景下不应该。
- 自增主键是指自增列上定义的主键: NOT NULL PRIMARY KEY AUTO_INCREMENT。
- 适合自增主键的场景
(1)性能。自增主键的插入模式:每次插入都是追加操作,不涉及挪动其他记录,不触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
(2)存储空间。
假设表中确实有一个唯一字段,比如字符串类型的身份证号,那用身份证号 还是 自增字段做主键?
- 由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约20个字节,而如果用int做主键,则只要4个字节,如果是bigint则是8个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
- 适合用业务字段直接做主键的场景
- 只有一个索引;
- 该索引必须是唯一索引。
你一定看出来了,这就是典型的KV场景。
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
优先考虑“尽量使用主键查询”原则,直接将这个索引设为主键,可避免每次查询需要搜索两棵树。
小结
- 分析了数据库引擎可用的数据结构,
- 介绍了InnoDB采用的B+树结构,
- 为什么InnoDB要这么选择。
- B+树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。
- 由于InnoDB是索引组织表,一般情况下我会建议你创建一个自增主键,这样非主键索引占用的空间最小。但事无绝对,
- 我也跟你讨论了使用业务逻辑字段做主键的应用场景。
思考
对于上面例子中的InnoDB表T,
- 如果你要重建索引 k,你的两个SQL语句可以这么写 alter table T drop index k; alter table T add index(k);*
- 如果要重建主键索引,可以这么写:alter table T drop primary key; alter table T add primary key(id);
问题:上面这两个重建索引的作法,说出你的理解。如果有不合适的,为什么,更好的方法是什么?
为什么要重建索引?
索引可能因为删除 / 页分裂等原因,导致数据页有空洞。重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。- 重建索引k的做法是合理的,可以达到省空间的目的。
- 但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,你可以用这个语句代替 : alter table T engine=InnoDB。(《为什么表数据删掉一半,表文件大小不变?》将分析这条语句的执行流程)
