在 PostgreSQL 的配置参数海洋里,有一类参数安静地躺在文档的 "Preset Options" 区域——它们从集群初始化那一刻起就写定了,运行时看得到,改不了。block_size 就是其中最值得深挖的一个。它告诉你 PostgreSQL 的一个数据页(page)有多大,默认 8192 字节,也就是 8KB。这个数字看似平淡,但它从磁盘布局到缓冲池管理、从索引分裂到行溢出策略,几乎牵动着存储引擎的每一条神经。
8KB 页面:PostgreSQL 存储的原子单位
PostgreSQL 把表文件拆成一个个 page,每个 page 的大小就是 block_size。一个 page 既是磁盘 I/O 的最小传输单位,也是 shared_buffers 里一个 buffer slot 的容量单位。这意味着:
- 读取一行数据,至少要把包含这行的整个 page 从磁盘拉进缓冲池。
- 一个 page 里能塞多少行,取决于行宽和 page 内的元数据开销(Line Pointer、PageHeader 等)。
- 当行太大塞不进一个 page 时,PostgreSQL 会走 TOAST 机制,把大字段的外部存储切成同样大小的 chunk。
这 8KB 不是随便选的。它是一个在 I/O 效率和空间利用率之间反复权衡的结果——太小则每页能装的行少、索引层级深;太大则一次 I/O 拖回太多无关数据,缓冲池浪费严重。
block_size 的"亲戚们":Preset Options 全家福
block_size 并不是孤例。在同一个 Preset Options 分组里,还有几个同样只读的参数,它们共同描绘了集群初始化时定下的硬件级约定:
| 参数 | 含义 | 典型值 |
|---|---|---|
block_size |
数据页大小 | 8192 |
wal_block_size |
WAL 页大小 | 8192 |
data_checksums |
是否开启数据页校验 | on/off |
server_version |
服务器版本号 | 17.x 等 |
你可以随时查询它们,但 ALTER SYSTEM 对它们无效。这些值要么在 initdb 时编译进去,要么由编译选项(如 --with-blocksize)决定,一旦集群创建完成,就是铁律。
下面这条 SQL 可以一次性列出所有 preset 参数:
-- 查看所有 Preset Options 类参数
SELECT name, setting, category, short_desc
FROM pg_settings
WHERE category = 'Preset Options'
ORDER BY name;
运行结果示例(PostgreSQL 17):
name | setting | category | short_desc
-------------------+---------+------------------------+----------------------------------
block_size | 8192 | Preset Options | Shows the size of a disk block.
data_checksums | on | Preset Options | Shows whether data checksums are enabled.
integer_datetimes | on | Preset Options | Datetimes are stored as 64-bit integers.
max_function_args | 100 | Preset Options | Shows the maximum number of function arguments.
max_identifier_length | 63 | Preset Options | Shows the maximum identifier length.
server_version | 170000 | Preset Options | Shows the server version.
server_version_num | 170000 | Preset Options | Shows the server version as an integer.
wal_block_size | 8192 | Preset Options | Shows the size of a WAL disk block.
8KB 页面如何影响你的日常操作
理解 block_size 不只是"知道一个数字",它直接影响你能观察到的若干现象。
行溢出与 TOAST 阈值
一个 page 扣除 PageHeader(24 字节)和 Line Pointer 数组后,留给行数据的可用空间大约是 8192 − 24 − (4 × 行数)。PostgreSQL 的规则是:如果一行数据的原始大小超过约 2KB(约 page 的 1/4),就会触发 TOAST——先尝试压缩,压缩后仍超限则把大字段值存到单独的 TOAST 表里,主表只留一个指针。
这意味着你设计宽表时,如果一行有多个 TEXT/VARCHAR 列且经常同时写入大值,TOAST 的额外 I/O 会成为性能隐患。
索引分裂频率
B-tree 索引的每个节点也是一个 page。8KB 的 page 决定了每个索引节点能装多少个 key+tuple pointer。key 越宽,每个节点能容纳的条目越少,索引分裂越频繁,树越深。对于 UUID 索引(16 字节 key)vs 整数索引(4 字节 key),前者的扇出明显更低,这是 block_size 在背后起作用。
实际观察:一个 page 里能塞多少行
你可以用 pg_relation_size 结合行数估算来验证:
-- 创建一个窄行测试表
CREATE TABLE narrow_rows (id int, val text);
-- 插入 10000 行短数据
INSERT INTO narrow_rows
SELECT g, md5(g::text)
FROM generate_series(1, 10000) AS g;
-- 查看表占用的 page 数
SELECT pg_relation_size('narrow_rows') / current_setting('block_size')::int AS pages,
count(*) AS rows,
round(count(*)::numeric /
(pg_relation_size('narrow_rows') / current_setting('block_size')::int), 1)
AS rows_per_page
FROM narrow_rows;
典型结果:8KB page 下,这种窄行大约每页能装 80+ 行。如果你把 val 换成更长的文本,rows_per_page 会迅速下降——这就是 block_size 在"收税"。
能改 block_size 吗?理论上可以,实际别碰
PostgreSQL 编译时提供了 --with-blocksize 选项,允许你把 page 大小设为 4KB、16KB 甚至 32KB。但:
- 改了之后,所有工具(pg_dump、pg_upgrade、流复制)都必须匹配同一个 block_size,生态兼容性立刻断裂。
- 大部分扩展和第三方备份工具假设 block_size = 8192,硬编码在二进制里。
- 性能收益并不确定:16KB page 在顺序扫描时可能更好,但随机读写和缓冲池利用率可能变差。
- 官方文档明确标注此参数为 "Preset",不是运行时调优参数。
结论:除非你在做嵌入式/特殊硬件的深度定制,并且愿意承担完整的生态隔离成本,否则不要动它。8KB 是 PostgreSQL 社区的事实标准。
实用检查清单
在日常运维和架构设计中,把 block_size 当作一个"已知常数"来用,而不是一个需要调优的变量:
- 估算表页数:
pg_relation_size(relid) / 8192,快速判断表是否大到需要分区。 - 评估索引深度:key 越宽,8KB page 里条目越少,三层 B-tree 能覆盖的行数越少。宽 key 场景考虑 hash 索引或 BRIN。
- TOAST 意识:行宽超过 ~2KB 就会触发外部存储,查询时多一次随机 I/O。宽表设计时优先拆字段或用 JSONB 压缩。
- vacuum 碎片理解:一个 page 里死行占比达到
vacuum_threshold才触发自动 vacuum,8KB page 下几条死行可能不够触发——小表反而容易膨胀。 - 迁移前确认:
pg_config --configure的输出里如果出现非默认--with-blocksize,目标集群必须完全一致,否则 pg_upgrade 直接报错。
# 快速确认当前集群的 block_size 和编译选项
psql -c "SHOW block_size;"
pg_config --configure
8KB 是 PostgreSQL 给你画好的格子线。你不需要移动它,但你需要知道每条线在哪里,才能把数据摆得又密又快。