DWS在线VACUUM FULL(VACUUM FULL ONLINE)最佳实践
在DWS数据库的日常运行中,频繁的UPDATE和DELETE操作会在表中产生大量已被标记为删除但未被物理回收的行(即dead tuple,又称脏页)。这些dead tuple持续占用磁盘空间,导致表膨胀、查询性能下降。数据库管理员通常需要定期执行VACUUM FULL来回收这些空间并提升查询效率。
然而,传统的VACUUM FULL操作在执行期间会持有8级排他锁(AccessExclusiveLock),阻塞对该表的所有并发读写操作。当数据量较大时,阻塞时长可达数小时甚至数天,严重影响业务的连续性。
那么,如何在回收表空间的同时不影响业务正常运行?DWS从9.1.1.300版本起支持在线VACUUM FULL(对应语法VACUUM FULL ONLINE,以下简称为OVF)。该功能通过"临时表+增量追增+relfilenode交换"的方式,在执行过程中不会长时间持有排他锁,允许对表进行并发读写操作,从而在回收存储空间的同时保障业务的连续性和数据库的可用性。
本文将以表级和分区级两个场景为例,演示在线VACUUM FULL的完整操作流程及其并发读写验证方法。此外,还将演示在线VACUUM FULL通过lock cancel机制解除长SELECT事务阻塞的特性。
在线VACUUM FULL工作原理
在线VACUUM FULL的执行过程分为以下几个阶段,每个阶段由"短暂高锁DDL事务 + 低锁数据事务"交替组成:
- 创建临时表(L1):获取原表7级锁(ExclusiveLock),在同一个事务中完成创建临时表、设置原表 append_mode=on、创建delta表等DDL操作,然后提交事务释放锁。7级锁阻塞DML写入但不阻塞SELECT。此阶段持续时间很短。
- 基线复制:在新事务中对原表获取1级锁(AccessShareLock),执行 INSERT INTO 临时表 SELECT FROM 原表,将原表现有数据复制到临时表。1级锁等价于SELECT的锁级别,完全不阻塞业务。复制完成后提交事务释放锁。
- 增量追增(catchup):通过多轮catchup,将基线复制期间产生的增量数据(新INSERT/UPDATE/DELETE的delta记录)从原表同步到临时表。每轮catchup分两步:
- DDL锁阶段:获取原表7级锁(ExclusiveLock),刷新 append_mode=on(会同步刷新start/end_ctid)、切换delta表等DDL操作,提交事务释放锁。此阶段短暂阻塞DML写入,但不阻塞SELECT。
- 数据追增阶段:在新事务中对原表获取1级锁(AccessShareLock),读取delta数据并写入临时表,提交事务释放锁。此阶段完全不阻塞业务。
- 随着增量数据逐渐减少,catchup轮次耗时递减。
- 最终catchup与表文件切换(L3):当catchup数据量足够小时,进入最终轮。此阶段获取7级锁后不再释放,直接延续执行:读取最终delta数据 → LockTableAndKillSelect 升级为8级锁(AccessExclusiveLock) → 交换原表和临时表的relfilenode → 关闭 append_mode → 删除辅助SCHEMA → 提交事务释放锁。如果此时有长SELECT事务阻塞VACUUM FULL ONLINE,会通过lock cancel机制主动终止该SELECT。此阶段是持续时间最长的高锁阶段,但通常仍占总时间5%以下。
| 阶段 | 锁级别 | 阻塞业务 | 说明 |
|---|---|---|---|
| L1 创建临时表 | 7级 ExclusiveLock | 阻塞DML,不阻塞SELECT | DDL批次:建临时表+设置append_mode=on+建delta表 |
| 基线复制 | 1级 AccessShareLock | 不阻塞 | INSERT INTO tmp SELECT FROM 原表 |
| L2_lock 每轮DDL | 7级 ExclusiveLock | 阻塞DML,不阻塞SELECT | DDL:append_mode=on+切换delta表 |
| L2_data 每轮追增 | 1级 AccessShareLock | 不阻塞 | 读delta数据写临时表 |
| L3 最终catchup+切换 | 7级→8级 | 阻塞一切(8级时) | 不释放锁直接延续到结束 |
总高锁时间 = L1 + Σ(L2_lock各轮) + L3,不含基线复制和L2_data数据追增阶段。
VACUUM、VACUUM FULL与在线VACUUM FULL的对比
DWS的VACUUM命令主要用于回收表或B-Tree索引中已删除行所占用的存储空间。在一般数据库操作中,已被DELETE的行并没有从表中物理删除,在执行VACUUM之前它们仍然存在。因此,有必要周期性地运行VACUUM,特别是在频繁更新的表上。
VACUUM有三种形式:VACUUM(轻量清理)、VACUUM FULL(重量级清理,锁表)和VACUUM FULL ONLINE(在线重量级清理,不锁表)。三者区别如下表所示。
| 比较维度 | VACUUM | VACUUM FULL | VACUUM FULL ONLINE |
|---|---|---|---|
| 空间清理 | 如果删除记录位于表的末端且满足截断阈值(不小于1000页或表大小的1/16),其所占用的空间将被物理释放并归还操作系统;否则将dead tuple所占用的空间标记为可用状态,供后续插入复用。 | 不论被清理的数据处于表中何处,其占用的空间都将被物理释放并归还操作系统。 | 同VACUUM FULL,物理释放所有空间。 |
| 锁类型 | 4级锁(ShareUpdateExclusiveLock),可以与其他操作并行。 | 8级锁(AccessExclusiveLock),执行期间锁表,所有操作被阻塞。 | 数据阶段1级锁(AccessShareLock)不阻塞业务;DDL阶段短暂7级锁(ExclusiveLock)阻塞DML不阻塞SELECT;最终切换阶段短暂8级锁(AccessExclusiveLock)。 |
| 物理空间 | 一般不释放。仅在表末端有大量连续空页面时可截断释放;中间碎片不释放。 | 释放。 | 释放。 |
| 事务ID(XID) | 不回收。 | 回收。 | 回收。 |
| 执行开销 | 开销较小,可以定期执行。 | 开销巨大,锁表期间业务全部中断。 | 开销较大,但大部分阶段不阻塞业务,高锁时间通常占总时间5%以下。 |
| 执行效果 | 性能有所提升。 | 操作效率大幅提升。 | 同VACUUM FULL,操作效率大幅提升。 |
基本概念
- V2表:即V2列存表,指建表时CREATE TABLE语法中colversion取值为2.0的表,表示列存表的每列合并存储在一个文件中,文件名以relfilenode.C1.0命名,数据存储在本地盘。存算一体场景下,不指定colversion取值时,用户创建的列存表默认为V2表。
- V3表:即V3列存表,指建表时CREATE TABLE语法中colversion取值为3.0的表,即存算分离表,表示列存表的每列合并存储在一个文件中,文件名以C1_field.0命名,数据存储在OBS文件系统。存算分离场景下,不指定colversion取值时,用户创建的列存表默认为V3表。
约束限制
- 仅9.1.1.300及以上集群版本中支持。
- 各版本支持的表类型:
表3 支持表类型 集群版本
支持的表类型
不支持的表类型
9.1.1.300
列存V2表、行存表。
临时表、系统表、外表、复制表、物化视图、LSM、binlog、冷热表、大宽表、行列混存表。
9.1.2及以上
行存表、V2列存表(含hstore/hstore_opt)、V3列存表(含hstore/hstore_opt)、LSM、行列混存表。
临时表、系统表、外表、复制表、物化视图、binlog、冷热表、大宽表。
- 在线VACUUM FULL与细粒度备份恢复同时执行时。恢复后可能残留VACUUM FULL ONLINE的辅助SCHEMA和append_mode选项。9.1.1.300版本需手动执行清理命令,9.1.2及以上版本已在备份恢复流程中集成了自动清理操作。
- 在线VACUUM FULL操作执行失败可能会导致数据残留。需手动执行以下命令并删除残留的辅助SCHEMA(以data_redis_ovf_<table_oid>命名)。
1ALTER TABLE [ table_name ] SET (append_mode=off);
- 处于重分布状态和扩缩容状态的集群,不能执行在线VACUUM FULL。
- 2.0集群升级3.0集群期间不能执行在线VACUUM FULL。
- 以上仅列出关键约束点,其他更多约束请参见VACUUM章节。
前提条件
已创建9.1.1.300及以上版本的DWS集群。
在线VACUUM FULL使用最佳实践
以下分别以表级和分区级的在线VACUUM FULL为例,演示在线VACUUM FULL执行过程中可对表进行并发读写的操作,验证存储空间清理的同时保障业务的连续性和数据可用性。此外,还将演示在线VACUUM FULL通过lock cancel机制解除长SELECT事务阻塞的特性。
- 使用客户端连接DWS数据库,例如默认gaussdb数据库,详情请参见连接DWS集群章节。
- 测试表级在线VACUUM FULL。 本步骤演示对整表执行在线VACUUM FULL,同时在另一会话中执行并发IUD(即插入Insert、更新Update、删除Delete,以下简称IUD)操作,验证清理过程中业务读写不受影响。需开启两个SQL会话窗口,以下分别称作SQL会话1和SQL会话2。
- 在SQL会话1中创建测试表并写入数据,模拟业务表中存在大量脏页的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
-- 删除已存在的同名表,CASCADE表示级联删除依赖该表的对象(如索引、视图) DROP TABLE IF EXISTS t_ovf_full_v2opt CASCADE; -- 创建测试表 CREATE TABLE t_ovf_full_v2opt (a INT, b TEXT) WITH (ORIENTATION = column, enable_hstore = true, enable_hstore_opt = true, --开启hstore和hstore_opt特性 colversion = 2.0) --表示V2列存表 DISTRIBUTE BY ROUNDROBIN; --表示数据按轮询方式分布到各DN节点 -- 初始插入1000行数据:generate_series(1,1000)生成1到1000的序列 INSERT INTO t_ovf_full_v2opt SELECT g, repeat('x', 100) || g FROM generate_series(1, 1000) g; -- repeat('x',100)将'x'重复100次作为字段b的一部分 -- 通过DO块循环5次将数据翻倍:每次将全表数据再插入一遍,数据量从1000→2000→4000→8000→16000→32000 -- 目的是制造大量数据,使后续DELETE产生的脏页更有代表性 DO $$ DECLARE rounds INT := 5; BEGIN FOR i IN 1..rounds LOOP INSERT INTO t_ovf_full_v2opt SELECT a, b FROM t_ovf_full_v2opt; END LOOP; END $$; -- 删除a不能被50整除的所有行(即保留a=50,100,150,...的数据) -- 这些被删除的行成为脏页(dead tuple),占用磁盘空间但不可见,模拟真实业务中的DELETE操作 DELETE FROM t_ovf_full_v2opt WHERE a % 50 != 0;
- 在SQL会话1中查看清理前的磁盘占用和数据量:记录执行在线VACUUM FULL前的表空间大小和行数,作为对比基准。
1 2
-- 查看清理前的表磁盘占用大小(pg_size_pretty将字节数转为人类可读格式,如KB、MB、GB) SELECT pg_size_pretty(pg_relation_size('t_ovf_full_v2opt')) AS size_before;

1 2
-- 查看清理前的表行数 SELECT count(*) AS rows_before FROM t_ovf_full_v2opt;

- 并发执行IUD操作:在SQL会话2中执行IUD语句模拟用户业务场景。注意需在IUD语句运行结束前执行2.d的在线VACUUM FULL,以验证并发场景下的清理效果。
IUD语句执行逻辑:循环150次,每次执行插入+更新,中间暂停0.1秒,延长执行时间以确保与2.d的VACUUM FULL ONLINE并发执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
-- 在SQL会话2中执行以下语句: -- 开启PL/pgSQL事务控制支持(使存储过程中可以使用COMMIT语句) SET behavior_compat_options = 'enable_pl_with_txnctl'; -- 创建模拟并发IUD的存储过程 -- 每轮循环执行INSERT、UPDATE、DELETE三种操作,COMMIT一次确保每轮IUD是独立事务 CREATE OR REPLACE PROCEDURE concurrent_iud_test() AS DECLARE v_start timestamp; v_round_start timestamp; BEGIN v_start := clock_timestamp(); FOR i IN 1..150 LOOP v_round_start := clock_timestamp(); -- 插入:向表中插入新数据(a值从90001到90150) INSERT INTO t_ovf_full_v2opt VALUES (90000 + i, 'concurrent_insert_' || i); -- 更新:更新a值为50,100,150,...,500的行 UPDATE t_ovf_full_v2opt SET b = 'concurrent_update_' || i WHERE a = 50 + (i % 10) * 50; -- 删除:删除原始数据中a值为550,600,...,1000的行(与UPDATE目标不冲突) IF i > 1 THEN DELETE FROM t_ovf_full_v2opt WHERE a = 550 + (i % 10) * 50; END IF; -- 暂停0.1秒,延长执行时间确保与步骤4的VACUUM FULL ONLINE并发 PERFORM pg_sleep(0.1); -- 提交当前事务,使每轮IUD成为独立事务 COMMIT; -- 打印本轮耗时和累计耗时 RAISE NOTICE 'Round %/150: round_time=%s, total_time=%s', i, clock_timestamp() - v_round_start, clock_timestamp() - v_start; END LOOP; END; / -- 执行存储过程(在执行在线VACUUM FULL之前启动) CALL concurrent_iud_test();
- 执行在线VACUUM FULL:在SQL会话1中执行表级在线VACUUM FULL。此操作在会话2的IUD操作运行期间执行,验证清理过程中业务读写不受阻塞。
1VACUUM FULL t_ovf_full_v2opt ONLINE;

结果显示,两边语句都在并行,没有上锁,且两边语句先后执行成功。
- 验证执行结果:在会话1中查看清理后的表空间大小和行数,确认空间已被回收且并发写入的数据完整保留。
1 2
-- 1. 查看清理后的表磁盘占用大小,与size_before对比确认空间是否回收 SELECT pg_size_pretty(pg_relation_size('t_ovf_full_v2opt')) AS size_after;

结果显示,size_after明显小于size_before,说明表空间已被有效回收。
1 2
-- 2. 查看清理后的表行数 SELECT count(*) AS rows_after FROM t_ovf_full_v2opt;

结果显示, 执行存储过程后,删除了320行,同时插入150行,最终得到470行,说明在线VACUUM FULL期间DML业务正常。
1 2
-- 3. 统计会话2并发插入的数据条数(a值在90001~90150之间),预期为150条 SELECT count(*) AS concurrent_insert_count FROM t_ovf_full_v2opt WHERE a >= 90001 AND a <= 90150;

结果显示,concurrent_insert_count等于150,说明会话2并发插入的数据全部保留。
1 2
-- 4. 查询被会话2并发更新的行,验证更新内容是否生效(a=50,100,150,...,500) SELECT DISTINCT a, b FROM t_ovf_full_v2opt WHERE a IN (50, 100, 150, 200, 250, 300, 350, 400, 450, 500) ORDER BY a;

查询结果显示会话2并发更新的数据,说明并发更新操作正常完成,在线VACUUM FULL不影响并发业务。
- 会话1:清理环境。
1 2
DROP TABLE IF EXISTS t_ovf_full_v2opt CASCADE; DROP PROCEDURE concurrent_iud_test;
- 在SQL会话1中创建测试表并写入数据,模拟业务表中存在大量脏页的场景。
- 测试分区级在线VACUUM FULL。 本步骤演示对指定分区执行在线VACUUM FULL,同时在另一会话中对目标分区执行并发IUD操作,验证分区级清理的同时目标分区仍可正常读写。
- 在SQL会话1中创建一个辅助函数,用于查看各分区的行数和磁盘占用大小,便于后续验证清理效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
CREATE OR REPLACE FUNCTION count_and_size_all_partitions ( p_table_name text ) RETURNS TABLE( partition_name text, row_count bigint, disk_size_pretty text ) LANGUAGE plpgsql AS $$ DECLARE v_rec record; v_sql text; v_parent_name text; BEGIN v_parent_name := format('%I', p_table_name); FOR v_rec IN SELECT relname AS p_name, parentid AS p_parent_oid, oid AS p_oid FROM pg_partition WHERE parentid = p_table_name::regclass AND parttype = 'p' ORDER BY oid LOOP v_sql := format('SELECT count(*) FROM %I PARTITION (%I)', p_table_name, v_rec.p_name); EXECUTE v_sql INTO row_count; SELECT pg_size_pretty(pg_partition_size(v_parent_name, format('%I', v_rec.p_name))) INTO disk_size_pretty; partition_name := v_rec.p_name; RETURN NEXT; END LOOP; RETURN; END; $$;
- 数据准备:在会话1中创建分区测试表并写入数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
-- 删除已存在的同名表 DROP TABLE IF EXISTS t_ovf_multipar_v2opt CASCADE; -- 创建分区测试表:列存V2表,开启hstore和hstore_opt CREATE TABLE t_ovf_multipar_v2opt (a INT, b TEXT) WITH (ORIENTATION = column, enable_hstore = true, enable_hstore_opt = true, colversion = 2.0) DISTRIBUTE BY ROUNDROBIN -- 按字段a的范围进行分区,共4个分区: -- p1: a∈[0,100), p2: a∈[100,400), p3: a∈[400,700), p4: a∈[700,+∞) PARTITION BY RANGE (a) ( PARTITION p1 START (0) END (100), PARTITION p2 START (100) END (400), PARTITION p3 START (400) END (700), PARTITION p4 START (700) END (MAXVALUE) ); -- 初始插入700行数据,数据分布在p1~p4各分区中 INSERT INTO t_ovf_multipar_v2opt SELECT g, repeat('x', 100) || g FROM generate_series(1, 700) g; -- 循环5次将数据翻倍,制造大量数据 DO $$ DECLARE rounds INT := 5; BEGIN FOR i IN 1..rounds LOOP INSERT INTO t_ovf_multipar_v2opt SELECT a, b FROM t_ovf_multipar_v2opt; END LOOP; END $$; -- 删除a不能被50整除的行,制造脏页 DELETE FROM t_ovf_multipar_v2opt WHERE a % 50 != 0;
- 在会话1中查看清理前的磁盘占用和数据量。
1SELECT * FROM count_and_size_all_partitions('t_ovf_multipar_v2opt');

- 并发执行IUD操作:在会话2中对p2和p3分区执行并发IUD操作。注意,需在该IUD语句运行结束前执行3.e。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
-- 在会话2中执行: -- 开启PL/pgSQL事务控制支持 SET behavior_compat_options = 'enable_pl_with_txnctl'; -- 创建分区级并发IUD存储过程 CREATE OR REPLACE PROCEDURE concurrent_iud_partition_test() AS DECLARE v_start timestamp; v_round_start timestamp; BEGIN v_start := clock_timestamp(); FOR i IN 1..150 LOOP v_round_start := clock_timestamp(); -- 插入:向p2分区插入数据(a值201~350,落在p2分区范围[100,400)内) INSERT INTO t_ovf_multipar_v2opt VALUES (200 + i, 'concurrent_insert_p2_' || i); -- 插入:向p3分区插入数据(a值401~550,落在p3分区范围[400,700)内) INSERT INTO t_ovf_multipar_v2opt VALUES (400 + i, 'concurrent_insert_p3_' || i); -- 更新:更新p2分区中的行 UPDATE t_ovf_multipar_v2opt SET b = 'concurrent_update_p2_' || i WHERE a = 100 + (i % 3) * 50; -- 更新:更新p3分区中的行(a=600,650轮转,均为原始数据中存在的行) UPDATE t_ovf_multipar_v2opt SET b = 'concurrent_update_p3_' || i WHERE a = 600 + (i % 2) * 50; -- 删除:删除p2分区中未被UPDATE的原始行(a=250,300,350轮转) -- 附加条件b NOT LIKE确保不误删并发INSERT的行 IF i > 1 THEN DELETE FROM t_ovf_multipar_v2opt WHERE a = 250 + (i % 3) * 50 AND b NOT LIKE 'concurrent_insert_%'; END IF; -- 删除:删除p3分区中未被UPDATE的原始行(a=400,450,500,550轮转) -- 附加条件b NOT LIKE确保不误删并发INSERT的行 IF i > 1 THEN DELETE FROM t_ovf_multipar_v2opt WHERE a = 400 + (i % 4) * 50 AND b NOT LIKE 'concurrent_insert_%'; END IF; -- 暂停0.1秒,确保与步骤5的VACUUM FULL ONLINE并发 PERFORM pg_sleep(0.1); -- 提交当前事务 COMMIT; -- 打印本轮耗时和累计耗时 RAISE NOTICE 'Round %/150: round_time=%s, total_time=%s', i, clock_timestamp() - v_round_start, clock_timestamp() - v_start; END LOOP; END; / -- 执行存储过程 CALL concurrent_iud_partition_test();
- 执行分区级在线VACUUM FULL:在会话1中对p2和p3分区执行在线VACUUM FULL,验证分区级清理的并发能力。
1VACUUM FULL t_ovf_multipar_v2opt PARTITION (p2, p3) ONLINE;

- 在会话1中验证执行结果:查看清理后各分区的空间大小和行数,确认目标分区空间已回收且并发写入数据完整保留。
1 2
-- 1. 查看清理后各分区的行数和磁盘大小,与清理前对比确认p2、p3分区空间是否回收 SELECT * FROM count_and_size_all_partitions('t_ovf_multipar_v2opt');

结果显示,p2和p3分区的磁盘大小明显小于清理前,说明目标分区空间已被回收。
1 2 3 4
-- 2. 统计p2分区中会话2并发插入的数据条数,预期为150条 SELECT count(*) AS concurrent_insert_p2 FROM t_ovf_multipar_v2opt PARTITION (p2) WHERE b LIKE 'concurrent_insert_p2_%'; -- 3. 统计p3分区中会话2并发插入的数据条数,预期为150条 SELECT count(*) AS concurrent_insert_p3 FROM t_ovf_multipar_v2opt PARTITION (p3) WHERE b LIKE 'concurrent_insert_p3_%';

结果显示,concurrent_insert_p2和concurrent_insert_p3应均等于150,说明并发插入的数据全部保留。
1 2 3 4
-- 4. 查询p2分区中被会话2并发更新的行,验证更新内容是否生效 SELECT DISTINCT a, b FROM t_ovf_multipar_v2opt PARTITION (p2) WHERE a IN (150, 200, 250) ORDER BY a; -- 5. 查询p3分区中被会话2并发更新的行,验证更新内容是否生效 SELECT DISTINCT a, b FROM t_ovf_multipar_v2opt PARTITION (p3) WHERE a IN (450, 500, 550, 600, 650) ORDER BY a;

结果显示了p2、p3分区并发更新的数据,说明并发更新操作正常完成。
- 会话1:清理环境。
1 2
DROP TABLE IF EXISTS t_ovf_multipar_v2opt CASCADE; DROP PROCEDURE concurrent_iud_partition_test;
- 在SQL会话1中创建一个辅助函数,用于查看各分区的行数和磁盘占用大小,便于后续验证清理效果。
- 测试在线VACUUM FULL的lock cancel机制。 本步骤演示在线VACUUM FULL在存在未提交的长SELECT事务时,通过lock cancel机制解除阻塞的特性。注意: 在线VACUUM FULL在基线复制和增量追增的数据读取阶段对原表只持有1级锁(AccessShareLock),不阻塞并发SELECT和DML。每轮catchup的DDL阶段短暂持有7级锁(ExclusiveLock),阻塞DML写入但不阻塞SELECT。最终进入表文件切换阶段(L3)时,升级为8级锁(AccessExclusiveLock)并一直持有到操作结束。如果此时有长SELECT事务持有共享锁阻塞OVF,OVF会通过lock cancel机制主动终止该SELECT事务,而非像传统VACUUM FULL那样一直等待。被终止的SELECT会收到错误信息:"query canceled by thread ... because ddl_select_concurrent_mode has been set"。
- 环境准备:在会话1中创建测试表并写入数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
-- 删除已存在的同名表 DROP TABLE IF EXISTS t_ovf_lockcancel_v2opt CASCADE; -- 创建测试表:列存V2表,开启hstore和hstore_opt CREATE TABLE t_ovf_lockcancel_v2opt (a INT, b TEXT) WITH (ORIENTATION = column, enable_hstore = true, enable_hstore_opt = true, colversion = 2.0) DISTRIBUTE BY ROUNDROBIN; -- 初始插入1000行数据 INSERT INTO t_ovf_lockcancel_v2opt SELECT g, repeat('x', 100) || g FROM generate_series(1, 1000) g; -- 循环5次将数据翻倍,制造大量数据 DO $$ DECLARE rounds INT := 5; BEGIN FOR i IN 1..rounds LOOP INSERT INTO t_ovf_lockcancel_v2opt SELECT a, b FROM t_ovf_lockcancel_v2opt; END LOOP; END $$; -- 删除a不能被50整除的行,制造脏页 DELETE FROM t_ovf_lockcancel_v2opt WHERE a % 50 != 0;
- 在会话1中查看清理前的磁盘占用:记录清理前的表空间大小。
1SELECT pg_size_pretty(pg_relation_size('t_ovf_lockcancel_v2opt')) AS size_before;

- 在会话2中开启长SELECT事务:在会话2中开启一个事务并执行一条SELECT语句,但不提交该事务,模拟存在未提交长事务的场景。
1 2 3 4 5 6
-- 在会话2中执行:开启一个显式事务 BEGIN; -- 执行一条SELECT查询,但不提交事务(不执行COMMIT) -- 该未提交事务持有共享锁,模拟存在长事务的场景 -- 若使用传统VACUUM FULL,此处会因等待该事务释放锁而阻塞 SELECT * FROM t_ovf_lockcancel_v2opt LIMIT 10;
- 执行在线VACUUM FULL:在会话1中执行在线VACUUM FULL。若使用传统VACUUM FULL,此处会被会话2的未提交事务阻塞,但在线VACUUM FULL在表文件切换阶段会通过lock cancel机制终止会话2的SELECT事务,从而正常执行。
1VACUUM FULL t_ovf_lockcancel_v2opt ONLINE;

- 提交长SELECT事务:在会话2中提交4.c中开启的事务,返回如下错误信息:
1COMMIT;

这是OVF的lock cancel机制在表文件切换阶段(L3)主动终止了阻塞的SELECT查询,属于预期行为。会话2的事务因错误自动回滚,无需手动执行COMMIT。
生产环境执行OVF前,应评估是否有重要长查询在运行。OVF的lock cancel机制会终止阻塞表文件切换的SELECT查询,被终止的查询需要重新执行。如果业务中有不可中断的长查询,建议在执行OVF前先确认无关键长查询在运行。
- 会话1中验证执行结果:查看清理后的表空间大小,确认空间已成功回收。
1SELECT pg_size_pretty(pg_relation_size('t_ovf_lockcancel_v2opt')) AS size_after;

- 会话1:清理环境。
1DROP TABLE IF EXISTS t_ovf_lockcancel_v2opt CASCADE;
- 环境准备:在会话1中创建测试表并写入数据。
- 清理测试辅助函数。
1DROP FUNCTION count_and_size_all_partitions(text);
失败恢复
在线VACUUM FULL操作执行失败可能会导致数据残留,包括辅助SCHEMA和表的append_mode设置。DWS提供了系统函数帮助扫描和清理残留数据。
- 扫描残留表:使用gs_scan_for_online_vacuum_full函数扫描所有存在残留辅助SCHEMA的表,该函数返回存在残留辅助SCHEMA的表的schema名、表名、完整名称、CN上的OID和辅助SCHEMA名。
1SELECT * FROM gs_scan_for_online_vacuum_full();
- 手动清理残留数据:使用gs_cleanup_for_online_vacuum_full函数清理残留的辅助SCHEMA和append_mode设置。该函数会自动完成以下操作:获取OVF咨询锁(防止与正在运行的OVF冲突)、关闭append_mode、删除辅助SCHEMA中的残留对象、删除辅助SCHEMA。执行完成后输出清理统计信息。
1 2 3 4 5
-- 清理单个表 SELECT gs_cleanup_for_online_vacuum_full('table_name'); -- 清理多个表 SELECT gs_cleanup_for_online_vacuum_full(ARRAY['table1'::text, 'table2'::text]);
- 手动清理(备选方案):如果不使用系统函数,也可以手动执行清理命令(获取oid需要连接CCN,即Central Coordinator Node,集群的中央协调节点)。
1 2 3 4 5 6
-- 1. 查询表的OID SELECT oid FROM pg_class WHERE relname = 'table_name'; -- 2. 关闭append_mode ALTER TABLE table_name SET (append_mode=off); -- 3. 删除辅助SCHEMA(名称为data_redis_ovf_<table_oid>) DROP SCHEMA IF EXISTS data_redis_ovf_<table_oid> CASCADE;
性能建议
在实际生产环境中使用在线VACUUM FULL时,建议关注以下性能要点:
- 高锁时间的影响因素:OVF的高锁时间由L1(创建临时表)、每轮L2_lock(DDL锁阶段)和L3(最终切换)组成,各阶段影响因素不同:
- L1与分区数量相关:创建临时表阶段需为每个分区执行建表+delta表等DDL,分区数量越多锁时间越长。单分区表L1通常不到1秒,多分区表可能显著增加。分区级OVF只创建指定目标分区的临时表,可以减少建表阶段的阻塞时间。
- L2_lock与数据量弱相关:每轮catchup的DDL锁阶段主要是append_mode=on和切换delta表等DDL操作,与数据量无关,单轮通常不到1秒。
- L3与并发更新频率相关:最终切换阶段不释放锁直接延续执行final catchup,需读取delta表中的增量数据。如果OVF执行期间并发IUD频率高,delta表积累的增量数据多,final catchup耗时就长,L3高锁时间相应增加。
总体而言,高锁时间通常占总时间的5%以下,基线复制和数据追增阶段只持有1级锁,不阻塞业务。
- 分区级OVF:对指定分区执行OVF时,仅清理目标分区,不影响其他分区。相比全表OVF,分区级OVF只创建目标分区的临时表、只复制目标分区数据、只重建目标分区索引,能够显著减少总执行时间和高锁时间。建议在分区表膨胀不均匀时,优先对膨胀严重的分区单独执行OVF,而非全表执行。
- 执行时机:建议在业务低峰期执行OVF,虽然OVF不长时间阻塞业务,但基线复制和索引重建会消耗IO和CPU资源。
- 磁盘空间检查:OVF执行前会自动检查DN磁盘空间是否充足(表大小不超过可用空间)。如果磁盘空间不足,OVF会报错终止。