“缓冲池把按块传输的数据库页缓存在固定大小的帧中。页表映射pageID到frameID,帧至少记录页号、装载状态、pin计数、dirty标记和短期保护状态。槽页的RID先定位页,再在驻留帧内查…”
形式陈述
数据库把文件划分为固定大小的页,记录是按模式解释的一段字节。本文只讨论单页可容纳的记录;字段可有定长区、变长区偏移与NULL位图。记录能被解码不表示它已提交或对某事务可见,这些由另外的事务协议决定。
槽页在页头之后放一个槽数组,每个槽保存记录的偏移、长度和存活状态。目录从低地址增长,记录区从高地址增长;页内移动记录只修改相应槽中的偏移。物理记录标识RID写为(页号,槽号)。在记录仍存活、槽未被复用、记录没有跨页迁移的条件下,页内压缩保持RID。
设页长为P字节、页头H字节、已分配n个槽,每槽d字节;紧凑记录区的总长为L。无对齐浪费时,可用连续空间为P−H−nd−L。新建槽并插入长ℓ记录,需要ℓ+d字节;复用已允许回收的空槽只需ℓ字节。碎片化时,总空闲字节足够不保证有一段足够大的连续空间。
插入必须在受保护的页状态上检查空间,预留槽和记录区,再发布有效槽。删除先使槽不可见;什么时候允许复用还要服从活跃引用和事务可见性。压缩时把存活记录安全搬到最终彼此不重叠的位置,更新其槽偏移,再发布新的空闲边界。单次搬移的源区与目标区可以重叠,必须用memmove、临时副本或正确搬移方向,不能直接假设memcpy合法。执行不变量是:每个有效槽指向页内完整记录,记录彼此不重叠,目录与记录区不交叠。
直觉
槽号像书目编号,偏移像书架位置。整理书架会搬书,只要书目更新,读者仍可用原编号找书。若直接把字节地址当记录身份,压缩一次就会让旧指针失效。
目录的这层间接访问也花空间。小记录越多,槽开销越明显;删除后留下的空槽和记录洞也不会凭空消失。因此“这页还有多少空闲字节”与“现在能插入多大记录”是两个问题。
例子与边界
128字节页上的完整移动
本页自定P=128、H=16、d=4,不考虑额外对齐。依次插入A长24、B长16、C长24:槽0指[104,128),槽1指[88,104),槽2指[64,88)。目录为[16,28),连续空闲[28,64),长36。总账为16+12+64+36=128。
删除B后,其[88,104)成为16字节洞;总空闲变52,最长连续空闲仍36。若新D长40需要一个新槽,则44≤52但44>36,不能直接放进去。假定B的槽尚不可复用,先压缩:A留在[104,128),C移到[80,104),这次源[64,88)与目标[80,104)重叠8字节,采用memmove或从高地址向低地址的处理次序,先复制源的末尾字节,避免覆盖尚未读取的源;槽2偏移改80。空闲变为[28,80),长52。再建槽3占[28,32),D放[40,80),还剩[32,40)长8。新账为16+16+24+24+40+8=128,A与C的RID均不变。
如果B的槽已被安全回收,D可以复用槽1;此时无需新增4字节槽,剩余空间是12。两种结果对应不同槽生命周期,不可把它们混算。
扩长与身份
将C从24扩为48字节,当前页只余8字节,不足新增24。合法策略可以是拒绝原位扩长,或迁移到新页并更新索引/转发项;选择后一种就必须规定旧RID如何处理。本页不声称RID跨迁移永恒稳定。槽删除并复用后,同一(页号,槽号)也可能指向另一条记录;长寿命外部引用需要版本号或逻辑主键。
NULL位图只说明某字段缺失,不等同于“该记录删除”。变长字段长度为零也不等于NULL。解码时必须检查偏移和长度界,不能把损坏的长度当作可信内存地址。
推论与应用
缓冲池负责让整个页在访问期间驻留;槽页负责在页内找到记录。数据库页号到文件偏移的目录,与页号到缓冲帧的驻留映射又是两张不同的表。
压缩可能只改变物理布局而不改变关系答案。并发读者若拿着记录字节指针,压缩仍可能使它失效,所以需要短期页保护或复制记录;RID稳定不能替代并发同步。
B+树叶条目可以保存RID,让树的键序与堆文件记录位置分离。覆盖查询可以少读堆页,普通索引查询仍须沿RID获取页与槽;这些访问在B+树的回表账本中另计。
参考资料
- CMU 15-445/645 Fall 2025,Database Storage I,§§7–11:页、槽目录、RID与记录布局。128字节移动轨迹为本文自定。
- PostgreSQL 18,Database Page Layout,§66.6:页头、条目标识与记录区;其实际字段尺寸不同于本文教学格式。