资讯动态

MySQL 二级索引的非叶子节点存了什么:索引值、主键值和子页号指针

发布时间:2026/10/27 10:34:08 来源:尧图企业网站定制
MySQL 二级索引的非叶子节点存了什么索引值、主键值和子页号指针二级索引的查找要先用索引键定位再拿主键回表。这篇从一条二级索引查询出发拆开非叶子节点的记录结构说明索引值、主键值和子页号各自负责什么并给出可复核的官方文档和验证入口。一、使用场景二级索引列的值重复度很高时这个结构问题才会真正暴露。比如订单表在 status 上建索引几十万行都是 paid仅靠 status 无法把记录排成唯一顺序也无法决定查询应该往哪个子页走。这时主键必须参与比较二级索引的键实际变成索引列加主键的组合。主键变长也有同样的影响。InnoDB 会把主键追加到每个二级索引主键越长二级索引占用的空间越大非叶子页能容纳的分隔键越少树的层数也更容易升高。手册 17.6.2.1 对此有一句直接说明主键较长时二级索引会占用更多空间因此短主键更有优势。二、核心概念InnoDB 的表数据按 B 树组织。聚簇索引的叶子页存放完整行二级索引的叶子页存放索引列和主键列位于两者之上的非叶子页只负责导航。页头的 PAGE_LEVEL 记录当前页在树中的层级0 表示叶子页。手册 17.11.2 还说明每个索引会分配两个 segment一个给非叶子节点一个给叶子节点。官方手册对二级索引记录的组成写得很明确In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index.这句话覆盖的是每条二级索引记录。再结合 10.3.10 的索引延伸规则InnoDB 会自动把主键列追加到每个二级索引声明为 k_d(d) 的索引在内部按 (d, i1, i2) 处理。二级索引的完整键是索引列在前、主键列在后键长和排序位置都按这个组合计算。非叶子页的每条记录由三部分组成索引值分隔键的头部决定第一轮比较。主键值索引值相同时继续比较让组合键在树中有唯一位置。子页号下一层页面的编号沿着它继续下降。用两层树表示这个结构根页 / 中间页PAGE_LEVEL 0 (d2000-01-01, i11, i22) - 子页号 12 (d2000-01-02, i11, i21) - 子页号 27 │ ▼ 叶子页PAGE_LEVEL 0 (d2000-01-01, i11, i22) - 聚簇索引中的 (1, 2) 行叶子记录与非叶子记录的差别在尾部叶子记录带完整键并用主键指向聚簇索引中的行非叶子记录在键后面接子页号。MySQL Server 源码文档对 btr_node_ptr_get_child_page_no 的说明是“Gets the child node file address in a node pointer”即 node pointer 中保存的是子页地址btr_page_get_level 的注释则明确“leaf level 0”可以据此区分层级。需要区分的是子页号指向 B 树中的页面并不指向数据行也不等同于聚簇索引的页号。二级索引只保存主键值回表时再由主键到聚簇索引里定位整行。三、最小代码用一张复合主键加单列二级索引的表就能看到这个结构createtablet1(i1intnotnulldefault0,i2intnotnulldefault0,ddatedefaultnull,primarykey(i1,i2),keyk_d(d))engineinnodb;insertintot1values(1,1,2000-01-01),(1,2,2000-01-01),(2,1,2000-01-01),(2,2,2000-01-01),(3,1,2000-01-01),(3,2,2000-01-01),(4,1,2000-01-01),(4,2,2000-01-01);主键是 (i1, i2)二级索引只声明了 d。八条记录的 d 完全相同仅看 d 无法排序InnoDB 把主键追加到 k_d 之后索引键变成 (d, i1, i2)记录才能按 (d, 1, 1)、(d, 1, 2)、(d, 2, 1) 这样的顺序排开。非叶子页保存的分隔键也必须是这个组合否则上层无法判断一条新记录该落到哪个子页。四、执行流程以select count(*) from t1 where i1 3 and d 2000-01-01为例查找沿下面这条路径推进优化器选择 k_d按扩展后的 (d, i1, i2) 把 d 和 i1 当作键的左侧前缀。从根页开始用目标键与每条分隔键比较先比 d相同再比 i1i2 没有出现在条件里用来在相同前缀内确定扫描起点。找到合适的子页号后进入下一层中间页重复同样的比较直到到达叶子页。叶子页内做二分命中记录后取出主键 (i1, i2)。查询列被索引覆盖时直接返回需要其他列时拿主键回聚簇索引取整行。插入导致页分裂时新页的首条键会作为 node pointer 写入上层上层也满时继续向上分裂根页分裂后树高增加一层。这套流程里非叶子页承担的职责很单一提供分隔键和下一跳页号。它不保存行数据也不保存聚簇索引的页号所以页分裂时只需要调整分隔键和子页号不必移动行。五、常见坑只记住“二级索引存索引列”漏掉主键参与排序。遇到重复值多的索引就无法解释定位过程也容易把索引延伸理解成优化器的可选行为。把子页号当成行地址或聚簇索引页号。二级索引叶子记录里只有主键值回表是另外一跳页号只服务于 B 树内部的下降。用 SHOW INDEX 判断索引的真实键。SHOW INDEX 只显示声明的 d追加的主键列不会出现key_len 和查询计划才能反映扩展后的前缀。低估长主键的代价。主键列会出现在每个二级索引的每一层字符型主键尤其明显索引体积和树高都会随之增加。把唯一二级索引当成例外。唯一索引的分隔键本身已经唯一路由时不需要再靠主键补足叶子记录仍然带主键用于回表索引延伸带来的空间开销不变。六、验证方式本文没有在本机 MySQL 实例上执行这些语句下面给的是可复现路径预期值来自手册 10.3.10 的示例输出。建表插数据后先清空表缓存和状态计数器flushtables;flushstatus;再看执行计划explainselectcount(*)fromt1wherei13andd2000-01-01;手册示例中优化器不考虑索引延伸时 key_len 是 4、ref 是 const、rows 是 5考虑延伸后 key_len 变成 8、ref 变成 const,const、rows 变成 1。key_len 从 4 增加到 8说明查询用上了 d 后面的 i1二级索引记录确实带主键列。当前数据里 i13 有两行rows 会与手册不同key_len 和 ref 这两个值仍然能说明问题。扫描行数可以进一步确认showstatuslikehandler_read%;手册示例中 Handler_read_next 从 5 降到 1。在当前数据下这个值应当接近匹配的记录数而不是按 d 值全扫。索引声明本身看不到扩展部分showindexfromt1;结果只列出 PRIMARY(i1, i2) 和 k_d(d)追加的主键列不会显示出来。这里正是“声明键”和“物理键”需要分开看的地方。需要直接观察页内记录时可以用 innodb_ruby 提供的 innodb_space 按页转储innodb_space-ft1.ibd-p3page-dump按该工具的说明非叶子页会列出 node pointer 的键值和子页号叶子页会列出完整记录。这个工具不属于 MySQL 自带命令适合本地观察页结构生产环境不要依赖它。七、总结二级索引的非叶子节点承担路由不承担数据。它保存的键是索引列加主键的组合尾部接子页号叶子记录才带完整键并用主键回表。判断一条二级索引查询的成本时先看键的完整组成再看查询列是否被索引覆盖就能判断会不会多一次回表。参考资料MySQL 8.4 Reference Manual 17.6.2.1 Clustered and Secondary IndexesMySQL 8.4 Reference Manual 10.3.10 Use of Index ExtensionsMySQL 8.4 Reference Manual 17.10 InnoDB Row FormatsMySQL 8.4 Reference Manual 17.11.2 File Space ManagementMySQL Internals: InnoDB Page StructureMySQL Internals: Page HeaderMySQL Server Source Documentation: btr0btr.h

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑