Skip to content

请详细描述 MySQL 的 B+ 树中查询数据的全过程 ​

  • 标签:后端、MySQL、数据库

🏛 业务架构师 ​

我把查询过程和容量规划绑在一起讲,这是它对业务最有价值的地方。

InnoDB 页大小 16KB。查询从根节点开始:根页(常驻内存)定位到中间层页(大概率也在 buffer pool),中间层把范围定位到具体叶子页,叶子页内二分找到目标行——三次页访问,通常只有最后一次可能发生磁盘 IO,这就是 B+ 树查询"快"的本质:把随机 IO 次数压到常数级。

再看容量估算:主键 bigint 8 字节,非叶子页每个条目约主键+页指针 14~16 字节,一页约 1170 个条目;叶子页每行按 1KB 算可放 16 行。于是三层 B+ 树 ≈ 1170 × 1170 × 16 ≈ 2000 万行——这就是业内"单表 2000 万考虑拆分"经验值的由来。

业务上的用法:

  • 单表容量水位线:数据量接近两千万、且访问模式使树高即将从 3 层变 4 层(每次查询多一次 IO)时,启动归档或分表评估,而不是等慢查询爆发;
  • buffer pool 是第一杠杆:树的中间层和热数据页能常驻内存,查询就是纯内存操作。加内存往往比分库分表便宜得多,先榨干这个再谈重型方案;
  • 主键有序插入让页写满即分,空间利用率高,这是主键规范背后的物理逻辑。

把原理转成水位线和预算,才算真正用上了它。

🏗 系统架构师 ​

我以 where id = 42(聚簇索引)和 where name = 'x'(二级索引)两条路径完整讲:

二级索引查询:

  1. 从 Buffer Pool 取根节点页(未命中则从磁盘 .ibd 文件加载,一次随机 IO);
  2. 根节点页内二分定位区间,页尾的 Page Directory(槽位)加速页内查找;
  3. 沿非叶子节点逐层下探——每层一次页访问,页内通过槽位二分 + 单向链表扫描;
  4. 到叶子节点拿到主键值,比如 id=42;
  5. 回表:拿着 42 再从聚簇索引根节点走一遍同样的过程,叶子节点拿到完整行返回。

代价量化:树高 3 层时,二级索引查询 ≈ 3 次页访问(索引树)+ 3 次页访问(回表)≈ 6 次页访问;若 Buffer Pool 命中,全部内存操作微秒级;全 misses 则是 6 次磁盘随机 IO,机械盘毫秒级、SSD 百微秒级。这也是为什么 2000 万行建议控制树高在 3 层内——千万行的表树高才到 3,这是"2000 万上限"经验值的由来,本质是页访问次数×单页 IO 的乘积不能越界。

💻 全栈开发专家 ​

以 SELECT * FROM user WHERE id = 100 走聚簇索引为例,我把过程拆成引擎视角的动作:

  1. 根页定位:服务器先看 buffer pool 有没有 root 页(16KB),没有就从磁盘读入。非叶子节点里每项是(主键下界, 页指针),按主键二分/顺序查找决定走哪个子页。
  2. 逐层下探:每层都是"读页 → 页内二分 → 拿到子页号",页在 buffer pool 直接用,不在就触发一次磁盘随机 IO。
  3. 叶子页命中:到叶子层,页内二分找到 id=100 的槽,这条记录的完整行数据就在聚簇索引叶子节点上,直接返回。
  4. 若是二级索引(如 WHERE name='张'):在 idx_name 树上重复上述过程拿到主键,再回聚簇索引把这套流程走一遍(回表)。

关键数字:16KB 页、主键 bigint(8B)+页指针(6B),一个非叶子页约放 1170 个指针,三层树容量约 1170×1170×16 ≈ 2000 万行——也就是说一次主键查询最多 2~3 次页 IO,且热页常驻内存后接近纯内存操作。这就是 B+ 树"矮胖"设计的价值,也是我评估"这表该不该分表"的经验底线(两千万行级)。