由 John Doe 五月 19, 2026
摘要:在本文中,我们将了解 PostgreSQL 删除对查询的影响,及其背后的原理。
目录
标记删除元组的事务状态
下面我们在 PostgreSQL 18.4 的数据库中,创建一个表,插入一些数据,并对表数据进行清理操作。
-- 会话一
CREATE TABLE t_large (id integer, data text);
INSERT INTO t_large (id, data)
SELECT i, repeat('Pg', 64)
FROM generate_series(1, 1000000) AS s(i);
CREATE INDEX large_idx ON t_large (id);
VACUUM ANALYZE t_large;
我们先创建一个简单的函数,该函数可以根据指定的 off 参数,遍历每个表页面中的一个元组,并计算出访问过的元组中的数据总大小:
CREATE OR REPLACE FUNCTION calc_data_size(off int)
RETURNS bigint
AS $$
DECLARE
i int := off;
siz bigint := 0;
val text;
BEGIN
PERFORM pg_current_xact_id();
WHILE i < 1000000 LOOP
SELECT data INTO val FROM t_large WHERE id = i;
siz := siz + char_length(COALESCE(val, ''));
i := i + 50;
END LOOP;
RETURN siz;
END $$ LANGUAGE plpgsql;
注意:在上面的函数体中,我们调用了系统函数
pg_current_xact_id,让函数运行在一个事务中,从而可以观察到产生的 WAL 日志量对于整个查询的性能影响。
在准备好基础表和初始数据后,让我们启动一个会话,在该会话中开始一个事务,并保持在空闲状态:
-- 会话二
BEGIN;
SELECT pg_current_xact_id();
让我们删除表中的一部分数据。再触发一次检查点操作,刷写缓冲区中的所有脏页面。
-- 会话一
DELETE FROM t_large WHERE mod(id, 50) = 1;
CHECKPOINT;
下面我们来查询表中已删除的记录,观察查询执行的情况。
-- 会话一
EXPLAIN (analyze, buffers, wal, costs false)
SELECT calc_data_size(1);
QUERY PLAN
-------------------------------------------------------------------
Result (actual time=323.068..323.069 rows=1.00 loops=1)
Buffers: shared hit=77306 read=2753 dirtied=20001
WAL: records=20001 fpi=20001 bytes=162183713 buffers full=17818
Planning Time: 0.020 ms
Execution Time: 323.118 ms
(5 rows)
可以看到,在页面不脏的情况下,查询删除的记录都会生成记录页面镜像的 WAL 日志。修改了 20001 个页面,产生了 20001 条 WAL 日志记录,WAL 日志总量为 155 MB。同时,这也给查询带来了一定的性能损耗。
查询过程中的页面清理
让我们结束会话二中的空闲事务。再触发一次检查点操作,刷写缓冲区中的所有脏页面。
-- 会话二
COMMIT;
CHECKPOINT;
下面我们来查询表中的活跃记录,观察查询执行的情况。
-- 会话一
EXPLAIN (analyze, buffers, wal, costs false)
SELECT calc_data_size(2);
QUERY PLAN
-------------------------------------------------------------------
Result (actual time=332.218..332.219 rows=1.00 loops=1)
Buffers: shared hit=80000 dirtied=19592
WAL: records=19592 fpi=19592 bytes=155893544 buffers full=17046
Planning Time: 0.020 ms
Execution Time: 332.233 ms
(5 rows)
可以看到,查询访问包含已删除记录的页面,会对访问的页面进行清理,并记录相应的 WAL 日志。修改了 19592 个页面,产生了 19592 条 WAL 日志记录,WAL 日志总量为 149 MB。同时,这也给查询带来了一定的性能损耗。
Redrock Postgres 的表现
下面我们在 Redrock Postgres 的数据库中,执行和上面相同的测试用例。在查询表中已删除的记录时,观察到的查询执行情况如下:
EXPLAIN (analyze, buffers, wal, costs false)
SELECT calc_data_size(1);
QUERY PLAN
-------------------------------------------------------
Result (actual time=68.575..68.576 rows=1.00 loops=1)
Buffers: shared hit=1 read=12 dirtied=3
WAL: records=1 fpi=3 bytes=11376
Planning Time: 0.023 ms
Execution Time: 68.630 ms
(5 rows)
输出结果显示,前面的查询操作只修改了回滚段上的 3 个页面,用于记录事务的状态,没有修改任何表数据页面。接下来,在空闲事务结束后,查询表中的活跃记录:
EXPLAIN (analyze, buffers, wal, costs false)
SELECT calc_data_size(2);
QUERY PLAN
-------------------------------------------------------
Result (actual time=89.976..89.977 rows=1.00 loops=1)
Buffers: shared hit=1 read=2 dirtied=3
WAL: records=1 fpi=3 bytes=14496
Planning Time: 0.023 ms
Execution Time: 89.993 ms
(5 rows)
输出结果显示,前面的查询操作也只修改了回滚段上的 3 个页面,没有修改任何表数据页面。
测试结论
PostgreSQL 的 DELETE 性能优势是有代价的,后面的元组事务状态标记和页面清理,都需要再次进行页面修改。PostgreSQL 采用标记删除 + 延迟清理,换取 DELETE 速度但需承担后续维护成本;Redrock Postgres 删除时会记录回滚日志和事务状态,牺牲 DELETE 速度以保证查询稳定性。在实际生产环境中:
- 对于 DELETE 操作频繁但查询压力可控的场景,PostgreSQL 的表现会更好;
- 对于 DELETE 与查询交替频繁的核心业务场景,如银行交易系统、支付平台,Redrock Postgres 的表现会更加稳定。