拜托,别再问我什么是 B+ 树了
前言
每当我们执行某个 SQL 发现很慢时,都会下意识地反应是否加了索引,那么大家是否有想过加了索引为啥会使数据查找更快呢,索引的底层一般又是用什么结构存储的呢,相信大家看了标题已经有答案了,没错!B+树!那么它相对于一般的链表,哈希等有何不同,为何多数存储引擎都使用它呢,今天我就来揭开 B+ 树的面纱,相信看了此文,B+ 树不再神秘,对你理解以下高频面试题会大有帮助!
- 为啥索引常用 B+ 树作为底层的数据结构
- 除了 B+ 树索引,你还知道什么索引
- 为啥推荐自增 id 作为主键,自建主键不行吗
- 什么是页分裂,页合并
- 怎么根据索引查找行记录
本文将会从以下几个方面来讲解 B+ 树
- 定义问题
- 几种常见的数据结构对比
- 创建索引有哪些需要考虑的问题,怎样更高效地建立索引
定义问题
要知道索引底层为啥使用 B+ 树,得看它解决了什么问题,我们可以想想,日常我们用到的比较多的 SQL 有哪些呢。
假设我们有一张以下的用户表:
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`idcard` varchar(20) DEFAULT NULL COMMENT '身份证号码',
`age` tinyint(10) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息';
一般我们会有如下需求:
1、根据用户 id 查用户信息
select * from user where id = 123;
2、根据区间值来查找用户信息
select * from user where id > 123 and id < 234;
3、按 id 逆序排列,分页取出用户信息
select * from user where id < 1234 order by id desc limit 10;
从以上的几个常用 SQL 我们可以看到索引所用的数据结构必须满足以下三个条件
- 根据某个值精确快速查找
- 根据区间值的上下限来快速查找此区间的数据
- 索引值需要排好序,并支持快速顺序查找和逆序查找
接下来我们以主键索引(id 索引)为例来看看如何用相应的数据结构来构造它
几种常见的数据结构对比
接下来我们想想有哪些数据结构满足以上的条件
1、散列表
散列表(也称哈希表)是根据关键码值(Key value)而直接进行访问的数据结构,它让码值经过哈希函数的转换映射到散列表对应的位置上,查找效率非常高。哈希索引就是基于散列表实现的,假设我们对名字建立了哈希索引,则查找过程如下图所示:
对于每一行数据,存储引擎都会对所有的索引列(上图中的 name 列)计算一个哈希码(上图散列表的位置),散列表里的每个元素指向数据行的指针,由于索引自身只存储对应的哈希值,所以索引的结构十分紧凑,这让哈希索引查找速度非常快!但是哈希索引也有它的劣势,如下:
- 针对哈希索引,只有精确匹配索引所有列的查询才有效,比如我在列(A,B)上建立了哈希索引,如果只查询数据列 A,则无法使用该索引。
- 哈希索引并不是按照索引值顺序存存储的,所以也就无法用于排序,也就是说无法根据区间快速查找
- 哈希索引只包含哈希值和行指针,不存储字段值,所以不能使用索引中的值来避免读取行,不过,由于哈希索引多数是在内存中完成的,大部分情况下这一点不是问题
- 哈希索引只支持等值比较查询,包括 =,IN(),不支持任何范围的查找,如 age > 17
综上所述,哈希索引只适用于特定场合, 如果用得对,确实能再带来很大的性能提升,如在 InnoDB 引擎中,有一种特殊的功能叫「自适应哈希索引」,如果 InnoDB 注意到某些索引列值被频繁使用时,它会在内存基于 B+ 树索引之上再创建一个哈希索引,这样就能让 B+树也具有哈希索引的优点,比如快速的哈希查找。
2、链表
双向链表支持顺序查找和逆序查找,如图下
但显然不支持我们说的按某个值或区间的快速查找,另外我们知道表中的数据是要不断增加的,索引也是要及时插入更新的,链表显然也不支持数据的快速插入,所以能否在链表的基础上改造一下,让它支持快速查找,更新,删除。有一种结构刚好能满足我们的需求,这里引入跳表的概念。
什么是跳表?简单地说,跳表是在链表之上加上多层索引构成的。如下图所示
假设我们现在要查找区间 7- 13 的记录,再也不用从头开始查找了,只要在上图中的二级索引开始找即可,遍历三次即可找到链表的区间位置,时间复杂度是 O(logn),非常快,这样看来,跳表是能满足我们的需求的,实际上它的结构已经和 B+ 树非常接近了,只不过 B+ 树是从平衡二叉查找树演化而来的而已,接下来我们一步步来看下如何将平衡二叉查找树改造成 B+ 树。
先来看看什么是平衡二叉查找树,平衡二叉查找树具有如下性质:
- 若左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
- 每个非叶子节点的左右子树的高度之差的绝对值(平衡因子)最多为1。
下图就是一颗平衡二叉查找树
从其特性就可以看到平衡二叉查找树查找节点的时间复杂度是 O(log2n)
现在我们将其改造成 B+ 树
可以看到主要区别就是所有的节点值都在最后叶节点上用双向链表连接在了一起,仔细和跳表对比一下 ,是不是很像,现在如果我们要找15 ~ 27 这个区间的数只要先找到 15 这个节点(时间复杂度 logn = 3 次)再从前往后遍历直到 27 这个节点即可,即可找到这区间的节点,这样它完美地支持了我们提的三个需求:快速查找值,区间,顺序逆序查找。
假设有 1 亿个节点,每个节点要查询多少次呢,显然最多为 log21亿 = 27 次,如果这 1 亿个节点都在内存里,那 27 次显然不是问题,可以说是非常快了,但一个新的问题出现了,这 1 亿个节点在内存大小是多少呢,我们简单算一下,假设每个节点 16 byte,则 1 亿个节点大概要占用 1.5G 内存!对于内存这么宝贵的资源来说是非常可怕的空间消耗,这还只是一个索引,一般我们都会在表中定义多个索引,或者库中定义多张表,这样的话内存很快就爆满了!所以在内存中完全装载一个 B+ 树索引显然是有问题的,如何解决呢。
内存放不下, 我们可以把它放到磁盘嘛,磁盘空间比内存大多了,但新的问题又来了,我们知道内存与磁盘的读取速度相差太大了,通常内存是纳秒级的,而磁盘是毫秒级的,读取同样大小的数据,两者可能相差上万倍,于是上一步我们计算的 27 次查询如果放在磁盘中来看就非常要命了(查找一个节点可以认为是一次磁盘 IO,也就是说有 27 次磁盘 IO!),27 次查询是否可以优化?
可以很明显地观察到查询次数和树高有关,那树高和什么有关,很明显和每个节点的子节点个数有关,即 N 叉树中的 N,假设现在有 16 个数,我们分别用二叉树和五叉树来构建,看下树高分别是多少
可以看到如果用二叉树 ,要遍历 5 个节点,如果用五叉树 ,只要遍历 3 次,一下少了两次磁盘 IO,回过头来看 上文的一亿个节点,如果我们用 100 叉树来构建,需要几次 IO 呢
可以看到,最多遍历五次(实际上根节点一般存在内存里的,所以可以认为是 4 次)!磁盘 IO 一下从 27 减少到了 5!性能可以说是大大提升了,有人说 5 次还是太多,是不是可以把 100 叉树改成 1000 或 10000 叉树呢,这样 IO 次数不就就能进一步减少了。
这里我们就需要了解页(page)的概念,在计算机里,无论是内存还是磁盘,操作系统都是按页的大小进行读取的(页大小通常为 4 kb),磁盘每次读取都会预读,会提前将连续的数据读入内存中,这样就避免了多次 IO,这就是计算机中有名的局部性原理,即我用到一块数据,很大可能这块数据附近的数据也会被用到,干脆一起加载,省得多次 IO 拖慢速度, 这个连续数据有多大呢,必须是是操作系统页大小的整数倍,这个连续数据就是 MySQL 的页,默认值为 16 KB,也就是说对于 B+ 树的节点,最好设置成页的大小(16 KB),这样一个 B+ 树上的节点就只会有一次 IO 读。
那有人就会问了,这个页大小是不是越大越好呢,设置大一点,节点可容纳的数据就越多,树高越小,IO 不就越小了吗,这里要注意,页大小并不是越大越好,InnoDB 是通过内存中的缓存池(pool buffer)来管理从磁盘中读取的页数据的。页太大的话,很快就把这个缓存池撑满了,可能会造成页在内存与磁盘间频繁换入换出,影响性能。
通过以上分析,相信我们不难猜测出 N 叉树中的 N 该怎么设置了,只要选的时候尽量保证每个节点的大小等于一个页(16kb)的大小即可。
页分裂与页合并
现在我们来看看开头的问题, 为啥推荐自增 id 作为主键,自建主键不行吗,有人可能会说用户的身份证是唯一的,可以用它来做主键,假设以身份证作主键,会有什么问题呢。
B+ 树为了维护索引的有序性,每插入或更新一条记录的时候,会对索引进行更新。假设原来基于身份证作索引的 B+ 树如下(假设为二叉树 ,图中只列出了身份证的前四位)
现在有一个开头是 3604 的身份证对应的记录插入 db ,此时要更新索引,按排序来更新的话,显然这个 3604 的身份证号应该插到左边节点 3504 后面(如下图示,假设为二叉树)
如果把 3604 这个身份证号插入到 3504 后面的话,这个节点的元素个数就有 3 个了,显然不符合二叉树的条件,此时就会造成页分裂,就需要调整这个节点以让它符合二叉树的条件
如图示:调整过后符合二叉树条件
这种由于页分裂造成的调整必然导致性能的下降,尤其是以身份证作为主键的话,由于身份证的随机性,必然造成大量的随机结点中的插入,进而造成大量的页分裂,进而造成性能的急剧下降,那如果是以自增 id 作为主键呢,由于新插入的表中生成的 id 比索引中所有的值都大,所以它要么合到已存在的节点(元素个数未满)中,要么放入新建的节点中(如下图示)所以如果是以自增 id 作为主键,就不存在页分裂的问题了,推荐!
有页分裂就必然有页合并,什么时候会发生页合并呢,当删除表记录的时候,索引也要删除,此时就有可能发生页合并,如图示
当我们删除 id 为 7,9 对应行的时候,上图中的索引就要更新,把 7,9 删掉,此时 8,10 就应该合到一个节点,不然 8,10 分散在两个节点上,可能造成两次 IO 读,势必会影响查找效率! 那什么时候会发生页合并呢,我们可以定个阈值,比如对于 N 叉树来说,当节点的个数小于 N/2 的时候就应该和附近的节点合并,不过需要注意的是合并后节点里的元素大小可能会超过 N,造成页分裂,需要再对父节点等进行调整以让它满足 N 叉树的条件。
怎么根据索引查找行记录
相信大家看完以上的 B+ 树索引的介绍应该还有个疑惑,怎么根据对应的索引值查找行记录呢,其实相应的行记录就放在最后的叶子节点中,找到了索引值,也就找到了行记录。如图示
可以看到,非叶子节点只存了索引值,只在最后一行才存放了行记录,这样极大地减小了索引了大小,而且只要找到索引值就找到了行记录,也提升了效率,
这种在叶节点存放一整行记录的索引被称为聚簇索引,其他的就称为非聚簇索引。
关于 B+ 树的总结
综上所述,B+树有以下特点:
- 每个节点中子节点的个数不能超过 N,也不能小于 N/2(不然会造成页分裂或页合并)
- 根节点的子节点个数可以不超过 m/2,这是一个例外
- m 叉树只存储索引,并不真正存储数据,只有最后一行的叶子节点存储行数据。
- 通过链表将叶子节点串联在一起,这样可以方便按区间查找
总结
本文由日常中常用的 SQL 由浅入深地总结了 B+ 树的特点,相信大家应该对 B+ 树索引有了比较清晰地认识,所以说为啥我们要掌握底层原来,学完了 B+ 树,再看开头提的几个问题,其实也不过如此,深挖底层,有时候确实能让你以不变应万变。
最后,欢迎大家关注我的公号「码海」,共同进步!
巨人的肩膀
http://www.rainybowe.com/blog/2016/05/10/mysql%E7%B4%A2%E5%BC%95/index.html
https://time.geekbang.org/column/article/69236
拜托,别再问我什么是 B+ 树了的更多相关文章
- 拜托!面试请不要再问我Spring Cloud底层原理[z]
[z]https://juejin.im/post/5be13b83f265da6116393fc7 拜托!面试请不要再问我Spring Cloud底层原理 欢迎关注微信公众号:石杉的架构笔记(id: ...
- Java设计模式(十三) 别人再问你设计模式,叫他看这篇文章
原创文章,转载请务注明出处 OOP三大基本特性 封装 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的属性和方法只让可信的类操作,对不可信的进行信息隐藏. 继承 继承是指这样一种能力,它可以使 ...
- vmware 安装配置 ,记住这一次不要再问我了。ok?
Linux 安装配置 ,记住这一次不要再问我了.ok? 第一步 选择版本 如果遇到问题无法自动获取的 老男孩教育-李泳谊<youjiu_linux@qq.com> 17:51:43明天开 ...
- 求你了,再问你Java内存模型的时候别再给我讲堆栈方法区了…
GitHub 4.1k Star 的Java工程师成神之路 ,不来了解一下吗? GitHub 4.1k Star 的Java工程师成神之路 ,真的不来了解一下吗? GitHub 4.1k Star 的 ...
- 面试官,不要再问我“Java GC垃圾回收机制”了
Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解> ...
- 面试官,不要再问我“Java 垃圾收集器”了
如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...
- 面试官,不要再问我“Java虚拟机类加载机制”了
关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...
- 面试官,不要再问我“Java虚拟机类加载机制”了(转载)
关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...
- 面试官,不要再问我“Java 垃圾收集器”了(转载)
如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在 面试过程中这个深度的问题涉及的比 ...
随机推荐
- Hello World!(这不是第一篇)
如题,这不是第一篇blog,但是为了表示这个闲置了1年多的blog现在被我正式启用了,我还是走个过场吧. #include <iostream> using namespace std; ...
- 致歉Note7用户后,三星要还给世界下一个机皇
1月23日,三星电子在韩国首尔公布了Note7事件原因调查结果.此次认定过程精密而繁琐,最终结果发布距离三星宣布全球召回Note7已经有3个月. 相比事发之初各路信息甚嚣尘上,现在虽然还不能说已经 ...
- C++走向远洋——49(项目一2、复数类中的运算符重载、类的友元函数)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- Java程序员常用的@Component、@Repository、@Controller、@Service系列【案例demo3】
Java程序员常用的@Component.@Repository.@Controller.@Service系列[案例demo3] 很多程序员通过在类上使用@Repository.@Componen ...
- cooking和session
## Cookie ## # 基本概念: >> 用于保存一次会话中的记录,存放在客户端(浏览器); |-- "一次会话" |-- 当客户端打开一个界面时 被称作一次会话 ...
- 丰富图文详解B-树原理,从此面试再也不慌
本文始发于个人公众号:TechFlow,原创不易,求个关注 本篇原计划在上周五发布,由于太过硬核所以才拖到了这周五.我相信大家应该能从标题当中体会到这个硬核. 周五的专题是大数据和分布式,我最初的打算 ...
- ES6/JavaScript一些‘巧用’
前言 第一次发表文章,如有不好的地方请见谅/ 在编写JavaScript代码的时候存在的一些方法和技巧,虽然有时候条条大路都通向罗马,但是也许总会有那么一条最短的路径可走.本文将一些都知道却不怎么用的 ...
- spring——AOP原理及源码(四)
前情回顾: 上文我们一路分析了从容器创建开始直到我们的AOP注解导入的核心组件AnnotationAwareAspectJAutoProxyCreator执行postProcessBeforeInst ...
- NSFileHandle的用法(用于读写文件)
利用NSFilehandle类提供的方法,允许更有效地使用文件. 一般而言,处理文件时都要经历以下三个步骤: 1.打开文件,并获取一个NSFileHandle对象,以便在后面的I/O操作中引用该文件 ...
- 分享一次C#调用Delphi编写Dll程序
1.前言: 最近接手了一个项目需要和Delphi语言编写的一个系统进行一些接口的对接,数据在传输过程中采用Des加密方式,因为Delphi 平台的加密方式和C#平台的加密方式不互通,所以采用的方式是C ...