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