每一种解决方案都是为了解决某一类问题而产生,所以在问为什么使用某种方案的时候,其本质就是在探索该方案是用来满足什么样的需求,解决什么样的问题。
所以探究 InnoDb 为什么使用 B+ 树这个问题,就是要弄清楚 B+ 树是用来满足什么的需求,解决什么样的问题。
要满足什么样的需求
我们先看一下一些常用的 SQL 语句
# 根据某个确定值来查询对应的信息
select id, name, email from user where id = 1;
# 通过区间值查询
select id, name, email from user where id > 12 and id < 20
# 通过范围查询并进行排序
select id, name, email from user where id < 123 order by id desc limit 10;
从以上的几个常用的 SQL 我们可以看到在对数据库进行查找数据的过程中主要有以下三类需求:
- 根据某个值精确快速查找
- 根据区间的上下限来快速查找此区间的数据
- 查询符合条件的记录并根据某些字段进行排序
所以,需要找到一种符合上面所有需求的方案。目前比较常用于查询的数据结构有以下两种:
- 散列表
- 树
散列表
散列表(哈希表)是根据是一种根据(key, value)直接进行访问的数据结构,它通过哈希函数将 key 值映射到散列表对应的位置上,查找效率非常高。
索引里其中的一种索引类型哈希索引就是基于散列表实现的,假设我们对名字建立哈希索引,则查找过程如下图所示:
对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(上图散列表的位置),散列表里的每个元素指向数据行的指针,由于索引自身只存储对应的哈希值,所以索引的结构十分紧凑,并且可以直接根据键值直接找到对应的数据记录,这让哈希索引查找速度非常快!但是哈希索引也有它的劣势,具体如下:
- 只有精确匹配索引所有列的查询才有效,比如我在列(name, address)建立哈希索引,如果只查询数据列 name, 则无法使用该索引。
- 哈希索引不是按照索引值顺序存储的,即 key 经过哈希函数计算后的哈希值不是按顺序的,所以也就无法用于排序,就不能根据区间进行查找。
- 哈希索引只支持等值比较查询,如 = 和 in(),不支持范围的查找,如 id > 17。
所以,哈希索引只适用于特定场合,在适当的场景使用,的确能带来很大的性能提升。比如在 InnoDB 里,就有一种特殊的功能叫 “自适应哈希索引”,如果 InnoDB 注意到某些索引列值被频繁使用时,它会在内存基于 B+ 树索引之上再创建一个哈希索引,这样就能让 B+ 树也具有哈希索引的优点。
所以散列表结构无法满足上文提到的需求。
接着我们来看看树。
树
平衡二叉树
平衡二叉树可用于查找,且其查找的时间复杂度近似 O(log2n),但是可以用平衡二叉树作为索引的结构吗?
答案是不能。
因为数据库表的数据通常是很多的,正常都是存放在磁盘上的。而磁盘的速度相比内存的速度是慢很多倍的,所以要尽量减少读取磁盘的次数,通过从内存读取数据来提高速度。
那么,如何将尽量多且有效的索引数据放到内存中呢?
这里有两个问题要解决:
1、尽量多
读取磁盘数据的时候,都是按磁盘块来读取的(局部性原理与磁盘预读),并不是一条一条的读。在使用树这种结构作为索引的数据结构时,我们每查找一次数据就需要从磁盘中读取一个树节点,也就是对应的一个磁盘块,所以如果我们能把尽量多的数据放到磁盘块中,那么每次读取的数据就会较多。
而平衡二叉树是每个节点只存储一个键值和数据,也就是说,存储的时候,每个磁盘块只存储一个键值和数据。
那如果存储了海量的数据,可以想象平衡二叉树的节点将会非常多,树高也会极其高,在查找数据的时候就会进行很多次磁盘 IO,效率将会极低。
所以平衡二叉树无法解决存储尽量多的索引到内存中这个问题。
2、有效的索引数据
我们所说的平衡二叉树,指的是逻辑结构上的平衡二叉树,其物理实现是数组。所以在逻辑相近的节点上,其物理位置可能相差会很远。因此,每次读取的磁盘页数据,很多可能是用不上的,即有效的索引数据并不多,所以在查找过程中还是要进行许多次的磁盘读取操作。
所以平衡二叉树也无法解决这个问题。
所以,能解决这两个问题的数据结构 —— B 树就被发明出来了。
B 树
B 树(Balance Tree),即平衡树的意思。B 树是从平衡二叉树演化而来,B树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点。也正因每个节点存储着非常多个关键字,树的深度就会非常的小。进而要执行的磁盘读取操作次数就会非常少,更多的是在内存中对读取进来的数据进行查找。B 树的结构示例如下图所示:
由于 B 树的每一个节点,即每一个磁盘块存储的数据较多,所以一定程度上解决了上文提到的存储尽量多的索引的问题。也一定程度上的解决了存储尽量多的有效索引的问题。
但是,B 树只是一定程度上的解决了问题,我们需要更好的解决问题。即能不能的做到存储更多的有效的索引呢?
答案是可以。这时候就就需要 B+ 树闪亮登场了。
更好的解决了问题的 B+ 树
B 树一定程度上的解决了问题,而从 B 树演化而来的 B+ 树能更好的解决问题,所以现实使用中几乎已经没有使用 B 树的情况了。
B + 树的结构示意图如下:
那么 B+ 树和 B 树有哪些不同?
- 在 B+ 树中,非叶子节点上是不存储数据的,仅存储键值。
因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16 KB,如果不存储数据,那么节点就可以存储更多的键值,相应的树的阶树就会更大,对于同样的数据量来说,需要的树高就会变低,树会更矮胖,如此一来查找数据的时候进行磁盘的 IO 次数就会减少,提升查询效率。
由于 B+ 树的阶数等于键值数量,假设 B+ 树的一个节点可以存储 1000 个键值,那么 3 层的 B+ 树 可以存储 1000 x 1000 x 1000 = 10亿个数据。并且一般根节点是常驻内存的,所以查找 10 亿个数据,只需要 2 次磁盘 IO。
B+ 这个特点很好的解决了上文提到的存储尽量多的索引数据的问题,并且查询效率也高。
- B+ 树的叶子节点中的索引数据是按顺序排列的,并且叶子节点间是通过双向链表进行连接的。
这个特点使 B+ 树在实现范围查找,排序查找,分组查找等操作时变得异常简单。而 B 树由于数据分散在各个节点,要实现这些操作很不容易。
由于索引数据是按顺序排序的,即每次读取了数据页的时候,里面的索引数据大部分都是需要用的,所以也很好的解决了上文提到的如何存储尽量多的有效的索引数据的问题。
总结
通过上面的分析,我们可以发现,在使用某种解决方案的时候,这种方案一定是用来满足某些需求的,在满足需求的过程中就会遇到一些问题,而最终的解决方案一定是能尽量好的解决问题并满足需求的。
所以,探究清楚某种方案是要满足什么样的需求,解决什么样的问题以及如何的解决了问题,也就明白了为什么使用这个方案。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。