让PostgreSQL拥抱全局临时表功能

来源:这里教程网 时间:2026-03-14 21:35:39 作者:

一、前言

在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,画画图之类的。碍于篇幅的原因,我这只带大家看了个大概的样貌,见谅见谅。

五、声明

若文中存在错误或不当之处,敬请指出,以便我进行修正和完善。希望这篇文章能够帮助到各位。

文章转载请联系,谢谢合作。

相关推荐