一般来说主键索引的树深度有几层?为什么是这个层数?
这是一个非常棒的问题,它触及了数据库索引设计的核心。简单答案是:
对于一个存储了数百万甚至数十亿条记录的大型表,其主键索引的B+树深度通常也只有3到4层。极少有情况会达到5层或以上。
这个看似神奇的层数背后,是B+树数据结构极其高效的高扇出(High Fan-out) 特性。
为什么层数这么少?—— 关键在于“扇出”
扇出(Fan-out) 指的是一个索引节点中可以存储的子节点指针(或键值)的数量。B+树之所以强大,就是因为它的每个节点都能存储非常多的键值和指针,这使得树可以变得“又宽又胖”,而不是“又瘦又高”。
树的深度(层数)直接决定了查找一条记录需要多少次磁盘I/O操作。因为每次向下遍历一层,通常就需要一次磁盘读取(除非节点已被缓存)。所以,保持较浅的深度是高性能的关键。
层数计算:一个具体的例子
让我们用一些合理的假设来估算一下:
-
假设1:页大小(Page Size)
在InnoDB中,默认的页大小是 16KB。这是B+树每个节点的大小。 -
假设2:主键字段大小
假设我们使用一个 BIGINT(8字节) 作为主键。 -
假设3:指针大小
在InnoDB中,一个指向子节点(或数据行)的指针大约需要 6字节。 -
计算非叶子节点的扇出
一个非叶子节点存储的内容是:(主键值 + 子节点指针)
。
每个这样的组合大约占用:8字节 + 6字节 = 14字节
。
一个16KB的页大约能存储:16 * 1024 / 14 ≈ 1170
个这样的组合。
也就是说,一个非叶子节点可以指向大约1170个子节点。 -
计算叶子节点的容量
叶子节点存储的是完整的行数据(在MySQL的聚集索引中)或主键值+指针(在PostgreSQL的堆表中)。这里我们以MySQL为例,假设一行数据平均大小为 1KB(这是一个很常见的估算值)。
那么一个16KB的页大约能存储:16 / 1 ≈ 16
行数据。
现在,我们来计算这棵B+树能存储多少数据:
-
深度 = 1:根节点也是叶子节点。
- 最多存储 16 行记录。
-
深度 = 2:1个根节点(非叶子) + 多个叶子节点。
- 根节点可以指向
1170
个叶子节点。 - 总记录数 =
1170 * 16 ≈ 18,720
行。
- 根节点可以指向
-
深度 = 3:
- 根节点可以指向
1170
个非叶子节点。 - 每个非叶子节点又可以指向
1170
个叶子节点。 - 总记录数 =
1170 * 1170 * 16 ≈ 21,902,400
行(约 2200万)。
- 根节点可以指向
-
深度 = 4:
- 总记录数 =
1170 * 1170 * 1170 * 16 ≈ 25,629,807,200
行(约 256亿)。
- 总记录数 =
从这个计算可以看出:
- 仅仅3层的B+树就能轻松支持两千万级别的数据量。
- 达到4层,就能支持两百五十亿级别的恐怖数据量,这已经远超绝大多数应用整个生命周期的数据总量。
影响树深度的关键因素
从上面的计算公式可以看出,深度主要由以下几个因素决定:
- 页大小(Page Size):这是最重要的因素。页越大,一个节点能存储的键值和指针就越多(扇出越高),树就越浅。MySQL可以配置页大小(如8KB, 16KB, 32KB, 64KB),但16KB是默认且最平衡的选择。
- 主键的大小:主键字段越小,每个非叶子节点能存储的键值就越多,扇出就越高。这就是为什么通常推荐使用 短小的自增整数(如BIGINT) 作为主键,而不是很长的字符串(如UUID)。
- 反面例子:如果你用
CHAR(100)
做主键,假设需要100字节。那么一个非叶子节点只能存储16*1024 / (100+6) ≈ 154
个键值指针对。扇出从1170暴跌到154,要存储同样多的数据,树的深度就必须增加。
- 反面例子:如果你用
- 行数据的大小(仅对MySQL聚集索引影响显著):行数据越小,每个叶子页能存放的行数就越多,整棵树能存储的总行数就越大。因此,避免设计过宽的表、将TEXT/BLOB等大字段分表存储,也有助于优化索引深度。
总结
- 核心原因:B+树拥有极高的扇出,使得树又宽又胖,而非又瘦又高。
- 典型层数:对于亿级以下的表,3层是常态。对于十亿到百亿级的表,4层也足够。5层以上的B+树极其罕见,通常意味着表设计可能存在问题(例如使用了非常低效的主键)。
- 设计启示:为了保持索引树浅而高效,应该使用短小、自增的整数作为主键。这不仅能减少非叶子节点的扇出,还能保证顺序写入,减少页分裂和碎片,对性能提升至关重要。