PostgreSQL 填充因子(fillfactor)大白话详解

一、先从一个生活场景理解

想象你有一排书架,每层能放 10 本书。

方案A(fillfactor=100) :每层放满 10 本,没有空隙。

→ 来了新书要插到某层中间?把后面的书全搬出来,插进去,再全塞回去。麻烦!

方案B(fillfactor=70) :每层只放 7 本,故意留 3 个空位。

→ 来了新书要插到某层中间?直接塞到空位里。轻松!

填充因子 = 每层书架故意留出的空位比例


二、PostgreSQL 为什么需要填充因子?

UPDATE 的本质:不是"改",而是"写新行"

PostgreSQL 的 MVCC(多版本并发控制)机制决定了:

复制代码
执行 UPDATE 时,PostgreSQL 实际上做的是:

  旧行 → 标记为"已过期"(不删除,只是打个标记)
  新行 → 写到新位置

所以 UPDATE = 写一条新记录 + 标记一条旧记录

问题来了:新行写到哪里?

复制代码
情况A:fillfactor=100(写满)
  当前 Page 已满 → 新行只能写到另一个 Page
  → 同一行数据被分散在两个不同的磁盘页
  → 索引必须更新,指向新 Page
  → 代价极高!

情况B:fillfactor=70(留有空位)
  当前 Page 有空位 → 新行直接写在同一个 Page 的空位
  → 触发 "HOT 更新"(Heap Only Tuple)
  → 索引完全不需要更新!
  → 速度极快!

三、HOT 更新是什么?

HOT(Heap Only Tuple)是 PostgreSQL 的一个重要优化:

复制代码
普通 UPDATE(跨 Page):

  索引 → 旧行(Page 1)  ×  旧行被废弃
  索引 → 新行(Page 2)  ✅  索引要重写,指向新 Page
  代价:索引维护 + IO

HOT 更新(同一 Page 内):

  索引 → 旧行(Page 1)→ [链接] → 新行(Page 1 空位)
  索引完全不动!
  代价:几乎为零

一句话:fillfactor 留出空位 → 触发 HOT 更新 → 索引不需要重建 → 写入速度大幅提升


四、结合本项目的实际场景

insertOrUpdateProcessor2 每次处理数百万行的 ON CONFLICT DO UPDATE,这个操作本质上就是大量 UPDATE:

sql 复制代码
-- 每次集成新批次,都要对已有数据执行 UPDATE
ON CONFLICT (sell_record_id) DO UPDATE SET
  version_number = excluded.version_number,
  product_no = excluded.product_no,
  -- ... 100+ 个字段

当前表 fillfactor = 100(默认)时:

复制代码
500 万行 ON CONFLICT UPDATE
→ 每行都写到新 Page(当前 Page 已满)
→ 500 万次索引更新
→ 表有 10 个索引 → 5000 万次索引写入
→ 极慢!

设置 fillfactor = 70 后:

复制代码
500 万行 ON CONFLICT UPDATE
→ 每行在原 Page 的空位直接写入(HOT 更新)
→ 索引完全不需要更新
→ 速度大幅提升

五、如何设置填充因子

5.1 建表时设置

sql 复制代码
CREATE TABLE mage_op_order_full (
    sell_through_record_id VARCHAR PRIMARY KEY,
    version_number VARCHAR,
    product_no VARCHAR
    -- ...
) WITH (fillfactor = 70);

5.2 已有表修改(不锁表)

sql 复制代码
-- 修改填充因子(立即生效,但只对新写入的 Page 有效)
ALTER TABLE mage_op_order_full SET (fillfactor = 70);

-- 要让已有 Page 也生效,需要重建表(会锁表,酌情执行)
VACUUM FULL mage_op_order_full;

-- 或者不锁表的方式(速度慢但不影响业务)
CLUSTER mage_op_order_full;

5.3 索引也可以设置

sql 复制代码
-- 索引默认 fillfactor = 90,可以单独调整
CREATE INDEX idx_xxx ON mage_op_order_full (version_number)
WITH (fillfactor = 70);

六、填充因子设多少合适?

场景 推荐值 原因
只有 INSERT,几乎不 UPDATE 100 不需要预留空间,磁盘利用率最高
少量 UPDATE(<10%) 90 少量预留,基本够用
频繁 UPDATE(本项目场景) 70~80 预留足够空间触发 HOT 更新
极端频繁 UPDATE(每行每天更新多次) 50~60 大量预留,减少 Page 分裂

注意 :fillfactor 越低,磁盘占用越多(因为每个 Page 都有空位)。

设置 70 意味着磁盘使用量比 100 多出约 43% (1/0.7 ≈ 1.43)。

需要在写入性能磁盘空间之间权衡。


七、如何判断当前表是否需要调整填充因子?

sql 复制代码
-- 查看表的死元组数量(死元组多 = UPDATE 频繁 = 应该降低 fillfactor)
SELECT
    relname AS 表名,
    n_live_tup AS 活跃行数,
    n_dead_tup AS 死元组数,
    ROUND(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS 死元组占比,
    last_autovacuum AS 上次自动清理时间
FROM pg_stat_user_tables
WHERE relname LIKE '%sell_through%'
ORDER BY n_dead_tup DESC;

判断标准:

复制代码
死元组占比 > 20%  → 说明 UPDATE 频繁,建议降低 fillfactor
死元组占比 < 5%   → 说明 UPDATE 不频繁,fillfactor 100 即可

八、填充因子 vs 其他优化手段

手段 解决的问题 代价
降低 fillfactor UPDATE 频繁导致索引膨胀 增加磁盘占用
部分索引 索引覆盖行太多、体积大 需要查询条件匹配
定期 VACUUM 死元组堆积、磁盘膨胀 消耗 IO,需要定期执行
增大 work_mem Hash Join 溢出磁盘 增加内存占用

这四个手段不冲突 ,可以同时使用。针对本项目 ON CONFLICT 慢的问题,降低 fillfactor + 定期 VACUUM 是最直接有效的组合。


九、一句话总结

填充因子就是"故意不坐满"------每个数据页留出一部分空位,让 UPDATE 能在原地写入新版本,避免数据搬到新页面后还要更新所有索引,从而大幅降低写入代价。

适合频繁 UPDATE 的大表,用 70~80 的填充因子,换取更快的写入速度,代价是多占一些磁盘空间。

相关推荐
IvorySQL7 小时前
从双解析器到循环工程:IvorySQL 五年技术演进路线的深度观察
大数据·数据库·人工智能·postgresql·开源
jnrjian1 天前
PG 切换用户 \c -
数据库·postgresql
IvorySQL2 天前
IvorySQL Agent 探索与实践
数据库·人工智能·postgresql·oracle·ivorysql
倒流时光三十年2 天前
PostgreSQL JSONB 操作符详解
大数据·数据库·postgresql
周杰伦的稻香3 天前
PostgreSQL中MVCC实现机制
数据库·postgresql
IvorySQL3 天前
PG 日报|多款 PostgreSQL 工具新版本集中发布
数据库·postgresql
IT 小阿姨(数据库)3 天前
PostgreSQL 因主库表空间错乱_缺少软链接、从库配置问题,导致主从复制故障【修复过程】
数据库·postgresql
EterNity_TiMe_4 天前
别让告警变成噪音:用Prometheus和Alertmanager搭一套可远程查看的监控系统
数据库·人工智能·ai·postgresql·prometheus·cpolar
ClouGence4 天前
保障公共安全,一线治安机构实时数据应用与实践
数据库·mysql·postgresql
AI的探索之旅5 天前
从 Ubuntu 14.04 到 24.04:TI AM335x 开发环境完整迁移与 Agent 接管方案
linux·数据库·嵌入式硬件·ubuntu·postgresql