我们都知道数据库的索引是用来提升检索速度的,但是索引究竟内部是怎么工作的呢,就来说说这个话题8
索引就像书的目录一样,帮助我们更快的查找到我们需要的数据。
索引的常见类型
索引的种类有很多种,接下来主要介绍三种常见,也比较简单的数据结构,分别是哈希表,有序数组和搜索树。
然后从使用的角度分析一下,三者的区别:
- 哈希表
哈希表是一种kv存储数据的形式,只要输入待查找的值,比如说是key,就可以找到value。
但如果多个k对应一个v,那么就会拉出一个链表。
那么我们该如何在这种情况下,找到我们需要的那个v呢,可以看如下解释。
假如有一张表,只有身份证号和姓名,需要根据身份证号找到姓名
这时候我们发现,ID-card-n2和ID-card-n4计算出的哈希值都为N,并且N对应着一张链表,那么,假如我们要利用ID-card-n2找出name2,该怎么办呢?
首先,将ID-card-n2通过哈希函数计算出N,然后按照顺序遍历找到User2。
有序哈希,插入不方便,区间查询方便,无序哈希,插入方便(指的是顺延插入),区间查询不方便。
所以,哈希表这种结构适用于等值查询的场景。
- 有序数组
如果以上的情况,我们用有序数组实现的话,示意图如下:
假设身份证账号没有重复,数组按照底层去保存,如果要查找ID-card-n2对应的名字,用二分法就可以查询到,这个时间复杂度是logN、
并且这个索引结构支持范围查询。而且查询效率也是相当高的。但是如果插入,就很麻烦,因为每一个都需要往后瞬移一个单位。
所以,有序数组只能进行静态存储引擎,就是那种不会修改的数据。
- 搜索树
二叉搜索树是非常经典的数据结构,也用上面的例子,效果如下:
二叉搜索树的特点是:每个节点的左儿子小于父节点,父节点又小于右儿子。
当然为了维持O(log(N))的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更 新的时间复杂度也是O(log(N))。
为了能够让一个查询更少的读取总存储,应该查询的时候访问更少的数据块,所以就不能使用二叉树,而是多叉树,至于多少叉,取决于数据块的大小,但是总体都满足大小从左往右递增。
以InnoDB的一个整数字段索引来说,N差不多是1200,当树高为4的时候,就可以存储1200的3次方个值,但是考虑到树根 是在内存的,所以访问速度相当快,只需要访问三次硬盘。
- 总结
在mysql中,索引是在存储引擎层实现的,所以没有统一的索引标准,即不同存储引擎的索引方式是不同的,以下以常用的InnoDB来说一下。
InnoDB的索引模型
在InnoDB中,表都是根据主键顺序以索引的形式发放的,这种存储形式的表称为索引组织表。
InnoDB使用了B+树索引模型,所以数据都是存储在B+树的。
每一个InnoDB索引都有一个B+树。
假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引。
建表语句如下:
mysql> create table T3(
id int unique key,
k int not null,
name varchar(16),
index (k))engine=InnoDB;
Query OK, 0 rows affected (0.03 sec)
然后插入语句
mysql> insert into T3 (id,k,name)values(100,1,'R1');
Query OK, 1 row affected (0.00 sec)
mysql> insert into T3 (id,k,name)values(200,2,'R2');
Query OK, 1 row affected (0.09 sec)
mysql> insert into T3 (id,k,name)values(300,3,'R3');
Query OK, 1 row affected (0.00 sec)
mysql> insert into T3 (id,k,name)values(500,5,'R4');
Query OK, 1 row affected (0.00 sec)
mysql> insert into T3 (id,k,name)values(600,6,'R5');
Query OK, 1 row affected (0.01 sec)
两棵树的示例图如下:
可从图看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子结点存储整行数据。
非主键索引的叶子结点内容是主键的值。
那么,基于主键索引和普通索引的查询有什么区别?
- 如果语句是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往后退一个流出位置。
更加糟糕的情况是,如果数据页已经满了,就得需要申请一个新的数据页,那么这个过程就叫做页分裂,性能会降低很多。
除了性能,还会影响数据页的利用率,原本是放在一个页数据,现在分到两个页中,整体利用率降低50%。
既然有页分裂也会有页合并,当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。
自增主键场景
在建表的时候,这样来定义自增主键。
NOT NULL PRIMARY KEY AUTO_INCREMENT
插入的时候不用指定自增的主键。
从业务角度去考虑,如果你的表内有除了自增主键以外的唯一字段,那么是用自增主键还是用唯一字段呢。
由于每个非主键索引的叶子节点上都是主键的值,如果用唯一字段,那么每个二级索引的叶子节点占用N(具体长度由唯一字段长度决定)字节,如果用整形做主键,则只需要4字节,如果是长整型就需要8字节。
显然,主键长度越小,普通索引占用空间就越少。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。