Fork me on GitHub

Mysql为什么使用B+树做索引

什么是索引?

  索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。

  索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,则索引效率会有所下降。

  一张表可以创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引(索引会占用磁盘空间,标准的以空间换时间),因此,索引越多,插入、更新和删除记录的速度就越慢。

  对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。

  注:身份证号、邮箱地址等,因为他们具有业务含义,因此不宜作为主键。

索引的类型

  不同类型的索引,可以为不同场景提供更好的性能。

  在MySQL中,索引是在存储引擎层面实现的,而不是在服务器层面实现的。正如大家所知道,MySQL支持多种类型的存储引擎。所以,在不同存储引擎中索引的实现方式并不是一样的,也不是所有类型的索引都被所有存储引擎支持的,即使多个存储引擎支持同一种类型的索引,它底层的实现也有可能是不相同的。
  下图是从Navicat截取的INDEXES配置,索引类型有全文索引(实际开发使用不多)、普通索引、唯一索引、空间引擎(GIS中介于应用程序和空间数据库之间的中间件技术,它为用户提供了访问空间数据库的统一接口,是GIS中的关键性技术);索引算法有 B-tree和Hash

 

 

  大家耳熟能详的B+Tree索引是一种非常优秀的数据结构,也是面试热点问题。本文将从数据结构和磁盘IO两个方面分析了为什么使用B+Tree,以及MySQL的InnoDB存储引擎的索引实现。在面试过程中,被问到MySQL索引时通常也是从底层数据结构特点以及结合磁盘IO两个角度去分析,屡试不爽。

  学习一门技术时,我们不仅要知道其优点更要了解其缺点和瓶颈。在分析MySQL索引的实现时,不妨试试从其他数据结构的缺点入手!

磁盘IO

  所谓磁盘IO,简单来讲就是就是将磁盘中的数据读取到内存或者是从内存写入磁盘。在系统开发与设计过程中,磁盘IO的瓶颈往往不可忽略,因为这是一个相对比较耗时的操作。磁盘的IO操作通常需要寻道,旋转和传输三个步骤。

  • 寻道,是指将读写磁头移动到正确的磁道,寻道时间越短,IO操作越快,目前磁盘的平均寻道时间一般在3-15ms左右。
  • 旋转,是指将盘片旋转到请求数据所在的扇区,这部分所需要的时间由硬盘的配置所决定。旋转延迟由磁盘转速所决定,也就是常说的7200转和5400转等。例如,7200转是指每分钟可以旋转7200圈,那么旋转一圈所需要的时间就是60*1000/7200 ≈ 8.33ms,而旋转延迟通常取旋转一周时间的1/2,也就是大约4.17ms。
  • 传输,磁盘传输的速度通常在几十到上百M每秒,假设速度为20M/s,要传输的数据为64kb,则传输时间则是 64 / 1024 / 20 * 1000 = 3.125ms。不过目前流行的SSD传输速度大幅度提升,SATA Ⅱ可以达到300M/s,传输速度往往远小于前两步操作所以传输时间往往可以忽略不记。

  机械硬盘的连续读写性能很好,但随机读写性能很差,这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停的移动,时间都浪费在了磁头寻址上,所以性能不高。

  由于磁盘IO是一个比较耗时的操作,而操作系统在设计时则定义一个空间局部性原则,局部性原理是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中

  在操作系统的文件系统中,数据也是按照page划分的,一般为4k或8k。当计算机访问一个地址数据时,不仅会加载当前数据所在的数据页,还会将当前数据页相邻的数据页一同加载到内存。而这个过程实际上只发生了1次磁盘IO,这个理论对于索引的数据结构设计非常有帮助。

索引数据结构演进

  索引是一种支持快速查找的数据结构,在运用中往往还要求能够支持顺序查询,而常见的数据结构有很多,比如数组,链表,二叉树,散列表,二叉搜索树,平衡搜索二叉树,红黑树,跳表等。仅仅从数据结构那么为什么选择B+Tree呢?

  数组,链表

  首先对于数组,链表这种线性表来说,适合存储数据,而不是查找数据,同样,对于普通二叉树来说,数据存储没有特定规律,所以也不适合。

  哈希(Hash)  

  哈希(Hash)是一种非常快的查找方法,在一般情况下这种查找的时间复杂度为O(1),即一般仅需要一次查找就能定位到数据。

  哈希结构在单条数据的等值查询是性能非常优秀,但是只能用来搜索等值的查询, 对于范围查询,模糊查询(最左前缀原则)都不支持,所以不能很好的支持业务需求;所以MySQL并没有显式支持Hash索引,而是根据数据的访问频次和模式自动的为热点数据页建立哈希索引,称之为自适应哈希索引。

  并且由于哈希函数的随机性,Hash索引通常都是随机的内存访问,对于缓存不友好,会造成频繁的磁盘IO。

  二叉搜索树

  二叉搜索树,如果左子树不为空,则左子树上所有节点均小于根节点,右子树节点均大于根节点;由其属性不难看出,这种树非常适合数据查找。不过有个致命的缺点是二叉搜索树的树型取决于数据的输入顺序,极端情况下会退化成链表。

  平衡二叉搜索树

  为了解决二叉搜索树的上述问题,平衡二叉搜索树就诞生了。在保证数据顺序的基础上,又能维持树型,保证每个节点的左右子树高度相差不超过1。

  不过由于要维持树的平衡,在插入数据时可能要进行大量的数据移动平衡搜索二叉树过于严格的平衡要求,导致几乎每次插入和删除节点都会破坏树的平衡性,使得树的性能大打折扣。

  红黑树

  红黑树和其他二叉搜索树类似, 都是在进行插入和删除操作时通过特定操作保持二叉查找树的性质,从而获得较高的查找性能。与之不同的是,红黑树的平衡性并不像平衡搜索二叉树一样严格的同时,又能保证在, O(log n) 时间复杂度内做查找和删除。红黑树通过改变节点的颜色,可以有效减少节点的移动次数

  乍一看红黑树是一种完美的数据结构,能够胜任索引的工作。但MySQL并未使用其作为索引的实现,主要原因在于红黑树的深度过大,数据检索时造成磁盘IO频繁,假设一个每个节点存储在一个page中,树的高度为10,则每次检索可能就需要进行10次磁盘IO。

  B-Tree

  B-Tree是一种自平衡的多叉搜索树,一个节点可以拥有两个以上的子节点。适合读写相对大的数据块的存储系统,例如磁盘。

  但由于MySQL索引一般都存储在内存中,如果使用B-Tree作为索引的话,索引和数据存储在一块,分布在各个节点中;而内存资源往往比较宝贵,一定内存的情况下可以存储的索引数量相对有限,毕竟每条数据的大小一般远大于索引列的大小,导致内存使用率不高。数据查询过程中往往会有顺序查询,而B-Tree和红黑树一样对于顺序查询并不友好

  B+Tree  

  那么有没有一种数据结构,即能够快速查找数据,又不需要频繁的调整以维持平衡,同时对磁盘IO比较友好呢?

  上面以及介绍B-Tree能够很好的解决前面两个问题,那能不能对B-Tree进行演进改造,使其对磁盘IO访问不那么频繁呢?再这样的需求背景下,B+Tree诞生了。B+Tree是在B-Tree基础上演进而来的。与之不同的是B+Tree的数据页只存储在叶子节点中,并且叶子节点之间通过指针相连,为双向链表结构。

  B+Tree的优点可以分为以四个:

  1. 充分利用空间局部性原理,适合磁盘存储。

  2. 树的高度很低,能够在存储大量数据情况下,进行较少的磁盘IO。

  3. 能够很好支持单值,范围查询,有序性查询。

  4. 索引和数据分开存储,让更多的索引存储在内存中。

MySql是如何使用B+Tree的呢?

  MySQL中的数据存储通常以Page为单位,俗称数据页,每个Page对应B+Tree的一个节点。页是InnoDB磁盘管理的最小单位,默认每个数据页的大小为16kb,也可以通过参数innodb_page_size将页的大小设置成其他值。

  数据库的页大小和操作系统类似,是指存放数据时,每一块连续区域数据的大小。比如一个1M的数据存放在数据库中时, 需要大概64个页来存放(1024=64*16)。

  在操作系统上安装数据库,最好将数据库页大小设置为操作系统页大小的倍数,才是最佳设置。

  通常情况下,一张MySQL表中有成千上万条数据,而磁盘IO次数往往与数的高度成正比。默认情况下一个Page的大小为16kb,由于每个Page中数据通过指针相连,且每个指针大小为6字节。在工作中,我们通常使用长度为8个字节的bigint类型作为主键id的类型。

  假设每条记录长度为1kb(包含指针),已知每一条数据都会包含一个6字节的指针(数据页中每条记录都有指向下一条记录的指针,但是没有指向上一条记录的指针);所以一条索引数据大约占用8+6=14个字节,一个Page中能存储16 * 1024 / 14 ≈ 1170条索引数据(注:InnoDB引擎中B+Tree的数据页存储在叶子结点)。高度为2的B+Tree大约能存储1170*16 = 18720条这样的记录。同理,高度为3的B+Tree的B+Tree大约能存储1170 * 1170 * 16 = 21902400,大约两千万条数据。 (每个节点大约能存储1170条记录,概括的理解为此时B+Tree为1170叉树)

  例如,要检索id=008的数据,则需要进行三次磁盘IO找到对应的数据页(最多三次,因为Page可能在缓存中),然后在数据页中进行二分查找,定位到对应的记录。

  关于Mysql根据主键检索数据为什么最多三次,我在网上找到了一个比较准确且清晰的回答,参考《MySQL一次查询理论上需要几次磁盘I/O?

补充

  最后补充几张Mysql查询的基本流程图。

  

 

 

 

 

 

 

 

 

 

 

posted @ 2022-07-25 12:00  JackpotHan  阅读(243)  评论(0编辑  收藏  举报