PostGreSQL/openGauss表膨胀处理

如果面试官问你,Oracle与PG/OG最大的区别是什么?你要是没回答出MVCC机制,表膨胀,那你多半挂了。

在PG/OG数据库中,命令vacuum full,插件pg_repack用于处理表膨胀,但是别高兴得太早,如果有长事务,vacuum full执行完之后空间还是无法回收。

pg_repack需要等执行pg_repack命令时之前的长事务都提交了才能往后执行,你愿意等吗?很明显不愿意。

下面我们来模拟一下有长事务情况下vacuum full无法回收空间的问题

现在有张表test01,表结构来自Oracle11g DBA_OBJECTS,反复插入了44537344行数据,占用磁盘空间6115 MB

sql 复制代码
openGauss=# \d+
                                       List of relations
 Schema |  Name  | Type  |   Owner   |  Size   |             Storage              | Description 
--------+--------+-------+-----------+---------+----------------------------------+-------------
 public | test01 | table | opengauss | 6115 MB | {orientation=row,compression=no} | 
 public | test02 | table | opengauss | 12 MB   | {orientation=row,compression=no} | 
(2 rows)

创建2个测试表t_idletransaction,t_astore

sql 复制代码
openGauss=# create table t_idletransaction with(storage_type=astore) as select * from test01 where rownum<=1000;
INSERT 0 1000
openGauss=# create table t_astore with(storage_type=astore) as select * from test01 where 1=0;
INSERT 0 0
openGauss=# insert into t_astore select * from test01 where rownum<=1000000;
INSERT 0 1000000

记录表大小

sql 复制代码
openGauss=# \d+
                                                       List of relations
 Schema |       Name        | Type  |   Owner   |  Size   |                       Storage                        | Description 
--------+-------------------+-------+-----------+---------+------------------------------------------------------+-------------
 public | t_astore          | table | opengauss | 137 MB  | {orientation=row,storage_type=astore,compression=no} | 
 public | t_idletransaction | table | opengauss | 176 kB  | {orientation=row,storage_type=astore,compression=no} | 
 public | test01            | table | opengauss | 6115 MB | {orientation=row,compression=no}                     | 
 public | test02            | table | opengauss | 12 MB   | {orientation=row,compression=no}                     | 
(4 rows)

session1 开启长事务

sql 复制代码
openGauss=# begin;
BEGIN
openGauss=# update t_idletransaction set object_id=0;
UPDATE 1000

session2 反复对表进行批量DML操作

sql 复制代码
penGauss=# \timing
Timing is on.
openGauss=# declare
openGauss-# v_date date;
openGauss-# begin
openGauss$#   for i in 1..10 loop
openGauss$#     select sysdate into v_date;
openGauss$#     update t_astore set owner='B' where object_id>2; 
openGauss$#     commit;
openGauss$#     delete from t_astore where object_id>2;
openGauss$#     commit;
openGauss$#     insert into t_astore select * from test01 where object_id>2 and rownum<=999988;
openGauss$#     commit;
openGauss$#     raise info 'i=%,current_date=%',i,v_date;
openGauss$#   end loop;
openGauss$# end;
openGauss$# /
INFO:  i=1,current_date=2025-04-07 15:33:43
INFO:  i=2,current_date=2025-04-07 15:33:48
INFO:  i=3,current_date=2025-04-07 15:33:56
INFO:  i=4,current_date=2025-04-07 15:34:04
INFO:  i=5,current_date=2025-04-07 15:34:14
INFO:  i=6,current_date=2025-04-07 15:34:26
INFO:  i=7,current_date=2025-04-07 15:34:39
INFO:  i=8,current_date=2025-04-07 15:34:52
INFO:  i=9,current_date=2025-04-07 15:35:07
INFO:  i=10,current_date=2025-04-07 15:35:23
ANONYMOUS BLOCK EXECUTE
Time: 117272.747 ms

查看表大小

sql 复制代码
openGauss=# \d+
                                                       List of relations
 Schema |       Name        | Type  |   Owner   |  Size   |                       Storage                        | Description 
--------+-------------------+-------+-----------+---------+------------------------------------------------------+-------------
 public | t_astore          | table | opengauss | 2844 MB | {orientation=row,storage_type=astore,compression=no} | 
 public | t_idletransaction | table | opengauss | 312 kB  | {orientation=row,storage_type=astore,compression=no} | 
 public | test01            | table | opengauss | 6115 MB | {orientation=row,compression=no}                     | 
 public | test02            | table | opengauss | 12 MB   | {orientation=row,compression=no}                     | 
(4 rows)

t_astore从137MB膨胀到2844MB,膨胀了20倍,执行vacuum full回收空间

sql 复制代码
openGauss=# vacuum full t_astore;
VACUUM
Time: 41075.170 ms
openGauss=# \d+
                                                       List of relations
 Schema |       Name        | Type  |   Owner   |  Size   |                       Storage                        | Description 
--------+-------------------+-------+-----------+---------+------------------------------------------------------+-------------
 public | t_astore          | table | opengauss | 2847 MB | {orientation=row,storage_type=astore,compression=no} | 
 public | t_idletransaction | table | opengauss | 312 kB  | {orientation=row,storage_type=astore,compression=no} | 
 public | test01            | table | opengauss | 6115 MB | {orientation=row,compression=no}                     | 
 public | test02            | table | opengauss | 12 MB   | {orientation=row,compression=no}                     | 
(4 rows)

t_astore从2844MB变到了2847MB,这说明在有长事务情况下,vacuum full不能回收空间

不得不说PG/OG的vacuum full做得很挫,我这里长事务的表是t_idletransaction,vacuum full的表是t_astore,两个表都没啥关系,还不让回收空间

内核只考虑怎么实现方便,不考虑怎么使用方便,嘿嘿

既然vacuum full无法回收,那就手工回收空间吧

1.检查表上有没有锁,如果有,不执行后面操作

2.开启事务

3.先锁住原表

4.创建一个备份表

5.truncate原表

6.把备份数据插入到原表

7.drop备份表

8.提交

sql 复制代码
openGauss=# begin;
BEGIN
Time: 0.222 ms
openGauss=# lock table t_astore;
LOCK TABLE
Time: 1002.649 ms
openGauss=# create table t_astore_bak as select * from t_astore;
INSERT 0 999999
Time: 5381.672 ms
openGauss=# truncate table t_astore;
TRUNCATE TABLE
Time: 2.081 ms
openGauss=# insert into t_astore select * from t_astore_bak;
INSERT 0 999999
Time: 628.223 ms
openGauss=# drop table t_astore_bak;
DROP TABLE
Time: 1.363 ms
openGauss=# commit;
COMMIT
Time: 235.081 ms

openGauss=# \d+
                                                       List of relations
 Schema |       Name        | Type  |   Owner   |  Size   |                       Storage                        | Description 
--------+-------------------+-------+-----------+---------+------------------------------------------------------+-------------
 public | t_astore          | table | opengauss | 137 MB  | {orientation=row,storage_type=astore,compression=no} | 
 public | t_idletransaction | table | opengauss | 312 kB  | {orientation=row,storage_type=astore,compression=no} | 
 public | test01            | table | opengauss | 6115 MB | {orientation=row,compression=no}                     | 
 public | test02            | table | opengauss | 12 MB   | {orientation=row,compression=no}                     | 
(4 rows)

可以看到t_astore空间已经回收,表从2847MB缩小到137MB

为什么要先锁住原表呢?是因为如果能锁住原表,说明表当前没有被select,也没有在事务中,对表人工进行空间回收是安全的

在openGauss中,select没执行完,另外的会话无法lock表

session1

select count(*) from test01 a,test01 b; ---4.4kw与4.4kw笛卡尔积,跑不出结果

session2

begin;

lock table test01; ---阻塞

在openGauss中,表在事务中,另外的会话无法lock表

session1

begin;

select * from test01 where object_id=1000;

session2

begin;

lock table test01; ---阻塞

通过上面2个实验,证明了手工回收空间操作是安全的

有的读者可能会问,你为什么要折腾出人工回收空间的方法呢?

那是因为之前在一个制造业客户中遇到了表膨胀问题,客户每个月月初要做月结,每次月结都会卡死2小时,客户一直在抱怨

后面定位到是索引膨胀,索引上死元组有3到4000W行,索引是十几个字段组成的联合主键,由于死元组有几千万行,批量INSERT到

该表,每插入一行数据就要进行唯一性检查,死元组太多了,外加上联合主键的原因导致唯一性检查特别慢,就这一步卡死2小时,正常情况下几十秒跑完

由于vacuum full回收不了空间,pg_repack也回收不了,所以被逼折腾出人工回收空间的方法

最后搞个了crontab,每隔一段时间检查n_dead_tup,当n_dead_tup大于某个阈值,检查表上有没有锁,没有锁就人工回收空间,自此之后,每次月结跑批就很稳定了

我做实验的表比较小,如果表特别大,几十GB,上百GB就不好处理了,所以大表最好分区,分区之后可以按照分区单独处理表膨胀

限于篇幅,就不讨论ustore了,下一篇讨论openGauss/GaussDB ustore

注意:人工处理表膨胀相当于把表上面的历史版本数据丢了,如果你有基于快照做备份恢复的需求,自己做个实验测试一把上面操作对你是否有影响

相关推荐
Mr.洛 白8 天前
OpenEuler/CentOS一键部署OpenGauss数据库教程(脚本+视频)
数据库·opengauss·gaussdb·国产数据库安装·安装脚本
尚雷55803 个月前
[openGauss 学废系列]-用户和模式的关系以及访问方式
数据库·oracle·opengauss
振华OPPO3 个月前
一文讲清楚PostgreSQL表膨胀
数据库·sql·mysql·postgresql·表膨胀·pg
GottdesKrieges3 个月前
openGauss与GaussDB系统架构对比
系统架构·opengauss·gaussdb
尚雷55804 个月前
Centos 7 系统 openGauss 3.1.0 一主两备集群安装部署指南
linux·运维·opengauss
DarkAthena5 个月前
【MogDB】MogDB5.2.0重磅发布第五篇-支持部分ORACLE的HINT
数据库·oracle·opengauss
太虚5 个月前
openGauss数据库-头歌实验1-3 创建和管理模式
数据库·opengauss
DarkAthena5 个月前
【MogDB】MogDB5.2.0重磅发布第九篇-SQL宽容性提升
数据库·sql·oracle·opengauss
太虚5 个月前
openGauss数据库-头歌实验1-4 数据库及表的创建
数据库·opengauss