为何 MySQL 使用 B+ 树

为何 MySQL 使用 B+ 树是面试中常常会出现的问题,不少人对于这个问题可能都有一些本身的理解,可是多数的回答都不够完整和准确,大多数人都只会简单说一下 B+ 树和 B 树的区别,可是都没有真正回答 MySQL 为何选择使用 B+ 树这个问题,咱们在这篇文章中就会深刻分析 MySQL 选择 B+ 树背后的一些缘由。git

 

概述github

 

首先须要澄清的一点是,MySQL 跟 B+ 树没有直接的关系,真正与 B+ 树有关系的是 MySQL 的默认存储引擎 InnoDB,MySQL 中存储引擎的主要做用是负责数据的存储和提取,除了 InnoDB 以外,MySQL 中也支持 MyISAM 做为表的底层存储引擎。面试

 

咱们在使用 SQL 语句建立表时就能够为当前表指定使用的存储引擎,你能在 MySQL 的文档 Alternative Storage Engines 中找到它支持的所有存储引擎,例如:MyISAM、CSV、MEMORY 等,然而默认状况下,使用以下所示的 SQL 语句来建立表就会获得 InnoDB 存储引擎支撑的表:sql

 

CREATE TABLE t1 (
a INT,
b CHAR (20
), PRIMARY KEY (a)) ENGINE=InnoDB;

 

想要详细了解 MySQL 默认存储引擎的读者,能够经过以前的文章 『浅入浅出』MySQL 和 InnoDB 了解包括 InnoDB 存储方式、索引和锁等内容,咱们在这里主要不会介绍 InnoDB 相关的过多内容。数据库

 

咱们今天最终将要分析的问题其实仍是,为何 MySQL 默认的存储引擎 InnoDB 会使用 MySQL 来存储数据,相信对 MySQL 稍微有些了解的人都知道,不管是表中的数据(主键索引)仍是辅助索引最终都会使用 B+ 树来存储数据,其中前者在表中会以 <id, row> 的方式存储,然后者会以 <index, id> 的方式进行存储,这其实也比较好理解:数据结构

 

  • 在主键索引中,id 是主键,咱们可以经过 id 找到该行的所有列;函数

  • 在辅助索引中,索引中的几个列构成了键,咱们可以经过索引中的列找到 id,若是有须要的话,能够再经过 id 找到当前数据行的所有内容;post

 

对于 InnoDB 来讲,全部的数据都是以键值对的方式存储的,主键索引和辅助索引在存储数据时会将 id 和 index 做为键,将全部列和 id 做为键对应的值。性能

 

 

在具体分析 InnoDB 使用 B+ 树背后的缘由以前,咱们须要为 B+ 树找几个『假想敌』,由于若是咱们只有一个选择,那么选择 B+ 树也并不值得讨论,找到的两个假想敌就是 B 树和哈希,相信这也是不少人会在面试中真实遇到的问题,咱们就以这两种数据结构为例,分析比较 B+ 树的优势。优化

 

设计

 

到了这里咱们已经明确了今天待讨论的问题,也就是为何 MySQL 的 InnoDB 存储引擎会选择 B+ 树做为底层的数据结构,而不选择 B 树或者哈希?在这一节中,咱们将经过如下的两个方面介绍 InnoDB 这样选择的缘由。

 

InnoDB 须要支持的场景和功能须要在特定查询上拥有较强的性能;

 

CPU 将磁盘上的数据加载到内存中须要花费大量的时间,这使得 B+ 树成为了很是好的选择;

 

数据的持久化以及持久化数据的查询实际上是一个常见的需求,而数据的持久化就须要咱们与磁盘、内存和 CPU 打交道;MySQL 做为 OLTP 的数据库不只须要具有事务的处理能力,并且要保证数据的持久化而且可以有必定的实时数据查询能力,这些需求共同决定了 B+ 树的选择,接下来咱们会详细分析上述两个缘由背后的逻辑。

 

读写性能

 

不少人对 OLTP 这个词可能不是特别了解,咱们帮助各位读者快速理解一下,与 OLTP 相比的还有 OLAP,它们分别是 Online Transaction Processing 和 Online Analytical Processing,从这两个名字中咱们就能够看出,前者指的就是传统的关系型数据库,主要用于处理基本的、平常的事务处理,然后者主要在数据仓库中使用,用于支持一些复杂的分析和决策。

 

 

做为支撑 OLTP 数据库的存储引擎,咱们常常会使用 InnoDB 完成如下的一些工做:

 

  • 经过 INSERT、UPDATE 和 DELETE 语句对表中的数据进行增长、修改和删除;

  • 经过 UPDATE 和 DELETE 语句对符合条件的数据进行批量的删除;

  • 经过 SELECT 语句和主键查询某条记录的所有列;

  • 经过 SELECT 语句在表中查询符合某些条件的记录并根据某些字段排序;

  • 经过 SELECT 语句查询表中数据的行数;

  • 经过惟一索引保证表中某个字段或者某几个字段的惟一性;

 

若是咱们使用 B+ 树做为底层的数据结构,那么全部只会访问或者修改一条数据的 SQL 的时间复杂度都是 O(log n),也就是树的高度,可是使用哈希却有可能达到 O(1) 的时间复杂度,看起来是否是特别的美好。可是当咱们使用以下所示的 SQL 时,哈希的表现就不会这么好了:

 

SELECT * FROM posts WHERE author = 'draven' ORDER BY created_at DESC
SELECT * FROM posts WHERE comments_count > 10
UPDATE posts SET github = 'github.com/draveness' WHERE author = 'draven'
DELETE FROM posts WHERE author = 'draven'

 

若是咱们使用哈希做为底层的数据结构,遇到上述的场景时,使用哈希构成的主键索引或者辅助索引可能就没有办法快速处理了,它对于处理范围查询或者排序性能会很是差,只能进行全表扫描并依次判断是否知足条件。全表扫描对于数据库来讲是一个很是糟糕的结果,这其实也就意味着咱们使用的数据结构对于这些查询没有其余任何效果,最终的性能可能都不如从日志中顺序进行匹配。

 

 

使用 B+ 树其实可以保证数据按照键的顺序进行存储,也就是相邻的全部数据其实都是按照天然顺序排列的,使用哈希却没法达到这样的效果,由于哈希函数的目的就是让数据尽量被分散到不一样的桶中进行存储,因此在遇到可能存在相同键 author = 'draven 或者排序以及范围查询 comments_count > 10 时,由哈希做为底层数据结构的表可能就会面对数据库查询的噩梦 —— 全表扫描。

 

B 树和 B+ 树在数据结构上其实有一些相似,它们均可以按照某些顺序对索引中的内容进行遍历,对于排序和范围查询等操做,B 树和 B+ 树相比于哈希会带来更好的性能,固然若是索引创建不够好或者 SQL 查询很是复杂,依然会致使全表扫描。

 

与 B 树和 B+ 树相比,哈希做为底层的数据结构的表可以以 O(1) 的速度处理单个数据行的增删改查,可是面对范围查询或者排序时就会致使全表扫描的结果,而 B 树和 B+ 树虽然在单数据行的增删查改上须要 O(log n) 的时间,可是它会将索引列相近的数据按顺序存储,因此可以避免全表扫描。

 

数据加载

 

既然使用哈希没法应对咱们常见的 SQL 中排序和范围查询等操做,而 B 树和 B 树和 B+ 树均可以相对高效地执行这些查询,那么为何咱们不选择 B 树呢?这个缘由其实很是简单 —— 计算机在读写文件时会以页为单位将数据加载到内存中。页的大小可能会根据操做系统的不一样而发生变化,不过在大多数的操做系统中,页的大小都是 4KB,你能够经过以下的命令获取操做系统上的页大小:

 

$ getconf PAGE_SIZE
4096

  

 

做者使用 macOS 系统的页大小就是 4KB,固然在不一样的计算机上获得不一样的结果是彻底有可能的。

 

当咱们须要在数据库中查询数据时,CPU 会发现当前数据位于磁盘而不是内存中,这时就会触发 I/O 操做将数据加载到内存中进行访问,数据的加载都是以页的维度进行加载的,然而将数据从磁盘读取到内存中所须要的成本是很是大的,普通磁盘(非 SSD)加载数据须要通过队列、寻道、旋转以及传输的这些过程,大概要花费 10ms 左右的时间。

 

 

咱们在估算 MySQL 的查询时就可使用 10ms 这个数量级对随机 I/O 占用的时间进行估算,这里想要说的是随机 I/O 对于 MySQL 的查询性能影响会很是大,而顺序读取磁盘中的数据时速度能够达到 40MB/s,这二者的性能差距有几个数量级,由此咱们也应该尽可能减小随机 I/O 的次数,这样才能提升性能。

 

B 树与 B+ 树的最大区别就是,B 树能够在非叶结点中存储数据,可是 B+ 树的全部数据其实都存储在叶子节点中,当一个表底层的数据结构是 B 树时,假设咱们须要访问全部『大于 4,而且小于 9 的数据』:

 

 

若是不考虑任何优化,在上面的简单 B 树中咱们须要进行 4 次磁盘的随机 I/O 才能找到全部知足条件的数据行:

 

  1. 加载根节点所在的页,发现根节点的第一个元素是 6,大于 4;

  2. 经过根节点的指针加载左子节点所在的页,遍历页面中的数据,找到 5;

  3. 从新加载根节点所在的页,发现根节点不包含第二个元素;

  4. 经过根节点的指针加载右子节点所在的页,遍历页面中的数据,找到 7 和 8;

 

固然咱们能够经过各类方式来对上述的过程进行优化,不过 B 树能作的优化 B+ 树基本均可以,因此咱们不须要考虑优化 B 树而带来的收益,直接来看看什么样的优化 B+ 树能够作,而 B 树不行。

 

因为全部的节点均可能包含目标数据,咱们老是要从根节点向下遍历子树查找知足条件的数据行,这个特色带来了大量的随机 I/O,也是 B 树最大的性能问题。

 

B+ 树中就不存在这个问题了,由于全部的数据行都存储在叶节点中,而这些叶节点能够经过『指针』依次按顺序链接,当咱们在以下所示的 B+ 树遍历数据时能够直接在多个子节点之间进行跳转,这样可以节省大量的磁盘 I/O 时间,也不须要在不一样层级的节点之间对数据进行拼接和排序;经过一个 B+ 树最左侧的叶子节点,咱们能够像链表同样遍历整个树中的所有数据,咱们也能够引入双向链表保证倒序遍历时的性能

 

 

有些读者可能会认为使用 B+ 树这种数据结构会增长树的高度从而增长总体的耗时,然而高度为 3 的 B+ 树就可以存储千万级别的数据,实践中 B+ 树的高度最多也就 4 或者 5,因此这并非影响性能的根本问题。

 

总结

 

任何不考虑应用场景的设计都不是最好的设计,当咱们明确的定义了使用 MySQL 时的常见查询需求并理解场景以后,再对不一样的数据结构进行选择就成了理所固然的事情,固然 B+ 树可能没法对全部 OLTP 场景下的查询都有着较好的性能,可是它可以解决大多数的问题。

 

咱们在这里从新回顾一下 MySQL 默认的存储引擎选择 B+ 树而不是哈希或者 B 树的缘由:

 

  • 哈希虽然可以提供 O(1) 的单数据行操做性能,可是对于范围查询和排序却没法很好地支持,最终致使全表扫描;

  • B 树可以在非叶节点中存储数据,可是这也致使在查询连续数据时可能会带来更多的随机 I/O,而 B+ 树的全部叶节点能够经过指针相互链接,可以减小顺序遍历时产生的额外随机 I/O;

 

若是想要追求各方面的极致性能也不是没有可能,只是会带来更高的复杂度,咱们能够为一张表同时建 B+ 树和哈希构成的存储结构,这样不一样类型的查询就能够选择相对更快的数据结构,可是会致使更新和删除时须要操做多份数据。

 

从今天的角度来看,B+ 树可能不是 InnoDB 的最优选择,可是它必定是可以知足当时设计场景的须要,从 B+ 树做为数据库底层的存储结构到今天已通过了几十年的时间,咱们不得不说优秀的工程设计确实有足够的生命力。而咱们做为工程师,在选择数据库时也应该很是清楚地知道不一样数据库适合的场景,由于软件工程中没有银弹。

 

到最后,咱们仍是来看一些比较开放的相关问题,有兴趣的读者能够仔细思考一下下面的问题:

 

  • 经常使用于分析的 OLAP 数据库通常会使用什么样的数据结构存储数据?为何?

  • Redis 是如何对数据进行持久化存储的?常见的数据结构都有什么?

 

若是对文章中的内容有疑问或者想要了解更多软件工程上一些设计决策背后的缘由,能够在博客下面留言,做者会及时回复本文相关的疑问并选择其中合适的主题做为后续的内容。

 

Reference

 

  • B+ tree · Wikipedia

  • What is the difference between Mysql InnoDB B+ tree index and hash index? Why does MongoDB use B-tree?

  • B+Trees and why I love them, part I

  • What are the main differences between INNODB and MYISAM

  • B+ Tree File Organization

  • Database Index: A Re-visit to B+ Tree

  • Fundamentals of database systems

相关文章
相关标签/搜索