4.1 页式存储、记录布局与 Buffer Pool
查询返回一行,不代表磁盘只读一行。存储引擎通常按页搬运数据,用缓冲池复用已读的页。本课沿着一次读取检查页布局、记录定位和脏页写回,解释为什么索引查找可能比顺序扫描更慢,也解释内存命中为什么还不等于事务可见。
本课目标
- 区分 row、tuple、page、extent/file 与 block device;
- 解释 slotted page 如何容纳变长 records;
- 正确比较 sequential scan 与 random lookup;
- 理解 buffer pool 的 page table、pin、dirty 与 eviction;
- 识别 OS page cache、database cache 与 durability 的不同职责。
一次查询经过哪些层
SQL executor
-> access method (heap/index)
-> buffer manager
-> database page in memory
-> database file
-> filesystem / block layer
-> storage devicequery executor 通常不会为每行直接调用一次磁盘读取。它请求 page,buffer manager 判断 page 是否已经驻留;缺页时才通过文件与操作系统读取。
Sequential scan 不是“随机 I/O × 页数”
假设 table 有 5,000,000 pages,scan 确实要检查大量 page,但它通常按物理/逻辑 page 顺序读取。操作系统与设备能 readahead、merge requests,现代 SSD 也有并行队列。
因此不能用“单次随机 I/O 10 ms × 500 万”估算顺序扫描。更合理的下界近似从数据量和可持续吞吐开始:
scan time >= bytes to read / effective read bandwidth例如假设每页 8192 bytes、500 万页全部需要从设备读取,总量为 40.96 GB;假设有效带宽为 1 GB/s,则仅传输的理想下界为 40.96 秒。这是指定假设下的计算,不是实测结果。CPU 过滤、解压、可见性检查和结果传输可能重叠执行,也可能形成新的瓶颈,不能把各项耗时机械相加。
若数据已在 buffer pool/OS cache,物理读可能很少;若 query 只需返回大部分 table,sequential scan 可能比大量 index random lookup 更便宜。index 不是“永远把 O(n) 变成 O(log n)”的通用按钮。
Page 是固定大小的管理单位
数据库通常选择固定 page size,具体大小由 engine/build/table setting 决定。一个 page 不能简单用 page_size / row_size 得出 row count,还要考虑:
- page header;
- line pointer/slot directory;
- tuple header、NULL bitmap、alignment;
- variable-length value;
- MVCC metadata;
- free space、fragmentation 和 engine-specific trailer。
所以“4 KiB page、200-byte row 恰好 20 行”只是粗略上限,不是布局事实。
Slotted page
常见 slotted-page 结构:
+-------------------------------+
| page header |
+-------------------------------+
| slot 0 | slot 1 | ... | grows downward
+-------------------------------+
| free space |
+-------------------------------+
| record ... | record 1 | rec 0 | grows upward
+-------------------------------+slot 保存 record offset/length 或状态。record 在 page 内移动时,只更新 slot,外部 record identifier 仍可引用 (page_id, slot_id)。删除 record 后可以标记 slot 空闲,稍后 compact page。
具体实现并不统一。PostgreSQL heap page 的 item identifiers 与 tuples 从两端增长;page header 还包含 LSN、flags 和 free-space boundaries。checksum 是否存在/启用取决于集群配置,不应写成每页必有且始终校验。
Variable-length 与 overflow
row 可能含 text/blob。engine 可采用:
- inline short value;
- page 内 variable-length record;
- overflow page/chained storage;
- compression;
- PostgreSQL TOAST 一类 out-of-line storage。
大 column 会改变 row width、cache density 和 index feasibility。SELECT * 不只是网络问题,也可能迫使 engine 读取原本可避免的 overflow data。
Record identifier 的稳定性边界
heap-organized table 的 index leaf 常保存 record locator,例如 page/slot。row update 可能:
- 原地更新;
- 在同 page 生成新 version;
- 移到另一 page 并留下 forwarding/version link;
- 更新所有相关 indexes;
- 保留旧 version 供并发 snapshot 读取。
因此 locator 往往是 engine-internal identity,不是业务主键。PostgreSQL 的 ctid 会随 row version/location 改变,不应持久化为 application ID。
Buffer Pool 的核心状态
buffer manager 至少要回答:
page_id -> frame是否命中;- frame 是否被使用者 pin,能否 eviction;
- page 是否 dirty;
- page 最近/频率信息;
- dirty page 写回前相应 WAL 是否满足 write-ahead rule;
- 并发读写怎样 latch/lock。
page table: page_id -> frame_id
free list / replacement candidates
frame metadata: pin_count, dirty, usage, io_state
frames: actual page bytespage hit 只说明 page bytes 在内存,不代表 row 对当前 transaction 可见;MVCC visibility 仍需检查。
Pin、latch 与 transaction lock 不同
- pin 防止 frame 在使用时被 eviction;
- latch 保护内存结构/page 的短临界区;
- transaction lock 保护 logical data concurrency,生命周期可能跨 statement/transaction。
混用三者会得到错误的性能和 deadlock 模型。读取一个 page 可能短暂持 latch,却不一定获得业务 row lock。
Replacement 不是纯 LRU
精确 LRU 维护成本高,而且一次大 scan 可能驱逐 hot working set。engine 会采用 clock-sweep、LRU-K、2Q、scan-resistant admission、sequential-scan ring 等策略或变体。
策略目标也不只是 hit ratio:dirty write cost、scan behavior、prefetch、concurrent contention 和 page reuse distance 都会参与。
“buffer pool 设为物理内存固定百分比”不是通用公式。还要给 OS、connection memory、sort/hash、background process、filesystem cache 和其他服务留空间。PostgreSQL 与 InnoDB 的 caching path 也不同,应以目标版本官方文档和 workload measurement 配置。
Dirty page 与 durability
修改发生时,page 可先在 buffer pool 中变 dirty。write-ahead rule 约束的是:当 dirty data page 要写入 durable data file 前,能重做该修改的 WAL 必须先 durable。
这不等于“每改一个内存字节前都先同步写日志”。将 memory modification、WAL insertion、WAL flush、data-page flush 和 commit acknowledgement 分开,才能理解 group commit 与 checkpoint。
OS page cache 与 direct I/O
数据库可能通过 buffered I/O 利用 OS page cache,也可能在部分路径使用 direct I/O。double caching 是否浪费,取决于 engine 与 workload;OS cache 还承担 filesystem metadata、executable 和其他文件缓存。
内存中看见数据与 durable storage 接受 flush 是两件事。write() 返回、kernel cache、device volatile cache 与 power-loss persistence 不能混为一谈。
观测问题
排查 I/O 时收集:
logical page reads vs physical reads
buffer hit/miss by relation/query
read/write latency distribution
dirty pages and checkpoint write rate
scan bytes, returned rows, filtered rows
temporary spill bytes
device queue and utilization
working-set change over time高 buffer hit ratio 不一定等于快:所有 page 都命中时,CPU、latch、decompression 或 query plan 仍可成为瓶颈。低 hit ratio 也不一定是问题:大而低复用的 analytical scan 本就不应污染 hot cache。
本课小结
页式存储把 row 管理转换为 page 管理;buffer pool 决定哪些 page 驻留、何时写回。顺序扫描、随机 lookup 和 cache hit 必须结合数据量与访问模式解释。下一课把 B+ 树放进这个 page model,分析 point lookup、range scan、split 和不同 engine 的 leaf payload。