当前位置: 首页 > news >正文

一颗B+树可以存储多少数据?

一、前言

这个问题,非常经典,考察的点很多:

比如:

        1、操作系统存储的单元,毕竟mysql也是运行在操作系统之上的应用。

        2、B+树是针对Mysql的InnoDB存储引擎,所以要理解InnoDb的最小存储单元,页,区,段的概念,尤其是页的格式,里面有哪些构成。

        2、明确这颗B+树一定是由主键索引构建的B+树,所以最终的数据是存储在叶子结点,非叶子节点,只存储主键索引,所以主键索引的大小决定了最终能存放多少数据。

答案:

主键为bigint(约2000w):

2层B+树的话:可以存放1170个*16条=18720条(行)数据。
3层B+树的话:可以存放1170个*1170个*16条=21902400条(行)数据。

主键为int(约4000w):

2层B+树的话:可以存放1600个*16条=25600条(行)数据。
3层B+树的话:可以存放1600个*1600个*16条=40960000条(行)数据。

在使用主流磁盘存储的条件一下,查询一条数据的时间在50ms内

二、磁盘存储和InnoDB的存储

我们都知道计算机在存储数据的时候,有最小存储单元,这就好比我们今天进行现金的流通最小单位是一毛。在计算机中磁盘存储数据最小单元是扇区,一个扇区的大小是512字节,而文件系统(例如XFS/EXT4)他的最小单元是块,一个块的大小是4k,

下面几张图可以帮你理解最小存储单元:

文件系统中一个文件大小只有1个字节,但不得不占磁盘上4KB的空间。

而对于我们的InnoDB存储引擎也有自己的最小储存单元——页(Page),一个页的大小是16K,如下图所示:

并且innodb所有的数据文件也就是我们之前提到的后缀为.ibd的文件,它们的大小都是16k的整数倍。

由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,然后读入到内存,也就是说查询过程中会发生多次磁盘 I/O,而磁盘 I/O 次数越多,所消耗的时间也就越大。

所以,我们希望索引的数据结构能在尽可能少的磁盘的 I/O 操作中完成查询工作,因为磁盘 I/O 操作越少,所消耗的时间也就越小。

另外,MySQL 是支持范围查找的,所以索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效地执行范围查找。

所以,要设计一个适合 MySQL 索引的数据结构,至少满足以下要求:

  • 能在尽可能少的磁盘的 I/O 操作中完成查询工作;
  • 要能高效地查询某一个记录,也要能高效地执行范围查找;

记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。

因此,InnoDB 的数据是按「数据页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。

三、Innodb数据页格式

数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。

数据页包括七个部分,结构如下图:

这 7 个部分的作用如下图:

在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图所示:

采用链表的结构是让数据页之间不需要是物理上的连续的,而是逻辑上的连续。

数据页的主要作用是存储记录,也就是数据库的数据,所以重点说一下数据页中的 User Records 是怎么组织数据的。

数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。

因此,数据页中有一个页目录,起到记录的索引作用,就像我们书那样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。

那 InnoDB 是如何给记录创建页目录的呢?页目录与记录的关系如下图:

页目录创建的过程如下:

  1. 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
  2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段)
  3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录

从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。

以上面那张图举个例子,5 个槽的编号分别为 0,1,2,3,4,我想查找主键为 11 的用户记录:

  • 先二分得出槽中间位是 (0+4)/2=2 ,2号槽里最大的记录为 8。因为 11 > 8,所以需要从 2 号槽后继续搜索记录;
  • 再使用二分搜索出 2 号和 4 槽的中间位是 (2+4)/2= 3,3 号槽里最大的记录为 12。因为 11 < 12,所以主键为 11 的记录在 3 号槽里;
  • 这里有个问题,「槽对应的值都是这个组的主键最大的记录,如何找到组里最小的记录」?比如槽 3 对应最大主键是 12 的记录,那如何找到最小记录 9。解决办法是:通过槽 3 找到 槽 2 对应的记录,也就是主键为 8 的记录。主键为 8 的记录的下一条记录就是槽 3 当中主键最小的 9 记录,然后开始向下搜索 2 次,定位到主键为 11 的记录,取出该条记录的信息即为我们想要查找的内容。

看到第三步的时候,可能有的同学会疑问,如果某个槽内的记录很多,然后因为记录都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是 O(n) 了吗?

这点不用担心,InnoDB 对每个分组中的记录条数都是有规定的,槽内的记录就只有几条:

  • 第一个分组中的记录只能有 1 条记录;
  • 最后一个分组中的记录条数范围只能在 1-8 条之间;
  • 剩下的分组中记录条数范围只能在 4-8 条之间。

四、B+ 树是如何进行查询的?

上面我们都是在说一个数据页中的记录检索,因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。

但是,当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。

为了解决这个问题,InnoDB 采用了 B+ 树作为索引。磁盘的 I/O 操作次数对索引的使用效率至关重要,因此在构造索引的时候,我们更倾向于采用“矮胖”的 B+ 树数据结构,这样所需要进行的磁盘 I/O 次数更少,而且 B+ 树 更适合进行关键字的范围查询。

InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:

通过上图,我们看出 B+ 树的特点:

  • 只有叶子节点(最底层的节点)才存放了数据,非叶子节点(其他上层节)仅用来存放目录项作为索引。
  • 非叶子节点分为不同层次,通过分层来降低每一层的搜索量;
  • 所有节点按照索引键大小排序,构成一个双向链表,便于范围查询;

我们再看看 B+ 树如何实现快速查找主键为 6 的记录,以上图为例子:

  • 从根节点开始,通过二分法快速定位到符合页内范围包含查询值的页,因为查询的主键值为 6,在[1, 7)范围之间,所以到页 30 中查找更详细的目录项;
  • 在非叶子节点(页30)中,继续通过二分法快速定位到符合页内范围包含查询值的页,主键值大于 5,所以就到叶子节点(页16)查找记录;
  • 接着,在叶子节点(页16)中,通过槽查找记录时,使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到主键为 6 的记录。

可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。

总结

InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。

数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。

为了高效查询记录所在的数据页,InnoDB 采用 b+ 树作为索引,每个节点都是一个数据页。

如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引,一个表中可以有多个二级索引。

在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。

同理三层高的B+树

叶子节点的一条记录按照1k算

1170*1170*16 约等于 两千万

根结点常驻内存,根据磁盘每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。

那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右。

那么在有索引的条件下,查询一个数据大约需要两次IO,在两千万数据的量级下,时间可以控制在20ms左右。

参考文献

https://www.cnblogs.com/leefreeman/p/8315844.html

mysql 最大建议行数2000w,靠谱吗? - 京东云开发者的个人空间 - OSCHINA - 中文开源技术交流社区

 MySQL InnoDB引擎——三层B+树可以存储多少数据量_3层b+树可以存多少数据-CSDN博客

面试官问我为啥B+树一般都不超过3层?3层B+树能存多少数据?redo log与binlog的两阶段提交?_b+树为什么三层-CSDN博客

相关文章:

  • 【大数据 复习】第11,12,13,14章
  • SSRF服务端请求伪造
  • STM32三种调试工具CMSIS-DAP、J-Link和ST-Link
  • Renesas MCU使用定时器之实现1ms定时中断
  • redis大key优化
  • csdn上传源码资源卖钱能买房买车吗?每天最高收入200-500?
  • Docker 可用镜像源
  • Qt 技术博客:深入理解 Qt 中的 delete 和 deleteLater 与信号槽机制
  • docker内apt-get update Waiting for headers 0%
  • 音视频的Buffer处理
  • Parallelize your massive SHAP computations with MLlib and PySpark
  • NTFS和exFAT哪个性能好 U盘格式化NTFS好还是exFAT好 mac不能读取移动硬盘怎么解决
  • 深信服科技:2023网络安全深度洞察及2024年趋势研判报告
  • [实践篇]13.29 再来聊下Pass Through设备透传
  • sixLabors.ImageSharp图片截取
  • 「前端早读君006」移动开发必备:那些玩转H5的小技巧
  • 2017-08-04 前端日报
  • 2019.2.20 c++ 知识梳理
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • IDEA常用插件整理
  • JavaScript DOM 10 - 滚动
  • orm2 中文文档 3.1 模型属性
  • Vue2.x学习三:事件处理生命周期钩子
  • 给github项目添加CI badge
  • 湖南卫视:中国白领因网络偷菜成当代最寂寞的人?
  • 前嗅ForeSpider中数据浏览界面介绍
  • 区块链共识机制优缺点对比都是什么
  • 算法-插入排序
  • 微信小程序上拉加载:onReachBottom详解+设置触发距离
  • 写给高年级小学生看的《Bash 指南》
  • 原生 js 实现移动端 Touch 滑动反弹
  • 走向全栈之MongoDB的使用
  • ​第20课 在Android Native开发中加入新的C++类
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • # 执行时间 统计mysql_一文说尽 MySQL 优化原理
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • (1)(1.13) SiK无线电高级配置(六)
  • (3)nginx 配置(nginx.conf)
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (博弈 sg入门)kiki's game -- hdu -- 2147
  • (二)hibernate配置管理
  • (附源码)springboot宠物管理系统 毕业设计 121654
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (学习日记)2024.02.29:UCOSIII第二节
  • (一)RocketMQ初步认识
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • (转)我也是一只IT小小鸟
  • .net core 的缓存方案
  • .Net MVC + EF搭建学生管理系统
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)
  • .net后端程序发布到nignx上,通过nginx访问
  • .Net下的签名与混淆