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

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.xdnf.cn/news/16795.html

相关文章:

  • Web 开发 10
  • Redis 核心概念、命令详解与应用实践:从基础到分布式集成
  • pyqt5显示任务栏菜单并隐藏主窗口,环境pyqt5+vscode
  • JVM 03 类加载机制
  • Python打卡Day30 模块和库的导入
  • LeetCode 刷题【26. 删除有序数组中的重复项、27. 移除元素、28. 找出字符串中第一个匹配项的下标】
  • vue2一种快速导入 Element UI(即 Element 2.x)方式
  • ARM Cortex-M异常处理高级特性详解
  • MCP Agent 工程框架Dify初探
  • 【C++】类和对象(2)
  • AI Agent开发学习系列 - LangGraph(4): 有多个输入的Graph(练习解答)
  • 设计模式篇:在前端,我们如何“重构”观察者、策略和装饰器模式
  • Android 运行 deno 的新方法 (3): Termux 胖喵安初
  • vue3pinia
  • 深度学习核心:卷积神经网络 - 原理、实现及在医学影像领域的应用
  • vue3 新手学习入门
  • Elasticsearch 混合检索一句 `retriever.rrf`,把语义召回与关键词召回融合到极致
  • Agents-SDK智能体开发[5]之集成MCP进阶
  • 【vue】创建响应式数据ref和reactive的区别
  • Ⅹ—6.计算机二级综合题23---26套
  • 两个服务之间的大规模数据推送
  • TOGAF指南1
  • Thymeleaf 模板引擎原理
  • 网站QPS多少才算高并发
  • c++和python联合编程示例
  • 5-EP4CE10F17C8-引脚配置
  • MySQL 高并发下如何保证事务提交的绝对顺序?
  • 向量投影计算,举例说明
  • 幂等性校验(订单重复提交问题)
  • X2Doris是SelectDB可视化数据迁移工具,安装与部署使用手册,轻松进行大数据迁移