# 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事务 + 低锁数据事务"交替组成：
1. **创建临时表（L1）**：获取原表7级锁（ExclusiveLock），在同一个事务中完成创建临时表、设置原表 append_mode=on、创建delta表等DDL操作，然后提交事务释放锁。7级锁阻塞DML写入但不阻塞SELECT。此阶段持续时间很短。
2. **基线复制**：在新事务中对原表获取1级锁（AccessShareLock），执行 INSERT INTO 临时表 SELECT FROM 原表，将原表现有数据复制到临时表。1级锁等价于SELECT的锁级别，完全不阻塞业务。复制完成后提交事务释放锁。
3. **增量追增（catchup）** ：通过多轮catchup，将基线复制期间产生的增量数据（新INSERT/UPDATE/DELETE的delta记录）从原表同步到临时表。每轮catchup分两步：
   1. **DDL锁阶段**：获取原表7级锁（ExclusiveLock），刷新 append_mode=on（会同步刷新start/end_ctid）、切换delta表等DDL操作，提交事务释放锁。此阶段短暂阻塞DML写入，但不阻塞SELECT。
   
   2. **数据追增阶段：**在新事务中对原表获取1级锁（AccessShareLock），读取delta数据并写入临时表，提交事务释放锁。此阶段完全不阻塞业务。
   
   3. 随着增量数据逐渐减少，catchup轮次耗时递减。
    
4. **最终catchup与表文件切换（L3）：**当catchup数据量足够小时，进入最终轮。此阶段获取7级锁后不再释放，直接延续执行：读取最终delta数据 → LockTableAndKillSelect 升级为8级锁（AccessExclusiveLock） → 交换原表和临时表的relfilenode → 关闭 append_mode → 删除辅助SCHEMA → 提交事务释放锁。如果此时有长SELECT事务阻塞VACUUM FULL ONLINE，会通过lock cancel机制主动终止该SELECT。此阶段是持续时间最长的高锁阶段，但通常仍占总时间5%以下。
表1**VACUUM FULL ONLINE锁模型总结** 
| 阶段             | 锁级别                 | 阻塞业务            | 说明                                  |
|:---|:---|:---|:---|
| 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（在线重量级清理，不锁表）。三者区别如下表所示。
表2**VACUUM、VACUUM FULL与在线VACUUM FULL的对比** 
| 比较维度    | 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\>命名）。
  ```
  ALTER TABLE [ table_name ] SET (append_mode=off);
  ```
  
- 处于重分布状态和扩缩容状态的集群，不能执行在线VACUUM FULL。
- 2.0集群升级3.0集群期间不能执行在线VACUUM FULL。
- 以上仅列出关键约束点，其他更多约束请参见[VACUUM](https://support.huaweicloud.com/sqlreference-911-dws/dws_06_0226.html)章节。
 
#### 前提条件
已创建9.1.1.300及以上版本的DWS集群。
#### 在线VACUUM FULL使用最佳实践
以下分别以**表级** 和**分区级**的在线VACUUM FULL为例，演示在线VACUUM FULL执行过程中可对表进行并发读写的操作，验证存储空间清理的同时保障业务的连续性和数据可用性。此外，还将演示在线VACUUM FULL通过lock cancel机制解除长SELECT事务阻塞的特性。
1. 使用客户端连接DWS数据库，例如默认gaussdb数据库，详情请参见[连接DWS集群](https://support.huaweicloud.com/mgtg-dws/dws_01_0131.html)章节。

2. 测试**表级** 在线VACUUM FULL。
   
   本步骤演示对整表执行在线VACUUM FULL，同时在另一会话中执行**并发IUD** （即插入Insert、更新Update、删除Delete，以下**简称IUD** ）操作，验证清理过程中业务读写不受影响。**需开启两个SQL会话窗口** ，以下分别称作**SQL会话1** 和**SQL会话2** 。
   1. 在**SQL** **会话1** 中创建测试表并写入数据，模拟业务表中存在大量脏页的场景。
      ```
      -- 删除已存在的同名表，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;
      ```
      
   
   2. **在**SQL**会话1中查看清理前的磁盘占用和数据量** ：记录执行在线VACUUM FULL前的表空间大小和行数，作为对比基准。
      ```
      -- 查看清理前的表磁盘占用大小（pg_size_pretty将字节数转为人类可读格式，如KB、MB、GB）
      SELECT pg_size_pretty(pg_relation_size('t_ovf_full_v2opt')) AS size_before;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002733268668.png "点击放大")
      ```
      -- 查看清理前的表行数
      SELECT count(*) AS rows_before FROM t_ovf_full_v2opt;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002733428572.png "点击放大")
      
   
   3. **并发执行IUD操作** ：在**SQL** **会话2** 中执行IUD语句模拟用户业务场景。**注意** **需在IUD语句运行结束前** 执行[2.d]的在线VACUUM FULL，以验证并发场景下的清理效果。
      **IUD语句** **执行逻辑** ：循环150次，每次执行插入+更新，中间暂停0.1秒，延长执行时间以确保与[2.d]的VACUUM FULL ONLINE并发执行。
      ```
      -- 在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();
      ```
      
   
   4. **执行在线VACUUM FULL** ：在**SQL** **会话1** 中执行表级在线VACUUM FULL。此操作在**会话2** 的IUD操作运行期间执行，验证清理过程中业务读写不受阻塞。
      ```
      VACUUM FULL t_ovf_full_v2opt ONLINE;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002762828651.png "点击放大")
      **结果显示，两边语句都在并行，没有上锁，且两边语句先后执行成功。**
      
   
   5. **验证执行结果** ：在会话1中查看清理后的表空间大小和行数，确认空间已被回收且并发写入的数据完整保留。
      ```
      -- 1. 查看清理后的表磁盘占用大小，与size_before对比确认空间是否回收
      SELECT pg_size_pretty(pg_relation_size('t_ovf_full_v2opt')) AS size_after;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002776319589.png)
      结果显示，size_after明显小于size_before，说明**表空间已被有效回收**。
      ```
      -- 2. 查看清理后的表行数
      SELECT count(*) AS rows_after FROM t_ovf_full_v2opt;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002746719806.png)
      结果显示， 执行存储过程后，删除了320行，同时插入150行，最终得到470行，说明在线VACUUM FULL期间DML业务正常。
      ```
      -- 3. 统计会话2并发插入的数据条数（a值在90001~90150之间），预期为150条
      SELECT count(*) AS concurrent_insert_count FROM t_ovf_full_v2opt WHERE a >= 90001 AND a <= 90150;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002762829965.png "点击放大")
      结果显示，concurrent_insert_count等于150，说明**会话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;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002762990613.png "点击放大")
      查询结果显示**会话2** 并发更新的数据，说明并发更新操作正常完成，**在线VACUUM FULL不影响并发业务**。
      
   
   6. **会话1** ：清理环境。
      ```
      DROP TABLE IF EXISTS t_ovf_full_v2opt CASCADE; 
      DROP PROCEDURE concurrent_iud_test;
      ```
      
    
   
   
3. 测试**分区级** 在线VACUUM FULL。
   
   本步骤演示对指定分区执行在线VACUUM FULL，同时在另一会话中对目标分区执行并发IUD操作，验证分区级清理的同时目标分区仍可正常读写。
   1. 在**SQL会话1** 中创建一个辅助函数，用于查看各分区的行数和磁盘占用大小，便于后续验证清理效果。
      ```
      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;
      $$;
      ```
      
   
   2. **数据准备** ：在**会话1** 中创建分区测试表并写入数据。
      ```
      -- 删除已存在的同名表
      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;
      ```
      
   
   3. 在会话1**中查看清理前的磁盘占用和数据量** 。
      ```
      SELECT * FROM count_and_size_all_partitions('t_ovf_multipar_v2opt');
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002746903804.png)
      
   
   4. **并发执行IUD操作** ：在**会话2中** 对p2和p3分区执行并发IUD操作。注意，**需在该IUD语句运行结束前** 执行[3.e]。
      ```
      -- 在会话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();
      ```
      
   
   5. **执行分区级在线VACUUM FULL** ：在会话1中对p2和p3分区执行在线VACUUM FULL，验证分区级清理的并发能力。
      ```
      VACUUM FULL t_ovf_multipar_v2opt PARTITION (p2, p3) ONLINE;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002762847229.png "点击放大")
      
   
   6. 在**会话1** 中**验证执行结果** ：查看清理后各分区的空间大小和行数，确认目标分区空间已回收且并发写入数据完整保留。
      ```
      -- 1. 查看清理后各分区的行数和磁盘大小，与清理前对比确认p2、p3分区空间是否回收
      SELECT * FROM count_and_size_all_partitions('t_ovf_multipar_v2opt');
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002746744162.png)
      结果显示，p2和p3分区的磁盘大小明显小于清理前，说明目标分区空间已被回收。
      ```
      -- 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_%';
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002763008173.png "点击放大")
      结果显示，concurrent_insert_p2和concurrent_insert_p3应均等于150，说明并发插入的数据全部保留。
      ```
      -- 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;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002746904124.png "点击放大")
      结果显示了p2、p3分区并发更新的数据，说明并发更新操作正常完成。
      
   
   7. **会话1** ：清理环境。
      ```
      DROP TABLE IF EXISTS t_ovf_multipar_v2opt CASCADE; 
      DROP PROCEDURE concurrent_iud_partition_test;
      ```
      
    
   
   

4. 测试在线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** 中创建测试表并写入数据。
      ```
      -- 删除已存在的同名表
      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;
      ```
      
   
   2. 在会话1中**查看清理前的磁盘占用** ：记录清理前的表空间大小。
      ```
      SELECT pg_size_pretty(pg_relation_size('t_ovf_lockcancel_v2opt')) AS size_before;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002762849627.png "点击放大")
      
   
   3. **在会话2中开启长SELECT事务** ：在**会话2** 中开启一个事务并执行一条SELECT语句，但不提交该事务，模拟存在未提交长事务的场景。
      ```
      -- 在会话2中执行：开启一个显式事务
      BEGIN;
      -- 执行一条SELECT查询，但不提交事务（不执行COMMIT）
      -- 该未提交事务持有共享锁，模拟存在长事务的场景
      -- 若使用传统VACUUM FULL，此处会因等待该事务释放锁而阻塞
      SELECT * FROM t_ovf_lockcancel_v2opt LIMIT 10;
      ```
      
   
   4. **执行在线VACUUM FULL** ：在**会话1** 中执行在线VACUUM FULL。若使用传统VACUUM FULL，此处会被**会话2** 的未提交事务阻塞，但在线VACUUM FULL在表文件切换阶段会通过lock cancel机制终止会话2的SELECT事务，从而正常执行。
      ```
      VACUUM FULL t_ovf_lockcancel_v2opt ONLINE;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002763009837.png "点击放大")
      
   
   5. **提交长SELECT事务** ：在**会话2** 中提交[4.c]中开启的事务，返回如下错误信息：
      ```
      COMMIT;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002776424809.png "点击放大")
      这是OVF的lock cancel机制在表文件切换阶段（L3）主动终止了阻塞的SELECT查询，属于预期行为。会话2的事务因错误自动回滚，无需手动执行COMMIT。
      ![](https://support.huaweicloud.com/bestpractice-dws/public_sys-resources/caution_3.0-zh-cn.png)
      生产环境执行OVF前，应评估是否有重要长查询在运行。OVF的lock cancel机制会终止阻塞表文件切换的SELECT查询，被终止的查询需要重新执行。如果业务中有不可中断的长查询，建议在执行OVF前先确认无关键长查询在运行。
      
   
   6. **会话1** 中**验证执行结果** ：查看清理后的表空间大小，确认空间已成功回收。
      ```
      SELECT pg_size_pretty(pg_relation_size('t_ovf_lockcancel_v2opt')) AS size_after;
      ```
      ![](https://support.huaweicloud.com/bestpractice-dws/figure/zh-cn_image_0000002762851543.png "点击放大")
      
   
   7. **会话1** ：清理环境。
      ```
      DROP TABLE IF EXISTS t_ovf_lockcancel_v2opt CASCADE;
      ```
      
    
   
   
5. 清理测试辅助函数。 
   ```
   DROP FUNCTION count_and_size_all_partitions(text);
   ```
   
   
 
#### 失败恢复
在线VACUUM FULL操作执行失败可能会导致数据残留，包括辅助SCHEMA和表的append_mode设置。DWS提供了系统函数帮助扫描和清理残留数据。
1. 扫描残留表：使用gs_scan_for_online_vacuum_full函数扫描所有存在残留辅助SCHEMA的表，该函数返回存在残留辅助SCHEMA的表的schema名、表名、完整名称、CN上的OID和辅助SCHEMA名。 
   ```
   SELECT * FROM gs_scan_for_online_vacuum_full();
   ```
   
   
2. 手动清理残留数据：使用gs_cleanup_for_online_vacuum_full函数清理残留的辅助SCHEMA和append_mode设置。该函数会自动完成以下操作：获取OVF咨询锁（防止与正在运行的OVF冲突）、关闭append_mode、删除辅助SCHEMA中的残留对象、删除辅助SCHEMA。执行完成后输出清理统计信息。 
   ```
   -- 清理单个表
   SELECT gs_cleanup_for_online_vacuum_full('table_name');  
   -- 清理多个表
   SELECT gs_cleanup_for_online_vacuum_full(ARRAY['table1'::text, 'table2'::text]);
   ```
   
   
3. 手动清理（备选方案）：如果不使用系统函数，也可以手动执行清理命令（获取oid需要连接CCN，即Central Coordinator Node，集群的中央协调节点）。 
   ```
   -- 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时，建议关注以下性能要点：
1. **高锁时间的影响因素** ：OVF的高锁时间由L1（创建临时表）、每轮L2_lock（DDL锁阶段）和L3（最终切换）组成，各阶段影响因素不同：
   1. **L1与分区数量相关**：创建临时表阶段需为每个分区执行建表+delta表等DDL，分区数量越多锁时间越长。单分区表L1通常不到1秒，多分区表可能显著增加。分区级OVF只创建指定目标分区的临时表，可以减少建表阶段的阻塞时间。
   
   2. **L2_lock与数据量弱相关**：每轮catchup的DDL锁阶段主要是append_mode=on和切换delta表等DDL操作，与数据量无关，单轮通常不到1秒。
   
   3. **L3与并发更新频率相关**：最终切换阶段不释放锁直接延续执行final catchup，需读取delta表中的增量数据。如果OVF执行期间并发IUD频率高，delta表积累的增量数据多，final catchup耗时就长，L3高锁时间相应增加。
   
   
   总体而言，高锁时间通常占总时间的5%以下，基线复制和数据追增阶段只持有1级锁，不阻塞业务。
   
2. **分区级OVF**：对指定分区执行OVF时，仅清理目标分区，不影响其他分区。相比全表OVF，分区级OVF只创建目标分区的临时表、只复制目标分区数据、只重建目标分区索引，能够显著减少总执行时间和高锁时间。建议在分区表膨胀不均匀时，优先对膨胀严重的分区单独执行OVF，而非全表执行。
3. **执行时机**：建议在业务低峰期执行OVF，虽然OVF不长时间阻塞业务，但基线复制和索引重建会消耗IO和CPU资源。
4. **磁盘空间检查**：OVF执行前会自动检查DN磁盘空间是否充足（表大小不超过可用空间）。如果磁盘空间不足，OVF会报错终止。
 
