索引是一种数据结构,用于帮助我们在大量数据中快速定位到我们想要查找的数据。
索引好比一本好书的目录页,需要查询某个章节直接在目录页查找,然后打开响应页数。
但索引也不是就快,如果章节少,那就直接翻开书找即可很快找到,只有章节非常多时,我们就可以利用索引快速找到。
所以,如果想让索引发挥出其真正的实力,需要在数据量大之时才可放心使用索引,反之就是大材小用。
以下篇幅会以 InnoDB 存储引擎为例分析 B+ 树索引。
在此之前,看下 B+ 树 的演化:
graph LR A(二叉树) --> B(平衡二叉树) --> C(B 树) --> D(B+ 树)如上图中展示,商品表建立了一个二叉树查找的索引。
图中可以看到二叉树的节点,节点中存储了键(key)和数据(data);键对应商品表中的 id,数据对应商品表中的行数据。
二叉树特性:任何节点的左子节点的键值都小于当前节点的键值,右子节点的键值都大于当前节点的键值;顶端的节点为根节点,没有子节点的节点为叶子节点。
若现在要查询 id = 12 的商品信息,通过创建的二叉树索引,查询流程如下:
通过二叉树查找需要 3 次即可找到匹配的数据;若在表中一条一条的查询,需要 6 次才可以找到。
二叉树若是以上这样的结构,就变成了链表。
现在要查询 id = 17 的商品信息,需要查找 7 次,也就是相当于全表扫描;导致这样的原因主要是二叉树查找变得不平衡了,即:高度太高了,从而致使查找效率不稳定。
so,为了保证二叉树一直保持平衡,就要用到平衡二叉树呢。
平衡二叉树(AVL 树),在满足二叉查找树特性的基础上,要求每个节点的左右子树的高度差不能超过 1。如下为平衡二叉树与非平衡二叉树的比较图:
平衡二叉树保证了树的构造是保持平衡的,当插入或删除数据导致不满足平衡二叉树不平衡时,平衡二叉树会进行调整树上的节点来保持平衡。
数据在内存中存放是不可靠的,实际中会将例如商品表中的数据和索引存储在磁盘中;但磁盘相较于内存,读取数据的速度会慢上千百倍,所以应该尽量减少从磁盘中读取数据的次数;还有从磁盘读取数据时,都是按照磁盘块读取的,并不是一条记录一条记录读的;如果能将数据放进磁盘块中,那么一次磁盘读取操作就会读取更多的数据,超找数据的时间就会减低。如果用树这种数据结构作为索引的数据结构,那么每次查找数据就只需要从磁盘中读取一个节点(磁盘块)。而平衡二叉树每个节点只存储一个键值和数据,这样每个磁盘块就只能存一个键值和数据,大量数据存储的情况,就会出现二叉树节点多,高度高,导致查找数据进行的磁盘 IO 次数也随之变多,以至于查找数据的效率极具降低。如下图所示:
为了解决平衡二叉树的这个弊端,我们应该寻找一种单个节点可以存储多个键值和数据的平衡树,而 B 树就满足这种情况。
B 树(BalanC++e Tree)即为平衡树的意思,如下是一个 B 树:
图中的 p 节点为指向子节点的指针。
图中的每个节点称为页,页就是我们上面说的磁盘块,在 MySQL 中数据读取的基本单位都是页。
从上图可以看出,B 树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的 B 树为 3 阶 B 树,高度也会很低。
基于这个特性,B 树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。
假如我们要查找 id=28 的用户信息,那么我们在上图 B 树中查找的流程如下:
B+ 树与 B 树区别:
B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB。
如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。
另外,B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。
一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。
因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。
那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。
B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。
其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。也就是说上图中的 B+ 树索引就是 InnoDB 中 B+ 树索引真正的实现方式,准确的说应该是聚集索引。
通过上图可以看到,在 InnoDB 中,我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。
MyISAM 中的 B+ 树索引实现与 InnoDB 中的略有不同。在 MyISAM 中,B+ 树索引的叶子节点并不存储数据,而是存储数据的文件地址。
在 MySQL 中,B+ 树索引按照存储方式的不同分为聚集索引和非聚集索引。
只说 InnoDB 中的聚集索引和非聚集索引:
聚集索引(聚簇索引):以 InnoDB 作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。
这是因为 InnoDB 是把数据存放在 B+ 树中的,而 B+ 树的键值就是主键,在 B+ 树的叶子节点中,存储了表中所有的数据。
这种以主键作为 B+ 树索引的键值而构建的 B+ 树索引,我们称之为聚集索引。
非聚集索引(非聚簇索引):以主键以外的列值作为键值构建的 B+ 树索引,我们称之为非聚集索引。非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,称为回表。
以上是一个聚集索引。
假设查询 id >= 18 并且 id < 40 的用户数据。对应的 sql 语句为:
select * from user where id >= 18 and id < 40
id 是主键,查询过程如下:
(18,kl), (19,kl), (22,hj), (24,io), (25,vg) , (29,jk), (31,jk) , (33,rt) , (34,ty) , (35,yu) , (37,rt) , (39,rt)
查找详情流程图:
非聚簇索引会根据二级索引找到主键,之后,拿着主键回表查询(和聚簇索引流程一致);
一个查询可以完全通过索引来执行,无需访问实际的数据行。即:一个索引包含了需要查询的所有字段 -> 联合索引
尽可能把查询条件推到索引层面进行过滤,减少从磁盘读取的数据量。但是 ta 依赖于存储引擎层面