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

相关推荐
残 风3 小时前
PostgreSQL 存储机制详解
数据库·postgresql·开源·数据库开发
ylj_dev5 小时前
从 0 构建 AI Workload Platform(四):从单机工作流内核到可靠控制面
docker·postgresql·架构·golang
Wang's Blog6 小时前
PostgreSQL笔记48: 全文检索与向量检索的对比与混合检索实践
笔记·postgresql·全文检索
Wang's Blog7 小时前
PostgreSQL笔记49:向量检索核心算法、索引调优与过滤策略深度解析
笔记·算法·postgresql
Wang's Blog8 小时前
PostgreSQL笔记51: 基于 pgvector 构建企业级智能问答系统
数据库·笔记·postgresql
Wang's Blog10 小时前
PostgreSQL笔记50: 基于PGVECTOR的实时推荐与搜索系统构建
笔记·postgresql
Wang's Blog1 天前
PostgreSQL笔记34:索引优化策略全景解析——从B-tree到HOT的核心原理与实践
数据库·笔记·postgresql
JavaPub-rodert1 天前
Go 后台如何同时兼容 MySQL、PostgreSQL、SQLite 和 SQL Server?从 ShiyuAdmin 看 GORM 多数据库适配
数据库·mysql·postgresql·golang·javapub·王仕宇
瀚高PG实验室1 天前
SQL优化案例:存储过程、复杂SQL拆成简单SQL提升整体性能
数据库·sql·postgresql·瀚高数据库
winfredzhang1 天前
从零构建家庭照片管理系统:Django 模块化单体实战,以及我踩到的 4 个真实坑
python·postgresql·django·sqlite·bootsrap