首页
学习
活动
专区
圈层
工具
发布

MySQL 三层 B+ 树能存多少数据?

别再死背“三层 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,三层聚簇索引大概能放一千多万到两千万级别的数据。行越小,能放得越多;行越大,树越容易长高。

真到线上,先看页大小,再看平均行长度,再看索引统计。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Os9w9NzkVeDn6x6Liee1LQFQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券