别再死背“三层 B+ 树能存两千万”这个数了。
这个数不是 MySQL 写死的,也不是 InnoDB 哪个配置项规定的。它只是拿16KB 页大小、bigint 主键、单行 1KB 左右这么一套假设算出来的结果。
换个字段类型,换个行大小,立刻变。
我一般看到有人问这个问题,第一反应不是回答多少条,而是先问两个东西:
show variables like 'innodb_page_size';
show table status like 'order_info'\G
第一个看 InnoDB 页大小,默认通常是 16KB。第二个看平均行长度,Avg_row_length这个值比嘴上估算靠谱一点。
B+ 树不是按“条”长高的,是按“页”长高的。
InnoDB 里一页默认 16KB。索引节点放在页里,根节点一页,第二层内部节点很多页,第三层才是叶子节点。
如果是主键索引,叶子节点里放的是整行数据。这个地方很多人算错,直接拿主键大小去算叶子节点能放多少行,那就离谱了。主键索引的叶子页不是只放 id,它放整行。
大概结构可以这么理解:
root page
-> internal page
-> leaf page
-> row data
内部节点比较省空间,它主要放两类东西:
主键值 + 子页指针
假设主键是 bigint,8 字节。页号指针粗略按 6 字节算,再加上一些记录头、页目录、槽位这些开销。别抠到字节级,工程上没有意义。粗算一个内部节点能挂 1000 个左右子节点,是比较常见的估法。
叶子节点就看行大小了。
比如订单表长这样:
create table order_info (
id bigint primary key,
user_id bigint not null,
order_no varchar(32) not null,
status tinyint not null,
pay_amount bigint not null,
buyer_remark varchar(255),
create_time datetime not null,
update_time datetime not null
) engine = InnoDB;
这种表如果备注字段不大,平均一行可能几百字节。如果字段再肥一点,带几个 varchar、text 外溢指针、各种扩展字段,平均一行到 1KB 也不稀奇。
那三层能存多少,账就出来了:
第一层 root:大约 1000 个指针
第二层 internal:每个再挂大约 1000 个叶子页
第三层 leaf:每个叶子页放 N 行数据
总行数 ≈ 1000 * 1000 * N
如果一行按 1KB 算,一个 16KB 的叶子页别指望放满 16 行,页头、页目录、行记录开销都要吃空间。粗一点按 15 行算:
1000 * 1000 * 15 = 1500 万
如果一行 500 字节,一个叶子页能放 30 行左右:
1000 * 1000 * 30 = 3000 万
所以“三层 B+ 树两千万”不是神秘结论,就是这么来的。
我写个小 Java,把这个账算清楚一点:
public class InnoDbTreeCalc {
public static void main(String[] args) {
int pageSize = 16 * 1024;
int pkBytes = 8; // bigint 主键
int childPointerBytes = 6; // 子页指针,粗算
int recordExtraBytes = 8; // 记录头、槽位等开销,别算太死
int innerEntryBytes = pkBytes + childPointerBytes + recordExtraBytes;
int innerFanout = pageSize / innerEntryBytes;
int avgRowBytes = 1024; // 这里换成 show table status 看到的 Avg_row_length
int leafRows = pageSize / avgRowBytes;
long rows = 1L * innerFanout * innerFanout * leafRows;
System.out.println("internal fanout = " + innerFanout);
System.out.println("leaf rows/page = " + leafRows);
System.out.println("3-level rows ≈ " + rows);
}
}
跑出来大概是:
internal fanout = 744
leaf rows/page = 16
3-level rows ≈ 8856576
你看,这里就不是两千万了。
为什么?因为我给内部节点预留了更保守的开销。要是只拿8 + 6去算:
int innerEntryBytes = 8 + 6;
int innerFanout = 16 * 1024 / innerEntryBytes;
那就是:
innerFanout ≈ 1170
leafRows ≈ 16
rows ≈ 2190 万
这就是网上常见的两千万口径。
但线上排查时,我不会拿这个数当标准答案。尤其是看到这种表,我第一眼就不太信它三层能撑很久:
create table user_profile (
id bigint primary key,
nickname varchar(64),
avatar_url varchar(512),
address varchar(512),
ext_json text,
create_time datetime
) engine = InnoDB;
行太肥了。
B+ 树高度上去以后,最直接的影响是磁盘随机 IO 次数可能增加。三层查一次主键,理论上走 root、internal、leaf。根节点和部分内部节点通常在 buffer pool 里,真正麻烦的是叶子页不在内存,命中率一掉,延迟就开始抖。
看高度可以直接查:
select
name,
stat_name,
stat_value
from mysql.innodb_index_stats
where database_name = database()
and table_name = 'order_info'
and stat_name in ('size', 'n_leaf_pages');
n_leaf_pages能看到叶子页数量。拿行数除一下,基本能反推每个叶子页放了多少行。这个比背公式实在。
所以这个问题最后别记成“三层等于两千万”。
更准确的说法是:
在 InnoDB 默认 16KB 页大小下,如果主键是 bigint,单行数据大约 1KB,三层聚簇索引大概能放一千多万到两千万级别的数据。行越小,能放得越多;行越大,树越容易长高。
真到线上,先看页大小,再看平均行长度,再看索引统计。