Page 到底是什么:数据库如何把一条条记录放进磁盘

在前几篇中我们已经知道数据库不会简单地把数据当成一个巨大的字节流来处理。

数据库会把数据组织成 Page,再在 Page 之上组织 Record、Index 和 Table。

但一个新的问题出现了:

Page 里面到底长什么样?

如果一页只有 8KB、16KB,而记录大小又各不相同,数据库究竟如何知道一条记录在哪里、哪里还有空间,以及删除和更新之后如何继续使用这些空间?

这次进入数据库存储引擎内部。


一、从一个空 Page 开始

假设数据库使用:

text 复制代码
Page Size = 8KB

最开始,一个 Page 可能是空的:

text 复制代码
+--------------------------------+
|                                |
|                                |
|            8 KB                |
|                                |
|                                |
+--------------------------------+

现在插入第一条记录:

text 复制代码
User
id = 100
name = 张三
age = 20

数据库不能只是:Record

它还必须知道:

  • 这条 Record 在哪里?
  • 有多长?
  • Page 中还有多少空间?
  • Page 中有多少条记录?

因此 Page 必须拥有自己的元数据。


二、Page Header:一个 Page 的"身份证"

最基本的 Page 可以抽象成:

text 复制代码
+----------------------+
| Page Header          |
+----------------------+
| Record               |
| Record               |
| Record               |
|                      |
| Free Space           |
+----------------------+

Page Header 可以保存类似:

text 复制代码
Page ID
Page Type
Record Count
Free Space
LSN
Checksum

当然,不同数据库的实际字段完全不同。

例如:

cpp 复制代码
struct PageHeader
{
    uint32_t page_id;
    uint16_t page_type;
    uint16_t record_count;
};

这里需要特别注意:**这只是帮助理解的抽象结构,并不是某个真实数据库的 Page 格式。**真实数据库通常复杂得多。


三、为什么不能简单地把 Record 一个接一个放?

假设每条 Record 都固定大小:

text 复制代码
Record = 100 Bytes

那么很简单:

text 复制代码
Page

Record 0
Record 1
Record 2
Record 3
...

找到第 N 条:

text 复制代码
offset = N × 100

但是现实数据库很少这么简单。

例如:

text 复制代码
id      name
1       张三
2       李四
3       王五

字符串长度不同:

text 复制代码
张三      = 2
李四五六七 = 5
王五六    = 3

Record 大小可能不同。

于是:

text 复制代码
Record 0 = 50 Bytes
Record 1 = 80 Bytes
Record 2 = 45 Bytes

这时候第 N 条记录的位置不能简单通过 N × RecordSize 计算。数据库必须保存,**Record 到底在哪里。**于是出现了非常重要的概念:Slot


四、Slot:数据库如何定位 Page 中的 Record?

可以把 Page 想象成一个仓库。

Record:仓库里的货物。

Slot:仓库的货物目录。

例如:

text 复制代码
+----------------------+
| Page Header          |
+----------------------+
| Slot 0 → Record A    |
| Slot 1 → Record B    |
| Slot 2 → Record C    |
+----------------------+
|                      |
|      Free Space      |
|                      |
+----------------------+
| Record C             |
| Record B             |
| Record A             |
+----------------------+

Slot 保存:

text 复制代码
Offset
Length

例如:

text 复制代码
Slot 0
offset = 7900
length = 80

Slot 1
offset = 7820
length = 80

于是数据库可以通过:

text 复制代码
Page ID
+
Slot ID

定位一条 Record。


五、为什么 Slot 很重要?

假设 Page 中:

text 复制代码
Record A
Record B
Record C

删除:

text 复制代码
Record B

如果没有 Slot:

text 复制代码
A
C

数据库可能需要重新移动大量数据。

但是有 Slot:

text 复制代码
Slot 0 → A
Slot 1 → invalid
Slot 2 → C

Record C:可以继续留在原来的位置。只需要把slot1标记成无效状态。

这带来了一个非常重要的思想:逻辑位置和物理位置可以分离。


六、Heap Page 的经典布局

很多数据库的 Heap Page 都可以抽象成类似:

text 复制代码
高地址
+-------------------------+
| Record C                |
+-------------------------+
| Record B                |
+-------------------------+
| Record A                |
+-------------------------+
|                         |
|      Free Space         |
|                         |
+-------------------------+
| Slot 2                  |
+-------------------------+
| Slot 1                  |
+-------------------------+
| Slot 0                  |
+-------------------------+
| Page Header             |
+-------------------------+
低地址

也就是:

text 复制代码
Header
  ↓
Slots

      Free Space

Records
  ↑
从另一端向前增长

为什么 Slot 和 Record 从两边向中间增长?

因为这样可以:最大化利用 Page 中的空闲空间。


七、插入 Record 会发生什么?

假设:Page Size = 8192

当前:

text 复制代码
+----------------+
| Header         |
+----------------+
| Slot 0         |
+----------------+
| Free Space     |
|                |
|                |
+----------------+
| Record A       |
+----------------+

现在插入:

text 复制代码
Record B

数据库可能:

第一步

从 Free Space 分配空间。

text 复制代码
Free Space
     ↓
Record B

第二步

增加 Slot:

text 复制代码
Slot 0 → A
Slot 1 → B

最终:

text 复制代码
+----------------+
| Header         |
+----------------+
| Slot 0 → A     |
| Slot 1 → B     |
+----------------+
| Free Space     |
+----------------+
| Record B       |
| Record A       |
+----------------+

八、删除 Record 怎么办?

现在:

text 复制代码
Slot 0 → A
Slot 1 → B
Slot 2 → C

删除 B。

一种简单方案:

text 复制代码
Slot 1 = invalid

于是:

text 复制代码
Slot 0 → A
Slot 1 → deleted
Slot 2 → C

这里出现了一个问题,B 原来的空间怎么办?


九、删除并不意味着立即整理 Page

数据库通常不会因为删除一条记录,就立即把整个 Page 重新整理。

否则:

text 复制代码
Delete
 ↓
Move Records
 ↓
Rewrite Page

删除成本会非常高。所以数据库经常允许 Page 暂时出现,Fragmentation(碎片空间)

例如:

text 复制代码
+----------------+
| Record A       |
+----------------+
| Free           |
+----------------+
| Record C       |
+----------------+
| Free           |
+----------------+

这些空间以后可以重新利用。


十、这就是 Free Space Management

数据库必须知道哪些 Page 还有空间?否则插入一条数据时难道扫描整个数据库?

text 复制代码
Page 0
Page 1
Page 2
Page 3
...
Page 1000000

当然不能。因此数据库需要:Free Space Map

可以简单理解为:

text 复制代码
Page 0 → 80% full
Page 1 → 20% full
Page 2 → 95% full
Page 3 → 40% full

当插入一个较大的 Record 需要 1KB,数据库可以快速寻找 Page 1,而不是扫描所有 Page。


十一、更新是更复杂的问题

现在:

text 复制代码
Record A

name = 张三

假设原来的 Record:

text 复制代码
50 Bytes

修改以后:

text 复制代码
name = 李四伍六七

变成:

text 复制代码
80 Bytes

原来的位置可能放不下。怎么办?这是数据库存储引擎非常重要的问题。


十二、第一种方法:原地扩展

如果 Record 后面还有连续空间:

text 复制代码
Record A
↓
Free Space

可以直接扩大。

但是如果:

text 复制代码
Record A
Record B
Record C

后面没有足够空间就很麻烦。


十三、第二种方法:移动 Record

数据库可以:

text 复制代码
旧位置
 ↓
新位置

然后更新 Slot:

text 复制代码
Slot 0

旧 Offset
   ↓
新 Offset

这样 Slot ID 不变,但 Record 的物理位置变了,这就是为什么 Slot 非常重要。


十四、为什么不能让其他对象直接保存 Record 的物理地址?

这是一个非常重要的设计思想。

假设:

text 复制代码
Record A
address = 0x1000

其他结构直接保存:

cpp 复制代码
char* ptr = 0x1000;

如果 Record 移动:

text 复制代码
0x1000
   ↓
0x2000

那么:

text 复制代码
ptr

就失效了。

但如果保存的是:

text 复制代码
Page ID = 10
Slot ID = 3

那么:

text 复制代码
Page 10
Slot 3
   ↓
当前 Record

Record 即使移动 Slot 可以跟着更新。因此**Record ID 和 Record Physical Address 是两个概念。**这个思想在很多数据库内部都非常重要。


十五、PostgreSQL 就大量体现了这种思想

PostgreSQL 中:TID

可以理解为:

text 复制代码
Block Number
+
Offset

也就是:

text 复制代码
Heap Page
   +
Tuple Location

它并不是简单保存 "char*" 这样的内存地址。

因为内存地址只在当前进程生命周期内有效。而数据库需要的是可以持久化、可以重新定位的数据位置。


十六、为什么 Page Size 不能无限大?

这是另一个非常重要的问题。

假设:Page = 4KB

读取一个 Page:4KB

如果:Page = 1GB

读取一条很小的 Record也可能需要处理:1GB

显然浪费。

所以 Page Size 是一种权衡:

text 复制代码
Page 太小
 ↓
Tree Height 增加
I/O 次数增加

Page 太大
 ↓
单次 I/O 数据过多
Cache 利用率降低

因此数据库需要选择合适的 Page 大小。

常见:

text 复制代码
4KB
8KB
16KB
32KB

但具体大小取决于数据库设计。


十七、Page 不只是"数据容器"

到了这里,我们应该改变一个认识。

Page 并不是简单的 char[8192]

它实际上承担了很多职责:

text 复制代码
Page
 |
 +-- Identification(标识信息)
 |
 +-- Record Organization(记录组织方式)
 |
 +-- Free Space Management(空闲空间管理)
 |
 +-- Integrity Information(完整性信息)
 |
 +-- Recovery Metadata(恢复原数据)
 |
 +-- Concurrency Information(并发信息)

所以 Page 是数据库存储引擎最重要的抽象之一。


十八、不同数据库的 Page 并不一样

现在回到我们之前学习的三个数据库。


PostgreSQL

典型结构可以抽象成:

text 复制代码
Page Header
     |
     ↓
Item Pointers
     |
     ↓
Free Space
     |
     ↓
Tuples

其中 Item Pointer 类似我们今天讲的 Slot。


InnoDB

InnoDB 的 Page 结构更加复杂。

一个数据页会包含:

text 复制代码
Page Header
+
Infimum / Supremum
+
User Records
+
Free Space
+
Page Directory
+
File Header
+
File Trailer

它不是简单的 Heap Page。因为 InnoDB 的 Page 本身就是 B+Tree 数据结构的重要组成部分。


SQLite

SQLite 的 Page 同样服务于 B-Tree。

例如:

text 复制代码
Page Header
+
Cell Pointer Array
+
Unallocated Space
+
Cells

其中 Cell Pointer Array 承担了与 Slot 类似的定位作用。


十九、三个数据库放在一起

我们现在可以看到一个非常有意思的现象:

数据库 数据组织 Page 中的定位结构
PostgreSQL Heap Item Pointer
InnoDB B+Tree Page Directory 等
SQLite B-Tree Cell Pointer Array

它们虽然名字不同:

text 复制代码
Item Pointer
Page Directory
Cell Pointer

但背后的问题其实相同,如何在有限大小的 Page 中高效管理大小不一、不断变化的数据。


二十、Page 与 Buffer Pool 的关系

现在把前面几篇联系起来。

磁盘:

text 复制代码
Database File

+--------+
| Page 0 |
+--------+
| Page 1 |
+--------+
| Page 2 |
+--------+

内存:

text 复制代码
Buffer Pool

+--------+
| Page 0 |
+--------+
| Page 5 |
+--------+
| Page 8 |
+--------+

数据库不会把整个数据库加载进内存。

而是:

text 复制代码
Disk Page
    ↓
Buffer Pool
    ↓
CPU

因此Page 同时是磁盘组织单位,也是数据库内存管理的重要单位。

这也是为什么 Page 会成为数据库架构中的核心抽象。


二十一、Page、Record、Slot 三者是什么关系?

一定要把这三个概念分开。

text 复制代码
Page
 |
 +----------------------+
 | Header               |
 |                      |
 | Slot 0 ─────────┐    |
 | Slot 1 ──────┐  |    |
 | Slot 2 ───┐  |  |    |
 |           |  |  |    |
 | Free Space|  |  |    |
 |           ↓  ↓  ↓    |
 |       Record          |
 +----------------------+

**Page:**一个固定大小的存储空间。

**Record:**真正的数据。

**Slot:**帮助数据库定位 Record 的元数据。

三者不要混淆。


二十二、最重要的设计思想:间接寻址

如果让我从今天挑一个最值得记住的概念:

就是:Indirection ------ 间接寻址

不要:

text 复制代码
ID → Physical Address

而是:

text 复制代码
ID
 ↓
Slot
 ↓
Physical Location
 ↓
Record

这样 Record 可以移动,Slot 可以更新,上层逻辑引用不一定需要改变。这是一种非常经典的系统设计思想。


二十三、为什么这和数据库的 MVCC 有关系?

这里先埋一个伏笔。

如果数据库中的一条记录发生更新:

text 复制代码
Old Version
     ↓
New Version

数据库可能并不只是简单地覆盖旧数据,因为其他事务可能仍然需要看到旧版本。

于是:

text 复制代码
Record
 ↓
Version
 ↓
Transaction

开始产生联系,这会进入 MVCC,而 MVCC 会成为 PostgreSQL、InnoDB 等现代数据库最重要的机制之一。


二十四、知识地图

我们已经从:

text 复制代码
Database

逐渐走到:

text 复制代码
Database
    ↓
Storage Engine
    ↓
Data File
    ↓
Page
    ↓
Record
    ↓
Slot

同时:

text 复制代码
Page
 ├── Header
 ├── Slot
 ├── Free Space
 └── Record

而 Page 又连接着:

text 复制代码
Page
 |
 +── Disk
 |
 +── Buffer Pool
 |
 +── Index
 |
 +── Transaction

所以 Page 并不是一个孤立的数据结构。它实际上是数据库存储、缓存、索引和事务系统之间的连接点。


二十五、问题梳理

真正应该掌握的是下面这几个问题。

① 为什么需要 Page?

因为数据库需要一个固定大小、适合磁盘和内存管理的存储单位。


② 为什么需要 Slot?

因为 Record 大小不固定,而且 Record 的物理位置可能发生变化。


③ 为什么删除 Record 后不立即整理?

因为移动大量 Record 的成本很高。

数据库可以暂时保留碎片空间,之后再利用或整理。


④ 为什么更新可能需要移动 Record?

因为新 Record 可能比原来的 Record 更大。


⑤ 为什么不能让上层直接依赖物理地址?

因为数据可能移动。

所以数据库通常需要某种间接寻址机制。


最重要的一句话:数据库 Page 的本质,不只是"存数据的一块空间",而是一个能够独立管理数据、空间和定位关系的存储单元。

理解了这一点,后面很多数据库设计都会自然串起来。

相关推荐
小镇敲码人15 分钟前
【深入浅出】之Qt 事件系统实战:鼠标、键盘、滚轮、窗口与定时器事件
数据库·qt·网络协议·mysql·http
Sirens.19 分钟前
MySQL数据库:索引
数据库·mysql
treacle田1 小时前
达梦数据库-Linux DM主备集群动态增加节点-记录总结
linux·运维·数据库·达梦主备集群动态扩展
一只专注api接口开发的技术猿1 小时前
Open Claw 实战|快速搭建电商商品监控与数据分析能力
大数据·数据库·数据挖掘·数据分析
杨云龙UP1 小时前
Oracle 19c RMAN历史备份清理失败:CONTROL_FILE_RECORD_KEEP_TIME 问题排查与整改实战
linux·运维·网络·数据库·oracle·rac
2601_962073971 小时前
【MySQL】全面学习数据库查询技巧:查询指令深度学习指南
数据库·学习·mysql
新知图书1 小时前
12.3 智能体中Skill的约束与自进化实战
java·前端·数据库
小小龙学IT2 小时前
Qt 数据库编程(Qt SQL 模块)深度解析
数据库·sql·qt
TDengine (老段)2 小时前
TDengine vs InfluxDB — 全方位对比
大数据·数据库·物联网·时序数据库·tdengine·涛思数据·iotdb