由 John Doe 五月 18, 2026
摘要:在本文中,我们将了解 PostgreSQL 删除性能的表现,及其背后的原理。
目录
表数据删除场景: 用例
为了能够更清楚地观察 DELETE 操作的 WAL 日志开销,我们先将参数full_page_writes设置为 off。
SHOW server_version;
server_version
----------------
18.4
(1 row)
SHOW full_page_writes;
full_page_writes
------------------
off
(1 row)
下面我们在 PostgreSQL 的数据库中,创建一个表,插入一些数据,并对表数据进行清理操作,查看该表占用的空间大小。
CREATE TABLE t_large (id integer, data text);
INSERT INTO t_large (id, data)
SELECT i, repeat('Pg', 512)
FROM generate_series(1, 1000000) AS s(i);
VACUUM ANALYZE t_large;
CHECKPOINT;
SELECT pg_size_pretty(pg_table_size('t_large'));
pg_size_pretty
----------------
1116 MB
(1 row)
上面我们进行了检查点操作,刷写了缓冲区中的所有脏页面。这样,就可以通过 EXPLAIN ANALYZE 来查看修改的页面情况:
EXPLAIN (analyze, buffers, wal, costs false)
DELETE FROM t_large;
QUERY PLAN
--------------------------------------------------------------------------------
Delete on t_large (actual time=1157.257..1157.258 rows=0.00 loops=1)
Buffers: shared hit=1192561 read=93155 dirtied=142863 written=11970
I/O Timings: shared read=9.894 write=24.343
WAL: records=1000000 bytes=54000000 buffers full=26
-> Seq Scan on t_large (actual time=0.210..674.204 rows=1000000.00 loops=1)
Buffers: shared hit=49703 read=93155 written=11970
I/O Timings: shared read=9.894 write=24.343
Planning:
Buffers: shared hit=5
Planning Time: 0.073 ms
Execution Time: 1157.416 ms
(11 rows)
上面的 DELETE 操作删除了 1000000 条记录,修改了 142863 个页面,产生了 1000000 条 WAL 日志记录,WAL 日志总量为 51 MB。
等待大约 1 分钟之后(参数autovacuum_naptime默认值为 1 分钟),查看该表占用的空间大小:
SELECT pg_size_pretty(pg_table_size('t_large'));
pg_size_pretty
----------------
827 MB
(1 row)
在进行全表删除后,PostgreSQL 只回收了一部分删除的元组空间。
Redrock Postgres 的表现
下面我们在 Redrock Postgres 的数据库中,执行和上面相同的测试用例。在执行删除操作之前,查看下当前数据库中回滚段的使用情况:
SELECT oid, relname, pg_size_pretty(pg_relation_size(oid)) AS undsize
FROM pg_class WHERE relkind = 'u'
ORDER BY pg_relation_size(oid) DESC LIMIT 4;
oid | relname | undsize
------+-----------+---------
8102 | pg_undo_2 | 41 MB
8103 | pg_undo_3 | 1040 kB
8104 | pg_undo_4 | 1040 kB
8105 | pg_undo_5 | 1040 kB
(4 rows)
输出结果显示,前面的 INSERT 操作产生了 41 MB 的回滚日志。接下来,执行相同的删除操作:
EXPLAIN (analyze, buffers, wal, costs false)
DELETE FROM t_large;
QUERY PLAN
--------------------------------------------------------------------------------
Delete on t_large (actual time=1368.790..1368.791 rows=0.00 loops=1)
Buffers: shared hit=2054654 read=110057 dirtied=150071 written=26645
I/O Timings: shared read=5.627 write=100.487
WAL: records=1007204 bytes=125592003 buffers full=9792
-> Seq Scan on t_large (actual time=0.251..489.609 rows=1000000.00 loops=1)
Buffers: shared hit=32931 read=109927 written=17821
I/O Timings: shared read=4.972 write=32.084
Planning:
Buffers: shared hit=2
Planning Time: 0.106 ms
Execution Time: 1369.009 ms
(11 rows)
上面的 DELETE 操作删除了 1000000 条记录,修改了 150071 个页面,产生了 1007204 条 WAL 日志记录,WAL 日志总量为 120 MB。我们再次查看下回滚段的使用情况:
SELECT oid, relname, pg_size_pretty(pg_relation_size(oid)) AS undsize
FROM pg_class WHERE relkind = 'u'
ORDER BY pg_relation_size(oid) DESC LIMIT 4;
oid | relname | undsize
------+------------+---------
8117 | pg_undo_17 | 56 MB
8102 | pg_undo_2 | 41 MB
8104 | pg_undo_4 | 1040 kB
8105 | pg_undo_5 | 1040 kB
(4 rows)
输出结果显示,前面的 DELETE 操作产生了 56 MB 的回滚日志。
等待大约 1 分钟之后,查看该表占用的空间大小:
SELECT pg_size_pretty(pg_table_size('t_large'));
pg_size_pretty
----------------
24 kB
(1 row)
在进行全表删除后,Redrock Postgres 能够完全回收删除的元组空间。
DELETE 后的清理成本
实际上,PostgreSQL 的 DELETE 操作完成后,后面还会有一个延迟的二次更改的过程。
PostgreSQL 删除一个表中的元组时,元组头部会记录删除该元组的事务 ID,称为元组的 xmax。PostgreSQL 将每个事务的当前状态信息存储在提交日志 (CLOG) 中。检查 CLOG 中大量事务的状态会消耗大量资源,因此 PostgreSQL 会将事务状态信息直接缓存到元组的头部。例如,如果在执行 SELECT 语句时检测到 xmax 事务已完成,PostgreSQL 会将此信息保存到元组的所谓“提示位”中。这个过程会将表页面弄脏再写入磁盘,并需要记录 WAL 日志。
另外,对于删除过的元组,需要等待全表和索引执行过 VACUUM 操作后,才能完全重用删除的元组空间。
也就是说,PostgreSQL 中的 DELETE 实际上将一部分的成本,转移分摊到了后面的 SELECT 和 VACUUM 操作里面了。
测试结论
PostgreSQL 的 DELETE 性能优势是有条件的,它将一部分成本转移到了后续的 SELECT 和 VACUUM 操作中。在实际生产环境中:
- 如果您的应用偏向 “插入 + 删除” 或者 “插入 + 查询” 这两类场景,PostgreSQL 的表现会更好;
- 如果您的应用是混合型业务场景,更新删除操作频繁且有大量复杂分析查询,Redrock Postgres 的表现会更加稳定。