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 的填充因子,换取更快的写入速度,代价是多占一些磁盘空间。

相关推荐
Java陈序员2 小时前
主流数据库通吃!一款开源实用的数据库备份管理工具!
react.js·postgresql·go
xhaxy4 小时前
pgsql集群搭建(Patroni + etcd )
linux·postgresql·etcd
暴躁小师兄数据学院20 小时前
【AI大数据工程师特训笔记】第13讲:数据库性能手术刀
大数据·数据库·数据仓库·sql·postgresql
丷丩2 天前
Postgresql基础实践教程(十一)各种Join
数据库·postgresql·join
暴躁小师兄数据学院2 天前
【AI大数据工程师特训笔记】第10讲:数据库用户、权限管理、数据库约束
大数据·数据库·笔记·sql·postgresql
暴躁小师兄数据学院2 天前
【AI大数据工程师特训笔记】第02讲:PostgreSQL数据库生态全景
大数据·数据库·人工智能·postgresql
暴躁小师兄数据学院2 天前
【AI大数据工程师特训笔记】第08讲:集合运算与超级函数
大数据·笔记·sql·ai·postgresql
雷工笔记2 天前
SQL系列2:PostgreSQL 日期时间字段类型选择指南
数据库·sql·postgresql
逍遥德2 天前
PostgreSQL --- JSON 函数详解
数据库·sql·postgresql·json