GaussDB(DWS)表对象VACUUM FULL实践

DWS_Jack 发表于 2020/06/12 16:30:19 2020/06/12
【摘要】 DWS VACUUM FULL 列存

在长期使用DWS过程中,经常会遇到越跑越慢的情况,这个时候就要考虑是否遇到了数据文件碎片化过于严重的问题,有几个典型应用场景会导致该问题。

一、大量的删改操作

DWS中的表在经过大量的UPDATE/DELETE操作之后,会产生脏页,造成查询时访问过多的无效元组,影响检索性能,建议对脏页率超过30%的表执行清理操作。

二、列存表的小批量入库

众所周知DWS中的列存表的存储单元为CU,每次入库都会在各个datanode上产生CU文件,单个CU默认大小为6万条数据,但是在单次小批量入库场景下,会产生大量的小CU,过于小的CU会导致在查询过程中访问过多小的数据文件,影响检索性能,建议在存在列存表小批量入库场景下定期对小CU文件进行合并操作。

上述清理和合并操作都需要通过VACUUM FULL来实现。

那么如何确认一个表是否达到了需要VACUUM FULL的标准呢,在此分享一下我们在实践中总结的两个方法,供大家参考:

脏页/无效元组检查

DWS中提供了PGXC_GET_STAT_ALL_TABLES视图获取全库所有表的脏页情况,但是由于是该视图是全库统计,耗时很长,效率较低,如果需要对单表查询脏页时,可以借助于如下自定义函数,实现方案是通过封装pg_stat_get_tuples_deleted接口(入参是表oid)来进行分布式查询,统计单表在各个datanode上脏页情况:

CREATE OR REPLACE FUNCTION PUBLIC.check_dead_tuples(table_name varchar(100))
    RETURNS BIGINT
    LANGUAGE 'plpgsql'
    --IMMUTABLE
    AS $$
    DECLARE
    TYPE ARRAY_VARCHAR IS VARRAY(1024) OF VARCHAR;
    ARRNAME ARRAY_VARCHAR := ARRAY_VARCHAR();
    schemaname varchar default 'public';
    tablename varchar;
    query_str text;
    query_str_nodes text;
    table_oid int;
    temp bigint := 0;
    temp_sum bigint := 0;
    row_name record;
 BEGIN
    ARRNAME.extend(2);
    IF ( regexp_like(table_name, '.+\..+') = true )
        THEN
        ARRNAME := regexp_split_to_array(table_name,'\.');
        schemaname := COALESCE(ARRNAME(1),'public');
        tablename := ARRNAME(2);
    ELSIF ( regexp_like(table_name, '.+\..+') = FALSE ) THEN
        schemaname := 'public';
        tablename := table_name;
    END IF;
    query_str_nodes := 'SELECT node_name FROM pgxc_node where node_type = ''D''';
    FOR row_name IN EXECUTE(query_str_nodes) LOOP
        query_str := 'EXECUTE DIRECT ON (' || row_name.node_name || ') ''select a.oid from pg_class a, pg_namespace b where a.relnamespace=b.oid and a.relname='''''||tablename||''''' and b.nspname='''''||schemaname||'''''''';
        EXECUTE immediate query_str into table_oid;
        query_str := 'EXECUTE DIRECT ON (' || row_name.node_name || ') ''SELECT pg_stat_get_tuples_deleted('||table_oid||')''';
        EXECUTE immediate query_str into temp;
        temp_sum := temp_sum + temp;
    END LOOP;
    return temp_sum;
    END $$
;

本地测试效果如下:

postgres=> select count(*) from public.customer;
 count  
--------
 100000
(1 row)
postgres=> delete from public.customer;
DELETE 100000
postgres=> checkpoint;
CHECKPOINT
postgres=> select public.check_dead_tuples('public.customer');
 check_deleted 
---------------
  100000
(1 row)

在获取该表脏数据条数后,可以与总条数作对比,来评估是否需要在业务空窗期做VACUUM FULL清理操作。

 

列存表小CU检查

实现方案是通过获取在各个datanode统计列存表CU_DESC表中的数据条数,来统计CU个数,另外排除了隐藏列的CU,实现借助于两个自定义函数:

CREATE OR REPLACE FUNCTION public.get_cu_num_dn(table_name character varying)
RETURNS bigint
LANGUAGE plpgsql
--IMMUTABLE 
NOT FENCED
AS $function$
DECLARE
    TYPE ARRAY_VARCHAR IS VARRAY(1024) OF VARCHAR;
    ARRNAME ARRAY_VARCHAR := ARRAY_VARCHAR();
    schemaname varchar default 'public';
    tablename varchar;
    tb_oid oid;
    rowcount bigint;
    query_str text;
    query_str_nodes text;
    cudes_name text;
    temp bigint := 0;
    temp_cu_sum bigint := 0;
    temp_sum bigint := 0;
    row_name record;
    cu_name record;
BEGIN
    ARRNAME.extend(2);
    IF ( regexp_like(table_name, '.+\..+') = true )
    THEN
            ARRNAME := regexp_split_to_array(table_name,'\.');
            schemaname := COALESCE(ARRNAME(1),'public');
            tablename := ARRNAME(2);
    ELSIF ( regexp_like(table_name, '.+\..+') = FALSE ) THEN
            schemaname := 'public';
            tablename := table_name;
    END IF;
   SELECT a.oid FROM pg_class a, pg_namespace b WHERE a.relname = tablename and a.relnamespace=b.oid and b.nspname=schemaname into tb_oid;
   select count(*) from pg_partition where parentid=tb_oid and parttype='p' into rowcount;
 if rowcount = 0 then
        query_str := 'select (''cstore.''||relname) from pg_class where oid = (select a.relcudescrelid from pg_class a, pg_namespace b where a.relnamespace=b.oid and a.relname='''||tablename||''' and b.nspname='''||schemaname||''')';
    EXECUTE immediate query_str into cudes_name;
   query_str := 'SELECT count(*) from '||cudes_name||' where col_id > 0';
   EXECUTE immediate query_str into temp;
            temp_sum := temp_sum + temp;
    ELSE
            query_str := 'select (''cstore.''||relname) from pg_class where oid in (select c.relcudescrelid from pg_class a, pg_namespace b, pg_partition c where a.relnamespace=b.oid and c.parentid=a.oid and a.relname='''||tablename||''' and b.nspname='''||schemaname||''')';
              FOR cu_name in EXECUTE(query_str) LOOP
        query_str := 'SELECT count(*) from '||replace(replace(cu_name::text,'(',''),')','')||' where col_id > 0';
        EXECUTE immediate query_str into temp;
       temp_cu_sum := temp_cu_sum + temp;
       END LOOP;
                temp_sum := temp_sum + temp_cu_sum;
     end if;
        return temp_sum;
END $function$
;

CREATE OR REPLACE FUNCTION public.get_cu_num(table_name character varying)
 RETURNS bigint
 LANGUAGE plpgsql
--IMMUTABLE 
NOT FENCED
AS $function$
DECLARE
    TYPE ARRAY_VARCHAR IS VARRAY(1024) OF VARCHAR;
    ARRNAME ARRAY_VARCHAR := ARRAY_VARCHAR();
    schemaname varchar default 'public';
    tablename varchar;
    tb_oid oid;
    rowcount bigint;
    query_str text;
    query_str_nodes text;
    cudes_name text;
    temp bigint := 0;
    temp_cu_sum bigint := 0;
    temp_sum bigint := 0;
    row_name record;
    cu_name record;
BEGIN
    ARRNAME.extend(2);
    IF ( regexp_like(table_name, '.+\..+') = true )
    THEN
            ARRNAME := regexp_split_to_array(table_name,'\.');
            schemaname := COALESCE(ARRNAME(1),'public');
            tablename := ARRNAME(2);
    ELSIF ( regexp_like(table_name, '.+\..+') = FALSE ) THEN
            schemaname := 'public';
            tablename := table_name;
    END IF;
   SELECT a.oid FROM pg_class a, pg_namespace b WHERE a.relname = tablename and a.relnamespace=b.oid and b.nspname=schemaname into tb_oid;
   select count(*) from pg_partition where parentid=tb_oid and parttype='p' into rowcount;
   query_str_nodes := 'SELECT node_name FROM pgxc_node where node_type = ''D''';
    FOR row_name IN EXECUTE(query_str_nodes) LOOP
   query_str := 'EXECUTE DIRECT ON (' || row_name.node_name || ') ''SELECT public.get_cu_num_dn('''''||table_name||''''')''';
   EXECUTE immediate query_str into temp;
            temp_sum := temp_sum + temp;
    END LOOP;
        return temp_sum;
END $function$
;

本地测试效果如下:

postgres=# SELECT public.get_cu_num('public.t_v');
 get_cu_num
------------
        396
(1 row)

在统计CU个数之后,根据总数据条数,就可以评估小CU个数是否过多了,该自定义函数思路供大家参考,可以在此基础之上再针对需求进行优化。

VACUUM FULL清理

在统计完脏页和小CU后,就可以对表进行VACUUM FULL操作了,由于VACUUM FULL操作是较为重的表对象运维操作,会持有表对象的最高级锁,阻塞所有业务,所以建议在业务空窗期对表进行VACUUM FULL操作,另外对于按时间作分区的分区表来说,建议按分区粒度对最近一个频繁入库和操作的分区,以尽可能缩短维护窗口,降低对业务的影响,目前在DWS中尽管是VACUUM FULL单个分区,也会对整个表加排它锁,这一点建议后续版本中做一些优化,对单个分区加排它锁,降低影响的分区范围,VACUUM FULL操作示例如下:

postgres=# vacuum full t_v;
VACUUM
postgres=# vacuum full t_v partition(p1);
VACUUM

VACUUM FULL完成后,别忘了对表执行一下ANALYZE统计分析,获取最新的统计信息,在查询中生成最优的执行计划。

postgres=# analyze t_v;
ANALYZE

另外,在DWS中,随着对象的不断创建删除,以及临时表的大量使用,还有增删列操作,系统表也会产生一些无效元组,影响涉及系统表的操作性能,比如DDL,建议也定期对一些系统表进行清理操作,主要涉及的系统表如下:

VACUUM FULL PG_ATTRIBUTE;
VACUUM FULL PG_CLASS;
VACUUM FULL PG_PARTITION;
VACUUM FULL PG_STATISTIC;
VACUUM FULL PG_INDEX;
ANALYZE PG_ATTRIBUTE;
ANALYZE PG_CLASS;
ANALYZE PG_PARTITION;
ANALYZE PG_STATISTIC;
ANALYZE PG_INDEX;

系统表的清理也同样是阻塞业务的,必须在业务空窗期执行,系统表的维护操作频度不用太高,根据实际业务模型建议半年到一年再考虑是否需要清理。

最后,希望我们的分享能够对大家有所帮助,让DWS给您的业务中更加高效,同时希望DWS可以不断改进提升,在数据库的国产化进程中持续发力!

 

 

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。