ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL三层B+树能存多少数据?InnoDB页结构与容量估算全解析

MySQL三层B+树能存多少数据?InnoDB页结构与容量估算全解析 很多人在面试里被问过这道题MySQL的三层B树能存多少数据网上流传的标准答案是“两千万行左右”但你要是接着问“为什么是两千万而不是两百万或者两个亿”不少人就卡壳了。说实话这道题能答好的人不多因为它不是让你背数字而是要把InnoDB的页结构、主键设计、行存储方式、磁盘I/O成本全部串起来。把这套逻辑吃透之后你再去看索引设计、慢SQL优化、分库分表分界点心里会特别有底。这篇文章我就把“三层B树能存多少数据”从头到尾拆一遍手把手教你算清楚也算清楚你自己的表能扛多少量。1. 为什么 InnoDB 一定要用 B 树1.1 磁盘I/O的高成本决定了“矮胖”才能赢先想一个最基本的问题数据库的数据最终存在哪里答案是磁盘。只要数据不在内存里查询就必须走一次磁盘I/O。磁盘随机读有多慢传统机械硬盘一次随机寻道大概10毫秒上下换成固态硬盘会快不少但和内存的纳秒级访问速度比仍然不是一个量级。数据库里一条查询通常要走好几层索引才能定位到数据哪怕只多一次磁盘I/O放大到线上每秒几千上万次查询性能差距会被瞬间拉满。所以数据库索引首先要解决的核心问题是用尽可能少的磁盘I/O找到一个目标数据。假设我们用平衡二叉树当索引数据量到2000万的时候树的高度是多少log₂(2000万)大约是24层。也就是说最坏情况下定位一个叶子节点需要24次磁盘I/O。就算你有内存缓存24次I/O的代价也足以让很多查询慢到不可接受。而B树厉害的地方在于“矮胖”——每个节点可以拥有上千个子节点而不是只分两个叉。2000万条数据B树的高度通常只需要3到4层。一个查询从根节点走到叶子节点2到3次I/O就结束了。这个对比非常直观同样的数据量二叉树要24层B树只要3层差距就是天壤之别。这也是InnoDB为什么不选用二叉搜索树、平衡二叉树做索引的根本原因磁盘I/O太贵了树一定要矮。1.2 和B树比B树赢在哪既然提到B树绕不开的就是B树。很多人把这两个概念搞混其实它们长得还挺像都是多路搜索树。区别的关键在于节点里放不放数据。B树里每个节点既存放索引键也存放实际数据。这样带来的问题是每个节点能容纳的键数量被数据大小严重压缩。一个16KB的页如果既要装索引键又要装整行数据可能只能放下几十个键和指针那么扇出能力就弱树自然就长高了。B树不一样它把数据全部放到了叶子节点。非叶子节点只存放索引键和指向子节点的指针不存实际记录。这意味着非叶子节点的空间几乎全部用来“指路”一个16KB的页可以容纳上千个索引键加子指针。一层的扇出能力就能覆盖上千个页两层就能覆盖上百万个页。同时B树的叶子节点之间通过双向链表串了起来范围查询、排序、分页这类操作不需要反复回溯父节点直接顺着链表往下扫就行。这两点加起来让B树在数据库场景里成为比B树更合理的默认选择。说白了InnoDB选择B树不是图它名字好听而是因为它的设计天然贴合“磁盘I/O贵、查询路径要短、范围扫描要顺”这三个业务刚需。2. 三层B树“账本”是怎么算的2.1 先认识InnoDB的页要算B树能存多少数据第一步得知道InnoDB存储数据的最小单位页Page。InnoDB的默认页大小是16KB也就是16384字节。你可以用下面这条SQL确认你所在环境的页大小SHOW VARIABLES LIKE innodb_page_size;绝大多数生产环境跑出来都是16384。一个页里面并不是所有空间都能拿来存数据。页内会有固定开销比如文件头、页头、页目录、文件尾等真正能存用户记录的空间大概是15KB左右。我们估算的时候也不用抠得太细大致按有效空间15KB来算就行。可以把页理解成宿舍楼里的一个房间房间本身有墙、有门、有走廊真正能住人的使用面积不是总面积而是套内面积。页的“套内面积”才是能装记录的部分。2.2 非叶子节点一个页能管理多少个子页接下来算一个关键数字B树的非叶子节点里一条索引记录占多少空间。以bigint类型的主键为例主键本身占8字节指向下一层页的指针一般按6字节算再加上记录头等额外开销大概5字节一条索引记录大约占19字节。那我用15KB有效空间除以19字节大概得到808个指针。有些资料按更粗略的方法估算把每条索引记录算成16字节左右于是得到1000到1200个指针。到底取800还是1000其实都对因为量级完全相同。为了好记也为了和大多数文章口径一致我后面统一按“一个非叶子节点页能管理约1000个子页”来估算。但你要明白这个“1000”不是一个精确常量而是由主键长度和开销共同决定的变量。主键越长这个数字越小主键越短这个数字越大。这里最容易踩坑的点是很多人看到“1000”就直接背下来却不知道它背后的主键长度假设是8字节的bigint。如果主键是36字节的UUID同样16KB的页它能放下的索引记录就只有300多条了。这个差异会直接放大到最终的结果上。2.3 叶子节点一个页能存多少行非叶子节点算清楚了再看叶子节点。InnoDB的聚簇索引叶子节点里存放的是整行数据。那么一个16KB的页能放多少行完全取决于一行数据有多大。假设你有一张表主键是bigint再加上几个常用字段平均一行大小约1KB。那么15KB的有效空间可以放大约15行。如果一张表的行比较“瘦”平均500字节那一页能放30行左右。如果行里有大量varchar字段平均行大小到了4KB那一页只能放3到4行。需要特别注意的是行数据里除了字段值本身还有一些隐形的开销。比如每行记录头5字节、事务ID 6字节、回滚指针7字节如果存在变长字段和NULL值还有对应的长度列表和NULL值位图。所以“平均一行1KB”里的这个1KB指的是InnoDB真正在页里占用的物理长度而不是你建表时所有字段类型字节数简单相加的结果。后面我会再细说这个。2.4 把两者乘起来三层能存多少行现在可以算总账了。三层B树的结构是第一层是根节点第二层是内部节点第三层是叶子节点。第一层是一个页它能管理大约1000个第二层的页。每个第二层的页也能管理大约1000个第三层叶子层的页。所以第三层总共有1000 × 1000 100万个页。如果每页能放15行那么三层B树一共能存100万 × 15 1500万行这个就是网上常说“千万级”、“两千万左右”的由来。如果你把每页索引记录按1200算单行大小按1KB、每页16行来算就是1200 × 1200 × 16约2304万行。取我前面用的保守一点的口径就是1500万行左右。关键就在这个方法本身三个量每页能管理多少个子页取决于主键长度叶子层总页数第一层扇出 × 第二层扇出每个叶子页能放多少行取决于行大小你掌握了这套乘法定理遇到任何场景都能自己估算。3. 影响这个数字的几个真实变量3.1 主键类型int、bigint、varchar、UUID差得远很多人在设计表的时候主键随手就写一个varchar(32)或者UUID根本没有意识到这个选择会让三层B树的总容量发生多大变化。我在前面估算的时候默认主键是bigint一条索引记录约19字节每页能管理约800到1000个子页。如果主键是int类型主键只占4字节一条索引记录大约15字节每页能管理的子页数可以到1000甚至1100。如果主键是varchar(32)的字符串假设平均长度32字节一条索引记录大约43字节每页只能管理大约350个子页。如果主键是36字节的UUID那更惨每页大概只能管理340到360个指针。同样假设单行1KB不同主键长度下的三层存储能力差异如下主键类型主键长度每页索引记录数约三层可存行数约行1KBint4字节10002000万bigint8字节800-10001500万-2000万varchar(32)32字节350600万-700万UUID36字节350600万左右看到差距没有同样的三层B树用int做主键比用UUID做主键能多存2到3倍的数据。别忘了还有一个隐藏影响InnoDB生成二级索引时二级索引的叶子节点里会保存主键值。主键越长所有二级索引的体积都会跟着膨胀。这意味着如果你的主键是36字节的UUID不仅聚簇索引的容量下降了连每个普通索引都变大了一圈Buffer Pool能缓存的有效索引页就更少。所以从索引容量和缓存命中率两个角度来看主键都应该尽量短。3.2 行大小字段越多能装的行越少行大小这个变量体感更直观。一张表一行大概200字节一页能装70行一张表一行2KB一页只能装7行。三层B树的叶子页总数如果是100万个页那两者能存的数据量直接差10倍。真实的InnoDB行记录格式里一行占了哪些空间粗略拆解是这样每个字段的实际数据长度变长字段长度列表对应varchar、varbinary这类字段NULL值位图大约每8个可为NULL的列占用1字节记录头信息大约5字节事务ID6字节和回滚指针7字节所以哪怕你建了10个varchar(256)如果实际数据只写了10个字符行大小也不会按256字节去算而是按实际内容长度来算。这个特性很重要因为在估算容量时你不能只看建表语句里字段声明的上限而要看业务数据落库后的平均长度。还有一个常见误区是TEXT和BLOB类型。当一行数据大到超过页大小的一半时InnoDB会把这一列放到溢出页off-page存储数据页里只保留一个指针和部分前缀。这种行在叶子页里占用的实际空间并不大但它每次访问大字段都可能多一次额外I/O。遇到这类表我通常建议把大文本字段单独拆一张表或者至少不要和主表高频查询的字段堆在一起。3.3 用系统表查你自己的表“三层容量”理论归理论落到自己负责的数据库上怎么查一张表当前的状态MySQL的系统库里本来就有一张现成的表可以用SELECT table_name, table_rows, avg_row_length, data_length, index_length FROM information_schema.TABLES WHERE table_schema 你的库名 AND table_name 你的表名;这里面最有用的字段是avg_row_length和data_length。avg_row_length表示当前平均每行占多少字节。根据这个值你可以反推每个叶子页能放多少行每页可存行数 ≈ 15360 / avg_row_length如果你再想知道当前表已经到B树的第几层了可以用一个很简单的方法估算。先算出当前叶子页个数也就是data_length除以15360左右然后再看这个叶子页数量落在哪个区间叶子页数小于1000个大概率是两层到三层之间叶子页数在1000到100万个之间通常是三层叶子页数超过100万个就可能已经进入四层举个例子一张表的data_length是20GB页大小16KB那大概有130万个叶子页。如果第二层每个页能管理1000个叶子页那么130万个叶子页需要1300个第二层页已经超过单个根节点能直接管理的范围所以树就变成四层了。这也就是为什么单表数据量超过两三千万后性能往往会有一个肉眼可见的下滑因为树的层数变多了每一次索引查找都可能多一次磁盘I/O。4. 弄懂三层模型后能解决哪些实操问题4.1 为什么主键通常建议自增、建议短理解了页和树的结构后很多以前停留在“大家都说”层面的结论现在可以直接推导出来了。比如为什么主键要尽量用自增整数因为有随机性强的值做主键时新插入的记录需要落到B树中间的某个位置。InnoDB在插入时发现目标页满了就会触发页分裂把一半数据挪到新页里。这个过程不仅产生写入开销还会让页的空间利用率下降。页里留了大量空闲就又导致了更少的有效记录数间接让树更容易长高。而自增主键是顺序递增的新记录通常直接追加到当前最大叶子节点的末尾触发页分裂的频率低页利用率高树的容量也更大。这不是主观偏好而是从页结构推导出来的必然结论。在实际大表里见过太多UUID主键的例子了数据量刚突破千万索引页占用愣是比自增主键表的同量级数据多出一大截查询性能和磁盘占用双双吃亏。4.2 创建了索引为什么有的查询还是慢、还是不用索引很多人在创建索引后发现查询没变快就开始怀疑B树有问题。其实多数时候是查询写法让B树“没法走”。比如在索引列上用了函数像where date(create_time) 2024-01-01查询条件虽然带着create_time但由于索引里存的是原始值而不是函数结果优化器无法按B树的排序结构去快速定位只能全索引扫描或者干脆全表扫。再比如隐式类型转换where order_id 12345而order_id列是varchar类型MySQL可能会把每一行的字符串转成数字再做比较这个时候索引也就废了。还有最经典的联合索引最左前缀原则你在(a, b, c)联合索引上直接查b字段B树同样帮不上忙。这些问题的本质都是B树能够高效快速搜索的前提是查询条件能和索引键的排序前缀对齐。一旦查询条件破坏了这种对齐索引就退化成摆设。你在设计索引的时候心里要清楚B树的键顺序是怎样的索引能不能覆盖到你的查询模式。4.3 树高度从3变成4性能会差多少通常我们说的“三层B树”意味着从根页定位到数据最多3次页读取根节点读一次中间节点读一次叶子节点读一次。实际上对于大量常用数据根节点和第二层节点基本都会常驻在Buffer Pool里所以真正的磁盘I/O常常只需要读一次叶子页。这也是三层B树在千万级数据下依然能扛住的底气。但一旦树变成四层每次查询都可能多一次页读取。如果前三层都不在缓存里冷查询的I/O次数会明显增加。在一些高并发场景里这个临界点会表现为数据量到了一定阈值后某些敏感查询的响应时间出现阶梯式上升而不是匀速缓慢变差。这也是为什么很多分库分表的实践会把单表数据量相对保守地控制在千万级上下。不是因为到千万就一定要分而是因为在这个边界附近B树的层数往往在从三层往四层过渡继续涨下去成本会非线性上升。了解这个原理之后你在给业务做容量预估时就不用生硬地背“单表多少行”的结论而是可以结合自己的主键长度和行大小算出自己那张表的临界点大概在哪。4.4 面试官真正想听到的是什么这个问题在面试里高频出现并不是面试官真想考你心算能力而是想看你有没有把一个笼统的“性能数字”拆解成底层原理的能力。一个合格的答案应该包含这几个层次默认页大小是16KB非叶子节点只存主键和子页指针所以每页能管理约1000个子页叶子节点存整行数据每页能放多少行取决于行大小三层总量约等于1000 × 1000 × 每页行数用bigint自增主键、行大小1KB时结果大概是1500万到2000万行主键类型、行大小、页利用率会让这个值明显变化这套推导过程比最终数字值钱得多。你说出这套逻辑面试官就知道你不仅知道B树长什么样还知道它为什么这么设计以及这个设计如何影响真实的业务决策。关于这套计算再补几句个人经验我实际做慢SQL优化和数据量评估时很少真的去逐行验证“这张表现在到底几层了”但心里一定会过一遍这套估算逻辑。尤其是在讨论一张千万级大表要不要做归档、要不要分库分表的时候我会先看一眼主键类型和平均行大小心里就有个大概这表现在是三层还是已经摸到四层的门槛了。主键短一点、行瘦一点这个临界点就能往后推很久反过来主键又长、字段又多可能数据量刚到几百万就已经开始难受了。最后再分享一个小习惯建新表的时候我会刻意写一条备忘SQL把主键策略、预估计行数、平均行大小记在上面等表跑了大半年再回头用information_schema.TABLES核对。这套“设计时估算、运行后复盘”的流程特别朴素但真的能帮你积累非常准的容量直觉。理解了B树这层机制你判断MySQL的容量和性能才会从“书本结论”变成“自己的判断力”。
返回列表