@adamhand
2019-02-23T10:26:09.000000Z
字数 4034
阅读 1023
索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。
索引有不同的实现,对应着不同的模型。最常见的三种模型是:哈希表、有序数组和搜索树。
哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value。索引中的哈希表使用链地址法来处理哈希冲突,所以哈希表索引的数据结构是:数组+链表。
现在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,这时对应的哈希索引的示意图如下所示:
如果想查找某个人的名字,首先需要计算这个人身份证号的哈希值,根据哈希值找到数组对应的下标,然后再该下标对应的链表中寻找身份证号。
哈希表索引不是有序的,所以哈希索引做区间查询的速度是很慢的。试想要找身份证号在 [ID_card_X, ID_card_Y]
这个区间的所有用户,就必须全部扫描一遍。
所以,哈希表这种结构适用于只有等值查询的场景,即查找一个特定值,比如 Memcached
及其他一些 NoSQL
引擎。而有序数组在等值查询和范围查询场景中的性能就都非常优秀。
上面的例子如果使用有序数组来实现的话,示意图如下所示:
这个数组就是按照身份证号递增的顺序保存的。可以用二分法查找,时间复杂度是 O(log(N))
。
同时,这个索引结构支持范围查询。比如要查身份证号在 [ID_card_X, ID_card_Y]
区间的 User
,可以先用二分法找到 ID_card_X
(如果不存在 ID_card_X
,就找到大于 ID_card_X
的第一个 User
),然后向右遍历,直到查到第一个大于 ID_card_Y
的身份证号,退出循环。
可以看到,有序数组的查询效率很高,但是插入效率却不高,往中间插入元素时,后面元素都要挪动。所以,有序数组索引只适用于静态存储引擎。
如果上面的例子用二叉搜索树来实现,示意图如下:
上述索引的查找复杂度为O(log(N))
,但前提是要保证二叉搜索树的平衡性,为了保证这个特性,更新的时间复杂度也为O(log(N))
。
除了二叉搜索树,还可以使用多叉搜索树,这可以使得树高更低,每一次查询需要访问的数据就会更少,即访问磁盘的次数就会更少,效率会更高。
下面,分析一下InnoDB中的的索引模型。
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。
InnoDB 使用了 B+ 树索引模型。每一个索引在 InnoDB 里面对应一棵 B+ 树。
假设有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引。这个表的建表语句是:
mysql> 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),两棵树的示例示意图如下。
从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
那么,基于主键索引和普通索引的查询有什么区别?
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
。插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。那么,什么时候使用自增主键,什么时候不使用呢?
从上面可以看出,自增主键的插入数据模式,正符合了前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
除了考虑性能外,我们还可以从存储空间的角度来看。假设表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20
个字节,而如果用整型做主键,则只要 4
个字节,如果是长整型(bigint
)则是 8
个字节。显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
有没有什么场景适合用业务字段直接做主键的呢?还是有的。比如,有些业务的场景需求是这样的:
这就是典型的 KV 场景。
由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题
这时候我们就要优先考虑上一段提到的“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
聚簇索引是对磁盘上实际数据重新组织以按指定的一个或多个列的值排序的算法。特点是存储数据的顺序和索引顺序一致。一般情况下主键会默认创建聚簇索引,且一张表只允许存在一个聚簇索引。
聚簇索引默认是主键,如果表中没有定义主键,InnoDB 会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。如果已经设置了主键为聚簇索引而想要修改聚簇索引,必须先删除主键,然后添加想要的聚簇索引,最后恢复设置主键即可。
什么时候使用聚簇索引呢?
非聚簇索引引的记录的物理顺序和索引的顺序不一致。聚簇索引的叶节点就是数据节点,而非聚簇索引的叶节点仍然是索引节点。
在InnoDB 存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的额表称为索引组织表。在InnoDB存储引擎表中,每张表都有个主键(Primary key),如果在创建表时 没有显示地定义主键,则InnoDB存储引擎会按如下方式选择或创建主键:
当表中有多个非空唯一索引时,InnoDB 存储引擎将选择建表时第一个定义的非空唯一索引为主键。这里需要注意的是,主键的选择根据的是定义索引的顺序,而不是建表时列的顺序。
如下所示:
mysql> create table T(
a int not null,
b int null,
c int not null,
d int not null,
unique key(b), unique key(d), unique key(c));
上述表中,b
被首先声明为唯一索引,但是b
允许为空,所以b
不能作为主键索引;c
和d
中,虽然建表时先建立的c
,但是声明唯一索引时先声明的d
,所以d
为唯一索引。
InnoDB默认使用B+索引,B+索引中的所以可以分为两大类:主键索引和二级索引,二级索引又可以分为几类,具体如下:
参考:
Mysql InnoDB引擎 -索引组织表
mysql:InnoDB的主键采用聚簇索引,二级索引不采用聚簇索引
【Mysql优化】聚簇索引与非聚簇索引概念
深入理解四种数据库索引类型(- 唯一索引/非唯一索引 - 主键索引(主索引) - 聚集索引/非聚集索引 - 组合索引)
聚簇索引(Clustered Index)和非聚簇索引 (Non- Clustered Index)举例说明
聚簇索引与非聚簇索引(也叫二级索引)