由 John Doe 五月 6, 2026
摘要:在本文中,我们将了解 PostgreSQL 更新性能的表现,及其背后的原理。
目录
表数据更新场景: 用例
为了能够更清楚地观察 UPDATE 操作的 WAL 日志开销,我们先将参数full_page_writes设置为 off。
SHOW full_page_writes;
full_page_writes
------------------
off
(1 row)
下面我们在 PostgreSQL 的数据库中,创建一个表,插入一些数据,并对表数据进行清理操作,查看更新操作的性能。
\set fillerlen 64
\set fillfactor 100
\set scale 100
\set updatebid 0
DROP TABLE IF EXISTS pgbench_accounts;
CREATE TABLE pgbench_accounts (
aid int PRIMARY KEY,
bid int,
filler char(:fillerlen),
abalance int
) WITH (fillfactor = :fillfactor);
INSERT INTO pgbench_accounts(aid, bid, filler, abalance)
SELECT aid, (aid - 1) / 100000 + 1, '', 0
FROM generate_series(1, :scale * 100000) as aid;
VACUUM ANALYZE pgbench_accounts;
CHECKPOINT;
上面我们进行了检查点操作,刷写了缓冲区中的所有脏页面。这样,就可以通过 EXPLAIN ANALYZE 来查看修改的页面情况:
EXPLAIN (analyze, buffers, wal, costs false)
UPDATE pgbench_accounts
SET bid = bid + :updatebid, abalance = abalance + random(-5000, 5000)
WHERE mod(aid, 1000) = 1;
QUERY PLAN
---------------------------------------------------------------------------------------
Update on pgbench_accounts (actual time=491.377..491.378 rows=0.00 loops=1)
Buffers: shared hit=213611 dirtied=20142 written=133
WAL: records=30003 bytes=2720132
-> Seq Scan on pgbench_accounts (actual time=0.026..449.655 rows=10000.00 loops=1)
Filter: (mod(aid, 1000) = 1)
Rows Removed by Filter: 9990000
Buffers: shared hit=133334
Planning:
Buffers: shared hit=25 dirtied=1
Planning Time: 0.156 ms
Execution Time: 491.429 ms
(11 rows)
注意:在上面的 UPDATE 操作中,我们通过 updatebid 参数来控制是否更新 bid 列。当 updatebid 参数为 0 时,bid 列的值保持不变。
上面的 UPDATE 操作更新了 10000 条记录,不涉及修改索引相关的列。总共修改了 20224 个页面,产生了 30003 条 WAL 日志记录,WAL 日志总量为 2.6 MB。
依次指定 fillerlen 参数为 64、128,fillfactor 参数为 100、80,updatebid 参数为 0、1,执行上面的测试用例,收集 IO 相关的数据如下:
| filler 列长度 (fillerlen) | 填充因子 (fillfactor) | 更新 bid 列 (updatebid) | 修改页面数 | WAL 记录数 | WAL 日志量 (bytes) |
|---|---|---|---|---|---|
| 64 | 100 | 0 | 20142 | 30003 | 2720132 |
| 64 | 80 | 0 | 10006 | 10000 | 719690 |
| 64 | 80 | 1 | 10006 | 10000 | 1439716 |
| 128 | 100 | 0 | 20224 | 30003 | 3360132 |
| 128 | 80 | 0 | 10009 | 10000 | 719710 |
| 128 | 80 | 1 | 10009 | 10000 | 2079758 |
PostgreSQL 更新性能数据分析
针对上面的更新性能测试的数据,我们可以看出:
- 填充因子 fillfactor 为 100 时,更新需要同时修改堆表页面和索引页面,并在 WAL 日志中记录完整的堆元组和索引记录,更新效率最差。
- 填充因子 fillfactor 为 80 时,可以在单个堆表页面内进行 HOT 更新,当更新只修改单个列时,WAL 日志中只需要记录单个列的新值,更新效率最高。
- 填充因子 fillfactor 为 80 时,可以在单个堆表页面内进行 HOT 更新,当更新要修改两个列时,WAL 日志中需要记录两个列及两列之间的数据。在上面的测试结果中,当 updatebid 参数为 1 时(更新 bid 列),filler 列长度由 64 调整成 128 后,WAL 日志量出现了明显的变化。
另外,关于索引对 PostgreSQL 更新操作的性能影响,可参见更新 PostgreSQL 表带来的索引写放大。简单来说,PostgreSQL 的更新操作耗时,和表的索引个数成正比关系。而 Redrock Postgres 的更新操作性能能够始终保持稳定。
Redrock Postgres 的表现
Redrock Postgres 引入了撤消日志,修改元组时可实现就地更新,无需添加索引记录。由于无需修改索引,可避免冗余的索引插入操作,从而帮助减少索引膨胀。
下面我们在 Redrock Postgres 的数据库中,执行和上面相同的测试用例,收集 IO 相关的数据如下:
| filler 列长度 (fillerlen) | 填充因子 (fillfactor) | 更新 bid 列 (updatebid) | 修改页面数 | WAL 记录数 | WAL 日志量 (bytes) |
|---|---|---|---|---|---|
| 64 | 100 | 0 | 10079 | 10077 | 1457101 |
| 64 | 80 | 0 | 10078 | 10076 | 1457677 |
| 64 | 80 | 1 | 10087 | 10085 | 1558367 |
| 128 | 100 | 0 | 10078 | 10076 | 1457303 |
| 128 | 80 | 0 | 10079 | 10077 | 1456955 |
| 128 | 80 | 1 | 10088 | 10086 | 1558449 |
从上面的测试数据来看,无论是调整填充因子 fillfactor 的值,还是修改单个列或者两个列,IO 相关的开销没有明显的变化,更新操作的行为都能保持相对稳定。