文档首页/ 数据仓库服务 DWS/ 最佳实践/ 性能调优/ DWS在线VACUUM FULL(VACUUM FULL ONLINE)最佳实践
更新时间:2026-09-28 GMT+08:00
分享

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>命名)。
    1
    ALTER 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事务阻塞的特性。

  1. 使用客户端连接DWS数据库,例如默认gaussdb数据库,详情请参见连接DWS集群章节。
  1. 测试表级在线VACUUM FULL。

    本步骤演示对整表执行在线VACUUM FULL,同时在另一会话中执行并发IUD(即插入Insert、更新Update、删除Delete,以下简称IUD)操作,验证清理过程中业务读写不受影响。需开启两个SQL会话窗口,以下分别称作SQL会话1和SQL会话2。
    1. 在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;
      
    2. 在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;
      

    3. 并发执行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();
      
    4. 执行在线VACUUM FULL:在SQL会话1中执行表级在线VACUUM FULL。此操作在会话2的IUD操作运行期间执行,验证清理过程中业务读写不受阻塞。
      1
      VACUUM FULL t_ovf_full_v2opt ONLINE;
      

      结果显示,两边语句都在并行,没有上锁,且两边语句先后执行成功。

    5. 验证执行结果:在会话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不影响并发业务。

    6. 会话1:清理环境。
      1
      2
      DROP TABLE IF EXISTS t_ovf_full_v2opt CASCADE; 
      DROP PROCEDURE concurrent_iud_test;
      

  2. 测试分区级在线VACUUM FULL。

    本步骤演示对指定分区执行在线VACUUM FULL,同时在另一会话中对目标分区执行并发IUD操作,验证分区级清理的同时目标分区仍可正常读写。
    1. 在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;
      $$;
      
    2. 数据准备:在会话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;
      
    3. 在会话1中查看清理前的磁盘占用和数据量。
      1
      SELECT * FROM count_and_size_all_partitions('t_ovf_multipar_v2opt');
      

    4. 并发执行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();
      
    5. 执行分区级在线VACUUM FULL:在会话1中对p2和p3分区执行在线VACUUM FULL,验证分区级清理的并发能力。
      1
      VACUUM FULL t_ovf_multipar_v2opt PARTITION (p2, p3) ONLINE;
      

    6. 在会话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分区并发更新的数据,说明并发更新操作正常完成。

    7. 会话1:清理环境。
      1
      2
      DROP TABLE IF EXISTS t_ovf_multipar_v2opt CASCADE; 
      DROP PROCEDURE concurrent_iud_partition_test;
      

  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中创建测试表并写入数据。
       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;
      
    2. 在会话1中查看清理前的磁盘占用:记录清理前的表空间大小。
      1
      SELECT pg_size_pretty(pg_relation_size('t_ovf_lockcancel_v2opt')) AS size_before;
      

    3. 在会话2中开启长SELECT事务:在会话2中开启一个事务并执行一条SELECT语句,但不提交该事务,模拟存在未提交长事务的场景。
      1
      2
      3
      4
      5
      6
      -- 在会话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事务,从而正常执行。
      1
      VACUUM FULL t_ovf_lockcancel_v2opt ONLINE;
      

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

      这是OVF的lock cancel机制在表文件切换阶段(L3)主动终止了阻塞的SELECT查询,属于预期行为。会话2的事务因错误自动回滚,无需手动执行COMMIT。

      生产环境执行OVF前,应评估是否有重要长查询在运行。OVF的lock cancel机制会终止阻塞表文件切换的SELECT查询,被终止的查询需要重新执行。如果业务中有不可中断的长查询,建议在执行OVF前先确认无关键长查询在运行。

    6. 会话1中验证执行结果:查看清理后的表空间大小,确认空间已成功回收。
      1
      SELECT pg_size_pretty(pg_relation_size('t_ovf_lockcancel_v2opt')) AS size_after;
      

    7. 会话1:清理环境。
      1
      DROP TABLE IF EXISTS t_ovf_lockcancel_v2opt CASCADE;
      

  2. 清理测试辅助函数。

    1
    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名。

    1
    SELECT * FROM gs_scan_for_online_vacuum_full();
    

  2. 手动清理残留数据:使用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]);
    

  3. 手动清理(备选方案):如果不使用系统函数,也可以手动执行清理命令(获取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时,建议关注以下性能要点:

  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会报错终止。

相关文档