尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL InnoDB三层B+树能存多少行?从16KB页到2000万容量推导

MySQL InnoDB三层B+树能存多少行?从16KB页到2000万容量推导 1. 三层B树这个问题到底在问什么前段时间有个同事跑来问我订单表已经1800万行是不是该分表了我没直接给建议先反问他你知道 InnoDB 一棵三层 B 树大概能承载多少数据量吗他想了半天报了个“2000万”的答案但再往下问就说不清这个数字是怎么来的了。其实“MySQL InnoDB引擎三层B树可以存储多少数据量”这句话几乎每个数据库面试官都问过看起来像一道八股题实际上背后藏着三层逻辑一是你对 InnoDB 索引结构是否真的理解二是你能不能从物理存储的基本参数出发做估算三是你在真实业务里是否知道“单表多少行该紧张”的边界。这篇文章就是把这套东西从页结构讲到计算公式再从公式讲到生产实践给准备面试的朋友和已经在业务里纠结分表不分的同学都提供一个可以落地的判断方法。先说结论让大家有个锚点在默认 16KB 数据页、主键用 BIGINT、单行数据平均 1KB 左右的典型场景下一棵三层 B 树大约能存储 2000 万行左右的用户数据。注意“大约”两个字下面展开的所有内容都是在解释这个大约是怎么来的以及为什么它在不同行大小、不同主键长度下可以从几百万行差到几千万行。1.1 一个“面试八股”背后的底层逻辑面试官之所以爱问这个题不是想让你背一个数字而是想看看你对下面三件事有没有感觉第一B树的非叶子节点到底存了什么。很多人知道 InnoDB 的索引是 B树但不知道非叶子节点里存的是“键值 指向子节点的指针”而不是完整的数据行。正因为内部节点只存这些轻量级的“导航信息”一个 16KB 的页才能塞下一千多个键值树才能长得又矮又胖。第二InnoDB 读写数据的最小单位是页。不管你在 SQL 里是查一行还是扫一百行存储引擎真正干活的单位都是 16KB 的一个数据页。所以讨论“多少数据量”本质是在讨论“多少个数据页”和“每个数据页里平均塞了多少行”。第三聚簇索引的叶子节点存的是整行数据。InnoDB 默认的主键索引就是聚簇索引表数据本身就是按主键顺序存在 B树的叶子页里。因此“一棵树能存多少数据”约等于“这张表能存多少行”。二级索引则是另一回事它的叶子节点存的是索引列和主键值这会导致主键越大二级索引占用的空间也越大。明白了这三件事你就不需要死记硬背任何结论。只要手里有 16KB、主键大小、行大小这三个参数随时能自己算出来。1.2 为什么容量的讨论总是锚定“三层”原因很简单三层 B树对应的是“根节点 一层中间节点 一层叶子节点”的路径。InnoDB 做一次主键查找时最多要读取三个数据页。如果这三个页都在缓冲池里速度极快如果不在就最多可能产生三次磁盘随机 IO。三层是一个性能上很舒服的高度。到了四层意味着每次查询可能多一次磁盘随机 IO在传统机械硬盘时代这往往是性能恶化的临界点。所以大家讨论“单表多少数据量还算健康”时习惯用三层 B树作为参考上限。这也解释了为什么网上流行的说法是“两千万行”而不是“一亿行”一亿行往往已经让这棵树长到了四层以上或者至少是三层里的每个叶子页都塞得过满系统余量非常小。但要注意这里是“参考上限”不是“硬性限制”。如果数据全被缓冲池覆盖或者访问模式非常集中几千万行甚至上亿行也未必出问题。反过来如果一行数据特别大或者主键是乱七八糟的 UUID可能几百万行时树就已经很高了。这些都得靠完整推导才能看清。2. 从16KB数据页开始先搞清楚一个页能放多少东西2.1 数据页不是全用来放数据的InnoDB 默认的innodb_page_size是 16384 字节也就是 16KB。可以简单把它理解成一张“日报纸”一整版虽然看起来有 16KB 空间但真正留给正文的并不是全部报纸还要有报头、栏线、目录、页脚。一个标准的 InnoDB 数据页包含 File Header文件头、Page Header页头、Infimum Supremum 记录用来限定页内记录的最小值和最大值、User Records真正的用户数据区、Free Space空闲区、Page Directory页目录和 Fil Trailer页尾。这些固定结构会占用一部分空间所以严格意义上一页能装下的用户数据不是 16384 字节而是比它略小大概在 15KB 到 16KB 之间。不过在做容量估算时大多数人会先用 16384 作为分母省去这些开销。这样算出来的结果会有少量上浮但数量级是完全正确的。真要精确到“能存多少条”反而没有必要因为行数据本身还有记录头、变长字段长度列表、NULL 值列表等额外信息精确计算非常复杂收益却很低。2.2 非叶子节点一个页能塞多少“导航项”现在我们看 B树的内部节点也就是非叶子节点。它不像叶子节点那样存整行数据只存两部分一个键值通常是主键值加一个指向子页的 6 字节指针。为什么是 6 字节指针因为 InnoDB 的页号按表空间空间大小设计6 字节足够表达很大的地址空间。这个值在官方文档和主流技术书籍里经常被直接拿来做计算。如果主键是 BIGINT长度为 8 字节那么一个索引项大约是 8 6 14 字节。那么一个 16KB 的页能放多少个索引项$$16384 / 14 \approx 1170$$也就是说一个非叶子页大约可以保存 1170 个“导航项”。这意味着这一层的节点最多能扇出约 1170 个子页。如果主键是 INT长度 4 字节索引项就是 4 6 10 字节$$16384 / 10 \approx 1638$$一个非叶子页可以保存约 1638 个导航项。这就已经能看到主键长度对容量的影响了同样的三层树INT 主键的内层扇出比 BIGINT 主键多了将近一倍后面的总容量差距会非常明显。如果把真实页面的固定开销也纳入考虑比如用 15KB 作为可用空间BIGINT 主键时$$15360 / 14 \approx 1097$$数量和 1170 差别不大所以很多教材干脆直接用 16KB 算。我在实际给别人解释时也经常用 1170因为更好记而且容量估算本身就应该留有余地。3. 三层完整推导1170×1170×每页行数3.1 先说清楚三层树的结构一棵三层 B树从顶到底分别是第 1 层根节点它是一个内部节点存主键和指向第 2 层节点的指针第 2 层中间内部节点存主键和指向第 3 层叶子节点的指针第 3 层叶子节点存完整的用户数据行。从根节点出发经过根节点页、中间节点页、叶子节点页一共三次指针跳转就能定位到任意一行数据。因此计算整棵树能存多少行要分两步先算中间层能扇出多少个叶子页再算每个叶子页平均能放多少行。根节点能指向多少个中间节点大约 1170 个BIGINT 主键场景。每个中间节点又能指向多少个叶子页同样大约 1170 个。所以叶子页的总数是$$1170 \times 1170 \approx 1,368,900$$这个数就是整棵三层树最多能拥有的叶子页数量。接下来只要知道每个叶子页能放多少行就能算出总行数。3.2 叶子页能放多少行取决于行大小叶子页里放的是完整行数据所以“一个叶子页能放多少行”完全取决于你的表平均每行有多大。这和表结构、字段类型、编码方式、有没有大字段都有关系。在实际估算时我一般按下面几个常见档位来看平均行大小单页可容纳行数按 16384 字节估算三层层数容量500 字节约 32 行约 4380 万行1KB约 16 行约 2190 万行2KB约 8 行约 1095 万行4KB约 4 行约 548 万行8KB约 2 行约 273 万行这里的单页行数直接用 16384 除以行大小向下取整。比如 1KB 的行一页 16 行如果考虑真实页可用空间是 15KB那大概是 15 行计算结果是$$1170 \times 1170 \times 15 \approx 2053 万$$你会发现还是落到了“2000万”附近。这就是全网流行说法的来源默认 BIGINT 主键、默认 1KB 行大小三层 B树能装大约 2000 万行。但不要把它当成万能答案因为行大小一变结果立刻就变了。3.3 完整公式和手算示例把上面所有东西汇总成一个可复用的公式$$容量上限 \approx \left\lfloor \frac{页大小}{主键长度 6} \right\rfloor^2 \times \left\lfloor \frac{页大小}{平均行大小} \right\rfloor$$用 BIGINT 主键、平均行大小 1KB 代入$$容量上限 \approx \left\lfloor \frac{16384}{86} \right\rfloor^2 \times \left\lfloor \frac{16384}{1024} \right\rfloor$$$$ 1170^2 \times 16 \approx 2190万$$如果平均行大小变成 500 字节$$1170^2 \times 32 \approx 4380万$$如果平均行大小变成 4KB$$1170^2 \times 4 \approx 548万$$可以看到行大小对整个容量的影响是线性的而行大小的差异在真实业务里非常普遍。比如一张订单表如果字段很多、带大量备注信息平均行很容易超过 2KB而一张精简的流水表可能每行只有几百字节。同样是三层 B树容量可以相差四倍以上。所以下次再有人问“三层B树能存多少数据”最稳的回答不是一个固定数字而是“在 BIGINT 主键、平均 1KB 行的默认情况下大约是 2000 万但具体要看主键大小和行大小”。能说出这句话说明你真的理解了模型而不是只会背结论。4. 主键选型如何左右三层容量INT、BIGINT、UUID的对比4.1 主键长度如何左右“容量”上一章推导时非叶子节点的扇出是由“主键长度 6 字节指针”决定的。主键越短一个内部节点能存的导航项越多三层树能挂载的叶子页就越多。这个影响在数据量大了以后会被平方放大。我列一个对比表假设平均行大小都是 1KB主键类型索引项大小单页扇出三层树叶子页总数三层容量1KB行INT (4B)10 字节约 1638约 268 万个约 4290 万行BIGINT (8B)14 字节约 1170约 137 万个约 2190 万行CHAR(16) 十六进制字符串22 字节约 744约 55 万个约 880 万行VARCHAR(32) UUID字符串38 字节约 431约 18 万个约 290 万行从 INT 到 BIGINT三层容量从约 4300 万降到约 2200 万接近一半如果继续用 32 位字符串当主键三层容量甚至不到 300 万。这还只是聚簇索引层面的影响二级索引在存储时也要附带主键值主键越大二级索引的每个页能存下的索引项也越少整体空间占用和查询成本都会跟着涨。所以我一直建议MySQL 表的主键优先选自增 INT 或 BIGINT不是为了教条而是它在 B树容量模型里的表现确实最好。4.2 UUID主键会让三层容量大幅缩水UUID 主键的问题不只是“长度长”。很多团队选 UUID 是看中它全局唯一、好做分布式生成但从 InnoDB 存储引擎的视角看它有两个明显的缺点。第一UUID 作为字符串存储时通常是一个 32 位或 36 位的字符串按上面的计算三层容量会缩水到几百万行量级。如果你的业务规模不大问题不明显一旦数据量上来树的高度可能很早就从三层变成四层查询性能就会比 INT/BIGINT 主键的表更早遇到瓶颈。第二UUID 随机性很强插入时主键顺序基本是随机的。InnoDB 的聚簇索引在插入随机主键时会不断触发页面分裂和页重排导致大量随机写和索引碎片。这个影响甚至比容量更致命因为它会让“每个叶子页的实际可用空间”和“缓冲区命中率”都变得更差。页分裂还会在表空间里留下碎片进一步压缩有效容量。如果确实因为业务需要必须使用非自增主键可以考虑有序雪花 ID 之类的方案。它比 UUID 更适合 InnoDB因为具备一定的趋势递增性能减少页分裂。无论如何要明白主键选型不只是一个逻辑设计问题它会直接作用到 B树挂载叶子页的能力上。5. 行大小、溢出列与统计表别让估算偏离现实5.1 行记录结构和行溢出的影响在真实表里“平均行大小”往往不是一个固定值而且也很少等于建表时所有字段长度之和。因为 InnoDB 的行格式会对变长字段做一些处理例如 VARCHAR 只存实际长度NULL 值列表也会集中标记不会为每个 NULL 字段浪费一整个字段的空间。更需要注意的是行溢出。InnoDB 默认的行格式在 MySQL 5.7 以后是 DYNAMIC在这种格式下如果某个字段特别大例如大 TEXT 或 BLOBInnoDB 不会把整个大字段都塞进叶子页而是会把一部分数据放到溢出页Off-Page在叶子页里只保留 20 字节左右的指针。换句话说一个大字段行占用的总空间可能远超 16KB但它在叶子页里的“常驻体积”并没有那么大额外数据占用的是新的溢出页。这会对容量估算产生两个方向的影响一方面叶子页里的行数可能会比“按总行大小直接 16384 除”更多一些因为大字段被挪走了另一方面表空间整体占用的页数会变大因为溢出页也要算空间。因此如果你的表里主要存的是普通结构化字段用平均行大小估算 B树容量是靠谱的如果表里挂了大文本、JSON、图片二进制等字段更准确的说法是“聚簇索引叶子页里存储的压缩后元数据量”而不是全表数据总量。5.2 从统计表里粗略估算平均行长在实际业务中怎么才能快速得到一张表的平均行大小不需要去一行行量MySQL 的信息模式里已经给了大概数据。你可以执行下面这条 SQLSELECT TABLE_ROWS, DATA_LENGTH, AVG_ROW_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA your_database AND TABLE_NAME your_table;其中DATA_LENGTH的单位是字节表示聚簇索引叶子部分占用的数据页总数乘以页大小的近似值TABLE_ROWS是优化器估算的行数。AVG_ROW_LENGTH就是 MySQL 自己算出来的平均行长它大致等于DATA_LENGTH / TABLE_ROWS。注意TABLE_ROWS是估算值不是精确值但对于判断数量级已经够了。拿到平均行长度后你可以自己算一下这个表目前在几层。先估算叶子页数量$$叶子页数量 \approx DATA_LENGTH / 16384$$然后不断除以内部节点扇出看几次能降到 1。这就是“倒推树高”的简易方法。比如一张表有 2200 万行平均行长 1KB叶子页大约 137 万个除以扇出 1170得到中间层页约 1170 个再除以 1170得到根节点 1 个。正好三层。如果表数据量达到 5000 万行同样的行大小下叶子页约 312 万个除以 1170 是 2667再除以 1170 是 2.28说明树已经接近四层。到了这个阶段你就该认真评估分表、归档或冷热分离了。6. 三层容量在生产里的正确用法它决定不了分表但能修正预期6.1 2000万行不是“必须分表”的信号很多团队一看到某张表过了 2000 万行就开始焦虑急着拆库拆表。但三层容量只是一个基于页大小的理论上限它不直接等于“超过这个值性能就崩”。分表决策要看的其实是几个更现实的问题这张表的写入并发是不是已经打到了单点瓶颈主键写入顺序是否稳定常见查询是不是都是主键点查或者高效走索引表体积是否已经大到备份、DDL 需要数小时甚至数天如果这些问题都没有困扰你哪怕单表 4000 万行配合合理的索引和缓冲池配置依然能跑得不错。反过来如果一张表只有 500 万行但每天的全表扫描把缓冲池刷得干干净净那它比 1 亿行的点查表更值得改造。所以我更愿意把“三层容量两千万行”当成一个预期管理工具而不是一个阀门。它提醒你在千万量级后开始关注索引层级和页密度但真正的优化动作还是要回到慢查询、IO 吞吐和写入模式里去度量。6.2 结合缓冲池和访问热度的实际容量观一个很容易被忽略的事实是三层 B树的“三次页访问”只有在内层和根节点都命中缓冲池时才能做到快速。实际上根节点和上层中间节点通常都很小很容易常驻在 InnoDB Buffer Pool 里真正可能产生磁盘 IO 的往往是你想查的叶子页。如果一张表有 2000 万行、平均行长 1KB那么叶子页就有 137 万个。假设 Buffer Pool 是 16GB理论上可以放下很多页但业务不可能只访问这一张表。如果热点数据只有最近一周而这 2000 万行里只有 200 万行是热点那么缓冲池命中率会很高表再大也无所谓。如果业务是随机访问全量数据那么缓冲池再大也可能经常未命中三层树的容量支撑反而显得不够用。所以容量估算应该和访问特征放一起看。三层 B树能存多少数据是一个物理上限而业务能承受多少数据是一个系统上限两者之间隔着的实际是缓存命中率、磁盘随机 IOPS、查询复杂度以及你为这张表设计的索引是否合理。7. 最后分享我的实操习惯与压测心得我在自己负责的项目里会把容量估算作为建表评审的一部分。新表上线前我会先按预估的字段长度粗算一下平均行长再根据业务规划的年增长量估算两三年后的数据量反向确认树高会不会很快突破三层、二级索引大概要占多少空间。这套动作不需要很精确但能让我提前避开很多坑。另一个习惯是定期看information_schema.TABLES里的AVG_ROW_LENGTH和DATA_LENGTH。有一次表数据量看起来涨得很快仓库同事问要不要扩容我查了一下发现平均行长从 1.2KB 涨到了 3.5KB原因是最近半年新增了两个大文本字段。真实原因不是行数暴涨而是行宽涨了。如果不看这两个字段很容易误判成“单表容量到了瓶颈”进而做出不必要的分表方案。还有一个体会压测比估算更可靠。估算告诉我们预期但最终性能要以实际压测为准。我会在数据量达到预估容量的 80% 时做一些主键点查、范围查询、分页查询和批量写入的压测观察 P99 延迟和 IO 队列长度。如果在这个阶段仍然稳就说明这条容量线有余量如果已经出现明显劣化再启动分表或归档方案也来得及。最后提醒一句不要把这三层容量估算当成一个死数字到处套用。只要主键用得更短、行控制得更紧凑、热点访问更集中你完全可以让单表在几千行时稳步运行也能让表在几亿行时继续服务。真正值得你花时间的永远是理解它背后的存储模型然后用实际数据去验证和调整。
返回列表