专业书籍精读 · Database Internals · 第 4 章

实现 B-Tree

Database Internals · Ch 4 · Alex Petrov · 2019

EN →

这一章讲什么?

前两章你已经知道:数据库用一棵「目录树」找数据,树的每个节点就是硬盘上的一页纸。可那都是静态的样子。这一章问的是动态的问题:这本册子每天被塞进几百万条新记录、删掉几百万条旧记录,它是怎么一边被翻、一边自己长胖变瘦,还始终不乱的?

先打个比方

把数据库想成一排文件柜的抽屉,每个抽屉按字母顺序放卡片,抽屉外贴着「A–F」「G–M」这样的标签。

教科书讲到这里就结束了:抽屉满了就分成两个,再去总目录上加一行新标签。听起来两句话的事。可真管这排柜子的人知道,麻烦全在这两句话之外。

旧世界为什么难

四件事,教科书一件都没讲。

第一,分完家得通知上级。可你手上只有一个抽屉,怎么知道它的标签写在总目录的第几行?给每张卡片背面写上「我属于第几个抽屉」倒是可以,但抽屉一分家,里面成百上千张卡片全得重写一遍背面。

第二,分家太频繁。抽屉一满就劈成两半,两个新抽屉各只装一半——于是柜子永远只用了一半的容量,你却要多买一倍的柜子。

第三,号码牌总是递增时,对半分是纯浪费。新卡片的编号一天比一天大,永远只往最后一个抽屉里塞。这时候把满抽屉对半劈开,前半个抽屉这辈子都不会再有人往里放东西了——它就那么半空着,直到永远。

第四,删了的地方收不回来。抽走一张卡片只是留下一道缝;抽屉不会自动变小,柜子也不会自动变少。

核心机制:四个很朴素的对策

带来了什么 / 该怎么选

这解释了两件你多半亲眼见过的事。用自增编号当主键,比用随机乱码当主键快得多、也省得多——顺序插入让每个抽屉都塞到快满,随机插入把新卡片撒得到处都是,处处引发分家。以及,往一张空表里灌一亿行数据,先灌数据、最后再建索引,比边灌边建快得多——前者是「一屉屉码满」,后者是「一张张插」。

诚实的代价:把抽屉塞得越满,将来在中间插一张卡片就越容易立刻引发分家——所以正经数据库都会故意留一成空位,宁可多占一点柜子。

一句话记住

教科书说「抽屉满了就分家」,那是全章最容易的一句。真正的功夫在别处:用面包屑找到上级、先向邻居借地方少分家、顺序号别对半切、能整柜码满就别一张张插。

想看面包屑栈怎么处理并发、右倾分裂省下多少空间、PostgreSQL 与 InnoDB 的真实默认值? → 切到精读版