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

Mysql在页内是怎么查找数据的?

我们知道 B+ 树像一本层层递进的目录。现在,这段话描述的是这本字典的“最后一层”——也就是 具体的一页“正文页”(叶子节点) 内部,是怎么快速找到你想要的“词条”的。

想象一下,你翻到了字典的某一页,这一页密密麻麻写满了词。你不可能从头到尾一个一个字地看,对吧?这一页(16KB大小)内部也需要一个“迷你目录”来快速定位。这个“迷你目录”就是“页目录结构”。

它通过把页内的数据分成一个个小“组”(槽),并记录每个组的最大词条,然后利用二分查找快速定位到词条可能所在的“组”,最后再在这个小“组”里进行极短的顺序查找。

为什么需要“页目录结构”?——底层原理与效率考量

我们知道,B+ 树的每个节点(不管是内部节点还是叶子节点)在磁盘上都对应一个“数据页”(Page),默认是 16KB。

  1. 一个页能装多少数据?
    如果每条记录很小(比如几十字节),一个 16KB 的页就能装下成百上千条记录!
    例如,如果一条记录 100 字节,一个 16KB 的页就能装下 16 * 1024 / 100 ≈ 160 条记录。
    如果只有键值和指针,非叶子节点能装得更多。

  2. 如何在页内快速查找?

    • 当数据库把一个 16KB 的页从磁盘读到内存后,现在的问题是如何在内存中这 16KB 的数据里,快速找到我们想要的那条记录。
    • 如果只是把记录简单地堆在一起,要找某条记录就得一条一条地遍历,这是 O(N) 的线性查找,效率太低了。
    • 虽然数据在页内是按照键值排序的,我们可以进行二分查找 (O(logN)),但直接对所有记录进行二分查找,需要不断地计算偏移量和比较,仍然不够“优雅”和快速。
  3. “页目录”的解决方案:二分查找 + 局部遍历
    为了解决这个问题,InnoDB 在每个数据页(包括叶子节点和非叶子节点)的内部,又额外维护了一个精简的“页目录”(Page Directory)。

    这个页目录:

    • 把页内的所有记录逻辑上划分成若干个小的“组”(slotrecord group)。
    • 页目录本身存储的是指向每个组 最后一条记录的偏移量(或最大键值)
    • 由于这些组是按键值顺序排列的,所以页目录里的条目也是有序的。

    这样,查找一条记录就分成了两步:

    1. 在页目录中进行二分查找: 快速定位到目标记录所在的“组”。因为页目录条目少,这个二分查找非常快。
    2. 在找到的组内进行顺序遍历: 由于每个组的记录数量非常少(后面会讲到具体规则),在这个小范围内进行顺序查找也非常快。

这种组合方式,既利用了二分查找的效率,又避免了复杂的数据结构(如红黑树)在页内维护的开销。

详细解析:图文结合与模拟

我们用一个简化的例子来模拟一个叶子页的内部结构和查询过程。

假设我们一个叶子页,里面有按主键排序的记录:1, 2, 3, 4, 5, 6, 7, 8, 9, 10
根据 InnoDB 的规定,第一个组只有一条记录,中间组 4-8 条,最后一个组 1-8 条。为了方便演示,我们假设中间组都是 4 条记录。

原始记录序列(已排序):
[1, Record1], [2, Record2], [3, Record3], [4, Record4], [5, Record5], [6, Record6], [7, Record7], [8, Record8], [9, Record9], [10, Record10]
逻辑分组
页目录 (Page Directory)
记录存储区域 (物理上是链表,逻辑上是排序的)
对应槽0
对应槽1
对应槽2
对应槽3
组 1: [1]
组 2: [2, 3, 4, 5]
组 3: [6, 7, 8, 9]
组 4: [10]
槽 0: 指向记录 1 (组1最大键=1)
槽 1: 指向记录 5 (组2最大键=5)
槽 2: 指向记录 9 (组3最大键=9)
槽 3: 指向记录 10 (组4最大键=10)
记录 2
记录 1
记录 3
记录 4
记录 5
记录 6
记录 7
记录 8
记录 9
记录 10
16KB 数据页 (例如叶子节点)

这里,页目录的每个“槽”存储的是其对应组中最大记录的“指针”(或者说偏移量),并且这些“槽”自身是按键值大小有序排列的。

模拟查询过程:查找主键为 3 的记录

假设我们要在这个页中查找主键为 3 的记录。页目录有 4 个槽(low=0, high=3):

  • 槽 0 指向最大键 1
  • 槽 1 指向最大键 5
  • 槽 2 指向最大键 9
  • 槽 3 指向最大键 10
找到 3
开始在页内查找键值 3
1. 在页目录上进行二分查找 (low=0, high=3)
计算中间槽 mid = (0+3)/2 = 1
槽 1 对应的最大键是 5
目标键 3 <= 5?
将 high 移动到 mid (high=1)
low=0, high=1
2. 继续二分 mid = (0+1)/2 = 0
槽 0 对应的最大键是 1
目标键 3 <= 1?
将 low 移动到 mid+1 (low=1)
low=1, high=1
此时 high - low = 0
二分查找结束,目标槽是 high (槽 1)
3. 定位到槽 1 指向的组
这个组的起始记录是 槽 0 的下一条 (即记录 2),或槽 1 指向的记录5的组首
(具体实现是找到槽 high 对应的记录组的开始位置)
在组内从记录 2 开始顺序遍历:
- 记录 2 (id=2)
- 记录 3 (id=3)
4. 找到记录 3,返回
查询完成

模拟过程解释:

  1. 二分查找页目录:

    • 初始 low = 0, high = 3
    • 第一次 mid = (0 + 3) / 2 = 1。槽 1 的最大键是 5。因为 3 <= 5,说明目标键可能在槽 1 对应的组或更靠前的组中。所以 high = mid = 1
    • 现在 low = 0, high = 1
    • 第二次 mid = (0 + 1) / 2 = 0。槽 0 的最大键是 1。因为 3 > 1,说明目标键不在槽 0 对应的组,而在更靠后的组。所以 low = mid + 1 = 1
    • 现在 low = 1, high = 1high - low = 0,二分查找结束。我们找到了槽 1
  2. 在组内顺序遍历:

    • 定位到槽 1 对应的组。这个组包含了从主键 2 到主键 5 的记录(因为槽 0 的最大键是 1,槽 1 的最大键是 5)。
    • 我们从这个组的起始记录(即主键 2 的记录)开始,一条一条地往后找:
      • 遇到记录 2,不是 3
      • 遇到记录 3,Bingo!找到了。
    • 返回记录 3。

这个例子中的“槽 1 入手拿到主键 2 的记录”是关键。因为槽 1 指向的是组内最大键 5 的记录,它实际上代表的是 (max_key_of_previous_slot, max_key_of_current_slot] 这个范围。所以,找到槽 1,意味着我们找到了 (1, 5] 这个组。我们从这个组的第一条记录(主键 2)开始往后遍历,直到找到主键 3。

InnoDB 的具体规定

文中提到的 InnoDB 规定,是为了确保组内查找的效率:

  • 第一个分组只有一条记录: 这条记录通常是页中最小的那条,或者是一个特殊的“infimum”记录。
  • 中间的分组 4-8 条记录: 这是核心。将组大小限制在 4-8 条,意味着即使进行顺序遍历,最多也只需要比较 8 次,这是非常快的。
  • 最后一个分组 1-8 条记录: 和中间分组类似,保证末尾组的效率。

“因此不必担心遍历很长的链表导致性能问题。” 这句话就是对上面这些规则的总结。它强调的是,虽然页内的记录是通过链表组织(为了插入删除方便),但页目录的存在和严格的组大小限制,使得查找时不需要遍历整个链表,而是在极短的链表片段上进行遍历。

核心思想与总结

  • 分而治之: 就像 B+ 树将整个数据文件分成多个页,页目录又将一个页内的记录分成多个组。
  • 层次查找: 从 B+ 树的根节点,通过二分查找(或类似方式)定位到正确的子节点,最终找到目标数据所在的叶子节点。一旦页被加载到内存,页目录结构 再次使用二分查找定位到页内的小组,然后在这个小组内进行快速的顺序遍历。
  • 兼顾读写: 记录通过单向链表在页内串联,方便插入和删除,而页目录则主要优化了查找性能。
  • 极致优化磁盘I/O和CPU效率: B+ 树保证了少量的磁盘I/O次数,而页目录则确保了加载到内存后的数据页的CPU处理效率。

所以,当你说 SELECT * FROM table WHERE id = X; 时,MySQL 经历了:

  1. B+ 树全局定位: 从根节点出发,层层下降,每次读取一个节点(对应一次磁盘I/O),最终找到目标数据所在的叶子节点。
  2. 页内局部定位: 叶子节点(16KB)被加载到内存后,利用其内部的“页目录”,通过一次快速的二分查找,迅速定位到包含目标记录的小组。
  3. 小组内精确查找: 在这个极小的记录小组内进行简单的顺序遍历,找到最终的记录。

这个“页目录结构”就是 B+ 树能在大数据量下依然保持高效查询的关键细节之一。它展现了数据库系统在设计时,对各个层次性能瓶颈的细致考量。

http://www.dtcms.com/a/311213.html

相关文章:

  • 第14届蓝桥杯Python青少组中/高级组选拔赛(STEMA)2022年11月真题
  • web练习
  • 初始C语言---第四讲(数组)
  • Tlias案例-登录 退出 打包部署
  • 【自动化运维神器Ansible】YAML语法详解:Ansible Playbook的基石
  • 【自动化运维神器Ansible】YAML支持的数据类型详解:构建高效Playbook的基石
  • 人工智能与农业:智慧农业的发展与未来
  • 基于Postman进行http的请求和响应
  • 基于SpringBoot+MyBatis+MySQL+VUE实现的在线课程管理系统(附源码+数据库+毕业论文+开题报告+部署教程+配套软件)
  • Ubuntu系统VScode实现opencv(c++)图像放缩与插值
  • 高效轻量的C++ HTTP服务:cpp-httplib使用指南
  • Android 15 限制APK包手动安装但不限制自升级的实现方案
  • 把“多视图融合、深度传感”组合在一起,今天分享3篇3D传感技术干货
  • [硬件电路-120]:模拟电路 - 信号处理电路 - 在信息系统众多不同的场景,“高速”的含义是不尽相同的。
  • Word怎样转换为PDF
  • Qwen3 Embedding:新一代文本表征与排序模型
  • 2411. 按位或最大的最小子数组长度
  • Django开发中医针灸经络图系统实战
  • 【iOS】3GShare仿写
  • 【Linux网络】netstat 的 -anptu 各个参数各自表示什么意思?
  • 2025 年 VSCode 插件离线下载硬核攻略
  • 打破传统养育框架:梁婉昕的 “非矫正式教育” 探索|创客匠人
  • 八股取士--docker
  • 在 AKS 中运行 Azure DevOps 自托管代理-2
  • 贪心算法应用:3D打印支撑结构问题详解
  • CommonJS和ES6 Modules区别
  • 如何安装和使用 Cursor AI 编辑器
  • 深度解读 | 斯坦福:2025 AI 指数报告
  • 【深度学习新浪潮】什么是专业科研智能体?
  • 【OpenGL】LearnOpenGL学习笔记01 - 环境配置、窗口创建