你有没有想过为什么 VACUUM 不会使你的PostgreSQL表更小?你有没有想过 VACUUM 为什么不收缩数据文件?好吧,也许这就是您一直在寻找的文章。本文重点是:
- 通常清理后空间不会返回到操作系统,本文介绍了其作用原因,这很重要。
- 人们经常对 VACUUM 的内部工作原理做了一些错误的假设。深入挖掘并了解到底发生了什么也是有必要的。
以下内容向用户揭示了有关 VACUUM 的最有用的秘密。
了解元组可见性
要了解 PostgreSQL 中的 VACUUM ,重要的是要首先了解 PostgreSQL 如何处理可见性。整个概念基于一组隐藏的列,这些列是行的一部分。以下是它的工作原理:\
test=# CREATE TABLE t_test (id int);
CREATE TABLE
test=# INSERT INTO t_test VALUES (5), (6), (7);
INSERT 0 3
test=# INSERT INTO t_test VALUES (8), (9), (10);
INSERT 0 3
复制
为了保持简单,我们刚刚创建的表仅由一列组成。请注意,数据已使用两个单独的事务加载到表中。每个事务都插入了 3 行,这在隐藏列中明确指示:
test=# SELECT ctid, xmin, xmax, cmin, cmax, * FROM t_test;
ctid | xmin | xmax | cmin | cmax | id
------+------+------+------+------+----
(0,1) | 764 | 0 | 0 | 0 | 5
(0,2) | 764 | 0 | 0 | 0 | 6
(0,3) | 764 | 0 | 0 | 0 | 7
(0,4) | 765 | 0 | 0 | 0 | 8
(0,5) | 765 | 0 | 0 | 0 | 9
(0,6) | 765 | 0 | 0 | 0 | 10
(6 rows)
复制
xmin, xmax, cmin 和 cmax 是包含事务 ID 的隐藏列。如您所见,前三行已由事务编号 764 写入,而其余数据已使用事务编号 765 创建。
隐藏列将处理可见性,PostgreSQL将(通常但并非总是)根据这些列确定某个事务是否可以看到行。
运行 UPDATE 语句将如下显示:
test=# BEGIN;
BEGIN
test=*# UPDATE t_test SET id = id * 10 WHERE id > 9 RETURNING *;
id
-----
100
(1 row)
UPDATE 1
test=*# SELECT ctid, xmin, xmax, cmin, cmax, * FROM t_test;
ctid | xmin | xmax | cmin | cmax | id
------+------+------+------+------+-----
(0,1) | 764 | 0 | 0 | 0 | 5
(0,2) | 764 | 0 | 0 | 0 | 6
(0,3) | 764 | 0 | 0 | 0 | 7
(0,4) | 765 | 0 | 0 | 0 | 8
(0,5) | 765 | 0 | 0 | 0 | 9
(0,7) | 766 | 0 | 0 | 0 | 100
(6 rows)
复制
已更改一行。但是,让我们将重点放在 CTID 上,它表示磁盘上行的物理位置。请注意,(0, 6) 已消失,因为 PostgreSQL 必须复制该行。如果我们运行第二个 UPDATE ,则该行将再次被复制:
test=*# UPDATE t_test SET id = id * 10 WHERE id > 9 RETURNING *;
id
------
1000
(1 row)
UPDATE 1
test=*# UPDATE t_test SET id = id * 10 WHERE id > 9 RETURNING *;
id
-------
10000
(1 row)
UPDATE 1
复制
复制这些行很重要,因为我们必须保留旧行。否则 ROLLBACK 将不起作用,因此必须保留旧版本。
让我们再次检查该表:
test=*# SELECT ctid, xmin, xmax, cmin, cmax, * FROM t_test;
ctid | xmin | xmax | cmin | cmax | id
------+------+------+------+------+-------
(0,1) | 764 | 0 | 0 | 0 | 5
(0,2) | 764 | 0 | 0 | 0 | 6
(0,3) | 764 | 0 | 0 | 0 | 7
(0,4) | 765 | 0 | 0 | 0 | 8
(0,5) | 765 | 0 | 0 | 0 | 9
(0,9) | 766 | 0 | 2 | 2 | 10000
(6 rows)
test=*# COMMIT;
COMMIT
复制
5到9之间的一切都充满了必须删除的死行。
VACUUM :清理行
请记住,COMMIT 也不得杀死死行。因此,清理过程必须异步完成。这正是 VACUUM 的作用。让我们运行它,看看会发生什么:
test=# VACUUM VERBOSE t_test
INFO: vacuuming "test.public.t_test"
INFO: finished vacuuming "test.public.t_test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 3 removed, 6 remain, 0 are dead but not yet removable
removable cutoff: 767, which was 0 XIDs old when operation ended
new relfrozenxid: 764, which is 1 XIDs ahead of previous value
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 22.790 MB/s, avg write rate: 27.348 MB/s
buffer usage: 6 hits, 5 misses, 6 dirtied
WAL usage: 3 records, 3 full page images, 14224 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM
复制
VACUUM 正在积极寻找不再被任何人看到的行。这些行可以位于数据文件中间的某个位置。发生的事情是 VACUUM允许PostgreSQL重用该空间。但是,它不会将该空间返回给操作系统。它不能这样做,因为如果你有一个大小为1 GB的数据文件,你不能简单地将“文件的中间”返回到操作系统,以防它是空的,也没有文件系统操作能够支持。相反,PostgreSQL必须记住这个可用空间,并在以后重用它。
VACUUM 规则的例外
但是,该规则有一个例外。请考虑以下代码段:
test=# SELECT pg_relation_size('t_test');
pg_relation_size
------------------
8192
(1 row)
test=# DELETE FROM t_test;
DELETE 6
test=# SELECT pg_relation_size('t_test');
pg_relation_size
------------------
8192
(1 row)
复制
即使在 DELETE 语句之后,表仍将保留其大小。请记住:清理是异步完成的。因此,VACUUM 可以调用以删除这些行:
test=# VACUUM t_test;
VACUUM
复制
这是一个特例。规则是:如果从表中的某个位置开始,所有行都已死,VACUUM 则可以截断该表。这正是这个例子中发生的事情:
test=# SELECT pg_relation_size('t_test');
pg_relation_size
------------------
0
(1 row)
复制
毕竟,在大型表中,数据文件末尾总是有几行(在正常情况下)。因此,不要指望 VACUUM 则来缩小表。
VACUUM FULL vs. pg_squeeze
对抗表膨胀(这是用于描述表不成比例增长的技术术语)的一种方法是使用 VACUUM FULL。但是,问题在于 VACUUM FULL 需要一个表锁,如果您处于业务关键型环境中,这可能是一个真正的问题。因此,我们开发了一种无需大量锁定即可重新组织大型表的方法。
该解决方案称为pg_squeeze。从Github或我们的网站免费下载。
原文标题:VACUUM DOES NOT SHRINK MY POSTGRESQL TABLE
原文作者: Hans-Jürgen Schönig
原文地址:https://www.cybertec-postgresql.com/en/vacuum-does-not-shrink-my-postgresql-table/