一、前言
在PostgreSQL中存在两种临时表,一种是会话级的临时表,另一种是事务级的临时表, 更多有关PG临时表的内容,
可以参考我的上一篇文章 关于临时表的二三事 ,此处就不再重复进行描述了。 虽然PostgreSQL存在全局临时表的语法,但是实际上并不具备该功能。
那么怎么在PostgreSQL中使用全局临时表的功能呢? 那便是今天要说的主角——pgtt4.0。今天不仅会给大家带来新版本pgtt安装和使用,还将为大家剖析一下pgtt的设计实现思路。
不过在将这些内容之前,我们先来了解了解 全局临时表这个功能在Oracle数据库中是一个怎么样子的表现。
二、Oracle全局临时表
语法如下:
CREATE GLOBAL TEMPORARY TABLE ...
会话一:
创建全局临时表 temp_ta,插入数据,查询该表
与此同时我们打开一个新的会话,查询一下temp_ta
会话二:
可以看到该表是存在的,但是表中没有任何数据。
所以对于Oracle数据库而言,全局临时表的功能表现为会话间数据不共享,但表结构共享。
也就是说当会话退出时,数据将会被清空,但是临时表的依旧存在。
而与之对应的PostgreSQL的临时表的表现,则为无论是事务级的临时表亦或者是会话级的临时表,当会话退出之后,
对应的临时表(包含数据)都将消失不见。而如果你需要再次使用临时表,则需要重新创建。
在了解完了全局临时表的一个简单的表现之后,接下来让我们来瞅瞅pgtt的安装和使用。
三、pgtt的安装和使用
其实为什么我会选择写pgtt呢?其实还是前不久我在墨天轮看到了一篇关于pgtt的资讯, 印象中大概 是这个 PostgreSQL 全局临时表插件 pgtt v4.0 版本发布 。
在我最开始接触这个插件的时候,它才三点多少的版本,而且 当时还有些不是很稳定(现在不确定),我还顺带给它修复了个异常崩溃的问题,而且当时它使用起来也有点麻烦。
没想到它chua的一下就当4.0了。而且优化了使用方式,看了看站内关于pgtt的都是有些老了的文章,所以我就想写一下介绍一下新版pgtt,主要聊聊实现思路。
3.1、pgtt的安装
项目: https://github.com/darold/pgtt
-- 拉取项目 git clone https://github.com/darold/pgtt.git-- 编译安装make && make install
3.2、pgtt的使用
在4.0版本之前,直接将其加入session_preload_libraries会报出错误,而如果想要轻松简单的使用pgtt的话,可以配合login_hook这个插件使用。使用方式如下
CREATE EXTENSION login_hook;CREATE OR REPLACE FUNCTION login_hook.login() RETURNS VOID LANGUAGE PLPGSQL AS $$DECLAREex_state TEXT; ex_message TEXT; ex_detail TEXT; ex_hint TEXT; ex_context TEXT;BEGINLOAD '$libdir/plugins/pgtt';END$$;GRANT EXECUTE ON FUNCTION login_hook.login() TO PUBLIC;
而在4.0版本之后,仅需要在postgresql.conf配置文件中,添加如下设置即可
session_preload_libraries = 'pgtt'
3.3、一个较为的简单测试
1、完成3.2的设置,在配置文件中新增session_preload_libraries = 'pgtt'
2、安装pgtt插件
3、创建全局临时表temp_ta,插入数据,查询该表
可以看到会爆出一个警告,但是还是成功创建了,这个警告是原生PG的,如果我们不想看的警告的话,可以使用如下方式
接下来插入数据进行查询,然后我们重新登陆,再次查询看临时表是否依旧存在。
可以当重新连接会话之后,数据已经被删除了,但是该全局临时表依旧存在。更多的测试就不再演示了,这不是今天的重点。
实际玩了玩感觉还是存在不少小问题,如果又对这个项目感兴趣的同学可以多多测试,参与到pgtt这个项目中去。
接下来我们来聊聊如何在PG中实现全局临时表的功能。
四、如何实现?
想要在PG中实现全局临时表的功能的话,我们需要寻求Oracle的全局临时表的功能与PG原生的临时表中的差异。
对于PG而言,允许不同的会话创建同名的临时表,会话间数据不共享,当会话结束之后,创建的临时表和表中数据将会自动被删除。
对于Oracle而言,当使用全局临时表的功能之后,仅需要创建一次,其余的诸多会话均可直接使用该临时表,会话间数据不共享,会话退出之后,临时表将被清空。
可以看到其实对于两款数据库而言,关于临时表的差异并不大,而我们如果仔细观察的话,不难看出问题的关键点就在于,如果我们能在新的会话使用到全局临时表的时候,
提前把对应的同名临时表成功创建上,那么就可以达成和Oracle数据库大差不大的功能。
其实pgtt也是这么一个实现思路,当成功创建pgtt拓展的时候,会附加上表 pgtt_schema.pg_global_temp_tables,这张表将用作存储全局临时表的数据信息。
而如果我们需要验证是否是这样的逻辑,我们可以研究一个pgtt源码中的 gtt_create_table_statement函数,这个函数处理当输入全局临时表的SQL语句
/*
* Create the Global Temporary Table with all associated objects
* by creating the template table and register the GTT in the
* pg_global_temp_tables table.
*
*/static Oidgtt_create_table_statement(Gtt gtt){ char *newQueryString = NULL; int connected = 0; int finished = 0; int result = 0;
Oid gttOid = InvalidOid;
Datum oidDatum; bool isnull;
elog(DEBUG1, "proceeding to Global Temporary Table creation.");
connected = SPI_connect(); if (connected != SPI_OK_CONNECT)
ereport(ERROR, (errmsg("could not connect to SPI manager"))); /* Create the "template" table */
newQueryString = psprintf("CREATE UNLOGGED TABLE %s.%s (%s)",
quote_identifier(pgtt_namespace_name),
quote_identifier(gtt.relname),
gtt.code);
result = SPI_exec(newQueryString, 0); if (result < 0)
ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); /* Get Oid of the newly created table */
newQueryString = psprintf("SELECT c.relfilenode FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE c.relname='%s' AND n.nspname = '%s'",
gtt.relname,
pgtt_namespace_name);
result = SPI_exec(newQueryString, 0); if (result != SPI_OK_SELECT && SPI_processed != 1)
ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString)));
oidDatum = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull); if (!isnull)
gttOid = DatumGetInt32(oidDatum); if (isnull || !OidIsValid(gttOid))
ereport(ERROR,
(errmsg("can not get OID of newly created GTT template table %s",
quote_identifier(gtt.relname)))); /* Now register the GTT table */
newQueryString = psprintf("INSERT INTO %s.pg_global_temp_tables VALUES (%d, '%s', '%s', '%c', %s)",
quote_identifier(pgtt_namespace_name),
gttOid,
pgtt_namespace_name,
gtt.relname,
(gtt.preserved) ? 't' : 'f',
quote_literal_cstr(gtt.code)
);
result = SPI_exec(newQueryString, 0); if (result < 0)
ereport(ERROR, (errmsg("can not registrer new global temporary table"))); /* Set privilege on the unlogged table */
newQueryString = psprintf("GRANT ALL ON TABLE %s.%s TO public",
quote_identifier(pgtt_namespace_name),
quote_identifier(gtt.relname));
result = SPI_exec(newQueryString, 0); if (result < 0)
ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); /* Mark the GTT as been created before register the table in the cache */
gtt.created = true;
finished = SPI_finish(); if (finished != SPI_OK_FINISH)
ereport(ERROR, (errmsg("could not disconnect from SPI manager"))); return gttOid;
}
通过代码,可以看到当使用创建全局临时表语句的时候,在pgtt内部,会执行一下相关操作。
1、创建一个同名的UNLOGGED TABLE用作模板表,
2、查询pg_class中有关第一步创建的表的信息
3、将这个关于全局临时表的相关数据插入pg_global_temp_tables表中,用作后续别的会话查询(等会展开)
4、权限调整
这边完成了一个创建全局临时表的操作,相关信息也得到保存。
接下来来瞅瞅一个新的会话使用SELECT语句查询别的会话创建的全局临时表,是如何处理的。而这个时候就要关注另一个 名为gtt_post_parse_analyze的 函数
/*
* Post-parse-analysis hook: mark query with a queryId
*/static void#if PG_VERSION_NUM >= 140000gtt_post_parse_analyze(ParseState *pstate, Query *query, struct JumbleState * jstate)#elsegtt_post_parse_analyze(ParseState *pstate, Query *query)#endif{ /* Try to load pgtt if not already done. */
gtt_try_load(); if (NOT_IN_PARALLEL_WORKER && pgtt_is_enabled && query->rtable != NIL &&
GttHashTable != NULL)
{ /* replace the Oid of the template table by our new table in the rtable */
RangeTblEntry *rte = (RangeTblEntry *) linitial(query->rtable);
Relation rel;
Gtt gtt;
char *name = NULL; /* This must be a valid relation not from pg_catalog*/
if (rte->relid != InvalidOid && rte->relkind == RELKIND_RELATION
&& !is_catalog_relid(rte->relid))
{#if (PG_VERSION_NUM >= 120000)
rel = table_open(rte->relid, NoLock);#else
rel = heap_open(rte->relid, NoLock);#endif
name = RelationGetRelationName(rel);#if (PG_VERSION_NUM >= 120000)
table_close(rel, NoLock);#else
heap_close(rel, NoLock);#endif
gtt.relid = 0;
gtt.temp_relid = 0;
gtt.relname[0] = '\0';
gtt.preserved = false;
gtt.code = NULL;
gtt.created = false; /* Check if the table is in the hash list and it has not already be created */
if (PointerIsValid(name))
{
elog(DEBUG1, "gtt_post_parse_analyze() looking for table \"%s\" with relid %d into cache.", name, rte->relid);
GttHashTableLookup(name, gtt);
} else
elog(ERROR, "gtt_post_parse_analyze() table to search in cache is not valide pointer, relid: %d.", rte->relid); if (gtt.relname[0] != '\0')
{ /* After an error and rollback the table is still registered in cache but must be initialized */
if (gtt.created && OidIsValid(gtt.temp_relid)
&& !SearchSysCacheExists1(RELOID, ObjectIdGetDatum(gtt.temp_relid))
)
{
elog(DEBUG1, "invalid temporary table with relid %d (%s), reseting.", gtt.temp_relid, gtt.relname);
gtt.created = false;
gtt.temp_relid = 0;
} /* Create the temporary table if it does not exists */
if (!gtt.created)
{
elog(DEBUG1, "global temporary table from relid %d does not exists create it: %s", rte->relid, gtt.relname); /* Call create temporary table */
if ((gtt.temp_relid = create_temporary_table_internal(gtt.relid, gtt.preserved)) != InvalidOid)
{
elog(DEBUG1, "global temporary table %s (oid: %d) created", gtt.relname, gtt.temp_relid); /* Update hash list with table flagged as created*/
gtt.created = true;
GttHashTableDelete(gtt.relname);
GttHashTableInsert(gtt, gtt.relname);
} else
elog(ERROR, "can not create global temporary table %s", gtt.relname);
}
elog(DEBUG1, "temporary table exists with oid %d", gtt.temp_relid); if (rte->relid != gtt.temp_relid)
{#if PG_VERSION_NUM >= 160000
RTEPermissionInfo *rteperm = list_nth(query->rteperminfos,
rte->perminfoindex - 1);
rteperm->relid = gtt.temp_relid;#endif
LockRelationOid(gtt.temp_relid, rte->rellockmode); if (rte->rellockmode != AccessShareLock)
UnlockRelationOid(rte->relid, rte->rellockmode);
rte->relid = gtt.temp_relid;
elog(DEBUG1, "rerouting relid %d access to %d for GTT table \"%s\"", rte->relid, gtt.temp_relid, name);
}
} else
/* the table is not a global temporary table do nothing*/
elog(DEBUG1, "table \"%s\" not registered as GTT", name);
}
} /* restore hook */
if (prev_post_parse_analyze_hook) {#if PG_VERSION_NUM >= 140000
prev_post_parse_analyze_hook(pstate, query, jstate);#else
prev_post_parse_analyze_hook(pstate, query);#endif
}
}
而这个函数的关键就在于,/* Create the temporary table if it does not exists */
当使用SELECT查询(其实不止SELECT,INSERT/DELETE/UPDATE同理)时,根据之前保留的信息,如果发现查询的是一个全局临时表,并且发现此时会话中并没有对应的同名的本地临时表,然后将原本查询的表重定向至新创建的本地临时表,从而完成一系列的操作。
刚兴趣的同学可以自己研究研究,看看项目中的README,画画图之类的。碍于篇幅的原因,我这只带大家看了个大概的样貌,见谅见谅。
五、声明
若文中存在错误或不当之处,敬请指出,以便我进行修正和完善。希望这篇文章能够帮助到各位。
文章转载请联系,谢谢合作。
