B+ 树的扇出、页大小和填充因子如何影响性能?
简化版
B+ 树扇出越大,树高越低,点查需要访问的页数越少;页大小越大,通常能容纳更多 key,但也会增加单页读写和缓存压力;填充因子越低,预留空间越多,插入时页分裂更少,但索引更占空间、扫描页数更多。面试要能用“树高约等于 log_f(N)”解释这些取舍。
详细版
扇出 f 表示一个内部节点最多能指向多少个孩子。B+ 树高度大致与 log_f(N) 成正比,所以数据库索引喜欢高扇出:如果 f=100,100 万条记录树高大约 3 到 4 层;如果 f=2,就退化成普通二叉树那种高度量级。页大小、key 长度、指针长度都会影响扇出。
填充因子表示页平均装多满。填得太满,随机插入容易频繁分裂;填得太空,索引体积变大,缓存命中率下降,范围扫描读更多页。因此它不是越高越好,也不是越低越好,而是要根据读写比例、插入模式和存储引擎策略取平衡。
完整版教学
一、扇出决定树有多矮
B+ 树和二叉搜索树最大的工程差异是扇出。二叉树每个节点最多 2 个孩子;B+ 树内部页可以保存很多分隔 key 和孩子指针,一个节点可能有 100、200 甚至更多孩子。扇出越大,同样数量的数据需要的层数越少。
高度估算:
N = 1,000,000 条记录
f = 100
f^3 = 1,000,000
约 3 层叶子容量量级
树高低意味着点查访问页数少。一次索引查找通常访问根页、若干内部页和叶子页;树高每少一层,就少一次关键页访问。
二、页大小如何影响扇出
扇出不是凭空来的,它取决于页大小、key 大小和指针大小。粗略估算可以写成:
fanout ~= page_size / (key_size + child_pointer_size)
例如页大小 16KB,key 8 字节,孩子指针 8 字节,忽略页头和槽目录时,扇出大约是 16 * 1024 / 16 = 1024。真实系统里还要扣掉页头、变长字段、事务可见性信息等,所以实际扇出会更低。
| 因素 | 变化 | 对扇出的影响 |
|---|---|---|
| 页大小变大 | 单页容纳更多 key | 扇出上升 |
| key 变长 | 每个索引项更大 | 扇出下降 |
| 指针或元数据变大 | 单项开销增加 | 扇出下降 |
| 前缀压缩 | 减少重复前缀占用 | 扇出上升 |
这也是为什么长字符串索引、复合索引字段过多,会让索引更胖、树更高、缓存更吃紧。
三、页大小不是越大越好
页变大能提高扇出,但也有代价。单页读写更重,缓存里能放下的页数变少,小范围点查可能带入很多无用数据。页太小则相反:单页轻,但扇出下降,树高可能增加。
缓存 64MB:
页 4KB -> 可缓存约 16384 页
页 16KB -> 可缓存约 4096 页
页 64KB -> 可缓存约 1024 页
所以页大小是存储引擎的工程折中。数据库会根据磁盘、缓存、事务元数据、预读策略选择默认页大小,应用开发者通常更关心 key 设计是否让页变得过胖。
四、填充因子为什么影响写入
填充因子表示页装到多满。例如填充因子 90% 表示页里预留约 10% 空间。预留空间能吸收后续插入,减少页分裂。尤其是随机插入时,如果页总是接近满载,新记录插到中间位置就容易触发分裂。
页容量 100 个条目:
填充 100%:已有 100 个,插入 1 个就分裂
填充 80%:已有 80 个,还能吸收约 20 个插入
但填充因子低也会让索引占更多页。范围扫描需要读更多叶子页,缓存命中也可能下降。因此写多场景可能适当降低填充因子,读多且数据稳定场景可以更高。
五、主键模式也会影响页分裂
顺序递增主键通常把新记录追加到最右侧叶子页,分裂位置集中且可预测;随机 UUID 主键会把插入分散到整棵树的不同叶子页,更容易造成随机页分裂和缓存抖动。这不是说 UUID 不能用,而是要知道它对 B+ 树局部性不友好。
| 插入模式 | 页分裂特点 | 常见影响 |
|---|---|---|
| 自增 ID | 主要发生在右侧热点页 | 局部性好,但可能有右端热点 |
| 随机 UUID | 分散到多个叶子页 | 随机 I/O、页分裂、缓存压力更高 |
| 时间有序 ID | 大体顺序,局部有随机性 | 通常比完全随机更友好 |
面试时能把 B+ 树页结构和主键选择联系起来,会比只说“自增主键快”更扎实。
六、常见误区与追问
记忆钩子:扇出降树高,填充因子控分裂,页大小在 I/O 和缓存之间取平衡。
- 误区:B+ 树一定只有 3 层。 常见业务中可能是 3 到 4 层,但具体取决于数据量、页大小、key 长度和填充率。
- 误区:页越大性能越好。 页大提高扇出,但会降低缓存页数量,并增加单页读写成本。
- 误区:填充因子越高越省空间就一定越好。 太满会让写入更容易分裂,写放大和锁竞争都可能上升。
- 追问:为什么长字段索引会影响性能? key 越长,单页可放条目越少,扇出下降,树高和缓存压力可能增加。
- 追问:随机 UUID 为什么不适合聚簇索引? 它会让插入分散到不同叶子页,破坏顺序写局部性,增加页分裂。
- 追问:如何粗略估算树高? 用
height ~= log_f(N),再结合叶子页容量和填充因子修正。
七、加强记忆
B+ 树性能不是只由“大 O”决定,还受页大小、扇出、key 长度和填充因子影响。扇出越大树越矮,点查访问页越少;页大小变大能提升扇出,但会挤压缓存;填充因子低能减少写入分裂,但会让索引更占空间。回答这题时要把公式、数字例子和主键插入模式串起来,才能体现工程视角。