In-page vacuum工作速度很快,但仅释放了一部分空间。它在一个表页内工作,并且不涉及索引。
常规的vacuum操作是使用VACUUM命令完成的。
因此,vaccum是对整个表操作。它不仅清除死元组,而且清除所有索引中对死元组的引用。
vacuum与系统中的其他活动同时进行。表和索引可以按常规方式用于读取和更新(但是,不可能同时执行诸如CREATE INDEX,ALTER TABLE等命令)。
仅是对那些发生过活动的表页执行vacuum,为了检测这些页,使用了*visibility map*(*visibility map*跟踪那些包含非常老的元组的页面,这些页面肯定在所有数据快照中都可见)。仅处理那些*visibility map*未跟踪的页面,并且更新*visibility map*本身。
*free space map*也会在此过程中进行更新,以反映页面中的额外可用空间。
和往常一样,让我们创建一个表:
=> CREATE TABLE vac(
id serial,
s char(100)
) WITH (autovacuum_enabled = off);
=> CREATE INDEX vac_s ON vac(s);
=> INSERT INTO vac(s) VALUES ('A');
=> UPDATE vac SET s = 'B';
=> UPDATE vac SET s = 'C';
我们使用autovacuum_enabled参数来关闭自动清理过程。下次我们将进行讨论,现在手动控制vacuum对于我们的实验至关重要。
该表现在具有三个元组,每个元组都在索引中都有引用:
=> SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid
-------+--------+----------+----------+-----+-----+--------
(0,1) | normal | 4000 (c) | 4001 (c) | | | (0,2)
(0,2) | normal | 4001 (c) | 4002 | | | (0,3)
(0,3) | normal | 4002 | 0 (a) | | | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid
------------+-------
1 | (0,1)
2 | (0,2)
3 | (0,3)
(3 rows)
在被vacuum之后,死元组被清除掉,只剩下一个活元组。索引中只剩下一个引用:
=> VACUUM vac;
=> SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid
-------+--------+----------+-------+-----+-----+--------
(0,1) | unused | | | | |
(0,2) | unused | | | | |
(0,3) | normal | 4002 (c) | 0 (a) | | | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid
------------+-------
1 | (0,3)
(1 row)
可以看到,前两个指针获得的状态是«unused»,而不是«dead»,它们将在in-page vacuum中获得这种状态。
PostgreSQL如何确定哪些元组可以视为是dead状态? 在讨论数据快照时,我们已经谈到了事务范围的概念,但是重申这一重要问题并没有什么坏处。
让我们再次开始上一个实验。
=> TRUNCATE vac;
=> INSERT INTO vac(s) VALUES ('A');
=> UPDATE vac SET s = 'B';
但是在再次更新行之前,让另一个事务开始(而不是结束)。
在本例中,它将使用Read Committed级别,但是它必须获得一个真实的(而不是虚拟的)事务号。例如,事务可以改变甚至锁定任何表中的某些行,而不是强制性在表vac:
| => BEGIN;
| => SELECT s FROM t FOR UPDATE;
| s
| -----
| FOO
| BAR
| (2 rows)
=> UPDATE vac SET s = 'C';
现在表中有三行,索引中有三个引用。vacuum后会发生什么?
=> VACUUM vac;
=> SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid
-------+--------+----------+----------+-----+-----+--------
(0,1) | unused | | | | |
(0,2) | normal | 4005 (c) | 4007 (c) | | | (0,3)
(0,3) | normal | 4007 (c) | 0 (a) | | | (0,3)
(3 rows)
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid
------------+-------
1 | (0,2)
2 | (0,3)
(2 rows)
表中还剩下两个元组:VACUUM决定(0,2)元组还不能被清理。原因肯定是在数据库的事务范围内,在此示例中,这是由未完成的事务确定的:
| => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
| backend_xmin
| --------------
| 4006
| (1 row)
我们可以让vacuum报告正在发生的事情:
=> VACUUM VERBOSE vac;
INFO: vacuuming "public.vac"
INFO: index "vac_s" now contains 2 row versions in 2 pages
DETAIL: 0 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "vac": found 0 removable, 2 nonremovable row versions in 1 out of 1 pages
DETAIL: 1 dead row versions cannot be removed yet, oldest xmin: 4006
There were 1 unused item pointers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM
·2个不可删除的行版本-在表中找到两个无法删除的元组。
·1个死行版本尚无法删除-其中一个已死。
·最旧的xmin显示当前范围。
让我们重申一下结论:如果数据库的事务持续时间很长(未完成或执行的时间很长),则无论vacuum发生的频率如何,都可能导致表膨胀。 因此,**OLTP型和OLAP型工作负载很难在一个PostgreSQL数据库中共存**:运行数小时的报表不会让更新后的表被适当清理。创建用于报表目的单独副本可能是解决此问题的方法。
在完成未事务后,范围移动,情况得到了解决:
| => COMMIT;
=> VACUUM VERBOSE vac;
INFO: vacuuming "public.vac"
INFO: scanned index "vac_s" to remove 1 row versions
DETAIL: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
INFO: "vac": removed 1 row versions in 1 pages
DETAIL: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
INFO: index "vac_s" now contains 1 row versions in 2 pages
DETAIL: 1 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "vac": found 1 removable, 1 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 4008
There were 1 unused item pointers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM
现在,页面中只剩下行最新的、实时的版本:
=> SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid
-------+--------+----------+-------+-----+-----+--------
(0,1) | unused | | | | |
(0,2) | unused | | | | |
(0,3) | normal | 4007 (c) | 0 (a) | | | (0,3)
(3 rows)
索引也只有一行:
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid
------------+-------
1 | (0,3)
(1 row)
内部细节
vacuum必须同时处理表和索引,以免锁定其他进程。怎么做呢?
所有步骤都从scanning heap阶段开始(将visible map考虑在内)。在读取的页面中,检测到死元组,将其tid写到专用数组中。数组存储在vacuum进程的本地内存中,在该进程中为其分配了maintenance_work_mem字节的内存。此参数的默认值为64MB,不是按需分配。但是,如果表不大,则会分配较少的内存。
然后,我们要么到达表的末尾,要么为数组分配的内存结束了。在任何一种情况下,vacuuming indexes阶段都会开始。为此,将对表上创建的每个索引进行完全扫描,以查找引用记住的元组的行。找到的行将从索引页清除。
在这里,我们面临以下问题:索引已经没有对死元组的引用,而表中仍然有它们。但是这并没啥问题:执行查询时,我们要么不命中死元组(具有索引访问权限),要么在可见性检查中不命中它们(扫描表时)。
此后,vacuuming heap阶段开始。再次扫描该表以读取适当的页面,将已记住的元组清除它们并释放指针。因为不再有索引引用,所以我们可以这样做。
如果在第一个周期中未完全读取该表,则会清空数组,并从到达的位置重复所有操作。
综上所述:
·表始终被扫描两次。 ·如果清理删除了太多的元组,以致它们都无法容纳在大小为maintenance_work_mem的内存中,则所有索引将根据需要进行多次扫描。
对于大表,这可能需要很多时间,并会增加相当大的系统负载。 当然,查询不会被锁定,但是有额外的输入/输出。
为了加快处理速度,可以更频繁地调用VACUUM(这样就不会每次清理掉太多的元组),或者分配更多的内存。
从版本11开始,PostgreSQL可以跳过索引扫描,除非迫切需要,否则不建议。
监控
我们如何确定VACUUM无法在一个周期内完成其工作?
我们已经看到了第一种方法:使用VERBOSE选项调用VACUUM命令。 在这种情况下,有关过程阶段的信息将输出到控制台。
其次,从9.6版开始,可以使用pg_stat_progress_vacuum视图,该视图还提供了所有必要的信息。
(第三种方法也是可用的:将信息输出到消息日志,但这仅适用于autovacuum,这将在下次讨论。)
让我们在表中插入很多行,以使vacuum过程持续很长时间,并更新所有这些行,以使VACUUM可以完成工作。
=> TRUNCATE vac;
=> INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000);
=> UPDATE vac SET s = 'B';
让我们减少分配给标识符数组的内存大小:
=> ALTER SYSTEM SET maintenance_work_mem = '1MB';
=> SELECT pg_reload_conf();
让我们开始VACUUM,当它工作时,让我们访问pg_stat_progress_vacuum视图几次:
=> VACUUM VERBOSE vac;
| => SELECT * FROM pg_stat_progress_vacuum \gx
| -[ RECORD 1 ]------+------------------
| pid | 6715
| datid | 41493
| datname | test
| relid | 57383
| phase | vacuuming indexes
| heap_blks_total | 16667
| heap_blks_scanned | 2908
| heap_blks_vacuumed | 0
| index_vacuum_count | 0
| max_dead_tuples | 174762
| num_dead_tuples | 174480
| => SELECT * FROM pg_stat_progress_vacuum \gx
| -[ RECORD 1 ]------+------------------
| pid | 6715
| datid | 41493
| datname | test
| relid | 57383
| phase | vacuuming indexes
| heap_blks_total | 16667
| heap_blks_scanned | 5816
| heap_blks_vacuumed | 2907
| index_vacuum_count | 1
| max_dead_tuples | 174762
| num_dead_tuples | 174480
我们可以看到:
·当前阶段的名称-我们讨论了三个主要阶段,但总体上有更多阶段。
·表页的总数(heap_blks_total)。
·扫描页数(heap_blks_scanned)。
·已清除的页面数(heap_blks_vacuumed)。
·index vacuum cycles数(index_vacuum_count)。
总体进度由heap_blks_vacuumed与heap_blks_total之比确定,但我们应考虑到此值由于扫描索引而以较大的增量而不是平滑的方式变化。 但是,更应该注意的是vacuum cycles的次数:数字大于1表示分配的内存不足以在一个循环中完成vacuum。
VACUUM VERBOSE命令的输出:
INFO: vacuuming "public.vac"
INFO: scanned index "vac_s" to remove 174480 row versions
DETAIL: CPU: user: 0.50 s, system: 0.07 s, elapsed: 1.36 s
INFO: "vac": removed 174480 row versions in 2908 pages
DETAIL: CPU: user: 0.02 s, system: 0.02 s, elapsed: 0.13 s
INFO: scanned index "vac_s" to remove 174480 row versions
DETAIL: CPU: user: 0.26 s, system: 0.07 s, elapsed: 0.81 s
INFO: "vac": removed 174480 row versions in 2908 pages
DETAIL: CPU: user: 0.01 s, system: 0.02 s, elapsed: 0.10 s
INFO: scanned index "vac_s" to remove 151040 row versions
DETAIL: CPU: user: 0.13 s, system: 0.04 s, elapsed: 0.47 s
INFO: "vac": removed 151040 row versions in 2518 pages
DETAIL: CPU: user: 0.01 s, system: 0.02 s, elapsed: 0.08 s
INFO: index "vac_s" now contains 500000 row versions in 17821 pages
DETAIL: 500000 index row versions were removed.
8778 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "vac": found 500000 removable, 500000 nonremovable row versions in 16667 out of 16667 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 4011
There were 0 unused item pointers.
0 pages are entirely empty.
CPU: user: 1.10 s, system: 0.37 s, elapsed: 3.71 s.
VACUUM
我们可以看到在索引上完成了三个循环,并且在每个循环中,vacuum了指向死元组的174480个指针。为什么是这个数字? 一个tid占用6个字节,而1024 * 1024/6 = 174762,这是我们在pg_stat_progress_vacuum.max_dead_tuples中看到的数字。 实际上,可能会使用更少:这确保了在读取下一页时,所有指向无效元组的指针肯定会容纳在内存中。
分析
analysis,或者换句话说,为查询计划器收集统计信息,在形式上与vacuum完全无关。但我们不仅可以使用analyze命令进行分析,还可以使用VACUUM ANALYZE将vacuum与analyze结合起来进行分析。
但正如我们稍后将看到的,autovacuum 和automatic analysis是在一个过程中完成的,并以类似的方式进行控制。
VACUUM FULL
vacuum比in-page vacuum释放更多的空间,但仍不能完全解决问题。
如果由于某种原因,表或索引的大小增加了很多,VACUUM将释放现有页面内的空间:页内将出现«holes»,之后可以用于插入新的元组。但是页数不会改变,因此,从操作系统的角度来看,文件将占用与清理之前完全相同的空间。这是不好的,因为:
·对表(或索引)的完全扫描速度变慢。 ·可能需要更大的缓冲区高速缓存(因为页面存储在其中,有用信息的密度降低了)。 ·在索引树中,索引深度加深,这将减慢索引访问。 ·这些文件在磁盘和备份副本中会占用额外的空间。
(唯一的例外是位于文件末尾的完全清除的页面。这些页面已从文件中裁剪并返回给操作系统)
如果文件中有用信息的比例低于某个合理的限制,则管理员可以对表进行VACUUM FULL。在这种情况下,该表及其所有索引都是从头开始重建的,并且数据以最紧凑的方式打包(当然,考虑了fillfactor参数)。在重建过程中,PostgreSQL首先重建表,然后重建每个索引。对于每个对象,将创建新文件,并在重建结束时删除旧文件。我们应该考虑到在此过程中将需要额外的磁盘空间。
为了说明这一点,让我们再次在表中插入一定数量的行:
=> TRUNCATE vac;
=> INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000);
我们如何估计数据的密度呢? 为此,使用专门的扩展很方便:
=> CREATE EXTENSION pgstattuple;
=> SELECT * FROM pgstattuple('vac') \gx
-[ RECORD 1 ]------+---------
table_len | 68272128
tuple_count | 500000
tuple_len | 64500000
tuple_percent | 94.47
dead_tuple_count | 0
dead_tuple_len | 0
dead_tuple_percent | 0
free_space | 38776
free_percent | 0.06
该函数读取整个表并显示统计信息:哪些数据占据了文件中的空间。 现在,我们感兴趣的主要信息是tuple_percent字段:有用数据的百分比。 由于页面内不可避免的信息开销,它小于100,但仍然很高。
对于索引,将输出不同的信息,但是avg_leaf_density字段具有相同的含义:有用信息的百分比(在叶子页面中)。
=> SELECT * FROM pgstatindex('vac_s') \gx
-[ RECORD 1 ]------+---------
version | 3
tree_level | 3
index_size | 72802304
root_block_no | 2722
internal_pages | 241
leaf_pages | 8645
empty_pages | 0
deleted_pages | 0
avg_leaf_density | 83.77
leaf_fragmentation | 64.25
再看看表和索引大小:
=> SELECT pg_size_pretty(pg_table_size('vac')) table_size,
pg_size_pretty(pg_indexes_size('vac')) index_size;
table_size | index_size
------------+------------
65 MB | 69 MB
(1 row)
现在我们删除90%的行。我们随机选择要删除的行,这样至少有一行很可能会保留在每个页面中:
=> DELETE FROM vac WHERE random() < 0.9;
DELETE 450189
vacuum后表的大小是多少?
=> VACUUM vac;
=> SELECT pg_size_pretty(pg_table_size('vac')) table_size,
pg_size_pretty(pg_indexes_size('vac')) index_size;
table_size | index_size
------------+------------
65 MB | 69 MB
(1 row)
我们可以看到,大小没有改变:vacuum没有办法缩小文件的大小。尽管信息密度下降了大约10倍:
=> SELECT vac.tuple_percent, vac_s.avg_leaf_density
FROM pgstattuple('vac') vac, pgstatindex('vac_s') vac_s;
tuple_percent | avg_leaf_density
---------------+------------------
9.41 | 9.73
(1 row)
现在让我们看看vacuum full后我们得到了什么。现在表和索引使用以下文件:
=> SELECT pg_relation_filepath('vac'), pg_relation_filepath('vac_s');
pg_relation_filepath | pg_relation_filepath
----------------------+----------------------
base/41493/57392 | base/41493/57393
(1 row)
=> VACUUM FULL vac;
=> SELECT pg_relation_filepath('vac'), pg_relation_filepath('vac_s');
pg_relation_filepath | pg_relation_filepath
----------------------+----------------------
base/41493/57404 | base/41493/57407
(1 row)
这些文件现在被替换为新文件。表和索引的大小明显减小,信息密度相应增大:
=> SELECT pg_size_pretty(pg_table_size('vac')) table_size,
pg_size_pretty(pg_indexes_size('vac')) index_size;
table_size | index_size
------------+------------
6648 kB | 6480 kB
(1 row)
=> SELECT vac.tuple_percent, vac_s.avg_leaf_density
FROM pgstattuple('vac') vac, pgstatindex('vac_s') vac_s;
tuple_percent | avg_leaf_density
---------------+------------------
94.39 | 91.08
(1 row)
请注意,索引中的信息密度甚至大于原始信息。从可用数据重建索引(B树)比将数据逐行插入现有索引中更为有利。
我们使用的pgstattuple扩展功能读取了整个表。但是,如果表很大,这将很不方便,因此扩展具有pgstattuple_approx函数,该函数会跳过可见性图中标记的页面并显示近似数字。
另一种方法,但准确性更低,是使用系统目录粗略估计数据大小与文件大小的比率。你可以在Wiki中找到此类查询的示例。
VACUUM FULL不能用于常规用途,因为它在整个过程中都禁止使用表进行任何工作(包括查询)。显然,对于负载很高的系统,这似乎是不可接受的。锁将单独讨论,现在我们仅提及pg_repack扩展,该扩展在工作结束时仅将表锁定一小段时间。
类似的命令
有一些命令也可以完全重建表和索引,因此类似于VACUUM FULL。它们全部完全阻止了该表的任何工作,它们都删除了旧数据文件并创建了新文件。
CLUSTER命令与VACUUM FULL完全相似,但它实际上还会根据可用索引对元组进行排序。这使计划器在某些情况下可以更有效地使用索引访问。但是我们应该记住,不能clustering是不被维护的:元组的物理顺序将随着表的后续更改而中断。
REINDEX命令在表上重建一个单独的索引。VACUUM FULL和CLUSTER实际上使用此命令来重建索引。
TRUNCATE命令的逻辑类似于DELETE的逻辑—它删除所有表行。但是,正如已经提到的,DELETE只将元组标记为已删除,这需要进一步清理。而TRUNCATE只是创建一个新的干净文件。通常,这会更快地工作,但是我们应该注意,TRUNCATE会阻塞对表的任何工作,直到事务结束。
原文地址: