首页 > 数据库 > 数据库开发技术 > 临时表使用不规范,DBA哭死在厕所
在昨天的文章中,介绍了 Greenplum 中独特的临时表实现,今天在分析临时表的时候,又遇到了一些隐藏十分深的性能问题,在此也多谢家琪同志的帮助。
简而言之,如果在业务中使用了大量的 ON COMMIT DELETE ROWS 的临时表的话,使用不规范的话,那么你可能会面临严重的性能衰减以及锁冲突。
临时表的好处不言而喻:
但是彼之蜜糖,吾之砒霜,有好处自然也有危害。首当其冲的当然是系统表膨胀了,pg_atrribute、pg_class 的膨胀很容易理解,还有一个鲜为人知的是 pg_namespace,看个栗子:
postgres=# create database mydb;
CREATE DATABASE
postgres=# \c mydb
You are now connected to database "mydb" as user "postgres".
mydb=# create temp table t1(id int) on commit drop;
CREATE TABLE
mydb=# \d t1
Did not find any relation named "t1".
mydb=# select * from pg_namespace ;
oid | nspname | nspowner | nspacl
--------+--------------------+----------+---------------------------------------------------------------
99 | pg_toast | 10 |
11 | pg_catalog | 10 | {postgres=UC/postgres,=U/postgres}
2200 | public | 6171 | {pg_database_owner=UC/pg_database_owner,=U/pg_database_owner}
13324 | information_schema | 10 | {postgres=UC/postgres,=U/postgres}
158882 | pg_temp_4 | 10 |
158883 | pg_toast_temp_4 | 10 |
(6 rows)
为了清晰可见,我新建了一个库,可以看到创建的模式是会伴随数据库的,比如此例的 pg_temp_4 和 pg_toast_temp_4 (临时表的TOAST),这一点主要是基于一个会话内可能会创建多个临时表。
关于临时表的使用,有多种方式:
那么问题出在了哪里?首先自然是 temp_buffers,需要遍历,清空所有的 BUFFER,那么 temp_buffers 越大,其性能自然就越低,让我们看一组数字:
[postgres@mypg ~]$ cat run.sql
SET synchronous_commit TO off;
BEGIN;
CREATE TEMP TABLE if not exists x(id int);
INSERT INTO x VALUES (1);
truncate TABLE x;
COMMIT;
[postgres@mypg ~]$ ./test1.sh | grep tps
tps for 8 MB
tps = 832.937970 (without initial connection time)
tps for 32 MB
tps = 792.021925 (without initial connection time)
tps for 128 MB
tps = 711.783284 (without initial connection time)
tps for 256 MB
tps = 595.466552 (without initial connection time)
tps for 512 MB
tps = 443.828044 (without initial connection time)
tps for 1 GB
tps = 302.952138 (without initial connection time)
tps for 8 GB
tps = 55.827146 (without initial connection time)
十分明显。让我们再看一段代码,控制在事务提交之前需要做的一些前置动作:
foreach(l, on_commits)
{
OnCommitItem *oc = (OnCommitItem *) lfirst(l);
/* Ignore entry if already dropped in this xact */
if (oc->deleting_subid != InvalidSubTransactionId)
continue;
switch (oc->oncommit)
{
case ONCOMMIT_NOOP:
case ONCOMMIT_PRESERVE_ROWS:
/* Do nothing (there shouldn't be such entries, actually) */
break;
case ONCOMMIT_DELETE_ROWS:
/*
* If this transaction hasn't accessed any temporary
* relations, we can skip truncating ON COMMIT DELETE ROWS
* tables, as they must still be empty.
*/
if ((MyXactFlags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE))
oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid);
break;
case ONCOMMIT_DROP:
oids_to_drop = lappend_oid(oids_to_drop, oc->relid);
break;
}
}
通过 MyXactFlags 这个全局变量,来判断是否访问过某个 namespace。注意这一段注释:
If this transaction hasn't accessed any temporary relations, we can skip truncating ON COMMIT DELETE ROWS tables, as they must still be empty.
如果该事务没有访问任何临时关系,我们就可以跳过截断 ON COMMIT DELETE ROWS 表,因为它们肯定还是空的。
这个优化是在 9.3 引入的,也就意味着在 9.2 以前的代码中,不管是什么情况,全部一股脑加入到 OID 列表中,然后每个临时表都需要去遍历,恶性循环。
[postgres@mypg postgres]$ git show c9d7dbacd387ab3814bc6b38010a9e72a02ea4f5
commit c9d7dbacd387ab3814bc6b38010a9e72a02ea4f5
Author: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue Jan 29 10:40:22 2013 +0200
Skip truncating ON COMMIT DELETE ROWS temp tables, if the transaction hasn't
touched any temporary tables.
We could try harder, and keep track of whether we've inserted to any temp
tables, rather than accessed them, and which temp tables have been inserted
to. But this is dead simple, and already covers many interesting scenarios.
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6bc056b..1d5e0c6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10124,7 +10124,13 @@ PreCommit_on_commit_actions(void)
/* Do nothing (there shouldn't be such entries, actually) */
break;
case ONCOMMIT_DELETE_ROWS:
- oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid);
+ /*
+ * If this transaction hasn't accessed any temporary
+ * relations, we can skip truncating ON COMMIT DELETE ROWS
+ * tables, as they must still be empty.
+ */
+ if (MyXactAccessedTempRel)
+ oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid);
break;
case ONCOMMIT_DROP:
{
那让我们验证一下,基于 16,创建一个 on commit delete rows 的临时表,并且在事务中访问一下
postgres=# create temp table tt1(id int) on commit delete rows;
CREATE TABLE
postgres=# begin;
BEGIN
postgres=*# select relfilenode from pg_class where relname = 'tt1';
relfilenode
-------------
158890
(1 row)
postgres=*# select 1 from tt1;
?column?
----------
(0 rows)
可以很清晰地看到,此临时表会被加入到 oids_to_truncate
Breakpoint 1, PreCommit_on_commit_actions () at tablecmds.c:16828
(gdb) n
(gdb) p oc->relid
$1 = 158890 ---👈🏻tt1临时表在这里
不难想象,如果事务内访问的临时表越多 ,虽然想不出什么场景下去访问一个已经是空的表,但是比如业务代码中必须关联的迷惑行为?
也正如注释所说,如果该事务没有访问任何临时关系,我们就可以跳过截断 ON COMMIT DELETE ROWS 表,因为它们肯定还是空的。
那么让我们再看一下最新的 Greenplum 代码,尴尬的是,这一段被完全注释了,也就意味着,不管什么情况下,只要你在 Greenplum 中用到了 ONCOMMIT_DELETE_ROWS 的临时表,即使没有访问过,也需要全部 TRUNCATE,再叠加 Greenplum 中的临时表是放在 shared_buffers 中的...
switch (oc->oncommit)
{
case ONCOMMIT_NOOP:
case ONCOMMIT_PRESERVE_ROWS:
/* Do nothing (there shouldn't be such entries, actually) */
break;
case ONCOMMIT_DELETE_ROWS:
#if 0
/*
* If this transaction hasn't accessed any temporary
* relations, we can skip truncating ON COMMIT DELETE ROWS
* tables, as they must still be empty.
*/
if ((MyXactFlags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE))
#endif
oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid);
break;
case ONCOMMIT_DROP:
oids_to_drop = lappend_oid(oids_to_drop, oc->relid);
break;
}
让我们小结一下
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=c9d7dbacd387ab3814bc6b38010a9e72a02ea4f5

推荐阅读
原文作者:PostgreSQL学徒;如有侵权,请联系管理员删除。