项目经验_之_数据库分库分表
前言
Github:https://github.com/HealerJean
一、解释
我们知道互联网是由非常庞大的用户组成,所以肯定有非常绝大的请求,这些请求又会产生非常巨大的信息存储在数据库中,由于数据量非常巨大,单个数据库的表示很难容纳所有数据,所以就有了分库分表的需求。 对于数据的拆分主要有两个方面 :垂直拆分和水平拆分
1、垂直拆分
垂直拆分: 根据业务的维度,将原本的一个库(表)拆分为多个库(表〉,每个库(表) 与原有的结构不同。
1)垂直分表
也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如
text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。
2)垂直分库
垂直分库针对的是一个系统中的不同业务进行拆分,按照业务把不同的数据放到不同的库中。其实在一个大型而且臃肿的数据库中表和表之间的数据很多是没有关系的,比如用户
User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上
2、水平拆分
水平拆分: 根据分片(
sharding)算法,将一个库(表)拆分为多个库(表),每个库(表)依旧保留原有的结构。
1)水平分表
针对数据量巨大的单张表(比如订单表),按照某种规则(
Hash取模、地理区域、时间等),切分到多张表里面去。 但是这些表还是在同一个库中**结果:分表能解决数据量过大造成的查询效率低下的问题 **
问题:但是无法有效解决数据的并发访问能力。,所以库级别的数据库操作还是有
IO瓶颈。

2)水平分库+分表
将数据库拆分,提高数据库的写入能力就是所谓的分库。将单张表的数据切分到多个数据库中,表的结构是一样的 。
结果: 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。

3、水平分库分表的规则
路由:通过分库分表规则查找到对应的表和库的过程叫作路由。例如,分库分表的规则是
user_id % 4,当用户新注册了一个账号时,假设用户的ID是123,我们就可以通过123 % 4 = 3确定此账号应该被保存在User3表中。当ID为123的用户登录时,我们可通过123 % 4=3计算后,确定其被记录在User3中。
1)Hash 取模
使用场景:哈希分片常常应用于数据没有时效性的情况
有一家公司在一年内能做
10亿条交易,假设每个数据库分片能够容纳5000万条数据,则至少需要20个表才能容纳10亿条交易。在路由时,我们根据交易ID进行哈希取模来找到数据属于哪个分片,因此,在设计系统时要充分考虑如何设计数据库的分库分表的路由规则。
2)地理区域
比如按照华东,华南,华北这样来区分业务
3)时间
使用场景 :切片方式适用于有明显时间特点的数据,
按照时间切分,就是将
6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。比如一个用户的订单交易数据,我们可以根据月或者季度进行切片,具体由交易数据量来决定以什么样的时间周期进行切割
二、分库分表后的问题
1、分页问题
分库后,有些分页查询需要遍历所有库。 举个分页的例子,比如要求按时间顺序展示某个商家的订单,每页100条记录,假设库数量是8,我们来看下分页处理逻辑:
1)全局视野法:
如果取第1页数据,则需要从每个库里按时间顺序取前100条记录,8个库汇总后有800条,然后对这800条记录在应用里进行二次排序,最后取前100条。
如果取第10页数据,则需要从每个库里取前1000(100*10)条记录,汇总后有8000条记录,然后对这8000条记录二次排序后取(900,1000)条记录。
分库情况下,对于第k页记录,每个库要多取100*(k-1)条记录,所有库加起来,多取的记录更多,所以越是靠后的分页,系统要耗费更多内存和执行时间。
优点:对比没分库的情况,无论取那一页,都只要从单个DB里取100条记录,而且无需在应用内部做二次排序,非常简单。
缺点:每个分库都需要返回更多的数据,增大网络传输量;除了数据库要按照time排序,服务层也需要二次排序,损耗性能;随着页码的增大,性能极具下降,数据量和排序量都将大增
2)业务折中
禁止跳页查询,不提供“直接跳到指定页面”的功能,只提供下一页的功能。正常来讲,不管哪一个分库的第3页都不一定有全局第3页的所有数据,例如一下三种情况:
1、先找到上一页的time的最大值(可从前台传入),作为第二页数据拉去的查询条件,只取每页的记录数
2、这样服务层还是获得两页数据,再做一次排序,获取一页数据。
3、改进了不会因为页码增大而导致数据的传输量和排序量增大
3)允许数据精度丢失:
需要考虑业务员上是否接受在页码较大是返回的数据不是精准的数据。
在数据量较大,且 ID 映射分布足够随机的话,应该是满足等概率分布的情况的,所以取一页的数据,我们在每个数据库中取(每页数据/数据库数量)个数据。 当然这样的到的结果并不是精准的,但是当实际业务可以接受的话, 此时的技术方案的复杂度变大大降低。也不需要服务层内存排序了。
4)二次查询法
2 个数据库,假设一页只有5条数据,查询第200页的SQL语句为
select * from T order by time limit 1000 5;
- 讲sql改写为
select * from T order by time limit 500 5;
注意这里的500=1000/分表数量,并将这个sql下发至每个分库分表中执行,每个分库返回这个sql执行的结果。
- 找到所有分库返回结果的time的最小值

第一个库,5条数据的time最小值是1487501123
第二个库,5条数据的time最小值是1487501223
故,三页数据中,time最小值来自第一个库,time_min=1487501123,这个过程只需要比较各个分库第一条数据,时间复杂度很低
- 查询二次改写,第二次要改写成一个between语句,between的起点是time_min,between的终点是原来每个分库各自返回数据的最大值:
第一个分库,第一次返回数据的最大值是1487501523
所以查询改写为select * from T order by time where time between time_min and 1487501523
第二个分库,第一次返回数据的最大值是1487501699
所以查询改写为select * from T order by time where time between time_min and 1487501699

从上面图片可以看出,DB1比第一次查出来的数据多了两行,应为查询的范围扩大了
- 计算time_min这条记录在全局的偏移量

- 获取最终结果,讲第二次查询出的进行排序,最终获得结果

优点:可以精确的返回业务所需数据,每次返回的数据量都非常小,不会随着翻页增加数据的返回量。
缺点:需要进行两次数据库查询
5)其他方案
a、大宽表
如果仅仅是一些不重要的流水可以使用一张表进行记录,然后只保留一个月内的数据
b、数仓/ ES
订单数据落库之后,不管你通过
binlog还是MQ消息的都形式,把数据同步到数仓或者ES,他们支持的数量级对于这种查询条件来说就很简单了。同样这种方式肯定是稍微有延迟的,但是这种可控范围的延迟是可以接受的。
2、Join问题
互联网公司的业务,往往是并发场景多,DB 查询频繁,有一定用户规模后,往往要做分库分表。 分库分表 Join 肯定是不行的
1)不使用join的原因:
1、join的话,是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是OK的当表处于百万级别后,join导致性能下降;
2、分布式的分库分表。这种时候是不建议跨库 join 的。目前 mysql 的分布式中间件,跨库 join 表现不良。
3、join 写的 sql 语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。
4、 数据库是最底层的,一个系统性能好坏的瓶颈往往是数据库。建议数据库只是作为数据存储的工具,而不要添加业务上去。
2)不使用 join 的解决方法:
应用层面解决 :可以更容易对数据进行分库,更容易做到高性能和可扩展。(记得在小米金融供应链关联查询卖方,卖方 核心企业,授信企业的使用,就是这样,本来其实是两个企业,但是却有4个子段表示 join 查询肯定是不好的)
缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。单表查询出数据后,作为条件给下一个单表查询
查询本身效率也可能会有所提升。查询 id 集的时候,使用 IN 代替关联查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的关联要更高效。mysql 对 in 的数量没有限制,mysql 限制整条sql语句的大小。通过调整参数 max_allowed_packet ,可以修改一条 sq l的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的,但是最好不要超过 500条(小米规范)
可以减少多次重复查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。
Map<Long, CompanyDTO> map = companyDTOS.stream().collect(
Collectors.toMap(item -> item.getCompanyId(), item -> item));
Map<Long, Integer> poolCountMap = scfLoanCreditPoolMatchManager.countGroupByPoolId(scfLoanCreditPoolMatchQuery);
collect = data.stream().map(temp -> {
LoanCreditPoolDTO item = BeanUtils.loanCreditPoolToDTO(temp);
item.setCoreCompanyName(map.get(item.getCoreCompanyId()).getCompanyName()) ;
item.setCreditCompanyName(map.get(item.getCreditCompanyId()).getCompanyName()) ;
) );
return item ;
}).collect(Collectors.toList());
}
3、分组 :查出来再计算
分组实现较简单,只需对
128张表各自进行groupby,将128张表的结果,全都取到内存中,进行合并,如果有having条件再根据合并的结果进行筛选。
4、其他如 sum,avg,max等方法
查出来再计算
avg:在分片的环境中,以avg1+avg2+avg3/3计算平均值并不正确,需要改写为(sum1+sum2+sum3)/(count1+count2+ count3)。这就需要将包含avg的SQL改写为sum和count,然后再结果归并时重新计算平均值。
5、事务
分库分表后,就成了分布式事务了。
问题1:如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;
问题2:如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
1)传统事务
(1)、原子性(Atomicity) :原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
(2)一致性(Consistency) :一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行 之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
(3)隔离性(Isolation) :隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
(4)持久性(Durability) 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
2)事务方法回顾
我们知道,当
dbTransactional执行的时候,不管是userService.insert还是companyService.insert出现了异常,dbTransactional都可以整体回滚,达到原子操作的效果,其主要原因是
userService.insert和companyService.insert共享了同一个Connection,这是spring底层通过ThreadLocal缓存了Connection实现的。
@Transactional(rollbackFor = Exception.class)
@Override
public void dbTransactional(UserDTO userDTO, CompanyDTO companyDTO) {
userService.insert(userDTO);
companyService.insert(companyDTO);
}
public interface UserService {
UserDTO insert(UserDTO userDTO);
}
public interface CompanyService {
CompanyDTO insert(CompanyDTO companyDTO);
}
3)sharding-jdbc事务
public enum TransactionType {
LOCAL,//本地事务
XA, //二阶段事务
BASE;//
private TransactionType() {
}
}
| 本地事务 | 两阶段提交 | 柔性事务 | |
|---|---|---|---|
| 业务改造 | 无 | 无 | 实现相关接口 |
| 一致性 | 不支持 | 支持 | 最终一致 |
| 隔离性(不知道这里的隔离性代表啥) | 不支持 | 支持 | 业务方保证(规划中) |
| 并发性能 | 无影响 | 严重衰退 | 略微衰退 |
| 适合场景 | 业务方处理不一致 | 短事务 & 低并发 | 长事务 & 高并发 |
三、数据倾斜和分页
背景:
- 商家订单表按照商家ID拆分了4096张表
- 每张表约200多万条记录
- 但有些商家ID的订单特别多,几十张表数据超过5000万
要解决的问题:
- 数据倾斜:几十张表数据超过5000万,导致查询性能下降
- 分页需求:必须支持按订单创建时间进行高效分页
1、MySQL 解决方案
- 单表 ≤ 500 万行(硬性上限)
- 按日 / 周 / 月 三种时间粒度灵活建表
- 大商家“一商一表”,小商家“多商共用”,动态适配防倾斜
1)总体设计原则
| 原则 | 说明 |
|---|---|
| 1. 时间粒度由商家订单量决定 | 订单越多,时间粒度越细(月 → 周 → 日) |
| 2. 每个时间粒度下,再判断是否“独占” | 单商家在该粒度下 ≥ 100 万订单 → 独占;否则共用 |
| 3. 所有共用表必须严格控量 | 单表总行数 ≤ 500 万(预留 50 万 buffer) |
2)时间粒度分配策略
| 商家类型 | 时间粒度 | 是否独占 | 单表容量控制 | 表命名规则 |
|---|---|---|---|---|
| 日独占 | 日 | 一商一日一表 | ≤ 120 万(日上限) | order_{mid}_{YYYYMMDD} |
| 周独占 | 周 | 一商一周一表 | ≤ 150 万(周上限) | order_{mid}_w{year}_{week} |
| 周共用 | 周 | 多商一表 | ≤ 500 万(总和) | order_weekly_{bucket}_w{year}_{week} |
| 月独占 | 月 | 一商一月一表 | ≤ 300 万(月上限) | order_{mid}_m{YYYYMM} |
| 月共用 | 月 | 多商一表 | ≤ 500 万(总和) | order_monthly_{bucket}_m{YYYYMM} |
a、分库策略
| 场景 | 库名格式 | 分库规则 | 备注 |
|---|---|---|---|
| 日独占 | order_vip_d_{merchant_id} |
可以考虑每个商家独占一个库(人工或映射分配) | 超大商家特例,极致隔离 |
| 周独占 | order_vip_w_{0..15} |
merchant_id % N |
同一商家所有周表落在同一库 |
| 周共用 | order_shared_{0..255} |
bucket = mid % M → lib = bucket % N |
与月共用共享库池 |
| 月独占 | order_a_{0..63} |
merchant_id % N |
同一商家所有月表落在同一库 |
| 月共用 | order_shared_{0..255} |
bucket = mid % M → lib = bucket % N |
与周共用共享库池 |
b、分表策略
| 场景 | 表命名规则 | 分表逻辑 | 示例 |
|---|---|---|---|
| 日独占 | order_{merchant_id}_{YYYYMMDD} |
每商家每日一张独立表 | order_101_20251201 |
| 周独占 | order_{merchant_id}_w{year}_{week} |
每商家每周一张独立表 | order_8888_w2025_50 |
| 周共用 | order_weekly_{bucket:05d}_w{year}_{week} |
多个商家按桶聚合,每周一张共用表 | order_weekly_03001_w2025_50 |
| 月独占 | order_{merchant_id}_m{YYYYMM} |
每商家每月一张独立表 | order_12345_m202512 |
| 月共用 | order_monthly_{bucket:05d}_m{YYYYMM} |
多个商家按桶聚合,每月一张共用表 | order_monthly_45678_m202512 |
四、战斗
1、为什么要分库分标
分库分表要解决的是现存海量数据访问的性能瓶颈,对持续激增的数据量所做出的架构预见性。
单机数据库的存储能力,连接数都是有限的,很容易会成为系统的瓶颈。当单表数据量在百万级我们还能通过使用从库读写分离的方式来提升性能,
sql优化等,可唯独数据量大是MySQL无法通过自身优化解决的。慢的根本原因是InnoDB存储引擎,聚簇索引结构的B+tree层级变高,磁盘IO变多查询性能变慢时,再怎么优化都只是治标不治本。
总结,当单表数据量过大可能带来的风险:
-
性能下降:随着业务数据量的增长,索引数据也会变得庞大且低效,最直接的影响就是查询性能会变得越来越差。
-
扩展性:另外当系统的
tps不断增加时,单库可能无法承载更多的连接和请求,通过分库分表可以把压力分散提高并发处理能力。 -
稳定性:分库分表可以在不同的服务器上分布数据,即使某个服务器
cpu抖动也不至于影响所有请求。
2、什么时候开始考虑做分库分表
1、阿里的开发手册建议单表行数超过
500万行,或者单表容量超过2GB2、数据库连接数不够,磁盘使用率报警,数据库cpu使用率过高等;
3、如何做分库分表
- 垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
-
垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。
- 水平分库:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。
- 水平分表:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。
1)如何选择切分键
在
MySQL进行分库分表时选择分表键是一个非常重要的决策,因为它直接影响到数据分布的均匀性、查询效率以及后续的扩展性,
2)路由算法经验
hash取模具体的路由算法有很多种,最常用的是基于关键字段取模(对hash结果取余数hash(XXX) mod N),N为数据库实例数或子表数量),
优点:hash 取模的方式,不会存在明显的热点问题。
缺点:如果未来某个时候,表数据量又到瓶颈了,需要扩容,就比较麻烦。所以一般建议提前规划好,一次性分够。
3)如何评估分库分表数量
分库分表数量:建议用 2 的幂,可以将取模操作改为更加高效的位运算
性能提升:hash % 8 相当于 hash & (8-1),所以对于分 8 个表,可以使用哈希值与7进行按位与操作,将取模算法优化成位运算算法,这不仅简化了计算,还提升了性能。
均匀分配:通常分表都是伴随着分库的,比如我们分 16 个库 128 张表, 这样如果我们的分表数量和分库数量都是 2 的幂,那么就可以实现均匀分布。128 张表就可以均匀的分到 16个库中,每个库中有 8 张表
减少迁移:类似与 hashMap 扩容,如我们根据当前的业务发展,计算出可能需要分 4 张表就够了,但是随着业务增长,可能认为需要更多表才行,这时候如果我们把新的分表数量定位 8 张表,那么在做数据迁移的时候,就可以只迁移部分数据,从 4 张表扩容到 8 张表其实只需要迁移一半的数据
4、 分库主要解决的问题
分库主要解决的是并发量、磁盘使用率的问题: 因为当并发量一旦上来了,数据库的连接数有可能会成为瓶颈,虽然数据库连接数可以动态调整,但是
mysql会给每个链接提供缓冲区会消耗内存,所以要连接数还是要适当调整不能调整太多,所以当数据库的QPS过高时,就需要考虑分库了。可以参考我们的计算逻辑:
1)解决并发量,降低单库连接压力
分库不是减少“系统总连接数”,而是通过“数据隔离 + 请求路由”,让每个数据库实例只处理部分流量,从而显著降低其实际承受的连接数。
1、理论值 基本的计算逻辑:
假如单库设置的最大链接数是 10000,客户端1000台,每台客户端配置的最大连接数只能是10个,当并发量大于10000 时, 每台客户端10个连接数都被打满。连接数就出现了瓶颈
假如分两个库,每个库设置的最大链接数都是10000,客户端还是1000台,每台客户端配置的最大连接数还只能是10个,当并发量大于10000时,平均会把流量打到两个库上,每个客户端10个连接数平均都只用了5个。并发量可以提升至20000。
2、实际情况:实际情况还得综合考虑 CPU 使用率,单连接 TPS情况 等因素。如果 CPU 过高排除正常的慢 SQL 外也可以考虑分库缓解压力,就看瓶颈是在哪里,我们的系统瓶颈是在磁盘使用率上,所以优先考虑磁盘容量。算出预估的磁盘容量后,比如单库的最大的容量是 7T,就能算出总共需要分多少个库。
| 指标 | 单库架构 | 分 2 库架构 |
|---|---|---|
| 应用实例数 | 1000 台 | 1000 台 |
| 每台最大连接池 | 10(全连同一库) | 每库 10(共 20,但按需使用) |
| 总潜在连接能力 | 10,000 | 20,000(理论) |
| 单库实际连接数 | 10,000(打满) | ≈5,000(均匀分布) |
| 单库内存压力 | 高(~40GB) | 低(~20GB) |
3、每库10(共20,但按需使用)这样连接数不是增加了吗?
- 是的,理论最大连接数翻倍了(从1万→2万),但这正是分库的目的之一:
- 通过增加资源(多库)来提升系统整体容量。
- 因为请求分流,不会同时打满两个库
- 但每台客户端的资源消耗是否翻倍?
- 如果两个连接池都常驻且长连接,内存/CPU开销会略增。
- 但在高并发场景下,这是用少量客户端资源换取数据库可扩展性的合理代价。
3)解决磁盘使用率
分表解决的是性能问题: 现在就来论证下为什么单表行数超过
500万行,或者单表容量超过2GB;
a、B+ 树的高度(层数)直接影响 磁盘 I/O 次数。
500 万行是一个经验值:在此之下,B+ 树通常保持 3 层,查询性能稳定。
| 数据量 | B+ 树高度(估算) | 随机查询 I/O 次数 |
|---|---|---|
| 100 万行 | 3 层 | 3 次(根 → 内部 → 叶子) |
| 500 万行 | 3~4 层 | 3~4 次 |
| 1 亿行 | 4~5 层 | 4~5 次 |
虽然看起来只多 1 次 I/O,但在高并发场景下:
- 如果数据不在 Buffer Pool(缓存未命中)
- 每次多 1 次磁盘随机读(约 10ms)
- QPS 会显著下降
b、查询优化器容易“失智”
MySQL优化器依赖 统计信息(cardinality) 选择执行计划。
- 表过大 + 统计信息不准 → 优化器可能选错索引
- 例如:本该走索引,却走了全表扫描
- 尤其在 复合查询、JOIN、子查询 场景下更明显
而小表的统计信息更准确,执行计划更稳定。
5、B+ 树的高度计算
1)通过记录总数推导树的高度
a、初步估算
多路平衡树:每个节点可以有多个子节点,通常远多于二叉树的两个子节点。
所有数据存储在叶子节点:内部节点只存储键值和指向子节点的指针,不存储实际的数据记录。
叶子节点通过指针连接:叶子节点之间通过指针连接,形成一个有序链表,便于范围查询。
1、每个节点的可以有多少个子节点。在 InnoDB 中,页的大小通常是 16KB(可以配置),我们需要根据键的大小和指针的大小来计算一个节点可以存储多少个键和指针。
- 假设主键是
BIGINT类型,占用8字节。 - 指针在
InnoDB中通常是6字节。 - 因此,一个索引条目(键 + 指针)的大小是
8 + 6 = 14字节。 - 一个页(
16KB=16384字节)可以存储的索引条目数量大约是:16384 / 14 ≈ 1170个。
因此,一个内部节点大约可以有1170个子节点。
2、叶子节点的数据存储:
- 叶子节点存储的是实际的数据记录(对于聚簇索引)或主键和指向数据的指针(对于二级索引)。
- 假设我们讨论的是聚簇索引,每条记录的大小会影响叶子节点可以存储多少条记录。
- 假设每条记录的大小为
1KB(这是一个假设,实际大小取决于表结构),那么一个叶子节点可以存储大约16KB/1KB= 16条记录。
3、计算树的高度 :给定记录总数(N)和每个节点的扇出(F),树的高度(H)可以通过以下方式估算:
- 根节点可以有最多
F个子节点。 - 第二层最多可以有
F^2个叶子节点。 - 第
H层最多可以有F^H个叶子节点。
每个叶子节点可以存储大约16条记录,因此:总记录数= 叶子节点数量 × 每个叶子节点的记录数

N = 1000w:高度大约为 H ≈ 1.89 + 1 ≈ 2.89
N = 3000w:高度大约为 H ≈ 2.04 + 1 ≈ 3.04
总结:实际高度可能因记录大小、主键类型、页填充因子等因素略有不同,但通常在3到4之间。
b、验证和调整假设
我们的计算基于以下假设:
- 每个内部节点的扇出为
1170。 - 每个叶子节点存储
16条记录。
实际上,这些数字可能会根据实际情况有所变化:
- 键的大小:如果主键不是
BIGINT而是INT(4字节),那么键的大小会减小,扇出会增加。- 键:
4字节,指针:6字节,条目大小:10字节。 - 扇出:
16384/10≈1638。 - 这会降低树的高度。
- 键:
- 记录的大小:如果记录更小,叶子节点可以存储更多记录,从而降低高度;反之,记录更大,高度会增加。
- 页的填充因子:实际中,页可能不会
100%填满,可能会有一些空闲空间。
2)通过树的高度推导记录总数
InnoDB存储引擎最小储存单元是页,一页大小就是16k。
B+树叶子存的是数据,内部节点存的是 键值+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而再去数据页中找到需要的数据;
1、Mysql 数据库,一个页的大小是多少呢?
答案:查询结果 16384 字节,可以通过 1kb 等于 1024 字节方式,计算出16384 / 1024 = 16kb,说明 Mysql数据库默认页大小是16kb。
show variables like 'innodb_page_size'

2、计算每页存储多少数据?
答案:假设一行数据占用 1kb 的空间大小,然而实际上,除去字段很多的宽表外,其实很多简单的表行记录都远达不到 1kb空间占比。这里我们用最坏的情况来假设一行记录大小为 1kb ,那么,一个 16kb 的页就可以存储 16 行数据 16384/1024 = 16。
3、计算指针大小
答案:在 Mysql 数据库当中,指针地址大小为 6 字节,若索引是 bigint 类型,那么就为 8 字节,两者加起来总共是 14 字节
4、计算页地址指针 数量
答案:16384 字节 / 14 字节 = 1170 ,意味着,根节点有1170个页地址指针
5、计算每个节点存储索引个数
答案:根据 “根节点页地址指针数量 * 单个叶子节点记录行数”,计算 1170 * 16 = 18720 条记录,可见,两层 B+ 数可以存放 18720条记录,当然,这个数字是存在出入的,只是作为参考。
6、计算 3 层可以存放多少?
答案:
一、根节点最多有 1170 个指针数;
二、说明第二层最多会有 1170 个子节点,同时,每个子节点里最多有 1170 个指针数;
三、那么,第三层叶节点数量,可以通过 “第二层最多有 1170 个节点数量 * 每个节点里最多有 1170 个指针数量”,也就是 1170 * 1170
四、最后,计算第三层所有叶子数量 * 各个叶子节点存放的 16 条数据;
最后,1170 * 1170 * 16 = 21902400,得出两千万左右条数据。

3)实际高度
1、一张数据表一般对应一颗或多颗树的存储,树的数量与建索引的数量有关,每个索引都会有一颗单独的树;
2、叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引;
3、mysql 数据是存储在磁盘中的,被分成很多份小的数据页,每个数据页在 mysql 的 B+ 树中就是一个叶子节点,每一个叶子节点大小都是16K(可修改最大 64k ,最小 4k )
4、B+ 树的查询是从上往下一层层查询的,一般情况下我们认为 B+ 树的高度保持在 3 层以内是比较好的,也就是上两层是索引,最后一层存数据,这样查表的时候只需要进行 3 次磁盘 IO 就可以了。如果数据量过大,导致 B+ 数变成 4 层了,则每次查询就需要进行 4次磁盘 IO 了,从而使性能下降。所以我们才会去计 算 InnoDB 的 3 层 B+ 树最多可以存多少条数据。

5、数据的新增过程:从上图看出一个数据页的存储空间被划分成7个部分,我们的数据存储记录会按照我们指定的行格式存储到userRecords 中。实际上是先在 freeSpace 中开辟一块空间划分给 userRecords,当 freeSpace 部分的空间全部被 userRecords部分替换掉之后也就意味着这个页使用完了,如果还有新记录插入的话就需要去申请新的页了。

在上面已经介绍了页的结构,索引也不例外,除存在数据外其他内容大约占 1K 左右的空间,那么每页大约是 15K 的空间存放数据,以上面数据结构为例:
每页存储数据空间大小: 除了 User Records 和 Free Space 以外所占用的内存是 38+56+26+8 = 128 字节,每一页留给用户数据的空间就还剩 16KB * 15/ 16KB - 128 = 16 × 15 / 16 × 1024 −128 = 15232 字节(当新记录插入到 ``InnoDB 聚集索引中时,InnoDB` 会尝试留出 1/16 的页面空闲以供将来插入和更新索引记录。)
**非叶子节点存储条数 **索引页数据记录中主要包含:主键是 bigInt 类型(8 byte),指针信息(6 byte)、行标头(5 byte),大约是 15232 / ( 8 + 6 + 5 ) ≈ 801,
实际计算: ** 3层 -> 801 * 801 * 16 =10,265,616**


