大量使用临时表带来的问题,替代方案,以及如何擦屁股

本文涉及的产品
RDS PostgreSQL Serverless,0.5-4RCU 50GB 3个月
推荐场景:
对影评进行热评分析
云数据库 RDS SQL Server,基础系列 2核4GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介: 以前有使用Greenplum的朋友遇到元表膨胀的问题,总结了原因写了一篇文章如下。建议先阅读这篇文档,再阅读本文,里面有些原理我在本文就不讲了。http://blog.163.com/digoal@126/blog/static/1638770402014616113353555 近日,又有使用P

以前有使用Greenplum的朋友遇到元表膨胀的问题,总结了原因写了一篇文章如下。
建议先阅读这篇文档,再阅读本文,里面有些原理我在本文就不讲了。
http://blog.163.com/digoal@126/blog/static/1638770402014616113353555

近日,又有使用PostgreSQL的朋友也遇到类似的问题,一些元表膨胀非常厉害。
用户大量的使用临时表,频繁的创建(PG的临时表是需要随时用随时建的,每个会话都要自己建,而且每个临时表会在pg_class,pg_attribute 中留下痕迹,用完还需要从元表中 delete 这些元数据),因此元表pg_attribute, pg_rewrite, pg_class 会出现大量的dead tuple。
同时用户的业务需要在数据库长期跑长事务,这个也是膨胀的关键点。

图说问题, 一针见血

目前PostgreSQL的版本,在回收垃圾时,只能回收比现存最早事务更老的垃圾tuple。
如图
1
这张图表示,当前XID=99999,当前数据库集群中未结束的事务是XID=1000的事务,假设在XID=1000后系统产生了100GB的垃圾数据(如UPDATE了一个大表,或者删除或更新了一大批数据)。
那么这100GB是没有办法被vacuum回收的,只能回收XID=1000以前的垃圾。
这就是膨胀的起因。 (长事务同时伴随大量更新或删除产生垃圾)

PostgreSQL 9.6会引入快照过旧的功能,有点类似于Oracle 的undo snapshot too old的玩意。来防止因存在很长的事务带来的膨胀问题。
如图
2
例如用户设置了只保留最近1000个事务产生的垃圾,当老的事务访问到已经被vacuum清除的数据时,会报snapshot too old的错误。 这个方法大大降低了膨胀的概率。

如何戒掉临时表(替代方案)

回到本文的主题,用户除了用临时表,还有什么方法?
因为PG的临时表是随用随建的(会话断开又得建), 有什么好的替代方案,不要动到元数据吗?
答案是,当然有。
如果你是在存储过程中使用临时表,你可以建立composite type,然后用数组来存储这些临时数据,这样就不会动到元数据了。
例子 :

postgres=# create type t as (id int, info text, crt_time timestamp);
CREATE TYPE

create or replace function f() returns void as 
$$
                                                                    
declare 
  tmp_t t[];
  tmp t;
begin
  -- 产生一批临时记录,聚合后存入数组,原来你可能要存入临时表
  select array_agg((t1)::t) into tmp_t from (select id,md5(random()::text),clock_timestamp() from generate_series(1,100) t(id)) t1;

  -- 把数组当表使用, unnest数组, 
  -- select (unnest(tmp_t)).*;
  
  -- 你可能会用临时数据做一些JOIN操作, 用unnest和subquery就能和其他表一样JOIN操作
  -- select t2.* from t2 join (select (unnest(tmp_t)).*) t1 on t1.id=t2.id where ....;
  -- update t2 set xx=xx from (select (unnest(tmp_t)).*) t1 where t2.xx=t1.xx and t1.xx=xx and t2.xx=xx;

  -- 你可能会对临时数据做一些处理
  for i in 1..array_length(tmp_t, 1) loop
    raise notice '%', tmp_t[i];
    tmp := tmp_t[i];
    tmp.info := 'new '||tmp.info;
    tmp_t[i] := tmp;
    raise notice '%', tmp_t[i];
    -- 逻辑处理可以放在这里做
  end loop;
end;

$$
 language plpgsql strict;

...
NOTICE:  (91,dc3606d6b017f60fc10b945ab00b02bd,"2016-06-15 17:53:41.749235")
NOTICE:  (91,"new dc3606d6b017f60fc10b945ab00b02bd","2016-06-15 17:53:41.749235")
...

使用composite type的数组完全能满足原来使用临时表才能满足的需求。
但是也需要注意array是在内存中的,如果临时数据非常庞大,使用数组可能OOM。
虽然临时表也是在内存中的,但是它太大的时候可以落盘,不会OOM。
数组求记录数,使用 array_length就可以了,千万不要unnest转成行再count。

学会擦屁股

如果元数据已经膨胀了怎么处理?该擦的屁股还得擦。
对于PostgreSQL 9.5 你可以放心大胆的擦,(其他版本暂时没有验证过)。
可选的方法有 vacuum full, cluster, reindex.
如果要回收空间,同时重建索引,使用vacuum full。
如果要重排元表顺序,使用cluster。
如果要重新建索引,使用reindex 。
根据你自己的情况选择就可以了。

当你使用的是Greenplum时,要特别注意。
就是pg_class,这个元表是比较特殊的,因为其他元表的记录也记录在这里,包括索引的元表也记录在pg_class里面。
对它进行vacuum full, reindex, cluster都会导致有几个元表的记录会排到这张表的最后面去。

例如我的系统中有几百万张表,这样快速生成几百万表

CREATE OR REPLACE FUNCTION public.f(id integer)
 RETURNS void
 LANGUAGE plpgsql
 STRICT
AS $function$ 
declare 
  sql text;
  i int; 
begin 
  for i in id..id+100 loop 
    sql='create table if not exists tbl'||i||' (id int)';
    execute sql;
  end loop;
end;
$function$;

vi test.sql
\setrandom id 1 1000000000
select f(:id);

pgbench -M prepared -n -r -P 1 -f ./test.sql -c 20 -j 20 -T 1000

对pg_class这样操作后,有几条元数据就会跑到pg_class的最后面去。
.1.

vacuum full pg_class;  

然后你会发现这几个小家伙排到最后了
postgres=# select oid,relname,ctid from pg_class order by ctid desc limit 10;
   oid    |              relname              |    ctid    
----------+-----------------------------------+------------
     1259 | pg_class                          | (92424,30)
     3455 | pg_class_tblspc_relfilenode_index | (92424,29)
     2663 | pg_class_relname_nsp_index        | (92424,28)
     2662 | pg_class_oid_index                | (92424,27)

.2.

cluster pg_class using pg_class_oid_index;  

-- 你可能会想,oid=这几个的是小的,应该到前面的行了吧。 但实际并不是如此。
这几个小家伙排到最后了

postgres=# select oid,relname,ctid from pg_class order by ctid desc limit 10;
   oid    |              relname              |    ctid    
----------+-----------------------------------+------------
     1259 | pg_class                          | (92424,30)
     3455 | pg_class_tblspc_relfilenode_index | (92424,29)
     2663 | pg_class_relname_nsp_index        | (92424,28)
     2662 | pg_class_oid_index                | (92424,27)
 13089945 | test_pkey                         | (92424,25)
 13089944 | pg_toast_13089939_index           | (92424,24)
 13089942 | pg_toast_13089939                 | (92424,23)
 13089939 | test                              | (92424,22)

.3.

reindex table pg_class;  

-- 别看它只是重建索引,但是实际上它也会影响 pg_class_oid_index , pg_class_relname_nsp_index , pg_class_tblspc_relfilenode_index 这三在pg_class的顺序。
这几个小家伙又奇迹般的排到最后了

postgres=# select oid,relname,ctid from pg_class order by ctid desc limit 10;
   oid    |              relname              |    ctid    
----------+-----------------------------------+------------
     3455 | pg_class_tblspc_relfilenode_index | (92424,33)
     2663 | pg_class_relname_nsp_index        | (92424,32)
     2662 | pg_class_oid_index                | (92424,31)
     1259 | pg_class                          | (92424,30)
 13089945 | test_pkey                         | (92424,25)

以上诡异的问题,在Greenplum数据库中就是致命的(不知道现在的版本改进了没有,反正PostgreSQL 9.5是没这个问题的)。
如果你在GP中有几百万的表,这几条元数据因以上原因跑pg_class 的最后几页去了,那问题就大了(后面会讲回天的方法)。
http://cncc.bingj.com/cache.aspx?q=osdba+pg_index+reindex&d=5025474051580513&mkt=zh-CN&setlang=zh-CN&w=pa4BQX_-Pu7tUlh6kM34Dv-xu2uzi4m6

以前老唐在Greenplum上对元表做了这样的操作,结果很感人。
greenplum中reindex pg_class后发生狂读pg_class的问题分析
当访问任何一个表时,需要从pg_class表中提取这个表的元数据信息,而在pg_class中查找表的原数据信息,需要访问索引pg_class_oid_index,而访问索引pg_class_oid_index,也需要从pg_class表中获得索引pg_class_oid_index的元数据信息,而获得索引pg_class_oid_index自己的元数据信息,就不能再通过索引自己去查找自己的信息了,这里就只能从头扫描表pg_class来获得,而由于索引pg_class_oid_index的条目被移到了最后的位置,所以导致需要几乎把pg_class从头 扫描到尾才能找到pg_class_oid_index的条目,这样就大大的降低了数据库的性能。
以前也做过pg_class,为什么没有出来这个问题呢?我和任振中也做了测试发现,当原先的0号块中有空闲空间时,做reindex时,索引pg_class_oid_index的 条目仍会在0号块中,这样就不会出现上面的问题了。
由此可知,在greenplum中是不能随便对pg_class表做reindex了。

如果不小心把pg_class_oid_index等几个元表弄到pg_class 的末端了咋办

其实还是可以回天的,而且方法依旧很简单。
把头几个页的数据更新掉,然后用 vacuum回收前几页,然后把要挪动的记录更新一下,FSM会随机选择空页面给你放,就有机会回到前面的页。
例子
当前他们在这里

postgres=# select ctid,oid,relname from pg_class where oid in (1259,3455,2663,2662);
    ctid    | oid  |              relname              
------------+------+-----------------------------------
 (92424,42) | 1259 | pg_class
 (92424,43) | 2662 | pg_class_oid_index
 (92424,44) | 2663 | pg_class_relname_nsp_index
 (0,1)      | 3455 | pg_class_tblspc_relfilenode_index
(4 rows)

把前面5页的记录都更新一遍

postgres=# update pg_class set relkind=relkind where ctid::text ~ '^\(0,' or ctid::text ~ '^\(1,' or ctid::text ~ '^\(2,' or ctid::text ~ '^\(3,' or ctid::text ~ '^\(4,' or ctid::text ~ '^\(5,';
UPDATE 207

回收垃圾

postgres=# vacuum verbose pg_class;

更新要挪动的记录

postgres=# update pg_class set relkind=relkind where oid in (1259,3455,2663,2662);
UPDATE 4

已经挪到前面的页了,恭喜你

postgres=# select ctid,oid,relname from pg_class where oid in (1259,3455,2663,2662);
  ctid  | oid  |              relname              
--------+------+-----------------------------------
 (0,46) | 1259 | pg_class
 (0,48) | 2662 | pg_class_oid_index
 (1,1)  | 2663 | pg_class_relname_nsp_index
 (1,2)  | 3455 | pg_class_tblspc_relfilenode_index
(4 rows)

参考

src/backend/catalog/postgres.bki

create pg_proc 1255 bootstrap rowtype_oid 81
...
insert OID = 1242 ( boolin 11 10 12 1 0 0 0 f f f f t f i 1 0 16 "2275" _null_ _null_ _null_ _null_ _null_ boolin _null_ _null_ _null_ )
insert OID = 1243 ( boolout 11 10 12 1 0 0 0 f f f f t f i 1 0 2275 "16" _null_ _null_ _null_ _null_ _null_ boolout _null_ _null_ _null_ )
...

注意事项

.1. 因为vacuum full和cluster都是DDL操作,需要注意锁等待和本身需要运行的时间,建议空闲时间操作。
并且设置语句超时或锁超时。
.2. 对于Greenplum 千万别对pg_class执行vacuum full, reindex, cluster操作,否则可能是致命伤害。
万一你不小心这么做了,可以使用我上面教你的挪动记录的方法来补救。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
5月前
|
存储 缓存 监控
如何优化数据库子查询?
【7月更文挑战第22天】如何优化数据库子查询?
111 15
|
5月前
|
SQL 安全 大数据
如何安全的大数据量表在线进行DDL操作
如何安全的大数据量表在线进行DDL操作
74 0
如何安全的大数据量表在线进行DDL操作
|
5月前
分库分表中间表优化
【7月更文挑战第21天】
41 2
|
6月前
|
SQL 关系型数据库 Java
有大批量的数据导入到数据库,规则是数据库有相应主键的就update没有就insert怎么做效率快
有大批量的数据导入到数据库,规则是数据库有相应主键的就update没有就insert怎么做效率快
116 1
|
7月前
|
前端开发 关系型数据库 MySQL
MYSQL基础知识之【临时表】
MYSQL基础知识之【临时表】
72 0
|
存储 运维 安全
数据库运维之InnoDB存储引擎表损坏修复方法
InnoDB存储引擎表的损坏可能是多种因素导致的,比如服务器断电、系统崩溃、硬盘损坏、写数据过程中mysqld进程被kill掉。
1053 0
|
存储 Oracle 关系型数据库
[MySQL优化案例]系列 — 优化InnoDB表BLOB列的存储效率
[MySQL优化案例]系列 — 优化InnoDB表BLOB列的存储效率
187 0
[MySQL优化案例]系列 — 优化InnoDB表BLOB列的存储效率
|
SQL 测试技术
临时表在SQL优化中的作用
今天我们来讲讲临时表的优化技巧 临时表,顾名思义就只是临时使用的一张表,一种是本地临时表,只能在当前查询页面使用,新开查询是不能使用它的,一种是全局临时表,不管开多少查询页面均可使用。
临时表在SQL优化中的作用
|
SQL 存储 运维
PolarDB-X性能优化之多表连接时table group及广播表的使用
通过实验演示多表连接时,PolarDB-X使用table group和广播表优化sql执行
306 0
|
存储 SQL 架构师
频繁插入(insert)的业务,用什么存储引擎更合适? | 数据库系列
虽然MyISAM只支持表锁,但高并发select与insert的业务场景,3个知识点告诉你,MyISAM的表锁依然有非常强劲的性能。
1066 0
频繁插入(insert)的业务,用什么存储引擎更合适? | 数据库系列
下一篇
DataWorks