Subject: [Proposal] Global temporary tables

From: "曾文旌(义从)" <[email-masked]>
Date: Fri, 11 Oct 2019 20:15:27 +0800

Dear Hackers,

This propose a way to develop global temporary tables in PostgreSQL.

I noticed that there is an "Allow temporary tables to exist as empty by default in all sessions" in the postgresql todolist.
https://wiki.postgresql.org/wiki/Todo <https://wiki.postgresql.org/wiki/Todo>

In recent years, PG community had many discussions about global temp table (GTT) support. Previous discussion covered the following topics: 
(1)	The main benefit or function: GTT offers features like “persistent schema, ephemeral data”, which avoids catalog bloat and reduces catalog vacuum. 
(2)	Whether follows ANSI concept of temporary tables
(3)	How to deal with statistics, single copy of schema definition, relcache
(4)	More can be seen in https://www.postgresql.org/message-id/73954ab7-44d3-b37b-81a3-69bdcbb446f7%40postgrespro.ru
(5)	A recent implementation and design from Konstantin Knizhnik covered many functions of GTT: https://www.postgresql.org/message-id/attachment/103265/global_private_temp-1.patch <https://www.postgresql.org/message-id/attachment/103265/global_private_temp-1.patch>

However, as pointed by Konstantin himself, the implementation still needs functions related to CLOG, vacuum, and MVCC visibility.

We developed GTT based on PG 11 and included most needed features, such as how to deal with concurrent DDL and DML operations, how to handle vacuum and too old relfrozenxids, and how to store and access GTT statistics. 

This design followed many suggestions from previous discussion in community. Here are some examples:
	“have a separate 'relpersistence' setting for global temp tables…by having the backend id in all filename….   From Andres Freund
	Use session memory context to store information related to GTT.   From Pavel Stehule
	“extend the relfilenode mapper to support a backend-local non-persistent relfilenode map that's used to track temp table and index relfilenodes…” from Craig Ringer

Our implementation creates one record in pg_class for GTT’s schema definition. When rows are first inserted into the GTT in a session, a session specific file is created to store the GTT’s data. Those files are removed when the session ends. We maintain the GTT’s statistics in session local memory. DDL operations, such as DROP table or CREATE INDEX, can be executed on a GTT only by one session, while no other sessions insert any data into the GTT before or it is already truncated. This also avoids the concurrency of DML and DDL operations on GTT. We maintain a session level oldest relfrozenxids for GTT. This way, autovacuum or vacuum can truncate CLOG and increase global relfrozenxids based on all tables’ relfrozenxids, including GTT’s. 
The follows summarize the main design and implementation: 
	Syntax: ON COMMIT PRESERVE ROWS and ON COMMIT DELETE ROWS
	Data storage and buffering follows the same way as local temp table with a relfilenode including session id.
	A hash table(A) in shared memory is used to track sessions and their usage of GTTs and to serialize DDL and DML operations. 
	Another hash table(B) in session memory is introduced to record storage files for GTTs and their indexes. When a session ends, those files are removed. 
	The same hash table(B) in session memory is used to record the relfrozenxids of each GTT. The oldest one is stored in myproc so that autovacuum and vacuum may use it to determine global oldest relfrozenxids and truncate clog. 
	The same hash table(B) in session memory stores GTT’s session level statistics, It is generated during the operations of vacuum and analyze, and used by SQL optimizer to create execution plan. 
	Some utility functions are added for DBA to manage GTTs. 
	TRUNCATE command on a GTT behaves differently from that on a normal table. The command deletes the data immediately but keeps relfilenode using lower level table lock, RowExclusiveLock, instead of  AccessExclusiveLock. 
	Main limits of this version or future improvement: need suggestions from community: 
		1 VACUUM FULL and CLUSTER are not supported; any operations which may change relfilenode are disabled to GTT.
		2 Sequence column is not supported in GTT for now.
		3 Users defined statistics is not supported.

Details:

Requirement
The features list about global temp table:
	1. global temp table (ON COMMIT clause is omitted, SQL specifies that the default behavior is ON COMMIT DELETE ROWS)
	2. support with on commit DELETE ROWS
	3. support with on commit PRESERVE ROWS
	4. not support ON COMMIT DROP

Feature description
Global temp tables are defined just once and automatically exist (starting with empty contents) in every session that needs them.
Global temp table, each session use local buffer, read or write independent data files.
Use on commit DELETE ROWS for a transaction-specific global temp table. This is the default. database will truncate the table (delete all its rows) after each commit.
Use on commit PRESERVE ROWS Specify PRESERVE ROWS for a session-specific global temp table. databse will truncate the table (delete all its rows) when you terminate the session.

design
Global temp tables are designed based on local temp table(buffer and storage files). 
Because the catalog of global temp table is shared between sessions but the data is not shared, we need to build some new mechanisms to manage non-shared data and statistics for those data.

1. catalog
1.1 relpersistence
define RELPERSISTENCEGLOBALTEMP 'g'
Mark global temp table in pg_class relpersistence to 'T'. The relpersistence of the index created on the global temp table is also set to ’T'

1.2 on commit clause
In local temp table on commit DELETE ROWS and on commit PRESERVE ROWS not store in catalog, but GTT need.
Store a bool value oncommitdelete_rows to reloptions only for GTT and share with other session.

2. gram.y
Global temp table already has a syntax tree. jush need to remove the warning message "GLOBAL is deprecated in temporary table creation" and mark relpersistence = RELPERSISTENCEGLOBALTEMP

3. STORAGE
3.1. active_gtt_shared_hash
create a hash table in shared memory to trace the GTT files that are initialized in each session. 
Each hash entry contains a bitmap that records the backendid of the initialized GTT file.
With this hash table, we know which backend/session are using this GTT.
It will be used in GTT's DDL.

3.2. gtt_storage_local_hash
In each backend, create a local hashtable gtt_storage_local_hash for tracks GTT storage file and statistics.
1). GTT storage file track
When one session inserts data into a GTT for the first time, record to local hash.
2). normal clean GTT files
Use beforeshmemexit to ensure that all files for the session GTT are deleted when the session exits.
3). abnormal situation file cleanup
When a backend exits abnormally (such as oom kill), the startup process started to recovery before accept connect. startup process check and remove all GTT files before redo wal.

4 DDL
4.1 DROP GTT
One GTT table is allowed to be deleted when only the current session USES it. After get the AccessExclusiveLock of the GTT table, use active_gtt_shared_hash to check and make sure that.

4.2 ALTER GTT
Same as drop GTT.

4.3 CREATE INDEX ON GTT, DROP INDEX ON GTT
Same as drop GTT.

4.4 TRUNCATE GTT
The truncate GTT use RowExclusiveLock, not AccessExclusiveLock, Because truncate only cleans up local data file and local buffers in this session.
Also, truncate immediately deletes the data file without changing the relfilenode of the GTT table. btw, I'm not sure the implementation will be acceptable to the community.

4.5  create index on GTT
Same as drop GTT.

4.6 OTHERS
Any table operations about GTT that need to change relfilenode are disabled, such as vacuum full/cluster.

5. The statistics of GTT
	1 relpages reltuples relallvisible frozenxid minmulti from pg_class
	2 The statistics for each column from pg_statistic
All the above information will be stored to gtt_storage_local_hash.
When vacuum or analyze GTT's statistic will update, and the planner will use them. Of course, statistics only contain data within the current session.

5.1. View global temp table statistics
Provide pggttattstatistic get column statistics for GTT. Provide pggtt_relstats to rel statistics for GTT.
These functions are implemented in a plug-in, without add system view or function.

6. autovacuum
Autovacuum skips all GTT.

7. vacuum(frozenxid push, clog truncate)
The GTT data file contains transaction information. Queries for GTT data rely on transaction information such as clog. That's can not be vacuumed automatically by vacuum.
7.1 The session level gtt oldest frozenxid
When one GTT been create or remove, record the session level oldest frozenxid and put it into MyProc. 

7.1 vacuum
When vacuum push the db's frozenxid(vacupdatedatfrozenxid), need to consider the GTT. It needs to calculate the transactions required for the GTT(search all MyPorc), to avoid the clog required by GTT being cleaned.

8. Parallel query
Planner does not produce parallel query plans for SQL related to global temp table.

9. Operability
Provide pggttattachedpid lists all the pids that are using the GTT. Provide pglistgttrelfrozenxids lists the session level oldest frozenxid of using GTT.
These functions are implemented in a plug-in, without add system view or function.
DBA can use the above function and pgterminatebackend to force the cleanup of "too old" GTT tables and sessions.

10. Limitations and todo list
10.1. alter GTT
10.2. pg_statistic_ext
10.3. remove GTT's relfilenode can not change limit.
cluster/vacuum full, optimize truncate gtt.
10.4. SERIAL column type
The GTT from different sessions share a sequence(SERIAL type).
Need each session use the sequence independently.
10.5. Locking optimization for GTT.
10.6 materialized views is not support on GTT.

What do you thinking about this proposal?
Looking forward to your feedback.

Thanks!

regards

--
Zeng Wenjing
Alibaba Group-Database Products Business Unit

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 11 Oct 2019 16:50:07 +0300

On 11.10.2019 15:15, 曾文旌(义从) wrote:
> I noticed that there is an "Allow temporary tables to exist as empty 
> by default in all sessions" in the postgresql todolist.
> https://wiki.postgresql.org/wiki/Todo
>
> In recent years, PG community had many discussions about global temp 
> table (GTT) support. Previous discussion covered the following topics:
> (1)The main benefit or function: GTT offers features like “persistent 
> schema, ephemeral data”, which avoids catalog bloat and reduces 
> catalog vacuum.
> (2)Whether follows ANSI concept of temporary tables
> (3)How to deal with statistics, single copy of schema definition, relcache
> (4)More can be seen in 
> https://www.postgresql.org/message-id/73954ab7-44d3-b37b-81a3-69bdcbb446f7%40postgrespro.ru
> (5)A recent implementation and design from Konstantin Knizhnik covered 
> many functions of GTT: 
> https://www.postgresql.org/message-id/attachment/103265/global_private_temp-1.patch
>
> However, as pointed by Konstantin himself, the implementation still 
> needs functions related to CLOG, vacuum, and MVCC visibility.
>

Just to clarify.
I have now proposed several different solutions for GTT:

Shared vs. private buffers for GTT:
1. Private buffers. This is least invasive patch, requiring no changes 
in relfilenodes.
2. Shared buffers. Requires changing relfilenode but supports parallel 
query execution for GTT.

Access to GTT at replica:
1. Access is prohibited (as for original temp tables). No changes at all.
2. Tuples of temp tables are marked with forzen XID.  Minimal changes, 
rollbacks are not possible.
3. Providing special XIDs for GTT at replica. No changes in CLOG are 
required, but special MVCC visibility rules are used for GTT. Current 
limitation: number of transactions accessing GTT at replica is limited 
by 2^32
and bitmap of correspondent size has to be maintained (tuples of GTT are 
not proceeded by vacuum and not frozen, so XID horizon never moved).

So except the limitation mentioned above (which I do not consider as 
critical) there is only one problem which was not addressed: maintaining 
statistics for GTT.
If all of the following conditions are true:

1) GTT are used in joins
2) There are indexes defined for GTT
3) Size and histogram of GTT in different backends can significantly vary.
4) ANALYZE was explicitly called for GTT

then query execution plan built in one backend will be also used for 
other backends where it can be inefficient.
I also do not consider this problem as "show stopper" for adding GTT to 
Postgres.

I still do not understand the opinion of community which functionality 
of GTT is considered to be most important.
But the patch with local buffers and no replica support is small enough 
to become good starting point.

-- 
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 12 Oct 2019 07:16:56 +0200

pá 11. 10. 2019 v 15:50 odesílatel Konstantin Knizhnik <
[email-masked]> napsal:

> I noticed that there is an "Allow temporary tables to exist as empty by
> default in all sessions" in the postgresql todolist.
> (1) The main benefit or function: GTT offers features like “persistent
> schema, ephemeral data”, which avoids catalog bloat and reduces catalog
> vacuum.
> (4) More can be seen in
> (5) A recent implementation and design from Konstantin Knizhnik covered
> many functions of GTT:
> However, as pointed by Konstantin himself, the implementation still needs
> functions related to CLOG, vacuum, and MVCC visibility.
>
>
> Just to clarify.
> 1. Private buffers. This is least invasive patch, requiring no changes in
> relfilenodes.

This is important argument for using share buffers. Maybe the best is mix
of both - store files in temporal tablespace, but using share buffers.
More, it can be accessible for autovacuum.

> limitation: number of transactions accessing GTT at replica is limited by
> 2^32
> then query execution plan built in one backend will be also used for other
> backends where it can be inefficient.

The last issue is show stopper in my mind. It really depends on usage.
There are situation where shared statistics are ok (and maybe good
solution), and other situation, where shared statistics are just unusable.

Regards

Pavel

> I still do not understand the opinion of community which functionality of
> GTT is considered to be most important.
> But the patch with local buffers and no replica support is small enough to
> become good starting point.
>
>
> --
> Konstantin Knizhnik

---

From: "曾文旌(义从)" <[email-masked]>
Date: Tue, 15 Oct 2019 17:49:56 +0800

> 2019年10月12日 下午1:16，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> pá 11. 10. 2019 v 15:50 odesílatel Konstantin Knizhnik <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
>> (4)	More can be seen in https://www.postgresql.org/message-id/73954ab7-44d3-b37b-81a3-69bdcbb446f7%40postgrespro.ru <https://www.postgresql.org/message-id/73954ab7-44d3-b37b-81a3-69bdcbb446f7%40postgrespro.ru>
> 1. Private buffers. This is least invasive patch, requiring no changes in relfilenodes.
> 2. Shared buffers. Requires changing relfilenode but supports parallel query execution for GTT.
> 
> This is important argument for using share buffers. Maybe the best is mix of both - store files in temporal tablespace, but using share buffers. More, it can be accessible for autovacuum.
> 
> 2. Tuples of temp tables are marked with forzen XID.  Minimal changes, rollbacks are not possible.
> 3. Providing special XIDs for GTT at replica. No changes in CLOG are required, but special MVCC visibility rules are used for GTT. Current limitation: number of transactions accessing GTT at replica is limited by 2^32
> and bitmap of correspondent size has to be maintained (tuples of GTT are not proceeded by vacuum and not frozen, so XID horizon never moved).
> 
> So except the limitation mentioned above (which I do not consider as critical) there is only one problem which was not addressed: maintaining statistics for GTT. 
> then query execution plan built in one backend will be also used for other backends where it can be inefficient.
> I also do not consider this problem as "show stopper" for adding GTT to Postgres.
> 
> The last issue is show stopper in my mind. It really depends on usage. There are situation where shared statistics are ok (and maybe good solution), and other situation, where shared statistics are just unusable.
This proposal calculates and stores independent statistics(relpages reltuples and histogram of GTT) for the gtt data within each session, ensuring optimizer can get accurate statistics.

> Regards
> 
> Pavel
> 
> 
> 
> I still do not understand the opinion of community which functionality of GTT is considered to be most important.
> But the patch with local buffers and no replica support is small enough to become good starting point.
> 
> 
> -- 
> Konstantin Knizhnik
> Postgres Professional: http://www.postgrespro.com <http://www.postgrespro.com/>

---

From: "曾文旌(义从)" <[email-masked]>
Date: Thu, 17 Oct 2019 18:18:34 +0800

> 2019年10月11日 下午9:50，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
When session A writes 10000000 rows of data to gtt X, session B also uses X at the same time and it has 100 rows of different data. If B uses analyze to count the statistics of 100000 rows of data and updates it to catalog.
Obviously, session A will get inaccurate query plan based on misaligned statistics when calculating the query plan for X related queries. Session A may think that table X is too small to be worth using index scan, but it is not. Each session needs to get the statistics of the self data to make the query plan.

> But the patch with local buffers and no replica support is small enough to become good starting point.
Yes ,the first step, we focus on complete basic functions of gtt (dml ddl index on gtt (MVCC visibility rules) storage).
Abnormal statistics can cause problems with index selection on gtt, so index on gtt and accurate statistical information is necessary.

---

From: Robert Haas <[email-masked]>
Date: Fri, 25 Oct 2019 11:01:12 -0400

> 2. Shared buffers. Requires changing relfilenode but supports parallel query execution for GTT.

I vote for #1. I think parallel query for temp objects may be a
desirable feature, but I don't think it should be the job of a patch
implementing GTTs to make it happen. In fact, I think it would be an
actively bad idea, because I suspect that if we do eventually support
temp relations for parallel query, we're going to want a solution that
is shared between regular temp tables and global temp tables, not
separate solutions for each.

> and bitmap of correspondent size has to be maintained (tuples of GTT are not proceeded by vacuum and not frozen, so XID horizon never moved).

I again vote for #1. A GTT is defined to allow data to be visible only
within one session -- so what does it even mean for the data to be
accessible on a replica?

> I also do not consider this problem as "show stopper" for adding GTT to Postgres.

I think that's *definitely* a show stopper.

> But the patch with local buffers and no replica support is small enough to become good starting point.

Well, it seems we now have two patches for this feature. I guess we
need to figure out which one is better, and whether it's possible for
the two efforts to be merged, rather than having two different teams
hacking on separate code bases.

-- 
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 25 Oct 2019 17:13:30 +0200

> within one session -- so what does it even mean for the data to be

why not? there are lot of sessions on replica servers. One usage of temp
tables is fixing estimation errors. You can create temp table with partial
query result, run ANALYZE and evaluate other steps. Now this case is not
possible on replica servers.

One motivation for GTT  is decreasing port costs from Oracle. But other
motivations, like do more complex calculations on replica are valid and
valuable.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 25 Oct 2019 19:22:23 +0300

> separate solutions for each.

Sorry, may be I do not not understand you.
It seems to me that there is only one thing preventing usage of 
temporary tables in parallel plans: private buffers.
If global temporary tables are accessed as normal tables though shared 
buffers then them can be used in parallel queries
and no extra support is required for it.
At least I have checked that parallel queries are correctly worked for 
my implementation of GTT with shared buffers.
So I do not understand about which "separate solutions" you are talking 
about.

I can agree that private buffers may be  good starting point for GTT 
implementation, because it is less invasive and GTT access speed is 
exactly the same as of normal temp tables.
But I do not understand your argument why it is "actively bad idea".

> within one session -- so what does it even mean for the data to be

There are sessions at replica (in case of hot standby), aren't there?

> I think that's *definitely* a show stopper.
Well, if both you and Pavel think that it is really "show stopper", then 
this problem really has to be addressed.
I slightly confused about this opinion, because Pavel has told me 
himself that 99% of users never create indexes for temp tables
or run "analyze" for them. And without it, this problem is not a problem 
at all.

> hacking on separate code bases.

I am open for cooperations.
Source code of all my patches is available.

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 25 Oct 2019 19:00:06 +0200

> or run "analyze" for them. And without it, this problem is not a problem
Users doesn't do ANALYZE on temp tables in 99%. It's true. But second fact
is so users has lot of problems. It's very similar to wrong statistics on
persistent tables. When data are small, then it is not problem for users,
although from my perspective it's not optimal. When data are not small,
then the problem can be brutal. Temporary tables are not a exception. And
users and developers are people - we know only about fatal problems. There
are lot of unoptimized queries, but because the problem is not fatal, then
it is not reason for report it. And lot of people has not any idea how fast
the databases can be. The knowledges of  users and app developers are sad
book.

Pavel

---

From: "曾文旌(义从)" <[email-masked]>
Date: Mon, 28 Oct 2019 15:15:18 +0800

> 2019年10月26日 上午12:22，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
> It seems to me that there is only one thing preventing usage of temporary tables in parallel plans: private buffers.
> If global temporary tables are accessed as normal tables though shared buffers then them can be used in parallel queries
> At least I have checked that parallel queries are correctly worked for my implementation of GTT with shared buffers.
> So I do not understand about which "separate solutions" you are talking about.
> 
> I can agree that private buffers may be  good starting point for GTT implementation, because it is less invasive and GTT access speed is exactly the same as of normal temp tables.
> Well, if both you and Pavel think that it is really "show stopper", then this problem really has to be addressed.
> I slightly confused about this opinion, because Pavel has told me himself that 99% of users never create indexes for temp tables
> or run "analyze" for them. And without it, this problem is not a problem at all.
> 
We are also willing to cooperate to complete this feature.
Let me prepare the code(merge code to pg12) and up to community, then see how we work together.

---

From: Robert Haas <[email-masked]>
Date: Mon, 28 Oct 2019 08:07:07 -0400

On Fri, Oct 25, 2019 at 11:14 AM Pavel Stehule <[email-masked]> wrote:
> why not? there are lot of sessions on replica servers. One usage of temp tables is fixing estimation errors. You can create temp table with partial query result, run ANALYZE and evaluate other steps. Now this case is not possible on replica servers.
>
> One motivation for GTT  is decreasing port costs from Oracle. But other motivations, like do more complex calculations on replica are valid and valuable.

Hmm, I think I was slightly confused when I wrote my previous
response. I now see that what was under discussion was not making data
from the master visible on the standbys, which really wouldn't make
any sense, but rather allowing standby sessions to also use the GTT,
each with its own local copy of the data. I don't think that's a bad
feature, but look how invasive the required changes are. Not allowing
rollbacks seems dead on arrival; an abort would be able to leave the
table and index mutually inconsistent.  A separate XID space would be
a real solution, perhaps, but it would be *extremely* complicated and
invasive to implement.

One thing that I've learned over and over again as a developer is that
you get a lot more done if you tackle one problem at a time. GTTs are
a sufficiently-large problem all by themselves; a major reworking of
the way XIDs work might be a good project to undertake at some point,
but it doesn't make any sense to incorporate that into the GTT
project, which is otherwise about a mostly-separate set of issues.
Let's not try to solve more problems at once than strictly necessary.

---

From: Robert Haas <[email-masked]>
Date: Mon, 28 Oct 2019 08:13:46 -0400

> But I do not understand your argument why it is "actively bad idea".

Well, it sounds like you're talking about ending up in a situation
where local temporary tables are still in private buffers, but global
temporary table data is in shared buffers. I think that would be
inconsistent. And it would mean that when somebody wanted to make
local temporary tables accessible in parallel query, they'd have to
write a patch for that.  In other words, I don't support dividing the
patches like this:

Patch #1: Support global temporary tables + allow global temporary
tables to used by parallel query
Patch #2: Allow local temporary tables to be used by parallel query

I support dividing them like this:

Patch #1: Support global temporary tables
Patch #2: Allow (all kinds of) temporary tables to be used by parallel query

The second division looks a lot cleaner to me, although as always I
might be missing something.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Mon, 28 Oct 2019 16:37:50 +0300

> a real solution, perhaps, but it would be *extremely* complicated and

Sorry, but both statements are not true.
As I mentioned before, I have implemented both solutions.

I am not sure how vital is lack of aborts for transactions working with 
GTT at replica.
Some people said that there is no sense in aborts of read-only 
transactions at replica (despite to the fact that them are saving 
intermediate results in GTT).
Some people said something similar with your's "dead on arrival".
But inconsistency is not possible: if such transaction is really 
aborted, then backend is terminated and nobody can see this inconsistency.

Concerning second alternative: you can check yourself that it is not 
*extremely* complicated and invasive.
I extracted changes which are related with handling transactions at 
replica and attached them to this mail.
It is just 500 lines (including diff contexts). Certainly there are some 
limitation of this implementation: number of  transactions working with 
GTT at replica is limited by 2^32
and since GTT tuples are not frozen, analog of GTT CLOG kept in memory 
is never truncated.

> Let's not try to solve more problems at once than strictly necessary.
I agree with it and think that implementation of GTT with private 
buffers and no replica access is good starting point.

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index e954482..2f5f5ef 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -444,7 +444,7 @@ heapgetpage(TableScanDesc sscan, BlockNumber page)
 			if (all_visible)
 				valid = true;
 			else
-				valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
+				valid = HeapTupleSatisfiesVisibility(scan->rs_base.rs_rd, &loctup, snapshot, buffer);
 
 			CheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
 											&loctup, buffer, snapshot);
@@ -664,7 +664,8 @@ heapgettup(HeapScanDesc scan,
 				/*
 				 * if current tuple qualifies, return it.
 				 */
-				valid = HeapTupleSatisfiesVisibility(tuple,
+				valid = HeapTupleSatisfiesVisibility(scan->rs_base.rs_rd,
+													 tuple,
 													 snapshot,
 													 scan->rs_cbuf);
 
@@ -1474,7 +1475,7 @@ heap_fetch(Relation relation,
 	/*
 	 * check tuple visibility, then release lock
 	 */
-	valid = HeapTupleSatisfiesVisibility(tuple, snapshot, buffer);
+	valid = HeapTupleSatisfiesVisibility(relation, tuple, snapshot, buffer);
 
 	if (valid)
 		PredicateLockTuple(relation, tuple, snapshot);
@@ -1609,7 +1610,7 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 		if (!skip)
 		{
 			/* If it's visible per the snapshot, we must return it */
-			valid = HeapTupleSatisfiesVisibility(heapTuple, snapshot, buffer);
+			valid = HeapTupleSatisfiesVisibility(relation, heapTuple, snapshot, buffer);
 			CheckForSerializableConflictOut(valid, relation, heapTuple,
 											buffer, snapshot);
 
@@ -1749,7 +1750,7 @@ heap_get_latest_tid(TableScanDesc sscan,
 		 * Check tuple visibility; if visible, set it as the new result
 		 * candidate.
 		 */
-		valid = HeapTupleSatisfiesVisibility(&tp, snapshot, buffer);
+		valid = HeapTupleSatisfiesVisibility(relation, &tp, snapshot, buffer);
 		CheckForSerializableConflictOut(valid, relation, &tp, buffer, snapshot);
 		if (valid)
 			*tid = ctid;
@@ -1846,6 +1847,14 @@ ReleaseBulkInsertStatePin(BulkInsertState bistate)
 }

+static TransactionId
+GetTransactionId(Relation relation)
+{
+	return relation->rd_rel->relpersistence == RELPERSISTENCE_SESSION && RecoveryInProgress()
+		? GetReplicaTransactionId()
+		: GetCurrentTransactionId();
+}
+
 /*
  *	heap_insert		- insert tuple into a heap
  *
@@ -1868,7 +1877,7 @@ void
 heap_insert(Relation relation, HeapTuple tup, CommandId cid,
 			int options, BulkInsertState bistate)
 {
-	TransactionId xid = GetCurrentTransactionId();
+	TransactionId xid = GetTransactionId(relation);
 	HeapTuple	heaptup;
 	Buffer		buffer;
 	Buffer		vmbuffer = InvalidBuffer;
@@ -2105,7 +2114,7 @@ void
 heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 				  CommandId cid, int options, BulkInsertState bistate)
 {
-	TransactionId xid = GetCurrentTransactionId();
+	TransactionId xid = GetTransactionId(relation);
 	HeapTuple  *heaptuples;
 	int			i;
 	int			ndone;
@@ -2444,7 +2453,7 @@ heap_delete(Relation relation, ItemPointer tid,
 			TM_FailureData *tmfd, bool changingPart)
 {
 	TM_Result	result;
-	TransactionId xid = GetCurrentTransactionId();
+	TransactionId xid = GetTransactionId(relation);
 	ItemId		lp;
 	HeapTupleData tp;
 	Page		page;
@@ -2509,7 +2518,7 @@ heap_delete(Relation relation, ItemPointer tid,
 	tp.t_self = *tid;
 
 l1:
-	result = HeapTupleSatisfiesUpdate(&tp, cid, buffer);
+	result = HeapTupleSatisfiesUpdate(relation, &tp, cid, buffer);
 
 	if (result == TM_Invisible)
 	{
@@ -2628,7 +2637,7 @@ l1:
 	if (crosscheck != InvalidSnapshot && result == TM_Ok)
 	{
 		/* Perform additional check for transaction-snapshot mode RI updates */
-		if (!HeapTupleSatisfiesVisibility(&tp, crosscheck, buffer))
+		if (!HeapTupleSatisfiesVisibility(relation, &tp, crosscheck, buffer))
 			result = TM_Updated;
 	}
 
@@ -2895,7 +2904,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 			TM_FailureData *tmfd, LockTupleMode *lockmode)
 {
 	TM_Result	result;
-	TransactionId xid = GetCurrentTransactionId();
+	TransactionId xid = GetTransactionId(relation);
 	Bitmapset  *hot_attrs;
 	Bitmapset  *key_attrs;
 	Bitmapset  *id_attrs;
@@ -3065,7 +3074,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 l2:
 	checked_lockers = false;
 	locker_remains = false;
-	result = HeapTupleSatisfiesUpdate(&oldtup, cid, buffer);
+	result = HeapTupleSatisfiesUpdate(relation, &oldtup, cid, buffer);
 
 	/* see below about the "no wait" case */
 	Assert(result != TM_BeingModified || wait);
@@ -3258,7 +3267,7 @@ l2:
 	if (crosscheck != InvalidSnapshot && result == TM_Ok)
 	{
 		/* Perform additional check for transaction-snapshot mode RI updates */
-		if (!HeapTupleSatisfiesVisibility(&oldtup, crosscheck, buffer))
+		if (!HeapTupleSatisfiesVisibility(relation, &oldtup, crosscheck, buffer))
 		{
 			result = TM_Updated;
 			Assert(!ItemPointerEquals(&oldtup.t_self, &oldtup.t_data->t_ctid));
@@ -4014,7 +4023,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple,
 	tuple->t_tableOid = RelationGetRelid(relation);
 
 l3:
-	result = HeapTupleSatisfiesUpdate(tuple, cid, *buffer);
+	result = HeapTupleSatisfiesUpdate(relation, tuple, cid, *buffer);
 
 	if (result == TM_Invisible)
 	{
@@ -4189,7 +4198,7 @@ l3:
 					TM_Result	res;
 
 					res = heap_lock_updated_tuple(relation, tuple, &t_ctid,
-												  GetCurrentTransactionId(),
+												  GetTransactionId(relation),
 												  mode);
 					if (res != TM_Ok)
 					{
@@ -4437,7 +4446,7 @@ l3:
 				TM_Result	res;
 
 				res = heap_lock_updated_tuple(relation, tuple, &t_ctid,
-											  GetCurrentTransactionId(),
+											  GetTransactionId(relation),
 											  mode);
 				if (res != TM_Ok)
 				{
@@ -4546,7 +4555,7 @@ failed:
 	 * state if multixact.c elogs.
 	 */
 	compute_new_xmax_infomask(xmax, old_infomask, tuple->t_data->t_infomask2,
-							  GetCurrentTransactionId(), mode, false,
+							  GetTransactionId(relation), mode, false,
 							  &xid, &new_infomask, &new_infomask2);
 
 	START_CRIT_SECTION();
@@ -5566,7 +5575,7 @@ heap_finish_speculative(Relation relation, ItemPointer tid)
 void
 heap_abort_speculative(Relation relation, ItemPointer tid)
 {
-	TransactionId xid = GetCurrentTransactionId();
+	TransactionId xid = GetTransactionId(relation);
 	ItemId		lp;
 	HeapTupleData tp;
 	Page		page;
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 2dd8821..67a553a 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -226,7 +226,8 @@ heapam_tuple_satisfies_snapshot(Relation rel, TupleTableSlot *slot,
 	 * Caller should be holding pin, but not lock.
 	 */
 	LockBuffer(bslot->buffer, BUFFER_LOCK_SHARE);
-	res = HeapTupleSatisfiesVisibility(bslot->base.tuple, snapshot,
+
+	res = HeapTupleSatisfiesVisibility(rel, bslot->base.tuple, snapshot,
 									   bslot->buffer);
 	LockBuffer(bslot->buffer, BUFFER_LOCK_UNLOCK);
 
@@ -673,6 +674,7 @@ heapam_relation_copy_data(Relation rel, const RelFileNode *newrnode)
 			 * init fork of an unlogged relation.
 			 */
 			if (rel->rd_rel->relpersistence == RELPERSISTENCE_PERMANENT ||
+				rel->rd_rel->relpersistence == RELPERSISTENCE_SESSION ||
 				(rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED &&
 				 forkNum == INIT_FORKNUM))
 				log_smgrcreate(newrnode, forkNum);
@@ -2162,7 +2164,7 @@ heapam_scan_bitmap_next_block(TableScanDesc scan,
 			loctup.t_len = ItemIdGetLength(lp);
 			loctup.t_tableOid = scan->rs_rd->rd_id;
 			ItemPointerSet(&loctup.t_self, page, offnum);
-			valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
+			valid = HeapTupleSatisfiesVisibility(scan->rs_rd, &loctup, snapshot, buffer);
 			if (valid)
 			{
 				hscan->rs_vistuples[ntup++] = offnum;
@@ -2482,7 +2484,7 @@ SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer,
 	else
 	{
 		/* Otherwise, we have to check the tuple individually. */
-		return HeapTupleSatisfiesVisibility(tuple, scan->rs_snapshot,
+		return HeapTupleSatisfiesVisibility(scan->rs_rd, tuple, scan->rs_snapshot,
 											buffer);
 	}
 }
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index 537e681..3076f6a 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -77,6 +77,7 @@
 #include "utils/combocid.h"
 #include "utils/snapmgr.h"
 
+static bool TempTupleSatisfiesVisibility(HeapTuple htup, CommandId curcid, Buffer buffer);
 
 /*
  * SetHintBits()
@@ -454,7 +455,7 @@ HeapTupleSatisfiesToast(HeapTuple htup, Snapshot snapshot,
  *	test for it themselves.)
  */
 TM_Result
-HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid,
+HeapTupleSatisfiesUpdate(Relation relation, HeapTuple htup, CommandId curcid,
 						 Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -462,6 +463,13 @@ HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid,
 	Assert(ItemPointerIsValid(&htup->t_self));
 	Assert(htup->t_tableOid != InvalidOid);
 
+	if (relation->rd_rel->relpersistence == RELPERSISTENCE_SESSION && RecoveryInProgress())
+	{
+		AccessTempRelationAtReplica = true;
+		return TempTupleSatisfiesVisibility(htup, curcid, buffer) ? TM_Ok : TM_Invisible;
+	}
+	AccessTempRelationAtReplica = false;
+
 	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
 		if (HeapTupleHeaderXminInvalid(tuple))
@@ -1677,6 +1685,59 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 }
 
 /*
+ * TempTupleSatisfiesVisibility
+ *		True iff global temp table tuple is visible for the current transaction.
+ *
+ * Temporary tables are visible only for current backend, so there is no need to
+ * handle cases with tuples committed by other backends. We only need to exclude
+ * modifications done by aborted transactions or after start of table scan.
+ *
+ */
+static bool
+TempTupleSatisfiesVisibility(HeapTuple htup, CommandId curcid, Buffer buffer)
+{
+	HeapTupleHeader tuple = htup->t_data;
+	TransactionId xmin;
+	TransactionId xmax;
+
+	Assert(ItemPointerIsValid(&htup->t_self));
+	Assert(htup->t_tableOid != InvalidOid);
+
+	if (HeapTupleHeaderXminInvalid(tuple))
+		return false;
+
+	xmin = HeapTupleHeaderGetRawXmin(tuple);
+
+	if (IsReplicaTransactionAborted(xmin))
+		return false;
+
+	if (IsReplicaCurrentTransactionId(xmin)
+		&& HeapTupleHeaderGetCmin(tuple) >= curcid)
+	{
+		return false;	/* inserted after scan started */
+	}
+
+	if (tuple->t_infomask & HEAP_XMAX_INVALID)	/* xid invalid */
+		return true;
+
+	if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))	/* not deleter */
+		return true;
+
+	xmax = (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
+	    ? HeapTupleGetUpdateXid(tuple)
+		: HeapTupleHeaderGetRawXmax(tuple);
+
+	if (IsReplicaTransactionAborted(xmax))
+		return true; /* updating subtransaction aborted */
+
+	if (!IsReplicaCurrentTransactionId(xmax))
+		return false; /* updating transaction committed */
+
+	return (HeapTupleHeaderGetCmax(tuple) >= curcid);	/* updated after scan started */
+}
+
+
+/*
  * HeapTupleSatisfiesVisibility
  *		True iff heap tuple satisfies a time qual.
  *
@@ -1687,8 +1748,15 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
  *	if so, the indicated buffer is marked dirty.
  */
 bool
-HeapTupleSatisfiesVisibility(HeapTuple tup, Snapshot snapshot, Buffer buffer)
+HeapTupleSatisfiesVisibility(Relation relation, HeapTuple tup, Snapshot snapshot, Buffer buffer)
 {
+	if (relation->rd_rel->relpersistence == RELPERSISTENCE_SESSION && RecoveryInProgress())
+	{
+		AccessTempRelationAtReplica = true;
+		return TempTupleSatisfiesVisibility(tup, snapshot->curcid, buffer);
+	}
+	AccessTempRelationAtReplica = false;
+
 	switch (snapshot->snapshot_type)
 	{
 		case SNAPSHOT_MVCC:
diff --git a/src/backend/access/transam/transam.c b/src/backend/access/transam/transam.c
index 365ddfb..bce9c4a 100644
--- a/src/backend/access/transam/transam.c
+++ b/src/backend/access/transam/transam.c
@@ -22,6 +22,7 @@
 #include "access/clog.h"
 #include "access/subtrans.h"
 #include "access/transam.h"
+#include "access/xact.h"
 #include "utils/snapmgr.h"
 
 /*
@@ -126,6 +127,9 @@ TransactionIdDidCommit(TransactionId transactionId)
 {
 	XidStatus	xidstatus;
 
+	if (AccessTempRelationAtReplica)
+		return !IsReplicaCurrentTransactionId(transactionId) && !IsReplicaTransactionAborted(transactionId);
+
 	xidstatus = TransactionLogFetch(transactionId);
 
 	/*
@@ -182,6 +186,9 @@ TransactionIdDidAbort(TransactionId transactionId)
 {
 	XidStatus	xidstatus;
 
+	if (AccessTempRelationAtReplica)
+		return IsReplicaTransactionAborted(transactionId);
+
 	xidstatus = TransactionLogFetch(transactionId);
 
 	/*
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 9162286..cf6bf4e 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -192,6 +192,7 @@ typedef struct TransactionStateData
 	int			parallelModeLevel;	/* Enter/ExitParallelMode counter */
 	bool		chain;			/* start a new block after this one */
 	struct TransactionStateData *parent;	/* back link to parent */
+	TransactionId replicaTransactionId;    /* pseudo XID for inserting data in global temp tables at replica */
 } TransactionStateData;
 
 typedef TransactionStateData *TransactionState;
@@ -286,6 +287,12 @@ typedef struct XactCallbackItem
 
 static XactCallbackItem *Xact_callbacks = NULL;
 
+static TransactionId replicaTransIdCount = FirstNormalTransactionId;
+static TransactionId replicaTopTransId;
+static Bitmapset*    replicaAbortedXids;
+
+bool AccessTempRelationAtReplica;
+
 /*
  * List of add-on start- and end-of-subxact callbacks
  */
@@ -443,6 +450,48 @@ GetCurrentTransactionIdIfAny(void)
 }
 
 /*
+ * Transactions at replica can update only global temporary tables.
+ * Them are assigned backend-local XIDs which are independent from normal XIDs received from primary node.
+ * So tuples of temporary tables at replica requires special visibility rules.
+ *
+ * XIDs for such transactions at replica are created on demand (when tuple of temp table is updated).
+ * XID wrap-around and adjusting XID horizon is not supported. So number of such transactions at replica is
+ * limited by 2^32 and require up to 2^29 in-memory bitmap for marking aborted transactions.
+  */
+TransactionId
+GetReplicaTransactionId(void)
+{
+	TransactionState s = CurrentTransactionState;
+	if (!TransactionIdIsValid(s->replicaTransactionId))
+		s->replicaTransactionId = ++replicaTransIdCount;
+	return s->replicaTransactionId;
+}
+
+/*
+ * At replica transaction can update only temporary tables
+ * and them are assigned special XIDs (not related with normal XIDs received from primary node).
+ * As far as we see only own transaction it is not necessary to mark committed transactions.
+ * Only marking aborted ones is enough. All transactions which are not marked as aborted are treated as
+ * committed or self in-progress transactions.
+ */
+bool
+IsReplicaTransactionAborted(TransactionId xid)
+{
+	return bms_is_member(xid, replicaAbortedXids);
+}
+
+/*
+ * As far as XIDs for transactions at replica are generated individually for each backends,
+ * we can check that XID belongs to the current transaction or any of its subtransactions by
+ * just comparing it with XID of top transaction.
+ */
+bool
+IsReplicaCurrentTransactionId(TransactionId xid)
+{
+	return xid > replicaTopTransId;
+}
+
+/*
  *	GetTopFullTransactionId
  *
  * This will return the FullTransactionId of the main transaction, assigning
@@ -855,6 +904,9 @@ TransactionIdIsCurrentTransactionId(TransactionId xid)
 {
 	TransactionState s;
 
+	if (AccessTempRelationAtReplica)
+		return IsReplicaCurrentTransactionId(xid);
+
 	/*
 	 * We always say that BootstrapTransactionId is "not my transaction ID"
 	 * even when it is (ie, during bootstrap).  Along with the fact that
@@ -1206,7 +1258,7 @@ static TransactionId
 RecordTransactionCommit(void)
 {
 	TransactionId xid = GetTopTransactionIdIfAny();
-	bool		markXidCommitted = TransactionIdIsValid(xid);
+	bool		markXidCommitted = TransactionIdIsNormal(xid);
 	TransactionId latestXid = InvalidTransactionId;
 	int			nrels;
 	RelFileNode *rels;
@@ -1624,7 +1676,7 @@ RecordTransactionAbort(bool isSubXact)
 	 * rels to delete (note that this routine is not responsible for actually
 	 * deleting 'em).  We cannot have any child XIDs, either.
 	 */
-	if (!TransactionIdIsValid(xid))
+	if (!TransactionIdIsNormal(xid))
 	{
 		/* Reset XactLastRecEnd until the next transaction writes something */
 		if (!isSubXact)
@@ -1892,6 +1944,8 @@ StartTransaction(void)
 	s = &TopTransactionStateData;
 	CurrentTransactionState = s;
 
+	replicaTopTransId = replicaTransIdCount;
+
 	Assert(!FullTransactionIdIsValid(XactTopFullTransactionId));
 
 	/* check the current transaction state */
@@ -1905,6 +1959,7 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_START;
 	s->fullTransactionId = InvalidFullTransactionId;	/* until assigned */
+	s->replicaTransactionId = InvalidTransactionId;	/* until assigned */
 
 	/* Determine if statements are logged in this transaction */
 	xact_is_sampled = log_xact_sample_rate != 0 &&
@@ -2570,6 +2625,14 @@ AbortTransaction(void)
 	/* Prevent cancel/die interrupt while cleaning up */
 	HOLD_INTERRUPTS();
 
+	/* Mark transactions involved global temp table at replica as aborted */
+	if (TransactionIdIsValid(s->replicaTransactionId))
+	{
+		MemoryContext ctx = MemoryContextSwitchTo(TopMemoryContext);
+		replicaAbortedXids = bms_add_member(replicaAbortedXids, s->replicaTransactionId);
+		MemoryContextSwitchTo(ctx);
+	}
+
 	/* Make sure we have a valid memory context and resource owner */
 	AtAbort_Memory();
 	AtAbort_ResourceOwner();
@@ -2991,6 +3054,9 @@ CommitTransactionCommand(void)
 			 * and then clean up.
 			 */
 		case TBLOCK_ABORT_PENDING:
+			if (GetCurrentTransactionIdIfAny() == FrozenTransactionId)
+				elog(FATAL, "Transaction is aborted at standby");
+
 			AbortTransaction();
 			CleanupTransaction();
 			s->blockState = TBLOCK_DEFAULT;
@@ -4880,6 +4946,14 @@ AbortSubTransaction(void)
 	/* Prevent cancel/die interrupt while cleaning up */
 	HOLD_INTERRUPTS();
 
+	/* Mark transactions involved global temp table at replica as aborted */
+	if (TransactionIdIsValid(s->replicaTransactionId))
+	{
+		MemoryContext ctx = MemoryContextSwitchTo(TopMemoryContext);
+		replicaAbortedXids = bms_add_member(replicaAbortedXids, s->replicaTransactionId);
+		MemoryContextSwitchTo(ctx);
+	}
+
 	/* Make sure we have a valid memory context and resource owner */
 	AtSubAbort_Memory();
 	AtSubAbort_ResourceOwner();

---

From: Konstantin Knizhnik <[email-masked]>
Date: Mon, 28 Oct 2019 16:48:43 +0300

> might be missing something.
Logically it may be good decision. But piratically support of parallel 
access to GTT requires just accessing their data through shared buffer.
But in case of local temp tables we need also need to some how share 
table's metadata between parallel workers. It seems to be much more 
complicated if ever possible.

---

From: Robert Haas <[email-masked]>
Date: Mon, 28 Oct 2019 12:29:48 -0400

> complicated if ever possible.

Why? The backends all share a snapshot, and can load whatever they
need from the system catalogs.

---

From: Robert Haas <[email-masked]>
Date: Mon, 28 Oct 2019 12:40:46 -0400

> Sorry, but both statements are not true.

Well, I think they are true.

> aborted, then backend is terminated and nobody can see this inconsistency.

Aborting the current transaction is a very different thing from
terminating the backend.

Also, the idea that there is no sense in aborts of read-only
transactions on a replica seems totally wrong. Suppose that you insert
a row into the table and then you go to insert a row in each index,
but one of the index inserts fails - duplicate key, out of memory
error, I/O error, whatever. Now the table and the index are
inconsistent. Normally, we're protected against this by MVCC, but if
you use a solution that breaks MVCC by using the same XID for all
transactions, then it can happen.

> and since GTT tuples are not frozen, analog of GTT CLOG kept in memory

I admit that this patch is not lengthy, but there remains the question
of whether it is correct. It's possible that the problem isn't as
complicated as I think it is, but I do think there are quite a number
of reasons why this patch wouldn't be considered acceptable...

> buffers and no replica access is good starting point.

...but given that we seem to agree on this point, perhaps it isn't
necessary to argue about those things right now.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Tue, 29 Oct 2019 11:04:52 +0300

> transactions, then it can happen.

Certainly I understand the difference between abort of transaction and 
termination of backend.
I do not say that it is good solution. And definitely aborts can happen 
for read-only transactions.
I just wanted to express one moment: transaction aborts are caused by 
two reasons:
- expected programming errors: deadlocks, conversion errors, unique 
constraint violation,...
- unexpected system errors: disk space exhaustion, out of memory, I/O 
errors...

Usually at replica with read-only transactions we do not have to deal 
with errors of first kind.
So transaction may be aborted, but such abort most likely means that 
something is wrong with the system
and restart of backend is not so bad solution in this situation.

In any case, I do not insist on this "frozen XID" approach.
The only advantage of this approach is that it is very simple to 
implement: correspondent patch contains just 80 lines of code
and actually it requires just 5 (five) one-line changes.
I didn't agree with your statement just because restart of backend makes 
it not possible to observe any inconsistencies in the database.

> necessary to argue about those things right now.
Ok.
I attached new patch for GTT with local (private) buffer and no replica 
access.
It provides GTT for all built-in indexes

diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 647350c..ca5f22d 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -25,6 +25,7 @@
 #include "access/brin_revmap.h"
 #include "access/brin_tuple.h"
 #include "access/brin_xlog.h"
+#include "access/brin.h"
 #include "access/rmgr.h"
 #include "access/xloginsert.h"
 #include "miscadmin.h"
@@ -79,6 +80,11 @@ brinRevmapInitialize(Relation idxrel, BlockNumber *pagesPerRange,
 	meta = ReadBuffer(idxrel, BRIN_METAPAGE_BLKNO);
 	LockBuffer(meta, BUFFER_LOCK_SHARE);
 	page = BufferGetPage(meta);
+
+	if (GlobalTempRelationPageIsNotInitialized(idxrel, page))
+		brin_metapage_init(page, BrinGetPagesPerRange(idxrel),
+						   BRIN_CURRENT_VERSION);
+
 	TestForOldSnapshot(snapshot, idxrel, page);
 	metadata = (BrinMetaPageData *) PageGetContents(page);
 
diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c
index 439a91b..8a6ac71 100644
--- a/src/backend/access/gin/ginfast.c
+++ b/src/backend/access/gin/ginfast.c
@@ -241,6 +241,16 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
 	metabuffer = ReadBuffer(index, GIN_METAPAGE_BLKNO);
 	metapage = BufferGetPage(metabuffer);
 
+	if (GlobalTempRelationPageIsNotInitialized(index, metapage))
+	{
+		Buffer rootbuffer = ReadBuffer(index, GIN_ROOT_BLKNO);
+		LockBuffer(rootbuffer, BUFFER_LOCK_EXCLUSIVE);
+		GinInitMetabuffer(metabuffer);
+		GinInitBuffer(rootbuffer, GIN_LEAF);
+		MarkBufferDirty(rootbuffer);
+		UnlockReleaseBuffer(rootbuffer);
+	}
+
 	/*
 	 * An insertion to the pending list could logically belong anywhere in the
 	 * tree, so it conflicts with all serializable scans.  All scans acquire a
diff --git a/src/backend/access/gin/ginget.c b/src/backend/access/gin/ginget.c
index b18ae2b..41bab5d 100644
--- a/src/backend/access/gin/ginget.c
+++ b/src/backend/access/gin/ginget.c
@@ -1750,7 +1750,7 @@ collectMatchesForHeapRow(IndexScanDesc scan, pendingPosition *pos)
 /*
  * Collect all matched rows from pending list into bitmap.
  */
-static void
+static bool
 scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids)
 {
 	GinScanOpaque so = (GinScanOpaque) scan->opaque;
@@ -1774,6 +1774,12 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids)
 	LockBuffer(metabuffer, GIN_SHARE);
 	page = BufferGetPage(metabuffer);
 	TestForOldSnapshot(scan->xs_snapshot, scan->indexRelation, page);
+
+	if (GlobalTempRelationPageIsNotInitialized(scan->indexRelation, page))
+	{
+		UnlockReleaseBuffer(metabuffer);
+		return false;
+	}
 	blkno = GinPageGetMeta(page)->head;
 
 	/*
@@ -1784,7 +1790,7 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids)
 	{
 		/* No pending list, so proceed with normal scan */
 		UnlockReleaseBuffer(metabuffer);
-		return;
+		return true;
 	}
 
 	pos.pendingBuffer = ReadBuffer(scan->indexRelation, blkno);
@@ -1840,6 +1846,7 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids)
 	}
 
 	pfree(pos.hasMatchKey);
+	return true;
 }

@@ -1875,7 +1882,8 @@ gingetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 * to scan the main index before the pending list, since concurrent
 	 * cleanup could then make us miss entries entirely.
 	 */
-	scanPendingInsert(scan, tbm, &ntids);
+	if (!scanPendingInsert(scan, tbm, &ntids))
+		return 0;
 
 	/*
 	 * Now scan the main index.
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 0cc8791..3215b6f 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -677,7 +677,10 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace,
 		if (!xlocked)
 		{
 			LockBuffer(stack->buffer, GIST_SHARE);
-			gistcheckpage(state.r, stack->buffer);
+			if (stack->blkno == GIST_ROOT_BLKNO && GlobalTempRelationPageIsNotInitialized(state.r, BufferGetPage(stack->buffer)))
+				GISTInitBuffer(stack->buffer, F_LEAF);
+			else
+				gistcheckpage(state.r, stack->buffer);
 		}
 
 		stack->page = (Page) BufferGetPage(stack->buffer);
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index 22d790d..4c52dbe 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -344,7 +344,10 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem,
 	buffer = ReadBuffer(scan->indexRelation, pageItem->blkno);
 	LockBuffer(buffer, GIST_SHARE);
 	PredicateLockPage(r, BufferGetBlockNumber(buffer), scan->xs_snapshot);
-	gistcheckpage(scan->indexRelation, buffer);
+	if (pageItem->blkno == GIST_ROOT_BLKNO && GlobalTempRelationPageIsNotInitialized(r, BufferGetPage(buffer)))
+		GISTInitBuffer(buffer, F_LEAF);
+	else
+		gistcheckpage(scan->indexRelation, buffer);
 	page = BufferGetPage(buffer);
 	TestForOldSnapshot(scan->xs_snapshot, r, page);
 	opaque = GistPageGetOpaque(page);
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 45804d7..50b306a 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -1028,7 +1028,7 @@ gistGetFakeLSN(Relation rel)
 {
 	static XLogRecPtr counter = FirstNormalUnloggedLSN;
 
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	if (RelationHasSessionScope(rel))
 	{
 		/*
 		 * Temporary relations are only accessible in our session, so a simple
diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c
index 838ee68..4f794b3 100644
--- a/src/backend/access/hash/hashpage.c
+++ b/src/backend/access/hash/hashpage.c
@@ -75,13 +75,20 @@ _hash_getbuf(Relation rel, BlockNumber blkno, int access, int flags)
 
 	buf = ReadBuffer(rel, blkno);
 
-	if (access != HASH_NOLOCK)
-		LockBuffer(buf, access);
-
 	/* ref count and lock type are correct */
 
-	_hash_checkpage(rel, buf, flags);
-
+	if (blkno == HASH_METAPAGE && GlobalTempRelationPageIsNotInitialized(rel, BufferGetPage(buf)))
+	{
+		_hash_init(rel, 0, MAIN_FORKNUM);
+		if (access != HASH_NOLOCK)
+			LockBuffer(buf, access);
+	}
+	else
+	{
+		if (access != HASH_NOLOCK)
+			LockBuffer(buf, access);
+		_hash_checkpage(rel, buf, flags);
+	}
 	return buf;
 }
 
@@ -339,7 +346,7 @@ _hash_init(Relation rel, double num_tuples, ForkNumber forkNum)
 	bool		use_wal;
 
 	/* safety check */
-	if (RelationGetNumberOfBlocksInFork(rel, forkNum) != 0)
+	if (rel->rd_rel->relpersistence != RELPERSISTENCE_SESSION && RelationGetNumberOfBlocksInFork(rel, forkNum) != 0)
 		elog(ERROR, "cannot initialize non-empty hash index \"%s\"",
 			 RelationGetRelationName(rel));
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 2dd8821..92df373 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -673,6 +673,7 @@ heapam_relation_copy_data(Relation rel, const RelFileNode *newrnode)
 			 * init fork of an unlogged relation.
 			 */
 			if (rel->rd_rel->relpersistence == RELPERSISTENCE_PERMANENT ||
+				rel->rd_rel->relpersistence == RELPERSISTENCE_SESSION ||
 				(rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED &&
 				 forkNum == INIT_FORKNUM))
 				log_smgrcreate(newrnode, forkNum);
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 268f869..ed3ab70 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -763,7 +763,11 @@ _bt_getbuf(Relation rel, BlockNumber blkno, int access)
 		/* Read an existing block of the relation */
 		buf = ReadBuffer(rel, blkno);
 		LockBuffer(buf, access);
-		_bt_checkpage(rel, buf);
+		/* Session temporary relation may be not yet initialized for this backend. */
+		if (blkno == BTREE_METAPAGE && GlobalTempRelationPageIsNotInitialized(rel, BufferGetPage(buf)))
+			_bt_initmetapage(BufferGetPage(buf), P_NONE, 0);
+		else
+			_bt_checkpage(rel, buf);
 	}
 	else
 	{
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 45472db..a8497a2 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -106,6 +106,7 @@ spgGetCache(Relation index)
 		spgConfigIn in;
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
+		Page        metapage;
 		SpGistMetaPageData *metadata;
 
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
@@ -155,12 +156,32 @@ spgGetCache(Relation index)
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
 
-		metadata = SpGistPageGetMeta(BufferGetPage(metabuffer));
+		metapage = BufferGetPage(metabuffer);
+		metadata = SpGistPageGetMeta(metapage);
 
 		if (metadata->magicNumber != SPGIST_MAGIC_NUMBER)
-			elog(ERROR, "index \"%s\" is not an SP-GiST index",
-				 RelationGetRelationName(index));
+		{
+			if (GlobalTempRelationPageIsNotInitialized(index, metapage))
+			{
+				Buffer rootbuffer = ReadBuffer(index, SPGIST_ROOT_BLKNO);
+				Buffer nullbuffer = ReadBuffer(index, SPGIST_NULL_BLKNO);
+
+				SpGistInitMetapage(metapage);
+
+				LockBuffer(rootbuffer, BUFFER_LOCK_EXCLUSIVE);
+				SpGistInitPage(BufferGetPage(rootbuffer), SPGIST_LEAF);
+				MarkBufferDirty(rootbuffer);
+				UnlockReleaseBuffer(rootbuffer);
 
+				LockBuffer(nullbuffer, BUFFER_LOCK_EXCLUSIVE);
+				SpGistInitPage(BufferGetPage(nullbuffer), SPGIST_LEAF | SPGIST_NULLS);
+				MarkBufferDirty(nullbuffer);
+				UnlockReleaseBuffer(nullbuffer);
+			}
+			else
+				elog(ERROR, "index \"%s\" is not an SP-GiST index",
+					 RelationGetRelationName(index));
+		}
 		cache->lastUsedPages = metadata->lastUsedPages;
 
 		UnlockReleaseBuffer(metabuffer);
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 1af31c2..e60bdb7 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -402,6 +402,9 @@ GetNewRelFileNode(Oid reltablespace, Relation pg_class, char relpersistence)
 		case RELPERSISTENCE_TEMP:
 			backend = BackendIdForTempRelations();
 			break;
+		case RELPERSISTENCE_SESSION:
+			backend = BackendIdForSessionRelations();
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			backend = InvalidBackendId;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f6c31cc..d943b57 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -3652,7 +3652,7 @@ reindex_relation(Oid relid, int flags, int options)
 		if (flags & REINDEX_REL_FORCE_INDEXES_UNLOGGED)
 			persistence = RELPERSISTENCE_UNLOGGED;
 		else if (flags & REINDEX_REL_FORCE_INDEXES_PERMANENT)
-			persistence = RELPERSISTENCE_PERMANENT;
+			persistence = rel->rd_rel->relpersistence == RELPERSISTENCE_SESSION ? RELPERSISTENCE_SESSION : RELPERSISTENCE_PERMANENT;
 		else
 			persistence = rel->rd_rel->relpersistence;
 
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 625af8d..1e192fa 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -93,6 +93,10 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
 			backend = InvalidBackendId;
 			needs_wal = false;
 			break;
+		case RELPERSISTENCE_SESSION:
+			backend = BackendIdForSessionRelations();
+			needs_wal = false;
+			break;
 		case RELPERSISTENCE_PERMANENT:
 			backend = InvalidBackendId;
 			needs_wal = true;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index a23128d..5d131a7 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1400,7 +1400,7 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	 */
 	if (newrelpersistence == RELPERSISTENCE_UNLOGGED)
 		reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
-	else if (newrelpersistence == RELPERSISTENCE_PERMANENT)
+	else if (newrelpersistence != RELPERSISTENCE_TEMP)
 		reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
 
 	/* Report that we are now reindexing relations */
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index a13322b..be661a4 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -94,7 +94,7 @@ static HTAB *seqhashtab = NULL; /* hash table for SeqTable items */
  */
 static SeqTableData *last_used_seq = NULL;
 
-static void fill_seq_with_data(Relation rel, HeapTuple tuple);
+static void fill_seq_with_data(Relation rel, HeapTuple tuple, Buffer buf);
 static Relation lock_and_open_sequence(SeqTable seq);
 static void create_seq_hashtable(void);
 static void init_sequence(Oid relid, SeqTable *p_elm, Relation *p_rel);
@@ -222,7 +222,7 @@ DefineSequence(ParseState *pstate, CreateSeqStmt *seq)
 
 	/* now initialize the sequence's data */
 	tuple = heap_form_tuple(tupDesc, value, null);
-	fill_seq_with_data(rel, tuple);
+	fill_seq_with_data(rel, tuple, InvalidBuffer);
 
 	/* process OWNED BY if given */
 	if (owned_by)
@@ -327,7 +327,7 @@ ResetSequence(Oid seq_relid)
 	/*
 	 * Insert the modified tuple into the new storage file.
 	 */
-	fill_seq_with_data(seq_rel, tuple);
+	fill_seq_with_data(seq_rel, tuple, InvalidBuffer);
 
 	/* Clear local cache so that we don't think we have cached numbers */
 	/* Note that we do not change the currval() state */
@@ -340,18 +340,21 @@ ResetSequence(Oid seq_relid)
  * Initialize a sequence's relation with the specified tuple as content
  */
 static void
-fill_seq_with_data(Relation rel, HeapTuple tuple)
+fill_seq_with_data(Relation rel, HeapTuple tuple, Buffer buf)
 {
-	Buffer		buf;
 	Page		page;
 	sequence_magic *sm;
 	OffsetNumber offnum;
+	bool lockBuffer = false;
 
 	/* Initialize first page of relation with special magic number */
 
-	buf = ReadBuffer(rel, P_NEW);
-	Assert(BufferGetBlockNumber(buf) == 0);
-
+	if (buf == InvalidBuffer)
+	{
+		buf = ReadBuffer(rel, P_NEW);
+		Assert(BufferGetBlockNumber(buf) == 0);
+		lockBuffer = true;
+	}
 	page = BufferGetPage(buf);
 
 	PageInit(page, BufferGetPageSize(buf), sizeof(sequence_magic));
@@ -360,7 +363,8 @@ fill_seq_with_data(Relation rel, HeapTuple tuple)
 
 	/* Now insert sequence tuple */
 
-	LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+	if (lockBuffer)
+		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
 	/*
 	 * Since VACUUM does not process sequences, we have to force the tuple to
@@ -410,7 +414,8 @@ fill_seq_with_data(Relation rel, HeapTuple tuple)
 
 	END_CRIT_SECTION();
 
-	UnlockReleaseBuffer(buf);
+	if (lockBuffer)
+		UnlockReleaseBuffer(buf);
 }
 
 /*
@@ -502,7 +507,7 @@ AlterSequence(ParseState *pstate, AlterSeqStmt *stmt)
 		/*
 		 * Insert the modified tuple into the new storage file.
 		 */
-		fill_seq_with_data(seqrel, newdatatuple);
+		fill_seq_with_data(seqrel, newdatatuple, InvalidBuffer);
 	}
 
 	/* process OWNED BY if given */
@@ -1178,6 +1183,17 @@ read_seq_tuple(Relation rel, Buffer *buf, HeapTuple seqdatatuple)
 	LockBuffer(*buf, BUFFER_LOCK_EXCLUSIVE);
 
 	page = BufferGetPage(*buf);
+	if (GlobalTempRelationPageIsNotInitialized(rel, page))
+	{
+		/* Initialize sequence for global temporary tables */
+		Datum		value[SEQ_COL_LASTCOL] = {0};
+		bool		null[SEQ_COL_LASTCOL] = {false};
+		HeapTuple tuple;
+		value[SEQ_COL_LASTVAL-1] = Int64GetDatumFast(1); /* start sequence with 1 */
+		tuple = heap_form_tuple(RelationGetDescr(rel), value, null);
+		fill_seq_with_data(rel, tuple, *buf);
+	}
+
 	sm = (sequence_magic *) PageGetSpecialPointer(page);
 
 	if (sm->magic != SEQ_MAGIC)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8d25d14..50d0402 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -587,7 +587,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * Check consistency of arguments
 	 */
 	if (stmt->oncommit != ONCOMMIT_NOOP
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& !IsLocalRelpersistence(stmt->relation->relpersistence))
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 				 errmsg("ON COMMIT can only be used on temporary tables")));
@@ -1772,7 +1772,8 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 		 * table or the current physical file to be thrown away anyway.
 		 */
 		if (rel->rd_createSubid == mySubid ||
-			rel->rd_newRelfilenodeSubid == mySubid)
+			rel->rd_newRelfilenodeSubid == mySubid ||
+			rel->rd_rel->relpersistence == RELPERSISTENCE_SESSION)
 		{
 			/* Immediate, non-rollbackable truncation is OK */
 			heap_truncate_one_rel(rel);
@@ -7708,6 +7709,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 						 errmsg("constraints on unlogged tables may reference only permanent or unlogged tables")));
 			break;
+		case RELPERSISTENCE_SESSION:
+			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_SESSION)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("constraints on session tables may reference only session tables")));
+			break;
 		case RELPERSISTENCE_TEMP:
 			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
 				ereport(ERROR,
@@ -14140,6 +14147,13 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 							RelationGetRelationName(rel)),
 					 errtable(rel)));
 			break;
+		case RELPERSISTENCE_SESSION:
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot change logged status of session table \"%s\"",
+							RelationGetRelationName(rel)),
+					 errtable(rel)));
+			break;
 		case RELPERSISTENCE_PERMANENT:
 			if (toLogged)
 				/* nothing to do */
@@ -14627,14 +14641,7 @@ PreCommit_on_commit_actions(void)
 				/* 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);
+				oids_to_truncate = lappend_oid(oids_to_truncate, oc->relid);
 				break;
 			case ONCOMMIT_DROP:
 				oids_to_drop = lappend_oid(oids_to_drop, oc->relid);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3f67aaf..565c868 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3266,20 +3266,11 @@ OptTemp:	TEMPORARY					{ $$ = RELPERSISTENCE_TEMP; }
 			| TEMP						{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMPORARY			{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMP				{ $$ = RELPERSISTENCE_TEMP; }
-			| GLOBAL TEMPORARY
-				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
-					$$ = RELPERSISTENCE_TEMP;
-				}
-			| GLOBAL TEMP
-				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
-					$$ = RELPERSISTENCE_TEMP;
-				}
+			| GLOBAL TEMPORARY          { $$ = RELPERSISTENCE_SESSION; }
+			| GLOBAL TEMP               { $$ = RELPERSISTENCE_SESSION; }
+			| SESSION                   { $$ = RELPERSISTENCE_SESSION; }
+			| SESSION TEMPORARY         { $$ = RELPERSISTENCE_SESSION; }
+			| SESSION TEMP              { $$ = RELPERSISTENCE_SESSION; }
 			| UNLOGGED					{ $$ = RELPERSISTENCE_UNLOGGED; }
 			| /*EMPTY*/					{ $$ = RELPERSISTENCE_PERMANENT; }
 		;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ee47547..ea7fe4c 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -437,6 +437,14 @@ generateSerialExtraStmts(CreateStmtContext *cxt, ColumnDef *column,
 	seqstmt->options = seqoptions;
 
 	/*
+	 * Why we should not always use persistence of parent table?
+	 * Although it is prohibited to have unlogged sequences,
+	 * unlogged tables with SERIAL fields are accepted!
+	 */
+	if (cxt->relation->relpersistence != RELPERSISTENCE_UNLOGGED)
+		seqstmt->sequence->relpersistence = cxt->relation->relpersistence;
+
+	/*
 	 * If a sequence data type was specified, add it to the options.  Prepend
 	 * to the list rather than append; in case a user supplied their own AS
 	 * clause, the "redundant options" error will point to their occurrence,
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c1dd816..dcfc134 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2157,7 +2157,7 @@ do_autovacuum(void)
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (IsLocalRelpersistence(classForm->relpersistence))
 			continue;
 
 		relid = classForm->oid;
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 07f3c93..5db79ec 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -33,6 +33,7 @@
 #include "postmaster/bgwriter.h"
 #include "storage/fd.h"
 #include "storage/bufmgr.h"
+#include "storage/ipc.h"
 #include "storage/md.h"
 #include "storage/relfilenode.h"
 #include "storage/smgr.h"
@@ -87,6 +88,18 @@ typedef struct _MdfdVec
 
 static MemoryContext MdCxt;		/* context for all MdfdVec objects */
 
+/*
+ * Structure used to collect information created by this backend.
+ * Data of this related should be deleted on backend exit.
+ */
+typedef struct SessionRelation
+{
+	RelFileNodeBackend rnode;
+	struct SessionRelation* next;
+} SessionRelation;
+
+
+static SessionRelation* SessionRelations;
 
 /* Populate a file tag describing an md.c segment file. */
 #define INIT_MD_FILETAG(a,xx_rnode,xx_forknum,xx_segno) \
@@ -152,6 +165,45 @@ mdinit(void)
 								  ALLOCSET_DEFAULT_SIZES);
 }
 
+
+/*
+ * Delete all data of session relations and remove their pages from shared buffers.
+ * This function is called on backend exit.
+ */
+static void
+TruncateSessionRelations(int code, Datum arg)
+{
+	SessionRelation* rel;
+	for (rel = SessionRelations; rel != NULL; rel = rel->next)
+	{
+		/* Delete relation files */
+		mdunlink(rel->rnode, InvalidForkNumber, false);
+	}
+}
+
+/*
+ * Maintain information about session relations accessed by this backend.
+ * This list is needed to perform cleanup on backend exit.
+ * Session relation is linked in this list when this relation is created or opened and file doesn't exist.
+ * Such procedure guarantee that each relation is linked into list only once.
+ */
+static void
+RegisterSessionRelation(SMgrRelation reln)
+{
+	SessionRelation* rel = (SessionRelation*)MemoryContextAlloc(TopMemoryContext, sizeof(SessionRelation));
+
+	/*
+	 * Perform session relation cleanup on backend exit. We are using shared memory hook, because
+	 * cleanup should be performed before backend is disconnected from shared memory.
+	 */
+	if (SessionRelations == NULL)
+		on_shmem_exit(TruncateSessionRelations, 0);
+
+	rel->rnode = reln->smgr_rnode;
+	rel->next = SessionRelations;
+	SessionRelations = rel;
+}
+
 /*
  *	mdexists() -- Does the physical file exist?
  *
@@ -218,6 +270,8 @@ mdcreate(SMgrRelation reln, ForkNumber forkNum, bool isRedo)
 					 errmsg("could not create file \"%s\": %m", path)));
 		}
 	}
+	if (RelFileNodeBackendIsGlobalTemp(reln->smgr_rnode))
+		RegisterSessionRelation(reln);
 
 	pfree(path);
 
@@ -465,6 +519,19 @@ mdopenfork(SMgrRelation reln, ForkNumber forknum, int behavior)
 
 	if (fd < 0)
 	{
+		/*
+		 * In case of session relation access, there may be no yet files of this relation for this backend.
+		 * If so, then create file and register session relation for truncation on backend exit.
+		 */
+		if (RelFileNodeBackendIsGlobalTemp(reln->smgr_rnode))
+		{
+			fd = PathNameOpenFile(path, O_RDWR | PG_BINARY | O_CREAT);
+			if (fd >= 0)
+			{
+				RegisterSessionRelation(reln);
+				goto NewSegment;
+			}
+		}
 		if ((behavior & EXTENSION_RETURN_NULL) &&
 			FILE_POSSIBLY_DELETED(errno))
 		{
@@ -476,6 +543,7 @@ mdopenfork(SMgrRelation reln, ForkNumber forknum, int behavior)
 				 errmsg("could not open file \"%s\": %m", path)));
 	}
 
+  NewSegment:
 	pfree(path);
 
 	_fdvec_resize(reln, forknum, 1);
@@ -652,8 +720,13 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
 		 * complaining.  This allows, for example, the case of trying to
 		 * update a block that was later truncated away.
 		 */
-		if (zero_damaged_pages || InRecovery)
+		if (zero_damaged_pages || InRecovery || RelFileNodeBackendIsGlobalTemp(reln->smgr_rnode))
+		{
 			MemSet(buffer, 0, BLCKSZ);
+			/* In case of session relation we need to write zero page to provide correct result of subsequent mdnblocks */
+			if (RelFileNodeBackendIsGlobalTemp(reln->smgr_rnode))
+				mdwrite(reln, forknum, blocknum, buffer, true);
+		}
 		else
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
@@ -738,12 +811,18 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
 BlockNumber
 mdnblocks(SMgrRelation reln, ForkNumber forknum)
 {
-	MdfdVec    *v = mdopenfork(reln, forknum, EXTENSION_FAIL);
+	/*
+	 * If we access session relation, there may be no files yet of this relation for this backend.
+	 * Pass EXTENSION_RETURN_NULL to make mdopen return NULL in this case instead of reporting error.
+	 */
+	MdfdVec    *v = mdopenfork(reln, forknum, RelFileNodeBackendIsGlobalTemp(reln->smgr_rnode)
+							   ? EXTENSION_RETURN_NULL : EXTENSION_FAIL);
 	BlockNumber nblocks;
 	BlockNumber segno = 0;
 
 	/* mdopen has opened the first segment */
-	Assert(reln->md_num_open_segs[forknum] > 0);
+	if (reln->md_num_open_segs[forknum] == 0)
+		return 0;
 
 	/*
 	 * Start from the last open segments, to avoid redundant seeks.  We have
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index a87e721..2401361 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -994,6 +994,9 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 	/* Determine owning backend. */
 	switch (relform->relpersistence)
 	{
+		case RELPERSISTENCE_SESSION:
+			backend = BackendIdForSessionRelations();
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			backend = InvalidBackendId;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 585dcee..ce8852c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1098,6 +1098,10 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	relation->rd_newRelfilenodeSubid = InvalidSubTransactionId;
 	switch (relation->rd_rel->relpersistence)
 	{
+		case RELPERSISTENCE_SESSION:
+			relation->rd_backend = BackendIdForSessionRelations();
+			relation->rd_islocaltemp = false;
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			relation->rd_backend = InvalidBackendId;
@@ -3301,6 +3305,10 @@ RelationBuildLocalRelation(const char *relname,
 	rel->rd_rel->relpersistence = relpersistence;
 	switch (relpersistence)
 	{
+		case RELPERSISTENCE_SESSION:
+			rel->rd_backend = BackendIdForSessionRelations();
+			rel->rd_islocaltemp = false;
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			rel->rd_backend = InvalidBackendId;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bf69adc..fa7479c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -15637,8 +15637,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 											 tbinfo->dobj.catId.oid, false);
 
 		appendPQExpBuffer(q, "CREATE %s%s %s",
-						  tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED ?
-						  "UNLOGGED " : "",
+						  tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED ? "UNLOGGED "
+						  : tbinfo->relpersistence == RELPERSISTENCE_SESSION ? "SESSION " : "",
 						  reltypename,
 						  qualrelname);
 
diff --git a/src/common/relpath.c b/src/common/relpath.c
index 62b9553..cef99d2 100644
--- a/src/common/relpath.c
+++ b/src/common/relpath.c
@@ -166,7 +166,18 @@ GetRelationPath(Oid dbNode, Oid spcNode, Oid relNode,
 		}
 		else
 		{
-			if (forkNumber != MAIN_FORKNUM)
+			/*
+			 * Session relations are distinguished from local temp relations by adding
+			 * SessionRelFirstBackendId offset to backendId.
+			 * These is no need to separate them at file system level, so just subtract SessionRelFirstBackendId
+			 * to avoid too long file names.
+			 * Segments of session relations have the same prefix (t%d_) as local temporary relations
+			 * to make it possible to cleanup them in the same way as local temporary relation files.
+			 */
+			 if (backendId >= SessionRelFirstBackendId)
+				 backendId -= SessionRelFirstBackendId;
+
+			 if (forkNumber != MAIN_FORKNUM)
 				path = psprintf("base/%u/t%d_%u_%s",
 								dbNode, backendId, relNode,
 								forkNames[forkNumber]);
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 090b6ba..6a39663 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -165,6 +165,7 @@ typedef FormData_pg_class *Form_pg_class;
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
 #define		  RELPERSISTENCE_TEMP		't' /* temporary table */
+#define		  RELPERSISTENCE_SESSION	's' /* session table */
 
 /* default selection for replica identity (primary key or nothing) */
 #define		  REPLICA_IDENTITY_DEFAULT	'd'
diff --git a/src/include/storage/backendid.h b/src/include/storage/backendid.h
index 70ef8eb..f226e7c 100644
--- a/src/include/storage/backendid.h
+++ b/src/include/storage/backendid.h
@@ -22,6 +22,13 @@ typedef int BackendId;			/* unique currently active backend identifier */
 
 #define InvalidBackendId		(-1)
 
+/*
+ * We need to distinguish local and global temporary relations by RelFileNodeBackend.
+ * The least invasive change is to add some special bias value to backend id (since 
+ * maximal number of backed is limited by MaxBackends).
+ */
+#define SessionRelFirstBackendId (0x40000000)
+
 extern PGDLLIMPORT BackendId MyBackendId;	/* backend id of this backend */
 
 /* backend id of our parallel session leader, or InvalidBackendId if none */
@@ -34,4 +41,10 @@ extern PGDLLIMPORT BackendId ParallelMasterBackendId;
 #define BackendIdForTempRelations() \
 	(ParallelMasterBackendId == InvalidBackendId ? MyBackendId : ParallelMasterBackendId)
 
+
+#define BackendIdForSessionRelations() \
+	(BackendIdForTempRelations() + SessionRelFirstBackendId)
+
+#define IsSessionRelationBackendId(id) ((id) >= SessionRelFirstBackendId)
+
 #endif							/* BACKENDID_H */
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index 4ef6d8d..bac7a31 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -229,6 +229,13 @@ typedef PageHeaderData *PageHeader;
 #define PageIsNew(page) (((PageHeader) (page))->pd_upper == 0)
 
 /*
+ * Page of temporary relation is not initialized
+ */
+#define GlobalTempRelationPageIsNotInitialized(rel, page) \
+	((rel)->rd_rel->relpersistence == RELPERSISTENCE_SESSION && PageIsNew(page))
+
+
+/*
  * PageGetItemId
  *		Returns an item identifier of a page.
  */
diff --git a/src/include/storage/relfilenode.h b/src/include/storage/relfilenode.h
index 586500a..20aec72 100644
--- a/src/include/storage/relfilenode.h
+++ b/src/include/storage/relfilenode.h
@@ -75,10 +75,25 @@ typedef struct RelFileNodeBackend
 	BackendId	backend;
 } RelFileNodeBackend;
 
+/*
+ * Check whether it is local or global temporary relation, which data belongs only to one backend.
+ */
 #define RelFileNodeBackendIsTemp(rnode) \
 	((rnode).backend != InvalidBackendId)
 
 /*
+ * Check whether it is global temporary relation which metadata is shared by all sessions,
+ * but data is private for the current session.
+ */
+#define RelFileNodeBackendIsGlobalTemp(rnode) IsSessionRelationBackendId((rnode).backend)
+
+/*
+ * Check whether it is local temporary relation which exists only in this backend.
+ */
+#define RelFileNodeBackendIsLocalTemp(rnode) \
+	(RelFileNodeBackendIsTemp(rnode) && !RelFileNodeBackendIsGlobalTemp(rnode))
+
+/*
  * Note: RelFileNodeEquals and RelFileNodeBackendEquals compare relNode first
  * since that is most likely to be different in two unequal RelFileNodes.  It
  * is probably redundant to compare spcNode if the other fields are found equal,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index a5cf804..d42830f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -327,6 +327,18 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->parallel_workers : (defaultpw))
 
+/*
+ * Relation persistence is either TEMP either SESSION
+ */
+#define IsLocalRelpersistence(relpersistence) \
+	((relpersistence) == RELPERSISTENCE_TEMP || (relpersistence) == RELPERSISTENCE_SESSION)
+
+/*
+ * Relation is either global either local temp table
+ */
+#define RelationHasSessionScope(relation) \
+	IsLocalRelpersistence(((relation)->rd_rel->relpersistence))
+
 /* ViewOptions->check_option values */
 typedef enum ViewOptCheckOption
 {
@@ -335,6 +347,7 @@ typedef enum ViewOptCheckOption
 	VIEW_OPTION_CHECK_OPTION_CASCADED
 } ViewOptCheckOption;
 
+
 /*
  * ViewOptions
  *		Contents of rd_options for views
@@ -526,7 +539,7 @@ typedef struct ViewOptions
  *		True if relation's pages are stored in local buffers.
  */
 #define RelationUsesLocalBuffers(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	RelationHasSessionScope(relation)
 
 /*
  * RELATION_IS_LOCAL
diff --git a/src/test/isolation/expected/inherit-global-temp.out b/src/test/isolation/expected/inherit-global-temp.out
new file mode 100644
index 0000000..6114f8c
--- /dev/null
+++ b/src/test/isolation/expected/inherit-global-temp.out
@@ -0,0 +1,218 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+2              
+3              
+4              
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+3              
+4              
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+2              
+5              
+6              
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+5              
+6              
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s1_update_p s1_update_c s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s1_update_p: UPDATE inh_global_parent SET a = 11 WHERE a = 1;
+step s1_update_c: UPDATE inh_global_parent SET a = 13 WHERE a IN (3, 5);
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+2              
+11             
+4              
+13             
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+4              
+13             
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+2              
+11             
+5              
+6              
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+5              
+6              
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s2_update_c s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s2_update_c: UPDATE inh_global_parent SET a = 15 WHERE a IN (3, 5);
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+2              
+3              
+4              
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+3              
+4              
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+2              
+6              
+15             
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+6              
+15             
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s1_delete_p s1_delete_c s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s1_delete_p: DELETE FROM inh_global_parent WHERE a = 2;
+step s1_delete_c: DELETE FROM inh_global_parent WHERE a IN (4, 6);
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+3              
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+3              
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+5              
+6              
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+5              
+6              
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s2_delete_c s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s2_delete_c: DELETE FROM inh_global_parent WHERE a IN (4, 6);
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+2              
+3              
+4              
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+3              
+4              
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+1              
+2              
+5              
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+5              
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s1_truncate_p s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s1_truncate_p: TRUNCATE inh_global_parent;
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+5              
+6              
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+5              
+6              
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s2_truncate_p s1_select_p s1_select_c s2_select_p s2_select_c
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s2_truncate_p: TRUNCATE inh_global_parent;
+step s1_select_p: SELECT a FROM inh_global_parent;
+a              
+
+3              
+4              
+step s1_select_c: SELECT a FROM inh_global_temp_child_s1;
+a              
+
+3              
+4              
+step s2_select_p: SELECT a FROM inh_global_parent;
+a              
+
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2;
+a              
+
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s1_begin s1_truncate_p s2_select_p s1_commit
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s1_begin: BEGIN;
+step s1_truncate_p: TRUNCATE inh_global_parent;
+step s2_select_p: SELECT a FROM inh_global_parent; <waiting ...>
+step s1_commit: COMMIT;
+step s2_select_p: <... completed>
+a              
+
+5              
+6              
+
+starting permutation: s1_insert_p s1_insert_c s2_insert_c s1_begin s1_truncate_p s2_select_c s1_commit
+step s1_insert_p: INSERT INTO inh_global_parent VALUES (1), (2);
+step s1_insert_c: INSERT INTO inh_global_temp_child_s1 VALUES (3), (4);
+step s2_insert_c: INSERT INTO inh_global_temp_child_s2 VALUES (5), (6);
+step s1_begin: BEGIN;
+step s1_truncate_p: TRUNCATE inh_global_parent;
+step s2_select_c: SELECT a FROM inh_global_temp_child_s2; <waiting ...>
+step s1_commit: COMMIT;
+step s2_select_c: <... completed>
+a              
+
+5              
+6              
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index a2fa192..ef7aa85 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -88,3 +88,4 @@ test: plpgsql-toast
 test: truncate-conflict
 test: serializable-parallel
 test: serializable-parallel-2
+test: inherit-global-temp
diff --git a/src/test/isolation/specs/inherit-global-temp.spec b/src/test/isolation/specs/inherit-global-temp.spec
new file mode 100644
index 0000000..5e95dd6
--- /dev/null
+++ b/src/test/isolation/specs/inherit-global-temp.spec
@@ -0,0 +1,73 @@
+# This is a copy of the inherit-temp test with little changes for global temporary tables.
+#
+
+setup
+{
+  CREATE TABLE inh_global_parent (a int);
+}
+
+teardown
+{
+  DROP TABLE inh_global_parent;
+}
+
+# Session 1 executes actions which act directly on both the parent and
+# its child.  Abbreviation "c" is used for queries working on the child
+# and "p" on the parent.
+session "s1"
+setup
+{
+  CREATE GLOBAL TEMPORARY TABLE inh_global_temp_child_s1 () INHERITS (inh_global_parent);
+}
+step "s1_begin" { BEGIN; }
+step "s1_truncate_p" { TRUNCATE inh_global_parent; }
+step "s1_select_p" { SELECT a FROM inh_global_parent; }
+step "s1_select_c" { SELECT a FROM inh_global_temp_child_s1; }
+step "s1_insert_p" { INSERT INTO inh_global_parent VALUES (1), (2); }
+step "s1_insert_c" { INSERT INTO inh_global_temp_child_s1 VALUES (3), (4); }
+step "s1_update_p" { UPDATE inh_global_parent SET a = 11 WHERE a = 1; }
+step "s1_update_c" { UPDATE inh_global_parent SET a = 13 WHERE a IN (3, 5); }
+step "s1_delete_p" { DELETE FROM inh_global_parent WHERE a = 2; }
+step "s1_delete_c" { DELETE FROM inh_global_parent WHERE a IN (4, 6); }
+step "s1_commit" { COMMIT; }
+teardown
+{
+  DROP TABLE inh_global_temp_child_s1;
+}
+
+# Session 2 executes actions on the parent which act only on the child.
+session "s2"
+setup
+{
+  CREATE GLOBAL TEMPORARY TABLE inh_global_temp_child_s2 () INHERITS (inh_global_parent);
+}
+step "s2_truncate_p" { TRUNCATE inh_global_parent; }
+step "s2_select_p" { SELECT a FROM inh_global_parent; }
+step "s2_select_c" { SELECT a FROM inh_global_temp_child_s2; }
+step "s2_insert_c" { INSERT INTO inh_global_temp_child_s2 VALUES (5), (6); }
+step "s2_update_c" { UPDATE inh_global_parent SET a = 15 WHERE a IN (3, 5); }
+step "s2_delete_c" { DELETE FROM inh_global_parent WHERE a IN (4, 6); }
+teardown
+{
+  DROP TABLE inh_global_temp_child_s2;
+}
+
+# Check INSERT behavior across sessions
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+
+# Check UPDATE behavior across sessions
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s1_update_p" "s1_update_c" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s2_update_c" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+
+# Check DELETE behavior across sessions
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s1_delete_p" "s1_delete_c" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s2_delete_c" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+
+# Check TRUNCATE behavior across sessions
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s1_truncate_p" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s2_truncate_p" "s1_select_p" "s1_select_c" "s2_select_p" "s2_select_c"
+
+# TRUNCATE on a parent tree does not block access to temporary child relation
+# of another session, and blocks when scanning the parent.
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s1_begin" "s1_truncate_p" "s2_select_p" "s1_commit"
+permutation "s1_insert_p" "s1_insert_c" "s2_insert_c" "s1_begin" "s1_truncate_p" "s2_select_c" "s1_commit"
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
new file mode 100644
index 0000000..ae1adb6
--- /dev/null
+++ b/src/test/regress/expected/global_temp.out
@@ -0,0 +1,247 @@
+--
+-- GLOBAL TEMP
+-- Test global temp relations
+--
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE global_temptest(col int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO global_temptest VALUES (1);
+INSERT INTO global_temptest VALUES (2);
+SELECT * FROM global_temptest;
+ col 
+-----
+   1
+   2
+(2 rows)
+
+COMMIT;
+SELECT * FROM global_temptest;
+ col 
+-----
+(0 rows)
+
+DROP TABLE global_temptest;
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temptest(col) ON COMMIT DELETE ROWS AS SELECT 1;
+SELECT * FROM global_temptest;
+ col 
+-----
+   1
+(1 row)
+
+COMMIT;
+SELECT * FROM global_temptest;
+ col 
+-----
+(0 rows)
+
+DROP TABLE global_temptest;
+-- Test foreign keys
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temptest1(col int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE global_temptest2(col int REFERENCES global_temptest1)
+  ON COMMIT DELETE ROWS;
+INSERT INTO global_temptest1 VALUES (1);
+INSERT INTO global_temptest2 VALUES (1);
+COMMIT;
+SELECT * FROM global_temptest1;
+ col 
+-----
+   1
+(1 row)
+
+SELECT * FROM global_temptest2;
+ col 
+-----
+(0 rows)
+
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temptest3(col int PRIMARY KEY) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temptest4(col int REFERENCES global_temptest3);
+COMMIT;
+ERROR:  unsupported ON COMMIT and foreign key combination
+DETAIL:  Table "global_temptest4" references "global_temptest3", but they do not have the same ON COMMIT setting.
+-- For partitioned temp tables, ON COMMIT actions ignore storage-less
+-- partitioned tables.
+BEGIN;
+CREATE GLOBAL TEMP TABLE temp_parted_oncommit (a int)
+  PARTITION BY LIST (a) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE temp_parted_oncommit_1
+  PARTITION OF temp_parted_oncommit
+  FOR VALUES IN (1) ON COMMIT DELETE ROWS;
+INSERT INTO temp_parted_oncommit VALUES (1);
+COMMIT;
+-- partitions are emptied by the previous commit
+SELECT * FROM temp_parted_oncommit;
+ a 
+---
+(0 rows)
+
+DROP TABLE temp_parted_oncommit;
+-- Using ON COMMIT DELETE on a partitioned table does not remove
+-- all rows if partitions preserve their data.
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temp_parted_oncommit_test (a int)
+  PARTITION BY LIST (a) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temp_parted_oncommit_test1
+  PARTITION OF global_temp_parted_oncommit_test
+  FOR VALUES IN (1) ON COMMIT PRESERVE ROWS;
+INSERT INTO global_temp_parted_oncommit_test VALUES (1);
+COMMIT;
+-- Data from the remaining partition is still here as its rows are
+-- preserved.
+SELECT * FROM global_temp_parted_oncommit_test;
+ a 
+---
+ 1
+(1 row)
+
+-- two relations remain in this case.
+SELECT relname FROM pg_class WHERE relname LIKE 'global_temp_parted_oncommit_test%';
+              relname              
+-----------------------------------
+ global_temp_parted_oncommit_test
+ global_temp_parted_oncommit_test1
+(2 rows)
+
+DROP TABLE global_temp_parted_oncommit_test;
+-- Check dependencies between ON COMMIT actions with inheritance trees.
+-- Data on the parent is removed, and the child goes away.
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temp_inh_oncommit_test (a int) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temp_inh_oncommit_test1 ()
+  INHERITS(global_temp_inh_oncommit_test) ON COMMIT PRESERVE ROWS;
+INSERT INTO global_temp_inh_oncommit_test1 VALUES (1);
+INSERT INTO global_temp_inh_oncommit_test VALUES (1);
+COMMIT;
+SELECT * FROM global_temp_inh_oncommit_test;
+ a 
+---
+ 1
+(1 row)
+
+-- two relations remain
+SELECT relname FROM pg_class WHERE relname LIKE 'global_temp_inh_oncommit_test%';
+            relname             
+--------------------------------
+ global_temp_inh_oncommit_test
+ global_temp_inh_oncommit_test1
+(2 rows)
+
+DROP TABLE global_temp_inh_oncommit_test1;
+DROP TABLE global_temp_inh_oncommit_test;
+-- Global temp table cannot inherit from temporary relation
+BEGIN;
+CREATE TEMP TABLE global_temp_table (a int) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temp_table1 ()
+  INHERITS(global_temp_table) ON COMMIT PRESERVE ROWS;
+ERROR:  cannot inherit from temporary relation "global_temp_table"
+ROLLBACK;
+-- Temp table can inherit from global temporary relation
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temp_table (a int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_table1 ()
+  INHERITS(global_temp_table) ON COMMIT PRESERVE ROWS;
+CREATE TEMP TABLE temp_table2 ()
+  INHERITS(global_temp_table) ON COMMIT DELETE ROWS;
+INSERT INTO temp_table2 VALUES (2);
+INSERT INTO temp_table1 VALUES (1);
+INSERT INTO global_temp_table VALUES (0);
+SELECT * FROM global_temp_table;
+ a 
+---
+ 0
+ 1
+ 2
+(3 rows)
+
+COMMIT;
+SELECT * FROM global_temp_table;
+ a 
+---
+ 1
+(1 row)
+
+DROP TABLE temp_table2;
+DROP TABLE temp_table1;
+DROP TABLE global_temp_table;
+-- Global temp table can inherit from normal relation
+BEGIN;
+CREATE TABLE normal_table (a int);
+CREATE GLOBAL TEMP TABLE temp_table1 ()
+  INHERITS(normal_table) ON COMMIT PRESERVE ROWS;
+CREATE GLOBAL TEMP TABLE temp_table2 ()
+  INHERITS(normal_table) ON COMMIT DELETE ROWS;
+INSERT INTO temp_table2 VALUES (2);
+INSERT INTO temp_table1 VALUES (1);
+INSERT INTO normal_table VALUES (0);
+SELECT * FROM normal_table;
+ a 
+---
+ 0
+ 1
+ 2
+(3 rows)
+
+COMMIT;
+SELECT * FROM normal_table;
+ a 
+---
+ 0
+ 1
+(2 rows)
+
+DROP TABLE temp_table2;
+DROP TABLE temp_table1;
+DROP TABLE normal_table;
+-- Check SERIAL and BIGSERIAL pseudo-types
+CREATE GLOBAL TEMP TABLE global_temp_table ( aid BIGSERIAL, bid SERIAL );
+CREATE SEQUENCE test_sequence;
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+SELECT * FROM global_temp_table;
+ aid | bid 
+-----+-----
+   1 |   1
+   2 |   2
+   3 |   3
+(3 rows)
+
+SELECT NEXTVAL( 'test_sequence' );
+ nextval 
+---------
+       1
+(1 row)
+
+\c
+SELECT * FROM global_temp_table;
+ aid | bid 
+-----+-----
+(0 rows)
+
+SELECT NEXTVAL( 'test_sequence' );
+ nextval 
+---------
+       2
+(1 row)
+
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+SELECT * FROM global_temp_table;
+ aid | bid 
+-----+-----
+   1 |   1
+   2 |   2
+   3 |   3
+(3 rows)
+
+SELECT NEXTVAL( 'test_sequence' );
+ nextval 
+---------
+       3
+(1 row)
+
+DROP TABLE global_temp_table;
+DROP SEQUENCE test_sequence;
diff --git a/src/test/regress/expected/session_table.out b/src/test/regress/expected/session_table.out
new file mode 100644
index 0000000..1b9b3f4
--- /dev/null
+++ b/src/test/regress/expected/session_table.out
@@ -0,0 +1,64 @@
+create session table my_private_table(x integer primary key, y integer);
+insert into my_private_table values (generate_series(1,10000), generate_series(1,10000));
+select count(*) from my_private_table;
+ count 
+-------
+ 10000
+(1 row)
+
+\c
+select count(*) from my_private_table;
+ count 
+-------
+     0
+(1 row)
+
+select * from my_private_table where x=10001;
+ x | y 
+---+---
+(0 rows)
+
+insert into my_private_table values (generate_series(1,100000), generate_series(1,100000));
+create index on my_private_table(y);
+select * from my_private_table where x=10001;
+   x   |   y   
+-------+-------
+ 10001 | 10001
+(1 row)
+
+select * from my_private_table where y=10001;
+   x   |   y   
+-------+-------
+ 10001 | 10001
+(1 row)
+
+select count(*) from my_private_table;
+ count  
+--------
+ 100000
+(1 row)
+
+\c
+select * from my_private_table where x=100001;
+ x | y 
+---+---
+(0 rows)
+
+select * from my_private_table order by y desc limit 1;
+ x | y 
+---+---
+(0 rows)
+
+insert into my_private_table values (generate_series(1,100000), generate_series(1,100000));
+select * from my_private_table where x=100001;
+ x | y 
+---+---
+(0 rows)
+
+select * from my_private_table order by y desc limit 1;
+   x    |   y    
+--------+--------
+ 100000 | 100000
+(1 row)
+
+drop table  my_private_table;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fc0f141..507cf7d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath
 # NB: temp.sql does a reconnect which transiently uses 2 connections,
 # so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp global_temp session_table domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 68ac56a..3890777 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -172,6 +172,8 @@ test: limit
 test: plpgsql
 test: copy2
 test: temp
+test: global_temp
+test: session_table
 test: domain
 test: rangefuncs
 test: prepare
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
new file mode 100644
index 0000000..3058b9b
--- /dev/null
+++ b/src/test/regress/sql/global_temp.sql
@@ -0,0 +1,151 @@
+--
+-- GLOBAL TEMP
+-- Test global temp relations
+--
+
+-- Test ON COMMIT DELETE ROWS
+
+CREATE GLOBAL TEMP TABLE global_temptest(col int) ON COMMIT DELETE ROWS;
+
+BEGIN;
+INSERT INTO global_temptest VALUES (1);
+INSERT INTO global_temptest VALUES (2);
+
+SELECT * FROM global_temptest;
+COMMIT;
+
+SELECT * FROM global_temptest;
+
+DROP TABLE global_temptest;
+
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temptest(col) ON COMMIT DELETE ROWS AS SELECT 1;
+
+SELECT * FROM global_temptest;
+COMMIT;
+
+SELECT * FROM global_temptest;
+
+DROP TABLE global_temptest;
+
+-- Test foreign keys
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temptest1(col int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE global_temptest2(col int REFERENCES global_temptest1)
+  ON COMMIT DELETE ROWS;
+INSERT INTO global_temptest1 VALUES (1);
+INSERT INTO global_temptest2 VALUES (1);
+COMMIT;
+SELECT * FROM global_temptest1;
+SELECT * FROM global_temptest2;
+
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temptest3(col int PRIMARY KEY) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temptest4(col int REFERENCES global_temptest3);
+COMMIT;
+
+-- For partitioned temp tables, ON COMMIT actions ignore storage-less
+-- partitioned tables.
+BEGIN;
+CREATE GLOBAL TEMP TABLE temp_parted_oncommit (a int)
+  PARTITION BY LIST (a) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE temp_parted_oncommit_1
+  PARTITION OF temp_parted_oncommit
+  FOR VALUES IN (1) ON COMMIT DELETE ROWS;
+INSERT INTO temp_parted_oncommit VALUES (1);
+COMMIT;
+-- partitions are emptied by the previous commit
+SELECT * FROM temp_parted_oncommit;
+DROP TABLE temp_parted_oncommit;
+
+-- Using ON COMMIT DELETE on a partitioned table does not remove
+-- all rows if partitions preserve their data.
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temp_parted_oncommit_test (a int)
+  PARTITION BY LIST (a) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temp_parted_oncommit_test1
+  PARTITION OF global_temp_parted_oncommit_test
+  FOR VALUES IN (1) ON COMMIT PRESERVE ROWS;
+INSERT INTO global_temp_parted_oncommit_test VALUES (1);
+COMMIT;
+-- Data from the remaining partition is still here as its rows are
+-- preserved.
+SELECT * FROM global_temp_parted_oncommit_test;
+-- two relations remain in this case.
+SELECT relname FROM pg_class WHERE relname LIKE 'global_temp_parted_oncommit_test%';
+DROP TABLE global_temp_parted_oncommit_test;
+
+-- Check dependencies between ON COMMIT actions with inheritance trees.
+-- Data on the parent is removed, and the child goes away.
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temp_inh_oncommit_test (a int) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temp_inh_oncommit_test1 ()
+  INHERITS(global_temp_inh_oncommit_test) ON COMMIT PRESERVE ROWS;
+INSERT INTO global_temp_inh_oncommit_test1 VALUES (1);
+INSERT INTO global_temp_inh_oncommit_test VALUES (1);
+COMMIT;
+SELECT * FROM global_temp_inh_oncommit_test;
+-- two relations remain
+SELECT relname FROM pg_class WHERE relname LIKE 'global_temp_inh_oncommit_test%';
+DROP TABLE global_temp_inh_oncommit_test1;
+DROP TABLE global_temp_inh_oncommit_test;
+
+-- Global temp table cannot inherit from temporary relation
+BEGIN;
+CREATE TEMP TABLE global_temp_table (a int) ON COMMIT DELETE ROWS;
+CREATE GLOBAL TEMP TABLE global_temp_table1 ()
+  INHERITS(global_temp_table) ON COMMIT PRESERVE ROWS;
+ROLLBACK;
+
+-- Temp table can inherit from global temporary relation
+BEGIN;
+CREATE GLOBAL TEMP TABLE global_temp_table (a int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_table1 ()
+  INHERITS(global_temp_table) ON COMMIT PRESERVE ROWS;
+CREATE TEMP TABLE temp_table2 ()
+  INHERITS(global_temp_table) ON COMMIT DELETE ROWS;
+INSERT INTO temp_table2 VALUES (2);
+INSERT INTO temp_table1 VALUES (1);
+INSERT INTO global_temp_table VALUES (0);
+SELECT * FROM global_temp_table;
+COMMIT;
+SELECT * FROM global_temp_table;
+DROP TABLE temp_table2;
+DROP TABLE temp_table1;
+DROP TABLE global_temp_table;
+
+-- Global temp table can inherit from normal relation
+BEGIN;
+CREATE TABLE normal_table (a int);
+CREATE GLOBAL TEMP TABLE temp_table1 ()
+  INHERITS(normal_table) ON COMMIT PRESERVE ROWS;
+CREATE GLOBAL TEMP TABLE temp_table2 ()
+  INHERITS(normal_table) ON COMMIT DELETE ROWS;
+INSERT INTO temp_table2 VALUES (2);
+INSERT INTO temp_table1 VALUES (1);
+INSERT INTO normal_table VALUES (0);
+SELECT * FROM normal_table;
+COMMIT;
+SELECT * FROM normal_table;
+DROP TABLE temp_table2;
+DROP TABLE temp_table1;
+DROP TABLE normal_table;
+
+-- Check SERIAL and BIGSERIAL pseudo-types
+CREATE GLOBAL TEMP TABLE global_temp_table ( aid BIGSERIAL, bid SERIAL );
+CREATE SEQUENCE test_sequence;
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+SELECT * FROM global_temp_table;
+SELECT NEXTVAL( 'test_sequence' );
+\c
+SELECT * FROM global_temp_table;
+SELECT NEXTVAL( 'test_sequence' );
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+INSERT INTO global_temp_table DEFAULT VALUES;
+SELECT * FROM global_temp_table;
+SELECT NEXTVAL( 'test_sequence' );
+DROP TABLE global_temp_table;
+DROP SEQUENCE test_sequence;
diff --git a/src/test/regress/sql/session_table.sql b/src/test/regress/sql/session_table.sql
new file mode 100644
index 0000000..c6663dc
--- /dev/null
+++ b/src/test/regress/sql/session_table.sql
@@ -0,0 +1,18 @@
+create session table my_private_table(x integer primary key, y integer);
+insert into my_private_table values (generate_series(1,10000), generate_series(1,10000));
+select count(*) from my_private_table;
+\c
+select count(*) from my_private_table;
+select * from my_private_table where x=10001;
+insert into my_private_table values (generate_series(1,100000), generate_series(1,100000));
+create index on my_private_table(y);
+select * from my_private_table where x=10001;
+select * from my_private_table where y=10001;
+select count(*) from my_private_table;
+\c
+select * from my_private_table where x=100001;
+select * from my_private_table order by y desc limit 1;
+insert into my_private_table values (generate_series(1,100000), generate_series(1,100000));
+select * from my_private_table where x=100001;
+select * from my_private_table order by y desc limit 1;
+drop table  my_private_table;

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 1 Nov 2019 18:15:20 +0300

On 25.10.2019 20:00, Pavel Stehule wrote:
>
>     >
>     >> So except the limitation mentioned above (which I do not
>     consider as critical) there is only one problem which was not
>     addressed: maintaining statistics for GTT.
>     >> then query execution plan built in one backend will be also
>     used for other backends where it can be inefficient.
> Users doesn't do ANALYZE on temp tables in 99%. It's true. But second 
> fact is so users has lot of problems. It's very similar to wrong 
> statistics on persistent tables. When data are small, then it is not 
> problem for users, although from my perspective it's not optimal. When 
> data are not small, then the problem can be brutal. Temporary tables 
> are not a exception. And users and developers are people - we know 
> only about fatal problems. There are lot of unoptimized queries, but 
> because the problem is not fatal, then it is not reason for report it. 
> And lot of people has not any idea how fast the databases can be. The 
> knowledges of  users and app developers are sad book.
>
> Pavel

It seems to me that I have found quite elegant solution for per-backend 
statistic for GTT: I just inserting it in backend's catalog cache, but 
not in pg_statistic table itself.
To do it I have to add InsertSysCache/InsertCatCache functions which 
insert pinned entry in the correspondent cache.
I wonder if there are some pitfalls of such approach?

New patch for GTT is attached.

 

 	/*

 	pfree(pos.hasMatchKey);
+	return true;
 }

 	/*

 

 
 
 	/* safety check */

 

 
 
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 625af8d..1e192fa 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -93,6 +93,10 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
 			backend = InvalidBackendId;
 			needs_wal = false;
 			break;
+		case RELPERSISTENCE_SESSION:
+			backend = BackendIdForSessionRelations();
+			needs_wal = false;
+			break;
 		case RELPERSISTENCE_PERMANENT:
 			backend = InvalidBackendId;
 			needs_wal = true;
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 7accb95..9f2ea48 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -102,7 +102,7 @@ static int	acquire_inherited_sample_rows(Relation onerel, int elevel,
 										  HeapTuple *rows, int targrows,
 										  double *totalrows, double *totaldeadrows);
 static void update_attstats(Oid relid, bool inh,
-							int natts, VacAttrStats **vacattrstats);
+							int natts, VacAttrStats **vacattrstats, bool is_global_temp);
 static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 
@@ -318,6 +318,7 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 	Oid			save_userid;
 	int			save_sec_context;
 	int			save_nestlevel;
+	bool        is_global_temp = onerel->rd_rel->relpersistence == RELPERSISTENCE_SESSION;
 
 	if (inh)
 		ereport(elevel,
@@ -575,14 +576,14 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 		 * pg_statistic for columns we didn't process, we leave them alone.)
 		 */
 		update_attstats(RelationGetRelid(onerel), inh,
-						attr_cnt, vacattrstats);
+						attr_cnt, vacattrstats, is_global_temp);
 
 		for (ind = 0; ind < nindexes; ind++)
 		{
 			AnlIndexData *thisdata = &indexdata[ind];
 
 			update_attstats(RelationGetRelid(Irel[ind]), false,
-							thisdata->attr_cnt, thisdata->vacattrstats);
+							thisdata->attr_cnt, thisdata->vacattrstats, is_global_temp);
 		}
 
 		/*
@@ -1425,7 +1426,7 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
  *		by taking a self-exclusive lock on the relation in analyze_rel().
  */
 static void
-update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
+update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats, bool is_global_temp)
 {
 	Relation	sd;
 	int			attno;
@@ -1527,30 +1528,42 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 			}
 		}
 
-		/* Is there already a pg_statistic tuple for this attribute? */
-		oldtup = SearchSysCache3(STATRELATTINH,
-								 ObjectIdGetDatum(relid),
-								 Int16GetDatum(stats->attr->attnum),
-								 BoolGetDatum(inh));
-
-		if (HeapTupleIsValid(oldtup))
+		if (is_global_temp)
 		{
-			/* Yes, replace it */
-			stup = heap_modify_tuple(oldtup,
-									 RelationGetDescr(sd),
-									 values,
-									 nulls,
-									 replaces);
-			ReleaseSysCache(oldtup);
-			CatalogTupleUpdate(sd, &stup->t_self, stup);
+			stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+			InsertSysCache(STATRELATTINH,
+						   ObjectIdGetDatum(relid),
+						   Int16GetDatum(stats->attr->attnum),
+						   BoolGetDatum(inh),
+						   0,
+						   stup);
 		}
 		else
 		{
-			/* No, insert new tuple */
-			stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
-			CatalogTupleInsert(sd, stup);
-		}
+			/* Is there already a pg_statistic tuple for this attribute? */
+			oldtup = SearchSysCache3(STATRELATTINH,
+									 ObjectIdGetDatum(relid),
+									 Int16GetDatum(stats->attr->attnum),
+									 BoolGetDatum(inh));
 
+			if (HeapTupleIsValid(oldtup))
+			{
+				/* Yes, replace it */
+				stup = heap_modify_tuple(oldtup,
+										 RelationGetDescr(sd),
+										 values,
+										 nulls,
+										 replaces);
+				ReleaseSysCache(oldtup);
+				CatalogTupleUpdate(sd, &stup->t_self, stup);
+			}
+			else
+			{
+				/* No, insert new tuple */
+				stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+				CatalogTupleInsert(sd, stup);
+			}
+		}
 		heap_freetuple(stup);
 	}

 

 

 

 
 	/*
 
 	END_CRIT_SECTION();

 /*

 
 
 	/*
 
 		relid = classForm->oid;

+/*

+
+/*
 
 	pfree(path);

 	if (fd < 0)
 	{
+		/*
 
+  NewSegment:
 	pfree(path);

 
 	/*
 	 * Start from the last open segments, to avoid redundant seeks.  We have
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index a87e721..2401361 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -994,6 +994,9 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 	/* Determine owning backend. */
 	switch (relform->relpersistence)
 	{
+		case RELPERSISTENCE_SESSION:
+			backend = BackendIdForSessionRelations();
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			backend = InvalidBackendId;
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index c3e7d94..6d86c28 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1191,6 +1191,111 @@ SearchCatCache4(CatCache *cache,
 	return SearchCatCacheInternal(cache, 4, v1, v2, v3, v4);
 }
 
+
+void InsertCatCache(CatCache *cache,
+					Datum v1, Datum v2, Datum v3, Datum v4,
+					HeapTuple tuple)
+{
+	Datum		arguments[CATCACHE_MAXKEYS];
+	uint32		hashValue;
+	Index		hashIndex;
+	CatCTup    *ct;
+	dlist_iter	iter;
+	dlist_head *bucket;
+	int         nkeys = cache->cc_nkeys;
+	MemoryContext oldcxt;
+	int         i;
+
+	/*
+	 * one-time startup overhead for each cache
+	 */
+	if (unlikely(cache->cc_tupdesc == NULL))
+		CatalogCacheInitializeCache(cache);
+
+	/* Initialize local parameter array */
+	arguments[0] = v1;
+	arguments[1] = v2;
+	arguments[2] = v3;
+	arguments[3] = v4;
+	/*
+	 * find the hash bucket in which to look for the tuple
+	 */
+	hashValue = CatalogCacheComputeHashValue(cache, nkeys, v1, v2, v3, v4);
+	hashIndex = HASH_INDEX(hashValue, cache->cc_nbuckets);
+
+	/*
+	 * scan the hash bucket until we find a match or exhaust our tuples
+	 *
+	 * Note: it's okay to use dlist_foreach here, even though we modify the
+	 * dlist within the loop, because we don't continue the loop afterwards.
+	 */
+	bucket = &cache->cc_bucket[hashIndex];
+	dlist_foreach(iter, bucket)
+	{
+		ct = dlist_container(CatCTup, cache_elem, iter.cur);
+
+		if (ct->dead)
+			continue;			/* ignore dead entries */
+
+		if (ct->hash_value != hashValue)
+			continue;			/* quickly skip entry if wrong hash val */
+
+		if (!CatalogCacheCompareTuple(cache, nkeys, ct->keys, arguments))
+			continue;
+
+		/*
+		 * If it's a positive entry, bump its refcount and return it. If it's
+		 * negative, we can report failure to the caller.
+		 */
+		if (ct->tuple.t_len == tuple->t_len)
+		{
+			memcpy((char *) ct->tuple.t_data,
+				   (const char *) tuple->t_data,
+				   tuple->t_len);
+			return;
+		}
+		dlist_delete(&ct->cache_elem);
+		pfree(ct);
+		cache->cc_ntup -= 1;
+		CacheHdr->ch_ntup -= 1;
+		break;
+	}
+	/* Allocate memory for CatCTup and the cached tuple in one go */
+	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+
+	ct = (CatCTup *) palloc(sizeof(CatCTup) +
+							MAXIMUM_ALIGNOF + tuple->t_len);
+	ct->tuple.t_len = tuple->t_len;
+	ct->tuple.t_self = tuple->t_self;
+	ct->tuple.t_tableOid = tuple->t_tableOid;
+	ct->tuple.t_data = (HeapTupleHeader)
+		MAXALIGN(((char *) ct) + sizeof(CatCTup));
+	/* copy tuple contents */
+	memcpy((char *) ct->tuple.t_data,
+		   (const char *) tuple->t_data,
+		   tuple->t_len);
+	ct->ct_magic = CT_MAGIC;
+	ct->my_cache = cache;
+	ct->c_list = NULL;
+	ct->refcount = 1;			/* pinned*/
+	ct->dead = false;
+	ct->negative = false;
+	ct->hash_value = hashValue;
+	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
+	memcpy(ct->keys, arguments, nkeys*sizeof(Datum));
+
+	cache->cc_ntup++;
+	CacheHdr->ch_ntup++;
+	MemoryContextSwitchTo(oldcxt);
+
+	/*
+	 * If the hash table has become too full, enlarge the buckets array. Quite
+	 * arbitrarily, we enlarge when fill factor > 2.
+	 */
+	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+		RehashCatCache(cache);
+}
+
 /*
  * Work-horse for SearchCatCache/SearchCatCacheN.
  */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 585dcee..ce8852c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1098,6 +1098,10 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	relation->rd_newRelfilenodeSubid = InvalidSubTransactionId;
 	switch (relation->rd_rel->relpersistence)
 	{
+		case RELPERSISTENCE_SESSION:
+			relation->rd_backend = BackendIdForSessionRelations();
+			relation->rd_islocaltemp = false;
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			relation->rd_backend = InvalidBackendId;
@@ -3301,6 +3305,10 @@ RelationBuildLocalRelation(const char *relname,
 	rel->rd_rel->relpersistence = relpersistence;
 	switch (relpersistence)
 	{
+		case RELPERSISTENCE_SESSION:
+			rel->rd_backend = BackendIdForSessionRelations();
+			rel->rd_islocaltemp = false;
+			break;
 		case RELPERSISTENCE_UNLOGGED:
 		case RELPERSISTENCE_PERMANENT:
 			rel->rd_backend = InvalidBackendId;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 16297a5..e7a4d3c 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -1164,6 +1164,16 @@ SearchSysCache4(int cacheId,
 	return SearchCatCache4(SysCache[cacheId], key1, key2, key3, key4);
 }
 
+void
+InsertSysCache(int cacheId,
+			   Datum key1, Datum key2, Datum key3, Datum key4,
+			   HeapTuple value)
+{
+	Assert(cacheId >= 0 && cacheId < SysCacheSize &&
+		   PointerIsValid(SysCache[cacheId]));
+	InsertCatCache(SysCache[cacheId], key1, key2, key3, key4, value);
+}
+
 /*
  * ReleaseSysCache
  *		Release previously grabbed reference count on a tuple
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bf69adc..fa7479c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -15637,8 +15637,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 											 tbinfo->dobj.catId.oid, false);

 

+/*

+
 
 /*
 
+/*
 
 /*
+ * Check whether it is global temporary relation which metadata is shared by all sessions,
+ * but data is private for the current session.
+ */
+#define RelFileNodeBackendIsGlobalTemp(rnode) IsSessionRelationBackendId((rnode).backend)
+
+/*
+ * Check whether it is local temporary relation which exists only in this backend.
+ */
+#define RelFileNodeBackendIsLocalTemp(rnode) \
+	(RelFileNodeBackendIsTemp(rnode) && !RelFileNodeBackendIsGlobalTemp(rnode))
+
+/*
  * Note: RelFileNodeEquals and RelFileNodeBackendEquals compare relNode first
  * since that is most likely to be different in two unequal RelFileNodes.  It
  * is probably redundant to compare spcNode if the other fields are found equal,
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index ff1faba..31f615d 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -228,4 +228,8 @@ extern void PrepareToInvalidateCacheTuple(Relation relation,
 extern void PrintCatCacheLeakWarning(HeapTuple tuple);
 extern void PrintCatCacheListLeakWarning(CatCList *list);
 
+extern void InsertCatCache(CatCache *cache,
+						   Datum v1, Datum v2, Datum v3, Datum v4,
+						   HeapTuple tuple);
+
 #endif							/* CATCACHE_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index a5cf804..d42830f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -327,6 +327,18 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->parallel_workers : (defaultpw))
 
+/*
 
+
 /*
  * ViewOptions
 
 /*
  * RELATION_IS_LOCAL
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 918765c..5b1598b 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -216,4 +216,8 @@ extern bool RelationSupportsSysCache(Oid relid);
 
 #define ReleaseSysCacheList(x)	ReleaseCatCacheList(x)
 
+
+extern void InsertSysCache(int cacheId, 
+						   Datum v1, Datum v2, Datum v3, Datum v4,
+						   HeapTuple tuple);
 #endif							/* SYSCACHE_H */
 
 # ----------

---

From: Robert Haas <[email-masked]>
Date: Fri, 1 Nov 2019 11:26:40 -0400

On Fri, Nov 1, 2019 at 11:15 AM Konstantin Knizhnik
<[email-masked]> wrote:
> It seems to me that I have found quite elegant solution for per-backend statistic for GTT: I just inserting it in backend's catalog cache, but not in pg_statistic table itself.
> To do it I have to add InsertSysCache/InsertCatCache functions which insert pinned entry in the correspondent cache.

That sounds pretty hackish. You'd have to be very careful, for
example, that if the tables were dropped or re-analyzed, all of the
old entries got removed -- and then it would still fail if any code
tried to access the statistics directly from the table, rather than
via the caches. My assumption is that the statistics ought to be
stored in some backend-private data structure designed for that
purpose, and that the code that needs the data should be taught to
look for it there when the table is a GTT.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 1 Nov 2019 19:09:49 +0300

On 01.11.2019 18:26, Robert Haas wrote:
> old entries got removed --

I have checked it:
- when table is reanalyzed, then cache entries are replaced.
- when table is dropped, then cache entries are removed.

> and then it would still fail if any code

Yes, if you do "select * from pg_statistic" then you will not see 
statistic for GTT in this case.
But I do not think that it is so critical. I do not believe that anybody 
is trying to manually interpret values in this table.
And optimizer is retrieving statistic through sys-cache mechanism and so 
is able to build correct plan in this case.

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 2 Nov 2019 06:30:33 +0100

pá 1. 11. 2019 v 17:09 odesílatel Konstantin Knizhnik <
[email-masked]> napsal:

> statistic for GTT: I just inserting it in backend's catalog cache, but not
> in pg_statistic table itself.

Years ago, when I though about it, I wrote patch with similar design. It's
working, but surely it's ugly.

I have another idea. Can be pg_statistics view instead a table?

Some like

SELECT * FROM pg_catalog.pg_statistics_rel
UNION ALL
SELECT * FROM pg_catalog.pg_statistics_gtt();

Internally - when stat cache is filled, then there can be used
pg_statistics_rel and pg_statistics_gtt() directly. What I remember, there
was not possibility to work with queries, only with just relations.

Or crazy idea - today we can implement own types of heaps. Is possible to
create engine where result can be combination of some shared data and local
data. So union will be implemented on heap level.
This implementation can be simple, just scanning pages from shared buffers
and from local buffers. For these tables we don't need complex metadata.
It's crazy idea, and I think so union with table function should be best.

Regards

Pavel

---

From: Julien Rouhaud <[email-masked]>
Date: Sat, 2 Nov 2019 08:19:47 +0100

On Sat, Nov 2, 2019 at 6:31 AM Pavel Stehule <[email-masked]> wrote:
>
> pá 1. 11. 2019 v 17:09 odesílatel Konstantin Knizhnik <[email-masked]> napsal:
>>
> Years ago, when I though about it, I wrote patch with similar design. It's working, but surely it's ugly.
>
> Internally - when stat cache is filled, then there can be used pg_statistics_rel and pg_statistics_gtt() directly. What I remember, there was not possibility to work with queries, only with just relations.

It'd be a loss if you lose the ability to see the statistics, as there
are valid use cases where you need to see the stats, eg. understanding
why you don't get the plan you wanted.  There's also at least one
extension [1] that allows you to backup and use restored statistics,
so there are definitely people interested in it.

[1]: https://github.com/ossc-db/pg_dbms_stats

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 2 Nov 2019 08:23:05 +0100

so 2. 11. 2019 v 8:18 odesílatel Julien Rouhaud <[email-masked]> napsal:

> >> >> It seems to me that I have found quite elegant solution for
> per-backend statistic for GTT: I just inserting it in backend's catalog
> cache, but not in pg_statistic table itself.

I don't think - the extensions can use UNION and the content will be same
as caches used by planner.

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 2 Nov 2019 08:24:20 +0100

so 2. 11. 2019 v 8:23 odesílatel Pavel Stehule <[email-masked]>
napsal:

>> >> >> To do it I have to add InsertSysCache/InsertCatCache functions
>> which insert pinned entry in the correspondent cache.

sure, if some one try to modify directly system tables, then it should be
fixed.

---

From: Julien Rouhaud <[email-masked]>
Date: Sat, 2 Nov 2019 09:56:09 +0100

On Sat, Nov 2, 2019 at 8:23 AM Pavel Stehule <[email-masked]> wrote:
> I don't think - the extensions can use UNION and the content will be same as caches used by planner.

Yes, I agree that changing pg_statistics to be a view as you showed

>> >> is able to build correct plan in this case.

which is IMHO a wrong assumption.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Sat, 2 Nov 2019 18:09:42 +0300

> [1]: https://github.com/ossc-db/pg_dbms_stats
It seems to have completely no sense to backup and restore statistic for 
temporary tables which life time is limited to life time of backend,
doesn't it?

---

From: Konstantin Knizhnik <[email-masked]>
Date: Sat, 2 Nov 2019 18:15:50 +0300

On 02.11.2019 8:30, Pavel Stehule wrote:
>
>
> pá 1. 11. 2019 v 17:09 odesílatel Konstantin Knizhnik 
> <[email-masked] <mailto:[email-masked]>> napsal:
>
>
>
>     > <[email-masked] <mailto:[email-masked]>>
>     wrote:
>     per-backend statistic for GTT: I just inserting it in backend's
>     catalog cache, but not in pg_statistic table itself.

And pg_catalog.pg_statistics_gtt() is set returning functions?
I afraid that it is not acceptable solution from performance point of 
view: pg_statictic table is accessed by keys (<relid>,<attpos>,<inh>)
If it can not be done using index scan, then it can cause significant 
performance slow down.

> pg_statistics_rel and pg_statistics_gtt() directly. What I remember, 
> there was not possibility to work with queries, only with just relations.
>
> Or crazy idea - today we can implement own types of heaps. Is possible 
> to create engine where result can be combination of some shared data 
> and local data. So union will be implemented on heap level.
> This implementation can be simple, just scanning pages from shared 
> buffers and from local buffers. For these tables we don't need complex 
> metadata. It's crazy idea, and I think so union with table function 
> should be best.

Frankly speaking, implementing special heap access method for 
pg_statistic just to handle case of global temp tables seems to be overkill
from my point of view. It requires a lot coding (or at least copying a 
lot of code from heapam). Also, as I wrote above, we need also index for 
efficient lookup of statistic.

---

From: Julien Rouhaud <[email-masked]>
Date: Sat, 2 Nov 2019 16:30:08 +0100

> temporary tables which life time is limited to life time of backend,

In general yes I agree, but it doesn't if the goal is to understand
why even after an analyze on the temporary table your query is still
behaving poorly.  It can be useful to allow reproduction or just give
someone else the statistics to see what's going on.

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 2 Nov 2019 17:34:10 +0100

yes

I afraid that it is not acceptable solution from performance point of view:
> pg_statictic table is accessed by keys (<relid>,<attpos>,<inh>)
>

I don't think so it is problem. The any component, that needs to use fast
access can use some special function that check index or check some memory
buffers.

> performance slow down.

where you need fast access when you use SQL access? Inside postgres
optimizer is caches everywhere. And statistics cache should to know so have
to check index and some memory buffers.

The proposed view will not be used by optimizer, but it can be used by some
higher layers. I think so there is a agreement so GTT metadata should not
be stored in system catalogue. If are stored in some syscache or somewhere
else is not important in this moment. But can be nice if for user the GTT
metadata should not be black hole. I think so is better to change some
current tables to views, than use some special function just specialized
for GTT (these functions should to exists in both variants). When I think
about it - this is important not just for functionality that we expect from
GTT. It is important for consistency of Postgres catalog - how much
different should be GTT than other types of tables in system catalogue from
user's perspective.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 06 Nov 2019 21:24:36 +0800

Dear Hackers

I attached the patch of GTT implementationI base on PG12.
The GTT design came from my first email.
Some limitations in patch will be eliminated in later versions.

Later, I will comment on Konstantin's patch and make some proposals for cooperation.
Looking forward to your feedback.

Thanks.

Zeng Wenjing

> 2019年10月29日 上午12:40，Robert Haas <[email-masked]> 写道：
>

---

From: Konstantin Knizhnik <[email-masked]>
Date: Wed, 6 Nov 2019 19:08:12 +0300

> Looking forward to your feedback.

Thank you for this patch.
My first comments:

1.  I have ported you patch to the latest Postgres version (my patch is 
attached).
2. You patch is supporting only B-Tree index for GTT. All other indexes 
(hash, gin, gist, brin,...) are not currently supported.
3. I do not understand the reason for the following limitation:
"We allow to create index on global temp table only this session use it"

First of all it seems to significantly reduce usage of global temp tables.
Why do we need GTT at all? Mostly because we need to access temporary 
data in more than one backend. Otherwise we can just use normal table.
If temp table is expected to be larger enough, so that we need to create 
index for it, then it is hard to believe that it will be needed only in 
one backend.

May be the assumption is that all indexes has to be created before GTT 
start to be used.
But right now this check is not working correctly in any case - if you 
insert some data into the table, then
you can not create index any more:

postgres=# create global temp table gtt(x integer primary key, y integer);
CREATE TABLE
postgres=# insert into gtt values (generate_series(1,100000), 
generate_series(1,100000));
INSERT 0 100000
postgres=# create index on gtt(y);
ERROR:  can not create index when have one or more backend attached this 
global temp table

I wonder why do you need such restriction?

diff --git a/contrib/Makefile b/contrib/Makefile
index 92184ed..4b1a596 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -48,7 +48,8 @@ SUBDIRS = \
 		tsm_system_rows \
 		tsm_system_time \
 		unaccent	\
-		vacuumlo
+		vacuumlo	\
+		pg_gtt
 
 ifeq ($(with_openssl),yes)
 SUBDIRS += sslinfo
diff --git a/contrib/pg_gtt/Makefile b/contrib/pg_gtt/Makefile
new file mode 100644
index 0000000..1d2ef64
--- /dev/null
+++ b/contrib/pg_gtt/Makefile
@@ -0,0 +1,22 @@
+# contrib/pg_gtt/Makefile
+
+MODULE_big = pg_gtt
+OBJS = pg_gtt.o
+
+EXTENSION = pg_gtt
+DATA = pg_gtt--1.0.sql
+
+LDFLAGS_SL += $(filter -lm, $(LIBS))
+
+NO_INSTALLCHECK = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_pfs
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_gtt/pg_gtt--1.0.sql b/contrib/pg_gtt/pg_gtt--1.0.sql
new file mode 100644
index 0000000..3f794d7
--- /dev/null
+++ b/contrib/pg_gtt/pg_gtt--1.0.sql
@@ -0,0 +1,28 @@
+/* contrib/pg_gtt/pg_gtt--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_gtt" to load this file. \quit
+
+
+CREATE FUNCTION pg_gtt_att_statistic(text, text, int)
+RETURNS setof pg_statistic
+AS 'MODULE_PATHNAME','pg_gtt_att_statistic'
+LANGUAGE C STRICT;
+
+CREATE TYPE relstats_type AS (relpages int4, reltuples float4, relallvisible int4, relfrozenxid xid, relminmxid xid);
+CREATE TYPE rel_vac_type AS (pid int4, relfrozenxid xid, relminmxid xid);
+
+CREATE FUNCTION pg_gtt_relstats(text, text)
+RETURNS relstats_type
+AS 'MODULE_PATHNAME','pg_gtt_relstats'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pg_gtt_attached_pid(text, text)
+RETURNS setof int4
+AS 'MODULE_PATHNAME','pg_gtt_attached_pid'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pg_list_gtt_relfrozenxids()
+RETURNS setof rel_vac_type
+AS 'MODULE_PATHNAME','pg_list_gtt_relfrozenxids'
+LANGUAGE C STRICT;
diff --git a/contrib/pg_gtt/pg_gtt.c b/contrib/pg_gtt/pg_gtt.c
new file mode 100644
index 0000000..19e1ec0
--- /dev/null
+++ b/contrib/pg_gtt/pg_gtt.c
@@ -0,0 +1,474 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_gtt.c
+ *	code to management function for global temparary table
+ *
+ * IDENTIFICATION
+ *	  contrib/pg_gtt/pg_gtt.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <unistd.h>
+#include <sys/time.h>
+#include "port.h"
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/xlog.h"
+#include "catalog/pg_type.h"
+#include "catalog/storage_gtt.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
+#include "funcapi.h"
+#include "storage/ipc.h"
+#include "storage/shmem.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/proc.h"
+#include "tcop/utility.h"
+#include "utils/builtins.h"
+#include "utils/memutils.h"
+#include "utils/timeout.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include <nodes/makefuncs.h>
+#include "storage/sinvaladt.h"
+#include "miscadmin.h"
+
+PG_MODULE_MAGIC;
+
+static Oid pg_get_relid(const char *relname, char *schema);
+
+Datum pg_gtt_att_statistic(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pg_gtt_att_statistic);
+
+Datum pg_gtt_relstats(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pg_gtt_relstats);
+
+Datum pg_gtt_attached_pid(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pg_gtt_attached_pid);
+
+Datum pg_list_gtt_relfrozenxids(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pg_list_gtt_relfrozenxids);
+
+void		_PG_init(void);
+void		_PG_fini(void);
+
+void
+_PG_init(void)
+{
+	return;
+}
+
+void
+_PG_fini(void)
+{
+	return;
+}
+
+static Oid
+pg_get_relid(const char *relname, char *schema)
+{
+	Oid			relid;
+	RangeVar   *rv = makeRangeVar(schema, (char *)relname, -1);
+
+	relid = RangeVarGetRelid(rv, NoLock, true);
+
+	pfree(rv);
+	return relid;
+}
+
+Datum
+pg_gtt_att_statistic(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo	*rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc		tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext	oldcontext;
+	HeapTuple		tuple;
+	Relation	rel = NULL;
+	char	*relname = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	char	*schema = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	int		attnum = PG_GETARG_INT32(2);
+	Oid		reloid;
+	char	rel_persistence;
+
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = NULL;
+	rsinfo->setDesc = NULL;
+
+	tupdesc = CreateTemplateTupleDesc(31);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "starelid",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "staattnum",
+						INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "stainherit",
+						BOOLOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 4, "stanullfrac",
+						FLOAT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 5, "stawidth",
+						INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 6, "stadistinct",
+						FLOAT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 7, "stakind1",
+						INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 8, "stakind2",
+						INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 9, "stakind3",
+						INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 10, "stakind4",
+						INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 11, "stakind5",
+						INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 12, "staop1",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 13, "staop2",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 14, "staop3",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 15, "staop4",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 16, "staop5",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 17, "stacoll1",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 18, "stacoll2",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 19, "stacoll3",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 20, "stacoll4",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 21, "stacoll5",
+						OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 22, "stanumbers1",
+						FLOAT4ARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 23, "stanumbers2",
+						FLOAT4ARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 24, "stanumbers3",
+						FLOAT4ARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 25, "stanumbers4",
+						FLOAT4ARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 26, "stanumbers5",
+						FLOAT4ARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 27, "stavalues1",
+						ANYARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 28, "stavalues2",
+						ANYARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 29, "stavalues3",
+						ANYARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 30, "stavalues4",
+						ANYARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 31, "stavalues5",
+						ANYARRAYOID, -1, 0);
+
+	oldcontext = MemoryContextSwitchTo(
+			rsinfo->econtext->ecxt_per_query_memory);
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+	MemoryContextSwitchTo(oldcontext);
+
+	if (schema == NULL)
+		schema = "public";
+
+	reloid = pg_get_relid(relname, schema);
+	if (reloid == InvalidOid)
+	{
+		elog(WARNING, "relation %s.%s does not exist", schema, relname);
+		return (Datum) 0;
+	}
+
+	rel = table_open(reloid, AccessShareLock);
+	rel_persistence = get_rel_persistence(reloid);
+	if (rel_persistence != RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		elog(WARNING, "relation %s.%s not global temp table", schema, relname);
+		return (Datum) 0;
+	}
+
+	tuple = get_gtt_att_statistic(reloid, attnum, false);
+	if (tuple)
+	{
+		HeapTuple tp = heap_copytuple(tuple);
+		tuplestore_puttuple(tupstore, tp);
+	}
+	tuplestore_donestoring(tupstore);
+
+	table_close(rel, NoLock);
+	return (Datum) 0;
+}
+
+Datum
+pg_gtt_relstats(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo	*rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext	oldcontext;
+	HeapTuple	tuple;
+	char	*relname = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	char	*schema = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	Oid	reloid;
+	char	rel_persistence;
+	Datum	values[5];
+	bool	isnull[5];
+	BlockNumber	relpages = 0;
+	double		reltuples = 0;
+	BlockNumber	relallvisible = 0;
+	uint32		relfrozenxid = 0;
+	uint32		relminmxid = 0;
+	Relation	rel = NULL;
+
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = NULL;
+	rsinfo->setDesc = NULL;
+
+	tupdesc = CreateTemplateTupleDesc(5);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relpages",
+						INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "reltuples",
+						FLOAT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "relallvisible",
+						INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 4, "relfrozenxid",
+						XIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 5, "relminmxid",
+						XIDOID, -1, 0);
+
+	oldcontext = MemoryContextSwitchTo(
+			rsinfo->econtext->ecxt_per_query_memory);
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+	MemoryContextSwitchTo(oldcontext);
+
+	if (schema == NULL)
+		schema = "public";
+
+	reloid = pg_get_relid(relname, schema);
+	if (reloid == InvalidOid)
+	{
+		elog(WARNING, "relation %s.%s does not exist", schema, relname);
+		return (Datum) 0;
+	}
+
+	rel = table_open(reloid, AccessShareLock);
+	rel_persistence = get_rel_persistence(reloid);
+	if (rel_persistence != RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		elog(WARNING, "relation %s.%s not global temp table", schema, relname);
+		table_close(rel, NoLock);
+		return (Datum) 0;
+	}
+
+	get_gtt_relstats(reloid,
+					&relpages, &reltuples, &relallvisible,
+					&relfrozenxid, &relminmxid);
+
+	memset(isnull, false, sizeof(isnull));
+	memset(values, 0, sizeof(values));
+	values[0] = Int32GetDatum(relpages);
+	values[1] = Float4GetDatum((float4)reltuples);
+	values[2] = Int32GetDatum(relallvisible);
+	values[3] = UInt32GetDatum(relfrozenxid);
+	values[4] = UInt32GetDatum(relminmxid);
+	tuple = heap_form_tuple(tupdesc, values, isnull);
+	tuplestore_puttuple(tupstore, tuple);
+	tuplestore_donestoring(tupstore);
+
+	table_close(rel, NoLock);
+	return (Datum) 0;
+}
+
+Datum
+pg_gtt_attached_pid(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo	*rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext	oldcontext;
+	HeapTuple	tuple;
+	char	*relname = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	char	*schema = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	Oid	reloid;
+	char	rel_persistence;
+	Datum	values[1];
+	bool	isnull[1];
+	Relation	rel = NULL;
+	PGPROC		*proc = NULL;
+	Bitmapset	*map = NULL;
+	pid_t		pid = 0;
+	int			backendid = 0;
+
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = NULL;
+	rsinfo->setDesc = NULL;
+
+	tupdesc = CreateTemplateTupleDesc(1);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pid",
+						INT4OID, -1, 0);
+
+	oldcontext = MemoryContextSwitchTo(
+			rsinfo->econtext->ecxt_per_query_memory);
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+	MemoryContextSwitchTo(oldcontext);
+
+	if (schema == NULL)
+		schema = "public";
+
+	reloid = pg_get_relid(relname, schema);
+	if (reloid == InvalidOid)
+	{
+		elog(WARNING, "relation %s.%s does not exist", schema, relname);
+		return (Datum) 0;
+	}
+
+	rel = table_open(reloid, AccessShareLock);
+	rel_persistence = get_rel_persistence(reloid);
+	if (rel_persistence != RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		elog(WARNING, "relation %s.%s not global temp table", schema, relname);
+		return (Datum) 0;
+	}
+
+	map = copy_active_gtt_bitmap(rel->rd_node);
+	if (map)
+	{
+		backendid = bms_first_member(map);
+
+		do
+		{
+			proc = BackendIdGetProc(backendid);
+			pid = proc->pid;
+			if (pid > 0)
+			{
+				memset(isnull, false, sizeof(isnull));
+				memset(values, 0, sizeof(values));
+				values[0] = Int32GetDatum(pid);
+				tuple = heap_form_tuple(tupdesc, values, isnull);
+				tuplestore_puttuple(tupstore, tuple);
+			}
+			backendid = bms_next_member(map, backendid);
+		} while (backendid > 0);
+
+		pfree(map);
+	}
+
+	tuplestore_donestoring(tupstore);
+	table_close(rel, NoLock);
+
+	return (Datum) 0;
+}
+
+Datum
+pg_list_gtt_relfrozenxids(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo	*rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext	oldcontext;
+	HeapTuple	tuple;
+	Datum	values[3];
+	bool	isnull[3];
+	int			num_xid = MaxBackends + 1;
+	int			*pids = NULL;
+	uint32		*xids = NULL;
+	int			i = 0;
+	int			j = 0;
+	uint32		oldest = 0;
+
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = NULL;
+	rsinfo->setDesc = NULL;
+
+	tupdesc = CreateTemplateTupleDesc(3);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pid",
+						INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "relfrozenxid",
+						XIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "relminmxid",
+						XIDOID, -1, 0);
+
+	oldcontext = MemoryContextSwitchTo(
+			rsinfo->econtext->ecxt_per_query_memory);
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+	MemoryContextSwitchTo(oldcontext);
+
+	if (max_active_gtt <= 0)
+		return (Datum) 0;
+
+	if (RecoveryInProgress())
+		return (Datum) 0;
+
+	pids = palloc0(sizeof(int) * num_xid);
+	xids = palloc0(sizeof(int) * num_xid);
+	oldest = list_all_session_gtt_frozenxids(num_xid, pids, xids, &i);
+	if (i > 0)
+	{
+		if (i > 0)
+		{
+			pids[i] = 0;
+			xids[i] = oldest;
+			i++;
+		}
+
+		for(j = 0; j < i; j++)
+		{
+			memset(isnull, false, sizeof(isnull));
+			memset(values, 0, sizeof(values));
+			values[0] = Int32GetDatum(pids[j]);
+			values[1] = UInt32GetDatum(xids[j]);
+			values[2] = UInt32GetDatum(0);
+			tuple = heap_form_tuple(tupdesc, values, isnull);
+			tuplestore_puttuple(tupstore, tuple);
+		}
+	}
+	tuplestore_donestoring(tupstore);
+	pfree(pids);
+	pfree(xids);
+
+	return (Datum) 0;
+}
+
diff --git a/contrib/pg_gtt/pg_gtt.control b/contrib/pg_gtt/pg_gtt.control
new file mode 100644
index 0000000..342af1d
--- /dev/null
+++ b/contrib/pg_gtt/pg_gtt.control
@@ -0,0 +1,4 @@
+comment = 'pg_gtt'
+default_version = '1.0'
+module_pathname = '$libdir/pg_gtt'
+relocatable = true
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d8790ad..7e30039 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -158,6 +158,19 @@ static relopt_bool boolRelOpts[] =
 		},
 		true
 	},
+	/*
+	 * For global temp table only
+	 * use AccessExclusiveLock for ensure safety
+	 */
+	{
+		{
+			"on_commit_delete_rows",
+			"global temp table on commit options",
+			RELOPT_KIND_HEAP,
+			ShareUpdateExclusiveLock
+		},
+		true
+	},	
 	/* list terminator */
 	{{NULL}}
 };
@@ -1475,6 +1488,8 @@ bytea *
 default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 {
 	static const relopt_parse_elt tab[] = {
+		{"on_commit_delete_rows", RELOPT_TYPE_BOOL,
+		offsetof(StdRdOptions, on_commit_delete_rows)},
 		{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
 		{"autovacuum_enabled", RELOPT_TYPE_BOOL,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index a23dec7..79bdb26 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -1014,7 +1014,9 @@ gistGetFakeLSN(Relation rel)
 {
 	static XLogRecPtr counter = FirstNormalUnloggedLSN;
 
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	/* global temp is same as local temp table */
+	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		rel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		/*
 		 * Temporary relations are only accessible in our session, so a simple
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 5cc30da..49b7d2e 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -148,7 +148,9 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 * metapage, nor the first bitmap page.
 	 */
 	sort_threshold = (maintenance_work_mem * 1024L) / BLCKSZ;
-	if (index->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+	/* global temp table is same as local temp table */
+	if (!(index->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		index->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP))
 		sort_threshold = Min(sort_threshold, NBuffers);
 	else
 		sort_threshold = Min(sort_threshold, NLocBuffer);
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 2dd8821..24c56b9 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -601,7 +601,7 @@ heapam_relation_set_new_filenode(Relation rel,
 	 */
 	*minmulti = GetOldestMultiXactId();
 
-	srel = RelationCreateStorage(*newrnode, persistence);
+	srel = RelationCreateStorage(*newrnode, persistence, rel);
 
 	/*
 	 * If required, set up an init fork for an unlogged table so that it can
@@ -654,7 +654,7 @@ heapam_relation_copy_data(Relation rel, const RelFileNode *newrnode)
 	 * NOTE: any conflict in relfilenode value will be caught in
 	 * RelationCreateStorage().
 	 */
-	RelationCreateStorage(*newrnode, rel->rd_rel->relpersistence);
+	RelationCreateStorage(*newrnode, rel->rd_rel->relpersistence, rel);
 
 	/* copy main fork */
 	RelationCopyStorage(rel->rd_smgr, dstrel, MAIN_FORKNUM,
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index a3c4a1d..03fac55 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -60,6 +60,7 @@
 #include "utils/pg_rusage.h"
 #include "utils/timestamp.h"
 
+#include "catalog/storage_gtt.h"
 
 /*
  * Space/time tradeoff parameters: do these need to be user-tunable?
@@ -214,8 +215,10 @@ heap_vacuum_rel(Relation onerel, VacuumParams *params,
 	Assert(params->truncate != VACOPT_TERNARY_DEFAULT);
 
 	/* not every AM requires these to be valid, but heap does */
-	Assert(TransactionIdIsNormal(onerel->rd_rel->relfrozenxid));
-	Assert(MultiXactIdIsValid(onerel->rd_rel->relminmxid));
+	Assert((RELATION_IS_GLOBAL_TEMP(onerel) && onerel->rd_rel->relfrozenxid == InvalidTransactionId) ||
+		(!RELATION_IS_GLOBAL_TEMP(onerel) && TransactionIdIsNormal(onerel->rd_rel->relfrozenxid)));
+	Assert((RELATION_IS_GLOBAL_TEMP(onerel) && onerel->rd_rel->relminmxid == InvalidMultiXactId) ||
+		(!RELATION_IS_GLOBAL_TEMP(onerel) && MultiXactIdIsValid(onerel->rd_rel->relminmxid)));
 
 	/* measure elapsed time iff autovacuum logging requires it */
 	if (IsAutoVacuumWorkerProcess() && params->log_min_duration >= 0)
@@ -274,8 +277,19 @@ heap_vacuum_rel(Relation onerel, VacuumParams *params,
 
 	vacrelstats = (LVRelStats *) palloc0(sizeof(LVRelStats));
 
-	vacrelstats->old_rel_pages = onerel->rd_rel->relpages;
-	vacrelstats->old_live_tuples = onerel->rd_rel->reltuples;
+	/* get relstat from gtt localhash */
+	if (RELATION_IS_GLOBAL_TEMP(onerel))
+	{
+		get_gtt_relstats(RelationGetRelid(onerel),
+						&vacrelstats->old_rel_pages,
+						&vacrelstats->old_live_tuples,
+						NULL, NULL, NULL);
+	}
+	else
+	{
+		vacrelstats->old_rel_pages = onerel->rd_rel->relpages;
+		vacrelstats->old_live_tuples = onerel->rd_rel->reltuples;
+	}
 	vacrelstats->num_index_scans = 0;
 	vacrelstats->pages_removed = 0;
 	vacrelstats->lock_waiter_detected = false;
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 268f869..8b2e610 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -763,7 +763,14 @@ _bt_getbuf(Relation rel, BlockNumber blkno, int access)
 		/* Read an existing block of the relation */
 		buf = ReadBuffer(rel, blkno);
 		LockBuffer(buf, access);
-		_bt_checkpage(rel, buf);
+
+		/* global temp table may be not yet initialized for this backend. */
+		if (RELATION_IS_GLOBAL_TEMP(rel) &&
+			blkno == BTREE_METAPAGE &&
+			PageIsNew(BufferGetPage(buf)))
+			_bt_initmetapage(BufferGetPage(buf), P_NONE, 0);
+		else
+			_bt_checkpage(rel, buf);
 	}
 	else
 	{
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2e3cc51..b8a57b6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -6314,6 +6314,10 @@ StartupXLOG(void)
 	else
 		recoveryTargetTLI = ControlFile->checkPointCopy.ThisTimeLineID;
 
+	/* clean temp relation files */
+	if (max_active_gtt > 0)
+		RemovePgTempFiles();
+
 	/*
 	 * Check for signal files, and if so set up state for offline recovery
 	 */
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a511532..665b7fb 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -44,6 +44,8 @@ OBJS = \
 	storage.o \
 	toasting.o
 
+OBJS += storage_gtt.o
+
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 1af31c2..ecb516e 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -399,7 +399,9 @@ GetNewRelFileNode(Oid reltablespace, Relation pg_class, char relpersistence)
 
 	switch (relpersistence)
 	{
+		/* global temp table is same as local temp table */
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			backend = BackendIdForTempRelations();
 			break;
 		case RELPERSISTENCE_UNLOGGED:
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b7bcdd9..6e089dd 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -61,6 +61,7 @@
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
+#include "catalog/storage_gtt.h"
 #include "commands/tablecmds.h"
 #include "commands/typecmds.h"
 #include "executor/executor.h"
@@ -100,6 +101,7 @@ static void AddNewRelationTuple(Relation pg_class_desc,
 								Oid reloftype,
 								Oid relowner,
 								char relkind,
+								char relpersistence,
 								TransactionId relfrozenxid,
 								TransactionId relminmxid,
 								Datum relacl,
@@ -405,6 +407,10 @@ heap_create(const char *relname,
 									 relpersistence,
 									 relkind);
 
+	/* global temp table not create storage file when catalog create */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		create_storage = false;
+
 	/*
 	 * Have the storage manager create the relation's disk file, if needed.
 	 *
@@ -428,7 +434,7 @@ heap_create(const char *relname,
 
 			case RELKIND_INDEX:
 			case RELKIND_SEQUENCE:
-				RelationCreateStorage(rel->rd_node, relpersistence);
+				RelationCreateStorage(rel->rd_node, relpersistence, rel);
 				break;
 
 			case RELKIND_RELATION:
@@ -925,6 +931,7 @@ AddNewRelationTuple(Relation pg_class_desc,
 					Oid reloftype,
 					Oid relowner,
 					char relkind,
+					char relpersistence,
 					TransactionId relfrozenxid,
 					TransactionId relminmxid,
 					Datum relacl,
@@ -963,8 +970,18 @@ AddNewRelationTuple(Relation pg_class_desc,
 			break;
 	}
 
-	new_rel_reltup->relfrozenxid = relfrozenxid;
-	new_rel_reltup->relminmxid = relminmxid;
+	/* global temp table not remember transaction info in catalog */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		new_rel_reltup->relfrozenxid = InvalidTransactionId;
+		new_rel_reltup->relminmxid = InvalidMultiXactId;
+	}
+	else
+	{
+		new_rel_reltup->relfrozenxid = relfrozenxid;
+		new_rel_reltup->relminmxid = relminmxid;
+	}
+
 	new_rel_reltup->relowner = relowner;
 	new_rel_reltup->reltype = new_type_oid;
 	new_rel_reltup->reloftype = reloftype;
@@ -1326,6 +1343,7 @@ heap_create_with_catalog(const char *relname,
 						reloftypeid,
 						ownerid,
 						relkind,
+						relpersistence,
 						relfrozenxid,
 						relminmxid,
 						PointerGetDatum(relacl),
@@ -1410,11 +1428,15 @@ heap_create_with_catalog(const char *relname,
 	 */
 	StoreConstraints(new_rel_desc, cooked_constraints, is_internal);
 
-	/*
-	 * If there's a special on-commit action, remember it
-	 */
-	if (oncommit != ONCOMMIT_NOOP)
-		register_on_commit_action(relid, oncommit);
+	/* global temp table register action when storage init */
+	if (relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		/*
+		 * If there's a special on-commit action, remember it
+		 */
+		if (oncommit != ONCOMMIT_NOOP)
+			register_on_commit_action(relid, oncommit);
+	}
 
 	/*
 	 * ok, the relation has been cataloged, so close our relations and return
@@ -1906,6 +1928,13 @@ heap_drop_with_catalog(Oid relid)
 	if (relid == defaultPartOid)
 		update_default_partition_oid(parentOid, InvalidOid);
 
+	/* We allow to drop global temp table only this session use it */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+	{
+		if (is_other_backend_use_gtt(rel->rd_node))
+			elog(ERROR, "can not drop relation when other backend attached this global temp table");
+	}
+
 	/*
 	 * Schedule unlinking of the relation's physical files at commit.
 	 */
@@ -3132,9 +3161,10 @@ RemoveStatistics(Oid relid, AttrNumber attnum)
  *
  * The routine will truncate and then reconstruct the indexes on
  * the specified relation.  Caller must hold exclusive lock on rel.
+ *
  */
 static void
-RelationTruncateIndexes(Relation heapRelation)
+RelationTruncateIndexes(Relation heapRelation, LOCKMODE lockmode)
 {
 	ListCell   *indlist;
 
@@ -3146,7 +3176,7 @@ RelationTruncateIndexes(Relation heapRelation)
 		IndexInfo  *indexInfo;
 
 		/* Open the index relation; use exclusive lock, just to be sure */
-		currentIndex = index_open(indexId, AccessExclusiveLock);
+		currentIndex = index_open(indexId, lockmode);
 
 		/* Fetch info needed for index_build */
 		indexInfo = BuildIndexInfo(currentIndex);
@@ -3185,8 +3215,13 @@ heap_truncate(List *relids)
 	{
 		Oid			rid = lfirst_oid(cell);
 		Relation	rel;
+		LOCKMODE	lockmode = AccessExclusiveLock;
 
-		rel = table_open(rid, AccessExclusiveLock);
+		/* truncate global temp table only need RowExclusiveLock */
+		if (get_rel_persistence(rid) == RELPERSISTENCE_GLOBAL_TEMP)
+			lockmode = RowExclusiveLock;
+
+		rel = table_open(rid, lockmode);
 		relations = lappend(relations, rel);
 	}
 
@@ -3219,6 +3254,8 @@ void
 heap_truncate_one_rel(Relation rel)
 {
 	Oid			toastrelid;
+	LOCKMODE	lockmode = AccessExclusiveLock;
+	bool		truncate_toastrel = false;
 
 	/*
 	 * Truncate the relation.  Partitioned tables have no storage, so there is
@@ -3227,23 +3264,40 @@ heap_truncate_one_rel(Relation rel)
 	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 		return;
 
+	toastrelid = rel->rd_rel->reltoastrelid;
+
+	/*
+	 * Truncate global temp table only need RowExclusiveLock
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+	{
+		lockmode = RowExclusiveLock;
+		if (OidIsValid(toastrelid) &&
+			gtt_storage_attached(toastrelid))
+			truncate_toastrel = true;
+	}
+	else if (OidIsValid(toastrelid))
+		truncate_toastrel = true;
+
 	/* Truncate the underlying relation */
 	table_relation_nontransactional_truncate(rel);
 
 	/* If the relation has indexes, truncate the indexes too */
-	RelationTruncateIndexes(rel);
+	RelationTruncateIndexes(rel, lockmode);
 
 	/* If there is a toast table, truncate that too */
-	toastrelid = rel->rd_rel->reltoastrelid;
-	if (OidIsValid(toastrelid))
+	if (truncate_toastrel)
 	{
-		Relation	toastrel = table_open(toastrelid, AccessExclusiveLock);
+		Relation	toastrel = table_open(toastrelid, lockmode);
 
 		table_relation_nontransactional_truncate(toastrel);
-		RelationTruncateIndexes(toastrel);
+		RelationTruncateIndexes(toastrel, lockmode);
 		/* keep the lock... */
 		table_close(toastrel, NoLock);
 	}
+
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+		up_gtt_relstats(rel, 0, 0, 0, RecentXmin, InvalidMultiXactId);
 }
 
 /*
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7c34509..f623626 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -52,6 +52,7 @@
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
+#include "catalog/storage_gtt.h"
 #include "commands/event_trigger.h"
 #include "commands/progress.h"
 #include "commands/tablecmds.h"
@@ -879,6 +880,26 @@ index_create(Relation heapRelation,
 						indexRelationName, RelationGetRelationName(heapRelation))));
 	}
 
+	if (RELATION_IS_GLOBAL_TEMP(heapRelation))
+	{
+		if (accessMethodObjectId != BTREE_AM_OID)
+			elog(ERROR, "only support btree index on global temp table");
+
+		/* We allow to create index on global temp table only this session use it */
+		if (is_other_backend_use_gtt(heapRelation->rd_node) ||
+			gtt_storage_attached(heapRelation->rd_node.relNode))
+			elog(ERROR, "can not create index when have one or more backend attached this global temp table");
+
+		/* No support create index on global temp table use concurrent mode yet */
+		if (concurrent)
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot create indexes on global temporary tables using concurrent mode")));
+
+		/* if global temp table not init storage, then skip build index */
+		flags |= INDEX_CREATE_SKIP_BUILD;
+	}
+
 	/*
 	 * construct tuple descriptor for index tuples
 	 */
@@ -2047,6 +2068,13 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 	 */
 	CheckTableNotInUse(userIndexRelation, "DROP INDEX");
 
+	/* We allow to drop index on global temp table only this session use it */
+	if (RELATION_IS_GLOBAL_TEMP(userHeapRelation))
+	{
+		if (is_other_backend_use_gtt(userHeapRelation->rd_node))
+			elog(ERROR, "can not drop index when other backend attached this global temp table");
+	}
+
 	/*
 	 * Drop Index Concurrently is more or less the reverse process of Create
 	 * Index Concurrently.
@@ -2688,20 +2716,29 @@ index_update_stats(Relation rel,
 		else					/* don't bother for indexes */
 			relallvisible = 0;
 
-		if (rd_rel->relpages != (int32) relpages)
+		/* update index stats into localhash for global temp table */
+		if (RELATION_IS_GLOBAL_TEMP(rel))
 		{
-			rd_rel->relpages = (int32) relpages;
-			dirty = true;
+			up_gtt_relstats(rel, relpages, reltuples, relallvisible,
+							InvalidTransactionId, InvalidMultiXactId);
 		}
-		if (rd_rel->reltuples != (float4) reltuples)
-		{
-			rd_rel->reltuples = (float4) reltuples;
-			dirty = true;
-		}
-		if (rd_rel->relallvisible != (int32) relallvisible)
+		else
 		{
-			rd_rel->relallvisible = (int32) relallvisible;
-			dirty = true;
+			if (rd_rel->relpages != (int32) relpages)
+			{
+				rd_rel->relpages = (int32) relpages;
+				dirty = true;
+			}
+			if (rd_rel->reltuples != (float4) reltuples)
+			{
+				rd_rel->reltuples = (float4) reltuples;
+				dirty = true;
+			}
+			if (rd_rel->relallvisible != (int32) relallvisible)
+			{
+				rd_rel->relallvisible = (int32) relallvisible;
+				dirty = true;
+			}
 		}
 	}
 
@@ -2816,6 +2853,13 @@ index_build(Relation heapRelation,
 		pgstat_progress_update_multi_param(6, index, val);
 	}
 
+	/* POALR */
+	if (RELATION_IS_GLOBAL_TEMP(indexRelation))
+	{
+		if (!gtt_storage_attached(indexRelation->rd_node.relNode))
+			RelationCreateStorage(indexRelation->rd_node, RELPERSISTENCE_GLOBAL_TEMP, indexRelation);
+	}
+
 	/*
 	 * Call the access method's build procedure
 	 */
@@ -3148,12 +3192,22 @@ validate_index(Oid heapId, Oid indexId, Snapshot snapshot)
 	/*
 	 * Scan the index and gather up all the TIDs into a tuplesort object.
 	 */
+	memset(&ivinfo, 0, sizeof(IndexVacuumInfo));
 	ivinfo.index = indexRelation;
 	ivinfo.analyze_only = false;
 	ivinfo.report_progress = true;
 	ivinfo.estimated_count = true;
 	ivinfo.message_level = DEBUG2;
-	ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
+
+	/* get relstats abort global temp table from hashtable, not catalog */
+	if (RELATION_IS_GLOBAL_TEMP(heapRelation))
+	{
+		get_gtt_relstats(RelationGetRelid(heapRelation),
+						NULL, &ivinfo.num_heap_tuples, NULL,
+						NULL, NULL);
+	}
+	else
+		ivinfo.num_heap_tuples = heapRelation->rd_rel->reltuples;
 	ivinfo.strategy = NULL;
 
 	/*
@@ -3411,6 +3465,15 @@ reindex_index(Oid indexId, bool skip_constraint_checks, char persistence,
 				 errmsg("cannot reindex temporary tables of other sessions")));
 
 	/*
+	 * Because global temp table cannot change relfilenode
+	 * no support reindex on global temp table yet.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(iRel))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot reindex global temporary tables")));
+
+	/*
 	 * Also check for active uses of the index in the current transaction; we
 	 * don't want to reindex underneath an open indexscan.
 	 */
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index e251f5a..97bf247 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -647,6 +647,13 @@ RangeVarAdjustRelationPersistence(RangeVar *newRelation, Oid nspid)
 							 errmsg("cannot create temporary relation in non-temporary schema")));
 			}
 			break;
+		/* global temp table */
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			if (isAnyTempNamespace(nspid))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot create global temp relations in temporary schemas")));
+			break;
 		case RELPERSISTENCE_PERMANENT:
 			if (isTempOrTempToastNamespace(nspid))
 				newRelation->relpersistence = RELPERSISTENCE_TEMP;
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 625af8d..baf7d61 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -28,6 +28,7 @@
 #include "access/xlogutils.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
+#include "catalog/storage_gtt.h"
 #include "storage/freespace.h"
 #include "storage/smgr.h"
 #include "utils/memutils.h"
@@ -74,9 +75,10 @@ static PendingRelDelete *pendingDeletes = NULL; /* head of linked list */
  *
  * This function is transactional. The creation is WAL-logged, and if the
  * transaction aborts later on, the storage will be destroyed.
+ *
  */
 SMgrRelation
-RelationCreateStorage(RelFileNode rnode, char relpersistence)
+RelationCreateStorage(RelFileNode rnode, char relpersistence, Relation rel)
 {
 	PendingRelDelete *pending;
 	SMgrRelation srel;
@@ -86,6 +88,8 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		/* global temp table use same storage strategy as local temp table */
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			backend = BackendIdForTempRelations();
 			needs_wal = false;
 			break;
@@ -118,6 +122,10 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
 	pending->next = pendingDeletes;
 	pendingDeletes = pending;
 
+	/* remember global temp table storage info to localhash */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP && rel)
+		remember_gtt_storage_info(rnode, rel);
+
 	return srel;
 }
 
@@ -487,8 +495,15 @@ smgrDoPendingDeletes(bool isCommit)
 		smgrdounlinkall(srels, nrels, false);
 
 		for (i = 0; i < nrels; i++)
+		{
 			smgrclose(srels[i]);
 
+			/* clean global temp table flags when transaction commit or rollback */
+			if (SmgrIsTemp(srels[i]) && 
+				gtt_storage_attached(srels[i]->smgr_rnode.node.relNode))
+				forget_gtt_storage_info(srels[i]->smgr_rnode.node.relNode);
+		}
+
 		pfree(srels);
 	}
 }
diff --git a/src/backend/catalog/storage_gtt.c b/src/backend/catalog/storage_gtt.c
new file mode 100644
index 0000000..c7b9a48
--- /dev/null
+++ b/src/backend/catalog/storage_gtt.c
@@ -0,0 +1,810 @@
+/*-------------------------------------------------------------------------
+ *
+ * storage_gtt.c
+ *	  code to create and destroy physical storage for global temparary table
+ *
+ * IDENTIFICATION
+ *	  src/backend/catalog/storage_gtt.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/visibilitymap.h"
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xloginsert.h"
+#include "access/xlogutils.h"
+#include "access/htup_details.h"
+#include "access/multixact.h"
+#include "catalog/storage.h"
+#include "catalog/storage_xlog.h"
+#include "catalog/storage_gtt.h"
+#include "catalog/heap.h"
+#include "commands/tablecmds.h"
+#include "nodes/primnodes.h"
+#include "nodes/pg_list.h"
+#include "miscadmin.h"
+#include "storage/freespace.h"
+#include "storage/smgr.h"
+#include "storage/ipc.h"
+#include "storage/proc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/hsearch.h"
+#include "utils/catcache.h"
+#include <utils/relcache.h>
+#include "utils/inval.h"
+#include "utils/guc.h"
+#include "utils/guc.h"
+
+
+/* Copy from bitmapset.c, because gtt used the function in bitmapset.c */
+#define WORDNUM(x)	((x) / BITS_PER_BITMAPWORD)
+#define BITNUM(x)	((x) % BITS_PER_BITMAPWORD)
+
+#define BITMAPSET_SIZE(nwords)	\
+	(offsetof(Bitmapset, words) + (nwords) * sizeof(bitmapword))
+
+static bool gtt_cleaner_exit_registered = false;
+static HTAB *gtt_storage_local_hash = NULL;
+static HTAB *active_gtt_shared_hash = NULL;
+static MemoryContext gtt_relstats_context = NULL;
+
+/* relfrozenxid of all gtts in the current session */
+static List *gtt_session_relfrozenxid_list = NIL;
+static TransactionId gtt_session_frozenxid = InvalidTransactionId;
+
+typedef struct gtt_ctl_data
+{
+	LWLock			lock;
+	int			max_entry;
+	int			entry_size;
+}gtt_ctl_data;
+
+static gtt_ctl_data *gtt_shared_ctl = NULL;
+
+typedef struct
+{
+	RelFileNode	rnode;
+	Bitmapset	*map;
+	/* bitmap data */
+} gtt_shared_hash_entry;
+
+typedef struct
+{
+	Oid			relid;
+
+	Oid			spcnode;
+	/* pg_class stat */
+	int32		relpages;
+	float4		reltuples;
+	int32		relallvisible;
+	TransactionId relfrozenxid;
+	TransactionId relminmxid;
+	char		relkind;
+	bool		on_commit_delete;
+
+	/* pg_statistic */
+	int			natts;
+	int			*attnum;
+	HeapTuple	*att_stat_tups;
+} gtt_local_hash_entry;
+
+static Size action_gtt_shared_hash_entry_size(void);
+static void gtt_storage_checkin(RelFileNode rnode);
+static void gtt_storage_checkout(RelFileNode rnode, bool skiplock);
+static void gtt_storage_removeall(int code, Datum arg);
+static void insert_gtt_relfrozenxid_to_ordered_list(Oid relfrozenxid);
+static void remove_gtt_relfrozenxid_from_ordered_list(Oid relfrozenxid);
+static void set_gtt_session_relfrozenxid(void);
+
+static Size
+action_gtt_shared_hash_entry_size(void)
+{
+	int 	wordnum;
+	Size	hash_entry_size = 0;
+
+	if (max_active_gtt <= 0)
+		return 0;
+
+	wordnum = WORDNUM(MaxBackends + 1);
+	hash_entry_size += MAXALIGN(sizeof(gtt_shared_hash_entry));
+	hash_entry_size += MAXALIGN(BITMAPSET_SIZE(wordnum + 1));
+
+	return hash_entry_size;
+}
+
+Size
+active_gtt_shared_hash_size(void)
+{
+	Size	size = 0;
+	Size	hash_entry_size = 0;
+
+	if (max_active_gtt <= 0)
+		return 0;
+
+	size = MAXALIGN(sizeof(gtt_ctl_data));
+	hash_entry_size = action_gtt_shared_hash_entry_size();
+	size += hash_estimate_size(max_active_gtt, hash_entry_size);
+
+	return size;
+}
+
+void
+active_gtt_shared_hash_init(void)
+{
+	HASHCTL info;
+	bool	found;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	gtt_shared_ctl =
+		ShmemInitStruct("gtt_shared_ctl",
+						sizeof(gtt_ctl_data),
+						&found);
+
+	if (!found)
+	{
+		LWLockRegisterTranche(LWTRANCHE_GTT_CTL, "gtt_shared_ctl");
+		LWLockInitialize(&gtt_shared_ctl->lock, LWTRANCHE_GTT_CTL);
+		gtt_shared_ctl->max_entry = max_active_gtt;
+		gtt_shared_ctl->entry_size = action_gtt_shared_hash_entry_size();
+	}
+
+	info.keysize = sizeof(RelFileNode);
+	info.entrysize = gtt_shared_ctl->entry_size;
+	active_gtt_shared_hash =
+		ShmemInitHash("active gtt shared hash",
+						gtt_shared_ctl->max_entry,
+						gtt_shared_ctl->max_entry,
+						&info, HASH_ELEM | HASH_BLOBS | HASH_FIXED_SIZE);
+}
+
+static void
+gtt_storage_checkin(RelFileNode rnode)
+{
+	gtt_shared_hash_entry	*entry;
+	bool			found;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	LWLockAcquire(&gtt_shared_ctl->lock, LW_EXCLUSIVE);
+	entry = (gtt_shared_hash_entry *) hash_search(active_gtt_shared_hash,
+												&rnode, HASH_ENTER_NULL, &found);
+
+	if (entry == NULL)
+	{
+		LWLockRelease(&gtt_shared_ctl->lock);
+		ereport(ERROR,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("out of shared memory"),
+				 errhint("You might need to increase max_active_gtt.")));
+	}
+
+	if (found == false)
+	{
+		int			wordnum;
+
+		entry->map = (Bitmapset *)((char *)entry + MAXALIGN(sizeof(gtt_shared_hash_entry)));
+		wordnum = WORDNUM(MaxBackends + 1);
+		memset(entry->map, 0, BITMAPSET_SIZE(wordnum + 1));
+		entry->map->nwords = wordnum + 1;
+	}
+
+	bms_add_member(entry->map, MyBackendId);
+	LWLockRelease(&gtt_shared_ctl->lock);
+}
+
+static void
+gtt_storage_checkout(RelFileNode rnode, bool skiplock)
+{
+	gtt_shared_hash_entry	*entry;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	if (!skiplock)
+		LWLockAcquire(&gtt_shared_ctl->lock, LW_EXCLUSIVE);
+
+	entry = hash_search(active_gtt_shared_hash,
+					(void *) &(rnode), HASH_FIND, NULL);
+
+	if (entry == NULL)
+	{
+		if (!skiplock)
+			LWLockRelease(&gtt_shared_ctl->lock);
+
+		elog(WARNING, "relfilenode %u/%u/%u not exist in gtt shared hash when forget",
+						rnode.dbNode, rnode.spcNode, rnode.relNode);
+		return;
+	}
+
+	Assert(MyBackendId >= 1 && MyBackendId <= MaxBackends);
+	bms_del_member(entry->map, MyBackendId);
+
+	if (bms_is_empty(entry->map))
+	{
+		if (!hash_search(active_gtt_shared_hash, &rnode, HASH_REMOVE, NULL))
+			elog(PANIC, "gtt shared hash table corrupted");
+	}
+
+	if (!skiplock)
+		LWLockRelease(&gtt_shared_ctl->lock);
+
+	return;
+}
+
+Bitmapset *
+copy_active_gtt_bitmap(RelFileNode node)
+{
+	gtt_shared_hash_entry	*entry;
+	Bitmapset	*map_copy = NULL;
+
+	if (max_active_gtt <= 0)
+		return NULL;
+
+	LWLockAcquire(&gtt_shared_ctl->lock, LW_SHARED);
+	entry = hash_search(active_gtt_shared_hash,
+					(void *) &(node), HASH_FIND, NULL);
+
+	if (entry == NULL)
+	{
+		LWLockRelease(&gtt_shared_ctl->lock);
+		return NULL;
+	}
+
+	Assert(entry->map);
+	if (!bms_is_empty(entry->map))
+		map_copy = bms_copy(entry->map);
+
+	LWLockRelease(&gtt_shared_ctl->lock);
+
+	return map_copy;
+}
+
+bool
+is_other_backend_use_gtt(RelFileNode node)
+{
+	gtt_shared_hash_entry	*entry;
+	bool		in_use = false;
+	int			num_use = 0;
+
+	if (max_active_gtt <= 0)
+		return false;
+
+	LWLockAcquire(&gtt_shared_ctl->lock, LW_SHARED);
+	entry = hash_search(active_gtt_shared_hash,
+					(void *) &(node), HASH_FIND, NULL);
+
+	if (entry == NULL)
+	{
+		LWLockRelease(&gtt_shared_ctl->lock);
+		return false;
+	}
+
+	Assert(entry->map);
+	Assert(MyBackendId >= 1 && MyBackendId <= MaxBackends);
+
+	num_use = bms_num_members(entry->map);
+	if (num_use == 0)
+		in_use = false;
+	else if (num_use == 1)
+	{
+		if(bms_is_member(MyBackendId, entry->map))
+			in_use = false;
+		else
+			in_use = true;
+	}
+	else
+		in_use = true;
+
+	LWLockRelease(&gtt_shared_ctl->lock);
+
+	return in_use;
+}
+
+void
+remember_gtt_storage_info(RelFileNode rnode, Relation rel)
+{
+	gtt_local_hash_entry	*entry;
+	MemoryContext			oldcontext;
+	Oid			relid = rnode.relNode;
+	bool		found;
+
+	if (max_active_gtt <= 0)
+		ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("Global temporary table feature is disable"),
+			 errhint("You might need to increase max_active_gtt to enable this feature.")));
+
+	if (RecoveryInProgress())
+		elog(ERROR, "readonly mode not support access global temp table yet");
+
+	if (gtt_storage_local_hash == NULL)
+	{
+#define GTT_LOCAL_HASH_SIZE		1024
+		/* First time through: initialize the hash table */
+		HASHCTL		ctl;
+
+		MemSet(&ctl, 0, sizeof(ctl));
+		ctl.keysize = sizeof(Oid);
+		ctl.entrysize = sizeof(gtt_local_hash_entry);
+		gtt_storage_local_hash =
+			hash_create("global temp relation table",
+						GTT_LOCAL_HASH_SIZE,
+						&ctl, HASH_ELEM | HASH_BLOBS);
+
+		if (!CacheMemoryContext)
+			CreateCacheMemoryContext();
+
+		gtt_relstats_context =
+			AllocSetContextCreate(CacheMemoryContext,
+							"gtt relstats context",
+							ALLOCSET_DEFAULT_SIZES);
+	}
+
+	/* Look up or create an entry */
+	entry = hash_search(gtt_storage_local_hash,
+					(void *) &relid, HASH_ENTER, &found);
+
+	if (found)
+	{
+		elog(ERROR, "backend %d relid %u already exists in global temp table local hash",
+					MyBackendId, relid);
+	}
+
+	oldcontext = MemoryContextSwitchTo(gtt_relstats_context);
+
+	entry->spcnode = rnode.spcNode;
+	entry->relpages = 0;
+	entry->reltuples = 0;
+	entry->relallvisible = 0;
+	entry->relkind = rel->rd_rel->relkind;
+	/* only heap contain transaction information */
+	if (entry->relkind == RELKIND_RELATION)
+	{
+		int natts = RelationGetNumberOfAttributes(rel);
+		entry->natts = natts;
+		entry->attnum = palloc0(sizeof(int) * natts);
+		entry->att_stat_tups = palloc0(sizeof(HeapTuple) * natts);
+
+		if (RELATION_GTT_ON_COMMIT_DELETE(rel))
+		{
+			entry->on_commit_delete = true;
+			register_on_commit_action(rel->rd_node.relNode, ONCOMMIT_DELETE_ROWS);
+		}
+
+		entry->relfrozenxid = RecentXmin;
+		entry->relminmxid = GetOldestMultiXactId();
+		insert_gtt_relfrozenxid_to_ordered_list(entry->relfrozenxid);
+		set_gtt_session_relfrozenxid();
+		gtt_storage_checkin(rnode);
+	}
+	else
+	{
+		entry->natts = 0;
+		entry->attnum = 0;
+		entry->att_stat_tups = NULL;
+		entry->on_commit_delete = false;
+		entry->relfrozenxid = 0;
+		entry->relminmxid = 0;
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+
+	if (!gtt_cleaner_exit_registered)
+	{
+		before_shmem_exit(gtt_storage_removeall, 0);
+		gtt_cleaner_exit_registered = true;
+	}
+
+	return;
+}
+
+void
+forget_gtt_storage_info(Oid relid)
+{
+	gtt_local_hash_entry		*entry;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	if (gtt_storage_local_hash == NULL)
+		return;
+
+	entry = hash_search(gtt_storage_local_hash,
+				(void *) &(relid), HASH_FIND, NULL);
+
+	if (entry && entry->relkind == RELKIND_RELATION)
+	{
+		RelFileNode rnode;
+
+		rnode.spcNode = entry->spcnode;
+		rnode.dbNode = MyDatabaseId;
+		rnode.relNode = entry->relid;
+
+		gtt_storage_checkout(rnode, false);
+
+		Assert(TransactionIdIsNormal(entry->relfrozenxid));
+		remove_gtt_relfrozenxid_from_ordered_list(entry->relfrozenxid);
+		set_gtt_session_relfrozenxid();
+	}
+
+	hash_search(gtt_storage_local_hash,
+			(void *) &(relid), HASH_REMOVE, NULL);
+
+	return;
+}
+
+/* is the storage file was created in this backend */
+bool
+gtt_storage_attached(Oid relid)
+{
+	bool found;
+
+	if (max_active_gtt <= 0)
+		return false;
+
+	if (gtt_storage_local_hash == NULL)
+		return false;
+
+	hash_search(gtt_storage_local_hash,
+			(void *) &(relid), HASH_FIND, &found);
+
+	return found;
+}
+
+static void
+gtt_storage_removeall(int code, Datum arg)
+{
+	HASH_SEQ_STATUS				status;
+	gtt_local_hash_entry		*entry;
+	int			nrels = 0,
+				maxrels = 0;
+	SMgrRelation	*srels = NULL;
+	RelFileNode		*rnodes = NULL;
+	char			*relkinds = NULL;
+
+	if (gtt_storage_local_hash == NULL)
+		return;
+
+	hash_seq_init(&status, gtt_storage_local_hash);
+	while ((entry = (gtt_local_hash_entry *) hash_seq_search(&status)) != NULL)
+	{
+		SMgrRelation srel;
+		RelFileNode rnode;
+
+		rnode.spcNode = entry->spcnode;
+		rnode.dbNode = MyDatabaseId;
+		rnode.relNode = entry->relid;
+
+		srel = smgropen(rnode, MyBackendId);
+
+		/* allocate the initial array, or extend it, if needed */
+		if (maxrels == 0)
+		{
+			maxrels = 32;
+			srels = palloc(sizeof(SMgrRelation) * maxrels);
+			rnodes = palloc(sizeof(RelFileNode) * maxrels);
+			relkinds = palloc(sizeof(char) * maxrels);
+		}
+		else if (maxrels <= nrels)
+		{
+			maxrels *= 2;
+			srels = repalloc(srels, sizeof(SMgrRelation) * maxrels);
+			rnodes = repalloc(rnodes, sizeof(RelFileNode) * maxrels);
+			relkinds = repalloc(relkinds, sizeof(char) * maxrels);
+		}
+
+		srels[nrels] = srel;
+		rnodes[nrels] = rnode;
+		relkinds[nrels] = entry->relkind;
+		nrels++;
+	}
+
+	if (nrels > 0)
+	{
+		int i;
+
+		smgrdounlinkall(srels, nrels, false);
+
+		LWLockAcquire(&gtt_shared_ctl->lock, LW_EXCLUSIVE);
+		for (i = 0; i < nrels; i++)
+		{
+			smgrclose(srels[i]);
+			if (relkinds[i] == RELKIND_RELATION)
+				gtt_storage_checkout(rnodes[i], true);
+		}
+		LWLockRelease(&gtt_shared_ctl->lock);
+
+		pfree(srels);
+		pfree(rnodes);
+		pfree(relkinds);
+	}
+
+	MyProc->session_gtt_frozenxid = InvalidTransactionId;
+
+	return;
+}
+
+/*
+ * Update global temp table relstats(relpage/reltuple/relallvisible) 
+ * to local hashtable
+ */
+void
+up_gtt_relstats(Relation relation,
+					BlockNumber num_pages,
+					double num_tuples,
+					BlockNumber num_all_visible_pages,
+					TransactionId relfrozenxid,
+					TransactionId relminmxid)
+{
+	Oid		relid = RelationGetRelid(relation);
+	gtt_local_hash_entry	*entry;
+	bool					found;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	if (gtt_storage_local_hash == NULL)
+		return;
+
+	entry = hash_search(gtt_storage_local_hash,
+					(void *) &relid, HASH_FIND, &found);
+
+	if (!found)
+		return;
+
+	Assert(entry->spcnode);
+
+	if (num_pages >= 0 &&
+		entry->relpages != (int32)num_pages)
+		entry->relpages = (int32)num_pages;
+
+	if (num_tuples >= 0 &&
+		num_tuples != (float4)entry->reltuples)
+		entry->reltuples = (float4)num_tuples;
+
+	/* only heap contain transaction information and relallvisible */
+	if (entry->relkind == RELKIND_RELATION)
+	{
+		if (entry->relallvisible >= 0 &&
+			entry->relallvisible != (int32)num_all_visible_pages)
+		{
+			entry->relallvisible = (int32)num_all_visible_pages;
+		}
+
+		if (TransactionIdIsNormal(relfrozenxid) &&
+			entry->relfrozenxid != relfrozenxid &&
+			(TransactionIdPrecedes(entry->relfrozenxid, relfrozenxid) ||
+			 TransactionIdPrecedes(ReadNewTransactionId(), entry->relfrozenxid)))
+		{
+			remove_gtt_relfrozenxid_from_ordered_list(entry->relfrozenxid);
+			entry->relfrozenxid = relfrozenxid;
+			insert_gtt_relfrozenxid_to_ordered_list(relfrozenxid);
+			set_gtt_session_relfrozenxid();
+		}
+
+		if (MultiXactIdIsValid(relminmxid) &&
+			entry->relminmxid != relminmxid &&
+			(MultiXactIdPrecedes(entry->relminmxid, relminmxid) ||
+			 MultiXactIdPrecedes(ReadNextMultiXactId(), entry->relminmxid)))
+		{
+			entry->relminmxid = relminmxid;
+		}
+	}
+
+	return;
+}
+
+/*
+ * Search global temp table relstats(relpage/reltuple/relallvisible) 
+ * from local hashtable.
+ */
+void
+get_gtt_relstats(Oid relid, BlockNumber *relpages, double *reltuples,
+				BlockNumber *relallvisible, TransactionId *relfrozenxid,
+				TransactionId *relminmxid)
+{
+	gtt_local_hash_entry	*entry;
+	bool					found;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	if (gtt_storage_local_hash == NULL)
+		return;
+
+	entry = hash_search(gtt_storage_local_hash,
+					(void *) &relid, HASH_FIND, &found);
+
+	if (!found)
+		return;
+
+	Assert(entry->relid == relid);
+
+	if (relpages)
+		*relpages = entry->relpages;
+
+	if (reltuples)
+		*reltuples = entry->reltuples;
+
+	if (relallvisible)
+		*relallvisible = entry->relallvisible;
+
+	if (relfrozenxid)
+		*relfrozenxid = entry->relfrozenxid;
+
+	if (relminmxid)
+		*relminmxid = entry->relminmxid;
+
+	return;
+}
+
+/*
+ * Update global temp table statistic info(definition is same as pg_statistic)
+ * to local hashtable where ananyze global temp table
+ */
+void
+up_gtt_att_statistic(Oid reloid, int attnum, bool inh, int natts,
+					TupleDesc tupleDescriptor, Datum *values, bool *isnull)
+{
+	gtt_local_hash_entry	*entry;
+	bool					found;
+	MemoryContext			oldcontext;
+	int						i = 0;
+
+	if (max_active_gtt <= 0)
+		return;
+
+	if (gtt_storage_local_hash == NULL)
+		return;
+
+	entry = hash_search(gtt_storage_local_hash,
+						(void *) &reloid, HASH_FIND, &found);
+
+	if (!found)
+		return;
+
+	if (entry->relkind != RELKIND_RELATION)
+	{
+		elog(WARNING, "oid %u not a relation", reloid);
+		return;
+	}
+
+	/* todo */
+	if (entry->natts < natts)
+	{
+		elog(WARNING, "reloid %u not support update attstat after add colunm", reloid);
+		return;
+	}
+
+	oldcontext = MemoryContextSwitchTo(gtt_relstats_context);
+	Assert(entry->relid == reloid);
+	for (i = 0; i < entry->natts; i++)
+	{
+		if (entry->attnum[i] == 0)
+		{
+			entry->attnum[i] = attnum;
+			break;
+		}
+		else if (entry->attnum[i] == attnum)
+		{
+			Assert(entry->att_stat_tups[i]);
+			heap_freetuple(entry->att_stat_tups[i]);
+			entry->att_stat_tups[i] = NULL;
+			break;
+		}
+	}
+
+	Assert(i < entry->natts);
+	Assert(entry->att_stat_tups[i] == NULL);
+	entry->att_stat_tups[i] = heap_form_tuple(tupleDescriptor, values, isnull);
+	MemoryContextSwitchTo(oldcontext);
+
+	return;
+}
+
+/*
+ * Search global temp table statistic info(definition is same as pg_statistic)
+ * from local hashtable.
+ */
+HeapTuple
+get_gtt_att_statistic(Oid reloid, int attnum, bool inh)
+{
+	gtt_local_hash_entry	*entry;
+	bool					found;
+	int						i = 0;
+
+	if (max_active_gtt <= 0)
+		return NULL;
+
+	if (gtt_storage_local_hash == NULL)
+		return NULL;
+
+	entry = hash_search(gtt_storage_local_hash,
+						(void *) &reloid, HASH_FIND, &found);
+
+	if (!found)
+		return NULL;
+
+	for (i = 0; i < entry->natts; i++)
+	{
+		if (entry->attnum[i] == attnum)
+		{
+			Assert(entry->att_stat_tups[i]);
+			return entry->att_stat_tups[i];
+		}
+	}
+
+	return NULL;
+}
+
+void
+release_gtt_statistic_cache(HeapTuple tup)
+{
+	/* do nothing */
+	return;
+}
+
+static void
+insert_gtt_relfrozenxid_to_ordered_list(Oid relfrozenxid)
+{
+	MemoryContext	oldcontext;
+	ListCell	*cell;
+	int i;
+	Assert(TransactionIdIsNormal(relfrozenxid));
+
+	oldcontext = MemoryContextSwitchTo(gtt_relstats_context);
+	/* Does the datum belong at the front? */
+	if (gtt_session_relfrozenxid_list == NIL ||
+		TransactionIdFollowsOrEquals(relfrozenxid,
+			linitial_oid(gtt_session_relfrozenxid_list)))
+	{
+		gtt_session_relfrozenxid_list =
+			lcons_oid(relfrozenxid, gtt_session_relfrozenxid_list);
+		MemoryContextSwitchTo(oldcontext);
+
+		return;
+	}
+	/* No, so find the entry it belongs after */
+
+	i = 0;
+	foreach (cell, gtt_session_relfrozenxid_list)
+	{
+		if (TransactionIdFollowsOrEquals(relfrozenxid, lfirst_oid(cell)))
+			break;	/* it belongs after 'prev', before 'curr' */
+
+		i += 1;
+	}
+	/* Insert datum into list after 'prev' */
+	list_insert_nth_oid(gtt_session_relfrozenxid_list, i, relfrozenxid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+remove_gtt_relfrozenxid_from_ordered_list(Oid relfrozenxid)
+{
+	gtt_session_relfrozenxid_list =
+		list_delete_oid(gtt_session_relfrozenxid_list, relfrozenxid);
+}
+
+static void
+set_gtt_session_relfrozenxid(void)
+{
+	TransactionId gtt_frozenxid = InvalidTransactionId;
+
+	if (gtt_session_relfrozenxid_list)
+		gtt_frozenxid = llast_oid(gtt_session_relfrozenxid_list);
+
+	gtt_session_frozenxid = gtt_frozenxid;
+	if (MyProc->session_gtt_frozenxid != gtt_frozenxid)
+		MyProc->session_gtt_frozenxid = gtt_frozenxid;
+}
+
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 7accb95..e0dc376 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -65,6 +65,7 @@
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
+#include "catalog/storage_gtt.h"
 
 /* Per-index data for ANALYZE */
 typedef struct AnlIndexData
@@ -102,7 +103,7 @@ static int	acquire_inherited_sample_rows(Relation onerel, int elevel,
 										  HeapTuple *rows, int targrows,
 										  double *totalrows, double *totaldeadrows);
 static void update_attstats(Oid relid, bool inh,
-							int natts, VacAttrStats **vacattrstats);
+							int natts, VacAttrStats **vacattrstats, char relpersistence);
 static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 
@@ -575,14 +576,15 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 		 * pg_statistic for columns we didn't process, we leave them alone.)
 		 */
 		update_attstats(RelationGetRelid(onerel), inh,
-						attr_cnt, vacattrstats);
+						attr_cnt, vacattrstats, RelationGetRelPersistence(onerel));

 			update_attstats(RelationGetRelid(Irel[ind]), false,
-							thisdata->attr_cnt, thisdata->vacattrstats);
+							thisdata->attr_cnt, thisdata->vacattrstats,
+							RelationGetRelPersistence(Irel[ind]));
 		}
 
 		/*
@@ -659,11 +661,20 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 			IndexBulkDeleteResult *stats;
 			IndexVacuumInfo ivinfo;
 
+			memset(&ivinfo, 0, sizeof(IndexVacuumInfo));
 			ivinfo.index = Irel[ind];
 			ivinfo.analyze_only = true;
 			ivinfo.estimated_count = true;
 			ivinfo.message_level = elevel;
-			ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
+			/* get global temp relstats from localhash, not catalog */
+			if (RELATION_IS_GLOBAL_TEMP(onerel))
+			{
+				get_gtt_relstats(RelationGetRelid(onerel),
+								NULL, &ivinfo.num_heap_tuples, NULL,
+								NULL, NULL);
+			}
+			else
+				ivinfo.num_heap_tuples = onerel->rd_rel->reltuples;
 			ivinfo.strategy = vac_strategy;
 
 			stats = index_vacuum_cleanup(&ivinfo, NULL);
@@ -1425,7 +1436,7 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
  *		by taking a self-exclusive lock on the relation in analyze_rel().
  */
 static void
-update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
+update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats, char relpersistence)
 {
 	Relation	sd;
 	int			attno;
@@ -1527,31 +1538,45 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 			}
 		}
 
+		/* Update column statistic to localhash, not catalog */
+		if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			up_gtt_att_statistic(relid,
+								stats->attr->attnum,
+								inh,
+								natts,
+								nulls);
 
-		heap_freetuple(stup);
+			heap_freetuple(stup);
+		}
 	}
 
 	table_close(sd, RowExclusiveLock);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index a23128d..0d38988 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -392,6 +392,12 @@ cluster_rel(Oid tableOid, Oid indexOid, int options)
 					 errmsg("cannot vacuum temporary tables of other sessions")));
 	}
 
+	/* not support cluster global temp table yet */
+	if (RELATION_IS_GLOBAL_TEMP(OldHeap))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("not support cluster global temporary tables yet")));
+
 	/*
 	 * Also check for active uses of the relation in the current transaction,
 	 * including open scans and pending AFTER trigger events.
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 374e2d0..8e88a59 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2585,6 +2585,16 @@ ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 			!isTempNamespace(classtuple->relnamespace))
 			continue;
 
+		/* not support reindex on global temp table, so skip it */
+		if (classtuple->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			ereport(WARNING,
+				(errmsg("global temp table \"%s.%s\" skip reindexed",
+					get_namespace_name(get_rel_namespace(relid)),
+					get_rel_name(relid))));
+			continue;
+		}
+
 		/* Check user/system classification, and optionally skip */
 		if (objectKind == REINDEX_OBJECT_SYSTEM &&
 			!IsSystemClass(relid, classtuple))
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index bae3b38..ff9d5cb 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -107,7 +107,8 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
 	 * transaction.
 	 */
 	relpersistence = get_rel_persistence(relid);
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP ||
+		relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE;
 
 	/* Check permissions. */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5597be6..4bf4b9a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -48,6 +48,7 @@
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
 #include "catalog/toasting.h"
+#include "catalog/storage_gtt.h"
 #include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
@@ -531,6 +532,7 @@ static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
 								  Relation partitionTbl);
 static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
+static bool has_oncommit_option(List *options);

 /* ----------------------------------------------------------------
@@ -576,6 +578,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	LOCKMODE	parentLockmode;
 	const char *accessMethod = NULL;
 	Oid			accessMethodId = InvalidOid;
+	bool		has_oncommit_clause = false;
 
 	/*
 	 * Truncate relname to appropriate length (probably a waste of time, as
@@ -586,8 +589,10 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	/*
 	 * Check consistency of arguments
 	 */
+	/* global temp table same as local temp table */
 	if (stmt->oncommit != ONCOMMIT_NOOP
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& !(stmt->relation->relpersistence == RELPERSISTENCE_TEMP ||
+			 stmt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP))
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 				 errmsg("ON COMMIT can only be used on temporary tables")));
@@ -617,12 +622,28 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * code.  This is needed because calling code might not expect untrusted
 	 * tables to appear in pg_temp at the front of its search path.
 	 */
-	if (stmt->relation->relpersistence == RELPERSISTENCE_TEMP
+	/* global temp table same as local temp table */
+	if ((stmt->relation->relpersistence == RELPERSISTENCE_TEMP ||
+		 stmt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		&& InSecurityRestrictedOperation())
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("cannot create temporary table within security-restricted operation")));
 
+	/* Not support partitioned or inherited global temp table yet */
+	if (stmt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		if (relkind == RELKIND_PARTITIONED_TABLE)
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("not support create global temporary partition table yet")));
+
+		if (stmt->inhRelations)
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("not support create global temporary inheritance table yet")));
+	}
+
 	/*
 	 * Determine the lockmode to use when scanning parents.  A self-exclusive
 	 * lock is needed here.
@@ -718,6 +739,40 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	/*
 	 * Parse and validate reloptions, if any.
 	 */
+	/* global temp table */
+	has_oncommit_clause = has_oncommit_option(stmt->options);
+	if (stmt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		if (has_oncommit_clause)
+		{
+			if (stmt->oncommit != ONCOMMIT_NOOP)
+				elog(ERROR, "can not defeine global temp table with on commit and with clause at same time");
+		}
+		else
+		{
+			DefElem *opt = makeNode(DefElem);
+
+			opt->type = T_DefElem;
+			opt->defnamespace = NULL;
+			opt->defname = "on_commit_delete_rows";
+			opt->defaction = DEFELEM_UNSPEC;
+
+			/* use reloptions to remember on commit clause */
+			if (stmt->oncommit == ONCOMMIT_DELETE_ROWS)
+				opt->arg  = (Node *)makeString("true");
+			else if (stmt->oncommit == ONCOMMIT_PRESERVE_ROWS)
+				opt->arg  = (Node *)makeString("false");
+			else if (stmt->oncommit == ONCOMMIT_NOOP)
+				opt->arg  = (Node *)makeString("true");
+			else
+				elog(ERROR, "global temp table not support on commit drop clause");
+
+			stmt->options = lappend(stmt->options, opt);
+		}
+	}
+	else if (has_oncommit_clause)
+		elog(ERROR, "regular table cannot specifie on_commit_delete_rows");
+
 	reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
 									 true, false);
 
@@ -1772,7 +1827,8 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 		 * table or the current physical file to be thrown away anyway.
 		 */
 		if (rel->rd_createSubid == mySubid ||
-			rel->rd_newRelfilenodeSubid == mySubid)
+			rel->rd_newRelfilenodeSubid == mySubid ||
+			RELATION_IS_GLOBAL_TEMP(rel))
 		{
 			/* Immediate, non-rollbackable truncation is OK */
 			heap_truncate_one_rel(rel);
@@ -3330,6 +3386,13 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bo
 	 * specially.
 	 */
 	targetrelation = relation_open(myrelid, is_index ? ShareUpdateExclusiveLock : AccessExclusiveLock);
+
+	/* not support rename global temp table yet */
+	if (RELATION_IS_GLOBAL_TEMP(targetrelation))
+		ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("not support rename global temporary tables yet")));
+
 	namespaceId = RelationGetNamespace(targetrelation);
 
 	/*
@@ -3499,6 +3562,14 @@ AlterTable(Oid relid, LOCKMODE lockmode, AlterTableStmt *stmt)
 	/* Caller is required to provide an adequate lock. */
 	rel = relation_open(relid, NoLock);
 
+	/* not support alter global temp table yet */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+	{
+		ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("not support alter global temporary tables yet")));
+	}
+
 	CheckTableNotInUse(rel, "ALTER TABLE");
 
 	ATController(stmt, rel, stmt->cmds, stmt->relation->inh, lockmode);
@@ -7679,6 +7750,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 				 errmsg("referenced relation \"%s\" is not a table",
 						RelationGetRelationName(pkrel))));
 
+	/* global temp table not support foreign key constraint yet */
+	if (RELATION_IS_GLOBAL_TEMP(pkrel))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("referenced relation \"%s\" is not a global temp table",
+						RelationGetRelationName(pkrel))));
+
 	if (!allowSystemTableMods && IsSystemRelation(pkrel))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -7718,6 +7796,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 						 errmsg("constraints on temporary tables must involve temporary tables of this session")));
 			break;
+		/* global temp table not support foreign key constraint yet */
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("not support foreign key constraints on global temp table yet")));
+			break;
 	}
 
 	/*
@@ -12726,7 +12810,7 @@ index_copy_data(Relation rel, RelFileNode newrnode)
 	 * NOTE: any conflict in relfilenode value will be caught in
 	 * RelationCreateStorage().
 	 */
-	RelationCreateStorage(newrnode, rel->rd_rel->relpersistence);
+	RelationCreateStorage(newrnode, rel->rd_rel->relpersistence, rel);
 
 	/* copy main fork */
 	RelationCopyStorage(rel->rd_smgr, dstrel, MAIN_FORKNUM,
@@ -14133,7 +14217,9 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	 */
 	switch (rel->rd_rel->relpersistence)
 	{
+		/* global temp table same as local temp table */
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_GLOBAL_TEMP:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 errmsg("cannot change logged status of table \"%s\" because it is temporary",
@@ -16723,3 +16809,20 @@ ATDetachCheckNoForeignKeyRefs(Relation partition)
 		table_close(rel, NoLock);
 	}
 }
+
+static bool
+has_oncommit_option(List *options)
+{
+	ListCell   *listptr;
+
+	foreach(listptr, options)
+	{
+		DefElem    *def = (DefElem *) lfirst(listptr);
+
+		if (pg_strcasecmp(def->defname, "on_commit_delete_rows") == 0)
+			return true;
+	}
+
+	return false;
+}
+
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index da1da23..70278a6 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_database.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
+#include "catalog/storage_gtt.h"
 #include "commands/cluster.h"
 #include "commands/defrem.h"
 #include "commands/vacuum.h"
@@ -1070,12 +1071,25 @@ vac_estimate_reltuples(Relation relation,
 					   BlockNumber scanned_pages,
 					   double scanned_tuples)
 {
-	BlockNumber old_rel_pages = relation->rd_rel->relpages;
-	double		old_rel_tuples = relation->rd_rel->reltuples;
+	BlockNumber old_rel_pages = 0;
+	double		old_rel_tuples = 0;
 	double		old_density;
 	double		unscanned_pages;
 	double		total_tuples;
 
+	/* get relstat from gtt local hash */
+	if (RELATION_IS_GLOBAL_TEMP(relation))
+	{
+		get_gtt_relstats(RelationGetRelid(relation),
+						&old_rel_pages, &old_rel_tuples, NULL,
+						NULL, NULL);
+	}
+	else
+	{
+		old_rel_pages = relation->rd_rel->relpages;
+		old_rel_tuples = relation->rd_rel->reltuples;
+	}
+
 	/* If we did scan the whole table, just use the count as-is */
 	if (scanned_pages >= total_pages)
 		return scanned_tuples;
@@ -1175,24 +1189,8 @@ vac_update_relstats(Relation relation,
 	/* Apply statistical updates, if any, to copied tuple */
 
 	dirty = false;
-	if (pgcform->relpages != (int32) num_pages)
-	{
-		pgcform->relpages = (int32) num_pages;
-		dirty = true;
-	}
-	if (pgcform->reltuples != (float4) num_tuples)
-	{
-		pgcform->reltuples = (float4) num_tuples;
-		dirty = true;
-	}
-	if (pgcform->relallvisible != (int32) num_all_visible_pages)
-	{
-		pgcform->relallvisible = (int32) num_all_visible_pages;
-		dirty = true;
-	}
 
 	/* Apply DDL updates, but not inside an outer transaction (see above) */
-
 	if (!in_outer_xact)
 	{
 		/*
@@ -1217,37 +1215,64 @@ vac_update_relstats(Relation relation,
 		}
 	}
 
-	/*
-	 * Update relfrozenxid, unless caller passed InvalidTransactionId
-	 * indicating it has no new data.
-	 *
-	 * Ordinarily, we don't let relfrozenxid go backwards: if things are
-	 * working correctly, the only way the new frozenxid could be older would
-	 * be if a previous VACUUM was done with a tighter freeze_min_age, in
-	 * which case we don't want to forget the work it already did.  However,
-	 * if the stored relfrozenxid is "in the future", then it must be corrupt
-	 * and it seems best to overwrite it with the cutoff we used this time.
-	 * This should match vac_update_datfrozenxid() concerning what we consider
-	 * to be "in the future".
-	 */
-	if (TransactionIdIsNormal(frozenxid) &&
-		pgcform->relfrozenxid != frozenxid &&
-		(TransactionIdPrecedes(pgcform->relfrozenxid, frozenxid) ||
-		 TransactionIdPrecedes(ReadNewTransactionId(),
-							   pgcform->relfrozenxid)))
+	/* global temp table remember relstats to localhash not catalog */
+	if (RELATION_IS_GLOBAL_TEMP(relation))
 	{
-		pgcform->relfrozenxid = frozenxid;
-		dirty = true;
+		up_gtt_relstats(relation,
+						num_pages, num_tuples,
+						num_all_visible_pages,
+						frozenxid, minmulti);
 	}
-
-	/* Similarly for relminmxid */
-	if (MultiXactIdIsValid(minmulti) &&
-		pgcform->relminmxid != minmulti &&
-		(MultiXactIdPrecedes(pgcform->relminmxid, minmulti) ||
-		 MultiXactIdPrecedes(ReadNextMultiXactId(), pgcform->relminmxid)))
+	else
 	{
-		pgcform->relminmxid = minmulti;
-		dirty = true;
+		if (pgcform->relpages != (int32) num_pages)
+		{
+			pgcform->relpages = (int32) num_pages;
+			dirty = true;
+		}
+		if (pgcform->reltuples != (float4) num_tuples)
+		{
+			pgcform->reltuples = (float4) num_tuples;
+			dirty = true;
+		}
+		if (pgcform->relallvisible != (int32) num_all_visible_pages)
+		{
+			pgcform->relallvisible = (int32) num_all_visible_pages;
+			dirty = true;
+		}
+
+		/*
+		 * Update relfrozenxid, unless caller passed InvalidTransactionId
+		 * indicating it has no new data.
+		 *
+		 * Ordinarily, we don't let relfrozenxid go backwards: if things are
+		 * working correctly, the only way the new frozenxid could be older would
+		 * be if a previous VACUUM was done with a tighter freeze_min_age, in
+		 * which case we don't want to forget the work it already did.  However,
+		 * if the stored relfrozenxid is "in the future", then it must be corrupt
+		 * and it seems best to overwrite it with the cutoff we used this time.
+		 * This should match vac_update_datfrozenxid() concerning what we consider
+		 * to be "in the future".
+		 */
+		if (TransactionIdIsNormal(frozenxid) &&
+			pgcform->relfrozenxid != frozenxid &&
+			(TransactionIdPrecedes(pgcform->relfrozenxid, frozenxid) ||
+			 TransactionIdPrecedes(ReadNewTransactionId(),
+								   pgcform->relfrozenxid)))
+		{
+			pgcform->relfrozenxid = frozenxid;
+			dirty = true;
+		}
+
+		/* Similarly for relminmxid */
+		if (MultiXactIdIsValid(minmulti) &&
+			pgcform->relminmxid != minmulti &&
+			(MultiXactIdPrecedes(pgcform->relminmxid, minmulti) ||
+			 MultiXactIdPrecedes(ReadNextMultiXactId(), pgcform->relminmxid)))
+		{
+			pgcform->relminmxid = minmulti;
+			dirty = true;
+		}
 	}
 
 	/* If anything changed, write out the tuple. */
@@ -1339,6 +1364,10 @@ vac_update_datfrozenxid(void)
 			continue;
 		}
 
+		/* global temp table relstats not in pg_class */
+		if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			continue;
+
 		/*
 		 * Some table AMs might not need per-relation xid / multixid horizons.
 		 * It therefore seems reasonable to allow relfrozenxid and relminmxid
@@ -1396,6 +1425,25 @@ vac_update_datfrozenxid(void)
 	Assert(TransactionIdIsNormal(newFrozenXid));
 	Assert(MultiXactIdIsValid(newMinMulti));
 
+	/*
+	 * Global temp table get frozenxid from MyProc
+	 * to avoid the vacuum truncate clog that gtt need.
+	 */
+	if (max_active_gtt > 0)
+	{
+		TransactionId oldest_gtt_frozenxid =
+			list_all_session_gtt_frozenxids(0, NULL, NULL, NULL);
+
+		if (TransactionIdIsNormal(oldest_gtt_frozenxid) &&
+			TransactionIdPrecedes(oldest_gtt_frozenxid, newFrozenXid))
+		{
+			ereport(WARNING,
+				(errmsg("global temp table oldest FrozenXid is far in the past"),
+				 errhint("please truncate them or kill those sessions that use them.")));
+			newFrozenXid = oldest_gtt_frozenxid;
+		}
+	}
+
 	/* Now fetch the pg_database tuple we need to update. */
 	relation = table_open(DatabaseRelationId, RowExclusiveLock);
 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index db3a68a..b41a920 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -591,6 +591,8 @@ static void
 set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 						  RangeTblEntry *rte)
 {
+	char relpersistence;
+
 	/*
 	 * The flag has previously been initialized to false, so we can just
 	 * return if it becomes clear that we can't safely set it.
@@ -618,7 +620,11 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 			 * the rest of the necessary infrastructure right now anyway.  So
 			 * for now, bail out if we see a temporary table.
 			 */
-			if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
+			/* global temp table is same as local temp table */
+			relpersistence = get_rel_persistence(rte->relid);
+
+			if (relpersistence == RELPERSISTENCE_TEMP ||
+				relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 				return;
 
 			/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 17c5f08..fa6aee5 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -6307,7 +6307,9 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)
 	 * Furthermore, any index predicate or index expressions must be parallel
 	 * safe.
 	 */
+	/* global temp table is same as local temp table */
 	if (heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		heap->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ||
 		!is_parallel_safe(root, (Node *) RelationGetIndexExpressions(index)) ||
 		!is_parallel_safe(root, (Node *) RelationGetIndexPredicate(index)))
 	{
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index e5f9e04..ecc4543 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -53,6 +53,7 @@
 #include "utils/syscache.h"
 #include "utils/snapmgr.h"
 
+#include "catalog/storage_gtt.h"
 
 /* GUC parameter */
 int			constraint_exclusion = CONSTRAINT_EXCLUSION_PARTITION;
@@ -947,10 +948,10 @@ void
 estimate_rel_size(Relation rel, int32 *attr_widths,
 				  BlockNumber *pages, double *tuples, double *allvisfrac)
 {
-	BlockNumber curpages;
-	BlockNumber relpages;
-	double		reltuples;
-	BlockNumber relallvisible;
+	BlockNumber curpages = 0;
+	BlockNumber relpages = 0;
+	double		reltuples = 0;
+	BlockNumber relallvisible = 0;
 	double		density;
 
 	switch (rel->rd_rel->relkind)
@@ -964,6 +965,21 @@ estimate_rel_size(Relation rel, int32 *attr_widths,
 
 		case RELKIND_INDEX:
 
+			/* global temp table get relstats from localhash */
+			if (RELATION_IS_GLOBAL_TEMP(rel))
+			{
+				get_gtt_relstats(RelationGetRelid(rel),
+								&relpages, &reltuples, &relallvisible,
+								NULL, NULL);
+			}
+			else
+			{
+				/* coerce values in pg_class to more desirable types */
+				relpages = (BlockNumber) rel->rd_rel->relpages;
+				reltuples = (double) rel->rd_rel->reltuples;
+				relallvisible = (BlockNumber) rel->rd_rel->relallvisible;
+			}
+
 			/*
 			 * XXX: It'd probably be good to move this into a callback,
 			 * individual index types e.g. know if they have a metapage.
@@ -972,11 +988,6 @@ estimate_rel_size(Relation rel, int32 *attr_widths,
 			/* it has storage, ok to call the smgr */
 			curpages = RelationGetNumberOfBlocks(rel);
 
-			/* coerce values in pg_class to more desirable types */
-			relpages = (BlockNumber) rel->rd_rel->relpages;
-			reltuples = (double) rel->rd_rel->reltuples;
-			relallvisible = (BlockNumber) rel->rd_rel->relallvisible;
-
 			/* report estimated # pages */
 			*pages = curpages;
 			/* quick exit if rel is clearly empty */
@@ -986,10 +997,6 @@ estimate_rel_size(Relation rel, int32 *attr_widths,
 				*allvisfrac = 0;
 				break;
 			}
-			/* coerce values in pg_class to more desirable types */
-			relpages = (BlockNumber) rel->rd_rel->relpages;
-			reltuples = (double) rel->rd_rel->reltuples;
-			relallvisible = (BlockNumber) rel->rd_rel->relallvisible;
 
 			/*
 			 * Discount the metapage while estimating the number of tuples.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 85d7a96..67b200d 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -2579,6 +2579,11 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("materialized views must not use temporary tables or views")));
 
+		if (is_query_using_gtt(query))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("materialized views must not use global temporary tables or views")));
+
 		/*
 		 * A materialized view would either need to save parameters for use in
 		 * maintaining/loading the data or prohibit them entirely.  The latter
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3f67aaf..333e54f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3268,17 +3268,11 @@ OptTemp:	TEMPORARY					{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMP				{ $$ = RELPERSISTENCE_TEMP; }
 			| GLOBAL TEMPORARY
 				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
-					$$ = RELPERSISTENCE_TEMP;
+					$$ = RELPERSISTENCE_GLOBAL_TEMP;
 				}
 			| GLOBAL TEMP
 				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
-					$$ = RELPERSISTENCE_TEMP;
+					$$ = RELPERSISTENCE_GLOBAL_TEMP;
 				}
 			| UNLOGGED					{ $$ = RELPERSISTENCE_UNLOGGED; }
 			| /*EMPTY*/					{ $$ = RELPERSISTENCE_PERMANENT; }
@@ -11505,19 +11499,13 @@ OptTempTableName:
 				}
 			| GLOBAL TEMPORARY opt_table qualified_name
 				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
 					$$ = $4;
-					$$->relpersistence = RELPERSISTENCE_TEMP;
+					$$->relpersistence = RELPERSISTENCE_GLOBAL_TEMP;
 				}
 			| GLOBAL TEMP opt_table qualified_name
 				{
-					ereport(WARNING,
-							(errmsg("GLOBAL is deprecated in temporary table creation"),
-							 parser_errposition(@1)));
 					$$ = $4;
-					$$->relpersistence = RELPERSISTENCE_TEMP;
+					$$->relpersistence = RELPERSISTENCE_GLOBAL_TEMP;
 				}
 			| UNLOGGED opt_table qualified_name
 				{
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 4dd8150..637a15c 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -59,6 +59,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref,
 							List **colnames, List **colvars);
 static int	specialAttNum(const char *attname);
 static bool isQueryUsingTempRelation_walker(Node *node, void *context);
+static bool is_query_using_gtt_walker(Node *node, void *context);

 /*
@@ -3420,3 +3421,49 @@ isQueryUsingTempRelation_walker(Node *node, void *context)
 								  isQueryUsingTempRelation_walker,
 								  context);
 }
+
+/* check if the query uses global temp table */
+static bool
+is_query_using_gtt_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+		ListCell   *rtable;
+
+		foreach(rtable, query->rtable)
+		{
+			RangeTblEntry *rte = lfirst(rtable);
+
+			if (rte->rtekind == RTE_RELATION)
+			{
+				Relation	rel = relation_open(rte->relid, AccessShareLock);
+				char		relpersistence = rel->rd_rel->relpersistence;
+
+				relation_close(rel, AccessShareLock);
+				if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+					return true;
+			}
+		}
+
+		return query_tree_walker(query,
+								 is_query_using_gtt_walker,
+								 context,
+								 QTW_IGNORE_JOINALIASES);
+	}
+
+	return expression_tree_walker(node,
+								  is_query_using_gtt_walker,
+								  context);
+}
+
+/* check if the query uses global temp table */
+bool
+is_query_using_gtt(Query *query)
+{
+	return is_query_using_gtt_walker((Node *) query, NULL);
+}
+
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ee47547..d13e253 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -359,6 +359,13 @@ generateSerialExtraStmts(CreateStmtContext *cxt, ColumnDef *column,
 	AlterSeqStmt *altseqstmt;
 	List	   *attnamelist;
 
+	if (cxt->relation->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("Global temp table does not yet support serial column")));
+	}
+
 	/*
 	 * Determine namespace and name to use for the sequence.
 	 *
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c1dd816..f2d3df9 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2091,6 +2091,11 @@ do_autovacuum(void)
 			}
 			continue;
 		}
+		else if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			/* autovacuum skip vacuum global temp table */
+			continue;
+		}
 
 		/* Fetch reloptions and the pgstat entry for this table */
 		relopts = extract_autovac_opts(tuple, pg_class_desc);
@@ -2157,7 +2162,9 @@ do_autovacuum(void)
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		/* autovacuum skip vacuum global temp table */
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP || 
+			classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 			continue;
 
 		relid = classForm->oid;
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 7ad1073..8e81701 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -53,6 +53,8 @@
 #include "utils/resowner_private.h"
 #include "utils/timestamp.h"
 
+#include "utils/guc.h"
+#include "catalog/storage_gtt.h"
 
 /* Note: these two macros only work on shared buffers, not local ones! */
 #define BufHdrGetBlock(bufHdr)	((Block) (BufferBlocks + ((Size) (bufHdr)->buf_id) * BLCKSZ))
@@ -432,7 +434,7 @@ ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref)
 static Buffer ReadBuffer_common(SMgrRelation reln, char relpersistence,
 								ForkNumber forkNum, BlockNumber blockNum,
 								ReadBufferMode mode, BufferAccessStrategy strategy,
-								bool *hit);
+								bool *hit, Relation rel);
 static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy);
 static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf, bool fixOwner);
@@ -664,7 +666,8 @@ ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
 	 */
 	pgstat_count_buffer_read(reln);
 	buf = ReadBuffer_common(reln->rd_smgr, reln->rd_rel->relpersistence,
-							forkNum, blockNum, mode, strategy, &hit);
+							forkNum, blockNum, mode, strategy, &hit,
+							reln);
 	if (hit)
 		pgstat_count_buffer_hit(reln);
 	return buf;
@@ -692,7 +695,7 @@ ReadBufferWithoutRelcache(RelFileNode rnode, ForkNumber forkNum,
 	Assert(InRecovery);
 
 	return ReadBuffer_common(smgr, RELPERSISTENCE_PERMANENT, forkNum, blockNum,
-							 mode, strategy, &hit);
+							 mode, strategy, &hit, NULL);
 }

@@ -704,7 +707,8 @@ ReadBufferWithoutRelcache(RelFileNode rnode, ForkNumber forkNum,
 static Buffer
 ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 				  BlockNumber blockNum, ReadBufferMode mode,
-				  BufferAccessStrategy strategy, bool *hit)
+				  BufferAccessStrategy strategy, bool *hit,
+				  Relation rel)
 {
 	BufferDesc *bufHdr;
 	Block		bufBlock;
@@ -719,6 +723,15 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	isExtend = (blockNum == P_NEW);
 
+	/* create storage when first read data page for gtt */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+		(isExtend || blockNum == 0) &&
+		forkNum == MAIN_FORKNUM &&
+		!gtt_storage_attached(smgr->smgr_rnode.node.relNode))
+	{
+		RelationCreateStorage(smgr->smgr_rnode.node, relpersistence, rel);
+	}
+
 	TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
 									   smgr->smgr_rnode.node.spcNode,
 									   smgr->smgr_rnode.node.dbNode,
@@ -2799,6 +2812,16 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 BlockNumber
 RelationGetNumberOfBlocksInFork(Relation relation, ForkNumber forkNum)
 {
+	/*
+	 * When this backend not init gtt storage
+	 * return 0
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(relation) &&
+		!gtt_storage_attached(relation->rd_node.relNode))
+	{
+		return 0;
+	}
+
 	switch (relation->rd_rel->relkind)
 	{
 		case RELKIND_SEQUENCE:
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 8853706..b3544dd 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -21,6 +21,7 @@
 #include "access/nbtree.h"
 #include "access/subtrans.h"
 #include "access/twophase.h"
+#include "catalog/storage_gtt.h"
 #include "commands/async.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -147,6 +148,7 @@ CreateSharedMemoryAndSemaphores(void)
 		size = add_size(size, BTreeShmemSize());
 		size = add_size(size, SyncScanShmemSize());
 		size = add_size(size, AsyncShmemSize());
+		size = add_size(size, active_gtt_shared_hash_size());
 #ifdef EXEC_BACKEND
 		size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -217,6 +219,8 @@ CreateSharedMemoryAndSemaphores(void)
 	SUBTRANSShmemInit();
 	MultiXactShmemInit();
 	InitBufferPool();
+	/* global temporary table */
+	active_gtt_shared_hash_init();
 
 	/*
 	 * Set up lock manager
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 3da5307..21bd1f2 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -60,6 +60,7 @@
 #include "utils/builtins.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/guc.h"
 
 #define UINT32_ACCESS_ONCE(var)		 ((uint32)(*((volatile uint32 *)&(var))))
 
@@ -3973,3 +3974,77 @@ KnownAssignedXidsReset(void)
 
 	LWLockRelease(ProcArrayLock);
 }
+
+/*
+ * search all active backend to get oldest frozenxid
+ * for global temp table.
+ */
+int
+list_all_session_gtt_frozenxids(int max_size, int *pids, uint32 *xids, int *n)
+{
+	ProcArrayStruct *arrayP = procArray;
+	TransactionId result = InvalidTransactionId;
+	int			index;
+	int			flags = 0;
+	int			i = 0;
+
+	if (max_active_gtt <= 0)
+		return 0;
+
+	if (max_size > 0)
+	{
+		Assert(pids);
+		Assert(xids);
+		Assert(n);
+		*n = 0;
+	}
+
+	if (max_active_gtt <= 0)
+		return InvalidTransactionId;
+
+	if (RecoveryInProgress())
+		return InvalidTransactionId;
+
+	flags |= PROC_IS_AUTOVACUUM;
+	flags |= PROC_IN_LOGICAL_DECODING;
+
+	LWLockAcquire(ProcArrayLock, LW_SHARED);
+	if (max_size > 0 && max_size < arrayP->numProcs)
+	{
+		LWLockRelease(ProcArrayLock);
+		elog(ERROR, "list_all_gtt_frozenxids require more array");
+	}
+
+	for (index = 0; index < arrayP->numProcs; index++)
+	{
+		int			pgprocno = arrayP->pgprocnos[index];
+		volatile PGPROC *proc = &allProcs[pgprocno];
+		volatile PGXACT *pgxact = &allPgXact[pgprocno];
+
+		if (pgxact->vacuumFlags & flags)
+			continue;
+
+		if (proc->databaseId == MyDatabaseId &&
+			TransactionIdIsNormal(proc->session_gtt_frozenxid))
+		{
+			if (result == InvalidTransactionId)
+				result = proc->session_gtt_frozenxid;
+			else if (TransactionIdPrecedes(proc->session_gtt_frozenxid, result))
+				result = proc->session_gtt_frozenxid;
+
+			if (max_size > 0)
+			{
+				pids[i] = proc->pid;
+				xids[i] = proc->session_gtt_frozenxid;
+				i++;
+			}
+		}
+	}
+	LWLockRelease(ProcArrayLock);
+
+	if (max_size > 0)
+		*n = i;
+
+	return result;
+}
+
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b3c54a6..645456c 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -396,6 +396,7 @@ InitProcess(void)
 	MyProc->databaseId = InvalidOid;
 	MyProc->roleId = InvalidOid;
 	MyProc->tempNamespaceId = InvalidOid;
+	MyProc->session_gtt_frozenxid = InvalidTransactionId;	/* init session level gtt frozenxid */
 	MyProc->isBackgroundWorker = IsBackgroundWorker;
 	MyPgXact->delayChkpt = false;
 	MyPgXact->vacuumFlags = 0;
@@ -578,6 +579,7 @@ InitAuxiliaryProcess(void)
 	MyProc->databaseId = InvalidOid;
 	MyProc->roleId = InvalidOid;
 	MyProc->tempNamespaceId = InvalidOid;
+	MyProc->session_gtt_frozenxid = InvalidTransactionId;	/* init session level gtt frozenxid */
 	MyProc->isBackgroundWorker = IsBackgroundWorker;
 	MyPgXact->delayChkpt = false;
 	MyPgXact->vacuumFlags = 0;
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 07f3c93..cee8f9e 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -654,6 +654,12 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
 		 */
 		if (zero_damaged_pages || InRecovery)
 			MemSet(buffer, 0, BLCKSZ);
+		else if(SmgrIsTemp(reln) && blocknum == 0 && forknum == MAIN_FORKNUM)
+		{
+			/* global temp table init btree meta page */
+			MemSet(buffer, 0, BLCKSZ);
+			mdwrite(reln, forknum, blocknum, buffer, true);
+		}
 		else
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index a87e721..adce760 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1008,6 +1008,10 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 				Assert(backend != InvalidBackendId);
 			}
 			break;
+		/* For global temp table */
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			backend = BackendIdForTempRelations();
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c", relform->relpersistence);
 			backend = InvalidBackendId; /* placate compiler */
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 35a8995..42dc3a5 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -141,6 +141,7 @@
 #include "utils/timestamp.h"
 #include "utils/typcache.h"
 
+#include "catalog/storage_gtt.h"
 
 /* Hooks for plugins to get control when we ask for stats */
 get_relation_stats_hook_type get_relation_stats_hook = NULL;
@@ -4568,12 +4569,25 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
 						}
 						else if (index->indpred == NIL)
 						{
-							vardata->statsTuple =
-								SearchSysCache3(STATRELATTINH,
-												ObjectIdGetDatum(index->indexoid),
-												Int16GetDatum(pos + 1),
-												BoolGetDatum(false));
-							vardata->freefunc = ReleaseSysCache;
+							char rel_persistence = get_rel_persistence(index->indexoid);
+
+							if (rel_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+							{
+								vardata->statsTuple =
+									get_gtt_att_statistic(index->indexoid,
+														Int16GetDatum(pos + 1),
+														false);
+								vardata->freefunc = release_gtt_statistic_cache;
+							}
+							else
+							{
+								vardata->statsTuple =
+									SearchSysCache3(STATRELATTINH,
+													ObjectIdGetDatum(index->indexoid),
+													Int16GetDatum(pos + 1),
+													BoolGetDatum(false));
+								vardata->freefunc = ReleaseSysCache;
+							}
 
 							if (HeapTupleIsValid(vardata->statsTuple))
 							{
@@ -4652,15 +4666,27 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 	}
 	else if (rte->rtekind == RTE_RELATION)
 	{
-		/*
-		 * Plain table or parent of an inheritance appendrel, so look up the
-		 * column in pg_statistic
-		 */
-		vardata->statsTuple = SearchSysCache3(STATRELATTINH,
-											  ObjectIdGetDatum(rte->relid),
-											  Int16GetDatum(var->varattno),
-											  BoolGetDatum(rte->inh));
-		vardata->freefunc = ReleaseSysCache;
+		char rel_persistence = get_rel_persistence(rte->relid);
+
+		if (rel_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			vardata->statsTuple = get_gtt_att_statistic(rte->relid,
+														var->varattno,
+														rte->inh);
+			vardata->freefunc = release_gtt_statistic_cache;
+		}
+		else
+		{
+			/*
+			 * Plain table or parent of an inheritance appendrel, so look up the
+			 * column in pg_statistic
+			 */
+			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+												  ObjectIdGetDatum(rte->relid),
+												  Int16GetDatum(var->varattno),
+												  BoolGetDatum(rte->inh));
+			vardata->freefunc = ReleaseSysCache;
+		}
 
 		if (HeapTupleIsValid(vardata->statsTuple))
 		{
@@ -5972,6 +5998,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	{
 		/* Simple variable --- look to stats for the underlying table */
 		RangeTblEntry *rte = planner_rt_fetch(index->rel->relid, root);
+		char	rel_persistence = get_rel_persistence(rte->relid);
 
 		Assert(rte->rtekind == RTE_RELATION);
 		relid = rte->relid;
@@ -5989,6 +6016,13 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				!vardata.freefunc)
 				elog(ERROR, "no function provided to release variable stats with");
 		}
+		else if (rel_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			vardata.statsTuple = get_gtt_att_statistic(relid,
+													colnum,
+													rte->inh);
+			vardata.freefunc = release_gtt_statistic_cache;
+		}
 		else
 		{
 			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
@@ -6000,6 +6034,8 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	}
 	else
 	{
+		char	rel_persistence = get_rel_persistence(index->indexoid);
+
 		/* Expression --- maybe there are stats for the index itself */
 		relid = index->indexoid;
 		colnum = 1;
@@ -6015,6 +6051,13 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				!vardata.freefunc)
 				elog(ERROR, "no function provided to release variable stats with");
 		}
+		else if (rel_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			vardata.statsTuple = get_gtt_att_statistic(relid,
+													colnum,
+													false);
+			vardata.freefunc = release_gtt_statistic_cache;
+		}
 		else
 		{
 			vardata.statsTuple = SearchSysCache3(STATRELATTINH,
@@ -6881,6 +6924,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		/* attempt to lookup stats in relation for this index column */
 		if (attnum != 0)
 		{
+			char	rel_persistence = get_rel_persistence(rte->relid);
+
 			/* Simple variable -- look to stats for the underlying table */
 			if (get_relation_stats_hook &&
 				(*get_relation_stats_hook) (root, rte, attnum, &vardata))
@@ -6893,6 +6938,14 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 					elog(ERROR,
 						 "no function provided to release variable stats with");
 			}
+			else if (rel_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+			{
+				vardata.statsTuple =
+					get_gtt_att_statistic(rte->relid,
+										attnum,
+										false);
+				vardata.freefunc = release_gtt_statistic_cache;
+			}
 			else
 			{
 				vardata.statsTuple =
@@ -6905,6 +6958,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		}
 		else
 		{
+			char	rel_persistence = get_rel_persistence(index->indexoid);
+
 			/*
 			 * Looks like we've found an expression column in the index. Let's
 			 * see if there's any stats for it.
@@ -6924,6 +6979,14 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 					!vardata.freefunc)
 					elog(ERROR, "no function provided to release variable stats with");
 			}
+			else if (rel_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+			{
+				vardata.statsTuple =
+					get_gtt_att_statistic(index->indexoid,
+										attnum,
+										false);
+				vardata.freefunc = release_gtt_statistic_cache;
+			}
 			else
 			{
 				vardata.statsTuple = SearchSysCache3(STATRELATTINH,
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 27602fa..25a411a 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -34,6 +34,7 @@
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
+#include "catalog/storage_gtt.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "utils/array.h"
@@ -46,6 +47,7 @@
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 
+
 /* Hook for plugins to get control in get_attavgwidth() */
 get_attavgwidth_hook_type get_attavgwidth_hook = NULL;
 
@@ -2878,6 +2880,18 @@ get_attavgwidth(Oid relid, AttrNumber attnum)
 		if (stawidth > 0)
 			return stawidth;
 	}
+	if (get_rel_persistence(relid) == RELPERSISTENCE_GLOBAL_TEMP)
+	{
+		tp = get_gtt_att_statistic(relid, attnum, false);
+		if (!HeapTupleIsValid(tp))
+			return 0;
+
+		stawidth = ((Form_pg_statistic) GETSTRUCT(tp))->stawidth;
+		if (stawidth > 0)
+			return stawidth;
+		else
+			return 0;
+	}
 	tp = SearchSysCache3(STATRELATTINH,
 						 ObjectIdGetDatum(relid),
 						 Int16GetDatum(attnum),
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 585dcee..b8f2a41 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1130,6 +1130,16 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 				relation->rd_islocaltemp = false;
 			}
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			{
+				relation->rd_backend = BackendIdForTempRelations();
+				/*
+				 * For global temp table, all backend can use
+				 * this relation, so rd_islocaltemp always true.
+				 */
+				relation->rd_islocaltemp = true;
+			}
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c",
 				 relation->rd_rel->relpersistence);
@@ -3311,6 +3321,10 @@ RelationBuildLocalRelation(const char *relname,
 			rel->rd_backend = BackendIdForTempRelations();
 			rel->rd_islocaltemp = true;
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			rel->rd_backend = BackendIdForTempRelations();
+			rel->rd_islocaltemp = true;
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c", relpersistence);
 			break;
@@ -3425,6 +3439,9 @@ RelationSetNewRelfilenode(Relation relation, char persistence)
 	TransactionId freezeXid = InvalidTransactionId;
 	RelFileNode newrnode;
 
+	if (RELATION_IS_GLOBAL_TEMP(relation))
+		elog(ERROR, "global temp table does not allow setting new relfilenode");
+
 	/* Allocate a new relfilenode */
 	newrelfilenode = GetNewRelFileNode(relation->rd_rel->reltablespace, NULL,
 									   persistence);
@@ -3465,7 +3482,7 @@ RelationSetNewRelfilenode(Relation relation, char persistence)
 				/* handle these directly, at least for now */
 				SMgrRelation srel;
 
-				srel = RelationCreateStorage(newrnode, persistence);
+				srel = RelationCreateStorage(newrnode, persistence, relation);
 				smgrclose(srel);
 			}
 			break;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 31a5ef0..518d0fa 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -138,6 +138,18 @@ char	   *GUC_check_errmsg_string;
 char	   *GUC_check_errdetail_string;
 char	   *GUC_check_errhint_string;
 
+/*
+ * num = 0 means disable global temp table feature.
+ * global temp table define can still storage in catalog
+ * just can not use.
+ * num > 0 means database can management num active global temp table.
+ */
+#define	MIN_NUM_ACTIVE_GTT			0
+#define	DEFAULT_NUM_ACTIVE_GTT			1000
+#define	MAX_NUM_ACTIVE_GTT			1000000
+
+int		max_active_gtt = MIN_NUM_ACTIVE_GTT;
+
 static void do_serialize(char **destptr, Size *maxbytes, const char *fmt,...) pg_attribute_printf(3, 4);
 
 static void set_config_sourcefile(const char *name, char *sourcefile,
@@ -1962,6 +1974,15 @@ static struct config_bool ConfigureNamesBool[] =
 static struct config_int ConfigureNamesInt[] =
 {
 	{
+		{"max_active_global_temporary_table", PGC_POSTMASTER, UNGROUPED,
+			gettext_noop("max active global temporary table."),
+			NULL
+		},
+		&max_active_gtt,
+		DEFAULT_NUM_ACTIVE_GTT, MIN_NUM_ACTIVE_GTT, MAX_NUM_ACTIVE_GTT,
+		NULL, NULL, NULL
+	},
+	{
 		{"archive_timeout", PGC_SIGHUP, WAL_ARCHIVING,
 			gettext_noop("Forces a switch to the next WAL file if a "
 						 "new file has not been started within N seconds."),
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bf69adc..72e291b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -15585,6 +15585,7 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 	{
 		char	   *ftoptions = NULL;
 		char	   *srvname = NULL;
+		char		*table_type = NULL;
 
 		switch (tbinfo->relkind)
 		{
@@ -15636,9 +15637,15 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			binary_upgrade_set_pg_class_oids(fout, q,
 											 tbinfo->dobj.catId.oid, false);
 
+		if (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED)
+			table_type = "UNLOGGED ";
+		else if (tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			table_type = "GLOBAL TEMPORARY ";
+		else
+			table_type = "";
+
 		appendPQExpBuffer(q, "CREATE %s%s %s",
-						  tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED ?
-						  "UNLOGGED " : "",
+						  table_type,
 						  reltypename,
 						  qualrelname);
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 090b6ba..34b4683 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -165,6 +165,7 @@ typedef FormData_pg_class *Form_pg_class;
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
 #define		  RELPERSISTENCE_TEMP		't' /* temporary table */
+#define		  RELPERSISTENCE_GLOBAL_TEMP	'g' /* global temporary table */
 
 /* default selection for replica identity (primary key or nothing) */
 #define		  REPLICA_IDENTITY_DEFAULT	'd'
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 3579d3f..2bde386 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -19,7 +19,7 @@
 #include "storage/smgr.h"
 #include "utils/relcache.h"
 
-extern SMgrRelation RelationCreateStorage(RelFileNode rnode, char relpersistence);
+extern SMgrRelation RelationCreateStorage(RelFileNode rnode, char relpersistence, Relation rel);
 extern void RelationDropStorage(Relation rel);
 extern void RelationPreserveStorage(RelFileNode rnode, bool atCommit);
 extern void RelationTruncate(Relation rel, BlockNumber nblocks);
diff --git a/src/include/catalog/storage_gtt.h b/src/include/catalog/storage_gtt.h
new file mode 100644
index 0000000..ea41e66
--- /dev/null
+++ b/src/include/catalog/storage_gtt.h
@@ -0,0 +1,39 @@
+/*-------------------------------------------------------------------------
+ *
+ * storage_gtt.h
+ *	  prototypes for functions in backend/catalog/storage_gtt.c
+ *
+ * src/include/catalog/storage_gtt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef STORAGE_GTT_H
+#define STORAGE_GTT_H
+
+#include "access/htup.h"
+#include "storage/block.h"
+#include "storage/relfilenode.h"
+#include "utils/relcache.h"
+
+extern Size active_gtt_shared_hash_size(void);
+extern void active_gtt_shared_hash_init(void);
+extern void remember_gtt_storage_info(RelFileNode rnode, Relation rel);
+extern void forget_gtt_storage_info(Oid relid);
+extern bool is_other_backend_use_gtt(RelFileNode node);
+extern bool gtt_storage_attached(Oid relid);
+extern Bitmapset *copy_active_gtt_bitmap(RelFileNode node);
+extern void up_gtt_att_statistic(Oid reloid, int attnum, bool inh, int natts,
+								TupleDesc tupleDescriptor, Datum *values, bool *isnull);
+extern HeapTuple get_gtt_att_statistic(Oid reloid, int attnum, bool inh);
+extern void release_gtt_statistic_cache(HeapTuple tup);
+extern void up_gtt_relstats(Relation relation,
+							BlockNumber num_pages,
+							double num_tuples,
+							BlockNumber num_all_visible_pages,
+							TransactionId relfrozenxid,
+							TransactionId relminmxid);
+extern void get_gtt_relstats(Oid relid, BlockNumber *relpages, double *reltuples,
+							BlockNumber *relallvisible, TransactionId *relfrozenxid,
+							TransactionId *relminmxid);
+
+#endif							/* STORAGE_H */
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index f7e0781..4f5a353 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -130,4 +130,7 @@ extern Oid	attnumTypeId(Relation rd, int attid);
 extern Oid	attnumCollationId(Relation rd, int attid);
 extern bool isQueryUsingTempRelation(Query *query);
 
+/* global temp table check */
+extern bool is_query_using_gtt(Query *query);
+
 #endif							/* PARSE_RELATION_H */
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index f627dfe..cfc6c78 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -220,6 +220,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_TBM,
 	LWTRANCHE_PARALLEL_APPEND,
 	LWTRANCHE_SXACT,
+	LWTRANCHE_GTT_CTL,
 	LWTRANCHE_FIRST_USER_DEFINED
 }			BuiltinTrancheIds;
 
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 281e1db..2ab8e91 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -117,6 +117,8 @@ struct PGPROC
 	Oid			tempNamespaceId;	/* OID of temp schema this backend is
 									 * using */
 
+	TransactionId session_gtt_frozenxid;	/* session level global temp table relfrozenxid */
+
 	bool		isBackgroundWorker; /* true if background worker. */
 
 	/*
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index da8b672..8e7d4c7 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -124,4 +124,6 @@ extern void ProcArraySetReplicationSlotXmin(TransactionId xmin,
 extern void ProcArrayGetReplicationSlotXmin(TransactionId *xmin,
 											TransactionId *catalog_xmin);
 
+extern int list_all_session_gtt_frozenxids(int max_size, int *pids, uint32 *xids, int *n);
+
 #endif							/* PROCARRAY_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 6791e0c..82b672f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -276,6 +276,10 @@ extern int	tcp_user_timeout;
 extern bool trace_sort;
 #endif
 
+/* global temporary table */
+extern int	max_active_gtt;
+/* end */
+
 /*
  * Functions exported by guc.c
  */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 8b8b237..c1962b7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -272,6 +272,7 @@ typedef struct StdRdOptions
 	int			parallel_workers;	/* max number of parallel workers */
 	bool		vacuum_index_cleanup;	/* enables index vacuuming and cleanup */
 	bool		vacuum_truncate;	/* enables vacuum to truncate a relation */
+	bool		on_commit_delete_rows;	/* global temp table */
 } StdRdOptions;
 
 #define HEAP_MIN_FILLFACTOR			10
@@ -530,7 +531,8 @@ typedef struct ViewOptions
  *		True if relation's pages are stored in local buffers.
  */
 #define RelationUsesLocalBuffers(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP || \
+	 (relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 
 /*
  * RELATION_IS_LOCAL
@@ -609,6 +611,17 @@ typedef struct ViewOptions
  */
 #define RelationGetPartitionDesc(relation) ((relation)->rd_partdesc)
 
+/* global temp table implementations */
+#define RELATION_IS_GLOBAL_TEMP(relation)	((relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+
+#define RELATION_GTT_ON_COMMIT_DELETE(relation)	\
+	((relation)->rd_options && \
+	 (relation)->rd_rel->relkind == RELKIND_RELATION && \
+	 (relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ? \
+	 ((StdRdOptions *) (relation)->rd_options)->on_commit_delete_rows : false)
+
+#define RelationGetRelPersistence(relation) ((relation)->rd_rel->relpersistence)
+
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
 extern void RelationDecrementReferenceCount(Relation rel);
diff --git a/src/test/regress/expected/gtt_clean.out b/src/test/regress/expected/gtt_clean.out
new file mode 100644
index 0000000..50ca9ac
--- /dev/null
+++ b/src/test/regress/expected/gtt_clean.out
@@ -0,0 +1,7 @@
+reset search_path;
+drop schema gtt cascade;
+NOTICE:  drop cascades to 4 other objects
+DETAIL:  drop cascades to table gtt.gtt1
+drop cascades to table gtt.gtt2
+drop cascades to table gtt.gtt3
+drop cascades to table gtt.gtt_t_kenyon
diff --git a/src/test/regress/expected/gtt_error.out b/src/test/regress/expected/gtt_error.out
new file mode 100644
index 0000000..80c16dc
--- /dev/null
+++ b/src/test/regress/expected/gtt_error.out
@@ -0,0 +1,106 @@
+CREATE SCHEMA IF NOT EXISTS gtt_error;
+set search_path=gtt_error,sys;
+create global temp table gtt1(a int primary key, b text);
+create global temp table gtt2(a int primary key, b text) on commit delete rows;
+create global temp table gtt3(a int primary key, b text) on commit PRESERVE rows;
+create global temp table tmp_t0(c0 tsvector,c1 varchar(100));
+create table tbl_inherits_parent(
+a int not null,
+b varchar(32) not null default 'Got u',
+c int check (c > 0),
+d date not null
+);
+create global temp table tbl_inherits_parent_global_temp(
+a int not null,
+b varchar(32) not null default 'Got u',
+c int check (c > 0),
+d date not null
+);
+CREATE global temp TABLE products (
+    product_no integer PRIMARY KEY,
+    name text,
+    price numeric
+);
+-- ERROR
+create index CONCURRENTLY idx_gtt1 on gtt1 (b);
+ERROR:  cannot create indexes on global temporary tables using concurrent mode
+-- ERROR
+cluster gtt1 using gtt1_pkey;
+ERROR:  not support cluster global temporary tables yet
+-- ERROR
+create table gtt1(a int primary key, b text) on commit delete rows;
+ERROR:  ON COMMIT can only be used on temporary tables
+-- ERROR
+create table gtt1(a int primary key, b text) with(on_commit_delete_rows=true);
+ERROR:  regular table cannot specifie on_commit_delete_rows
+-- ERROR
+CREATE global temp TABLE measurement (
+    logdate         date not null,
+    peaktemp        int,
+    unitsales       int
+) PARTITION BY RANGE (logdate);
+ERROR:  not support create global temporary partition table yet
+-- ERROR
+create global temp table tbl_inherits_partition() inherits (tbl_inherits_parent);
+ERROR:  not support create global temporary inheritance table yet
+-- ERROR
+create global temp table gtt3(a int primary key, b text) on commit drop;
+ERROR:  global temp table not support on commit drop clause
+-- ERROR
+create global temp table gtt4(a int primary key, b text) with(on_commit_delete_rows=true) on commit delete rows;
+ERROR:  can not defeine global temp table with on commit and with clause at same time
+-- ok
+create global temp table gtt5(a int primary key, b text) with(on_commit_delete_rows=true);
+--ERROR
+alter table gtt1 rename to gttx;
+ERROR:  not support rename global temporary tables yet
+-- ERROR
+ALTER TABLE gtt1 ADD COLUMN address varchar(30);
+ERROR:  not support alter global temporary tables yet
+-- ERROR
+CREATE TABLE orders (
+    order_id integer PRIMARY KEY,
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+ERROR:  referenced relation "products" is not a global temp table
+-- ERROR
+CREATE global temp TABLE orders (
+    order_id integer PRIMARY KEY,
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+ERROR:  not support alter global temporary tables yet
+-- ERROR
+CREATE GLOBAL TEMPORARY TABLE mytable (
+  id SERIAL PRIMARY KEY,
+  data text
+) on commit preserve rows;
+ERROR:  Global temp table does not yet support serial column
+-- ERROR
+create global temp table gttx(id int GENERATED ALWAYS AS IDENTITY (START WITH 2));
+ERROR:  Global temp table does not yet support serial column
+--ERROR
+CREATE MATERIALIZED VIEW mv_gtt1 as select * from gtt1;
+ERROR:  materialized views must not use global temporary tables or views
+-- ALL ERROR
+create index idx_err on gtt1 using hash (a);
+ERROR:  only support btree index on global temp table
+create index idx_err on gtt1 using gist (a);
+ERROR:  data type integer has no default operator class for access method "gist"
+HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+create index idx_tmp_t0_1 on tmp_t0 using gin (c0);
+ERROR:  only support btree index on global temp table
+create index idx_tmp_t0_1 on tmp_t0 using gist (c0);
+ERROR:  only support btree index on global temp table
+reset search_path;
+drop schema gtt_error cascade;
+NOTICE:  drop cascades to 8 other objects
+DETAIL:  drop cascades to table gtt_error.gtt1
+drop cascades to table gtt_error.gtt2
+drop cascades to table gtt_error.gtt3
+drop cascades to table gtt_error.tmp_t0
+drop cascades to table gtt_error.tbl_inherits_parent
+drop cascades to table gtt_error.tbl_inherits_parent_global_temp
+drop cascades to table gtt_error.products
+drop cascades to table gtt_error.gtt5
diff --git a/src/test/regress/expected/gtt_parallel_1.out b/src/test/regress/expected/gtt_parallel_1.out
new file mode 100644
index 0000000..30d8a7b
--- /dev/null
+++ b/src/test/regress/expected/gtt_parallel_1.out
@@ -0,0 +1,84 @@
+set search_path=gtt,sys;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+begin;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+(1 row)
+
+commit;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+begin;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+(1 row)
+
+rollback;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+truncate gtt1;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+begin;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+(1 row)
+
+truncate gtt1;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+insert into gtt1 values(1, 'test1');
+rollback;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+begin;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+truncate gtt1;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+(1 row)
+
+truncate gtt1;
+commit;
+select * from gtt1 order by a;
+ a | b 
+---+---
+(0 rows)
+
+reset search_path;
diff --git a/src/test/regress/expected/gtt_parallel_2.out b/src/test/regress/expected/gtt_parallel_2.out
new file mode 100644
index 0000000..70bf4a6
--- /dev/null
+++ b/src/test/regress/expected/gtt_parallel_2.out
@@ -0,0 +1,142 @@
+set search_path=gtt,sys;
+insert into gtt3 values(1, 'test1');
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+(1 row)
+
+begin;
+insert into gtt3 values(2, 'test1');
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+ 2 | test1
+(2 rows)
+
+commit;
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+ 2 | test1
+(2 rows)
+
+begin;
+insert into gtt3 values(3, 'test1');
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+ 2 | test1
+ 3 | test1
+(3 rows)
+
+rollback;
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+ 2 | test1
+(2 rows)
+
+truncate gtt3;
+select * from gtt3 order by a;
+ a | b 
+---+---
+(0 rows)
+
+insert into gtt3 values(1, 'test1');
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+(1 row)
+
+begin;
+insert into gtt3 values(2, 'test2');
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 1 | test1
+ 2 | test2
+(2 rows)
+
+truncate gtt3;
+select * from gtt3 order by a;
+ a | b 
+---+---
+(0 rows)
+
+insert into gtt3 values(3, 'test3');
+update gtt3 set a = 3 where b = 'test1';
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 3 | test3
+(1 row)
+
+rollback;
+select * from gtt3 order by a;
+ a | b 
+---+---
+(0 rows)
+
+begin;
+select * from gtt3 order by a;
+ a | b 
+---+---
+(0 rows)
+
+truncate gtt3;
+insert into gtt3 values(5, 'test5');
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 5 | test5
+(1 row)
+
+truncate gtt3;
+insert into gtt3 values(6, 'test6');
+commit;
+select * from gtt3 order by a;
+ a |   b   
+---+-------
+ 6 | test6
+(1 row)
+
+truncate gtt3;
+insert into gtt3 values(generate_series(1,100000), 'testing');
+select count(*) from gtt3;
+ count  
+--------
+ 100000
+(1 row)
+
+analyze gtt3;
+explain (COSTS FALSE) select * from gtt3 where a =300;
+             QUERY PLAN             
+------------------------------------
+ Index Scan using gtt3_pkey on gtt3
+   Index Cond: (a = 300)
+(2 rows)
+
+insert into gtt_t_kenyon select generate_series(1,2000),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God',500);
+insert into gtt_t_kenyon select generate_series(1,2),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',2000);
+insert into gtt_t_kenyon select generate_series(3,4),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',4000);
+insert into gtt_t_kenyon select generate_series(5,6),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',5500);
+insert into gtt_t_kenyon select generate_series(1,2),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',10000);
+select relname, pg_relation_size(oid),pg_relation_size(reltoastrelid) from pg_class where relname = 'gtt_t_kenyon';
+   relname    | pg_relation_size | pg_relation_size 
+--------------+------------------+------------------
+ gtt_t_kenyon |           450560 |            16384
+(1 row)
+
+select relname from pg_class where relname = 'gtt_t_kenyon' and reltoastrelid != 0;
+   relname    
+--------------
+ gtt_t_kenyon
+(1 row)
+
+reset search_path;
diff --git a/src/test/regress/expected/gtt_prepare.out b/src/test/regress/expected/gtt_prepare.out
new file mode 100644
index 0000000..9e8f5f0
--- /dev/null
+++ b/src/test/regress/expected/gtt_prepare.out
@@ -0,0 +1,7 @@
+CREATE SCHEMA IF NOT EXISTS gtt;
+set search_path=gtt,sys;
+create global temp table gtt1(a int primary key, b text);
+create global temp table gtt2(a int primary key, b text) on commit delete rows;
+create global temp table gtt3(a int primary key, b text) on commit PRESERVE rows;
+create global temp table gtt_t_kenyon(id int,vname varchar(48),remark text) on commit PRESERVE rows;
+reset search_path;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fc0f141..6836eeb 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -121,3 +121,9 @@ test: fast_default
 
 # run stats by itself because its delay may be insufficient under heavy load
 test: stats
+
+# global temp table test
+test: gtt_error
+test: gtt_prepare
+test: gtt_parallel_1 gtt_parallel_2
+test: gtt_clean
diff --git a/src/test/regress/sql/gtt_error.sql b/src/test/regress/sql/gtt_error.sql
new file mode 100644
index 0000000..e895574
--- /dev/null
+++ b/src/test/regress/sql/gtt_error.sql
@@ -0,0 +1,106 @@
+
+CREATE SCHEMA IF NOT EXISTS gtt_error;
+
+set search_path=gtt_error,sys;
+
+create global temp table gtt1(a int primary key, b text);
+
+create global temp table gtt2(a int primary key, b text) on commit delete rows;
+
+create global temp table gtt3(a int primary key, b text) on commit PRESERVE rows;
+
+create global temp table tmp_t0(c0 tsvector,c1 varchar(100));
+
+create table tbl_inherits_parent(
+a int not null,
+b varchar(32) not null default 'Got u',
+c int check (c > 0),
+d date not null
+);
+
+create global temp table tbl_inherits_parent_global_temp(
+a int not null,
+b varchar(32) not null default 'Got u',
+c int check (c > 0),
+d date not null
+);
+
+CREATE global temp TABLE products (
+    product_no integer PRIMARY KEY,
+    name text,
+    price numeric
+);
+
+-- ERROR
+create index CONCURRENTLY idx_gtt1 on gtt1 (b);
+
+-- ERROR
+cluster gtt1 using gtt1_pkey;
+
+-- ERROR
+create table gtt1(a int primary key, b text) on commit delete rows;
+
+-- ERROR
+create table gtt1(a int primary key, b text) with(on_commit_delete_rows=true);
+
+-- ERROR
+CREATE global temp TABLE measurement (
+    logdate         date not null,
+    peaktemp        int,
+    unitsales       int
+) PARTITION BY RANGE (logdate);
+
+-- ERROR
+create global temp table tbl_inherits_partition() inherits (tbl_inherits_parent);
+
+-- ERROR
+create global temp table gtt3(a int primary key, b text) on commit drop;
+
+-- ERROR
+create global temp table gtt4(a int primary key, b text) with(on_commit_delete_rows=true) on commit delete rows;
+
+-- ok
+create global temp table gtt5(a int primary key, b text) with(on_commit_delete_rows=true);
+
+--ERROR
+alter table gtt1 rename to gttx;
+
+-- ERROR
+ALTER TABLE gtt1 ADD COLUMN address varchar(30);
+
+-- ERROR
+CREATE TABLE orders (
+    order_id integer PRIMARY KEY,
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+
+-- ERROR
+CREATE global temp TABLE orders (
+    order_id integer PRIMARY KEY,
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+
+-- ERROR
+CREATE GLOBAL TEMPORARY TABLE mytable (
+  id SERIAL PRIMARY KEY,
+  data text
+) on commit preserve rows;
+
+-- ERROR
+create global temp table gttx(id int GENERATED ALWAYS AS IDENTITY (START WITH 2));
+
+--ERROR
+CREATE MATERIALIZED VIEW mv_gtt1 as select * from gtt1;
+
+-- ALL ERROR
+create index idx_err on gtt1 using hash (a);
+create index idx_err on gtt1 using gist (a);
+create index idx_tmp_t0_1 on tmp_t0 using gin (c0);
+create index idx_tmp_t0_1 on tmp_t0 using gist (c0);
+
+reset search_path;
+
+drop schema gtt_error cascade;
+
diff --git a/src/test/regress/sql/gtt_parallel_1.sql b/src/test/regress/sql/gtt_parallel_1.sql
new file mode 100644
index 0000000..d7d81de
--- /dev/null
+++ b/src/test/regress/sql/gtt_parallel_1.sql
@@ -0,0 +1,42 @@
+
+
+set search_path=gtt,sys;
+
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+
+begin;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+commit;
+select * from gtt1 order by a;
+
+begin;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+rollback;
+select * from gtt1 order by a;
+
+truncate gtt1;
+select * from gtt1 order by a;
+
+begin;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+truncate gtt1;
+select * from gtt1 order by a;
+insert into gtt1 values(1, 'test1');
+rollback;
+select * from gtt1 order by a;
+
+begin;
+select * from gtt1 order by a;
+truncate gtt1;
+insert into gtt1 values(1, 'test1');
+select * from gtt1 order by a;
+truncate gtt1;
+commit;
+select * from gtt1 order by a;
+
+reset search_path;
+
diff --git a/src/test/regress/sql/gtt_parallel_2.sql b/src/test/regress/sql/gtt_parallel_2.sql
new file mode 100644
index 0000000..cb2f7a6
--- /dev/null
+++ b/src/test/regress/sql/gtt_parallel_2.sql
@@ -0,0 +1,62 @@
+
+
+set search_path=gtt,sys;
+
+insert into gtt3 values(1, 'test1');
+select * from gtt3 order by a;
+
+begin;
+insert into gtt3 values(2, 'test1');
+select * from gtt3 order by a;
+commit;
+select * from gtt3 order by a;
+
+begin;
+insert into gtt3 values(3, 'test1');
+select * from gtt3 order by a;
+rollback;
+select * from gtt3 order by a;
+
+truncate gtt3;
+select * from gtt3 order by a;
+
+insert into gtt3 values(1, 'test1');
+select * from gtt3 order by a;
+
+begin;
+insert into gtt3 values(2, 'test2');
+select * from gtt3 order by a;
+truncate gtt3;
+select * from gtt3 order by a;
+insert into gtt3 values(3, 'test3');
+update gtt3 set a = 3 where b = 'test1';
+select * from gtt3 order by a;
+rollback;
+select * from gtt3 order by a;
+
+begin;
+select * from gtt3 order by a;
+truncate gtt3;
+insert into gtt3 values(5, 'test5');
+select * from gtt3 order by a;
+truncate gtt3;
+insert into gtt3 values(6, 'test6');
+commit;
+select * from gtt3 order by a;
+
+truncate gtt3;
+insert into gtt3 values(generate_series(1,100000), 'testing');
+select count(*) from gtt3;
+analyze gtt3;
+explain (COSTS FALSE) select * from gtt3 where a =300;
+
+insert into gtt_t_kenyon select generate_series(1,2000),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God',500);
+insert into gtt_t_kenyon select generate_series(1,2),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',2000);
+insert into gtt_t_kenyon select generate_series(3,4),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',4000);
+insert into gtt_t_kenyon select generate_series(5,6),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',5500);
+insert into gtt_t_kenyon select generate_series(1,2),repeat('kenyon here'||'^_^',2),repeat('^_^ Kenyon is not God,Remark here!!',10000);
+select relname, pg_relation_size(oid),pg_relation_size(reltoastrelid) from pg_class where relname = 'gtt_t_kenyon';
+select relname from pg_class where relname = 'gtt_t_kenyon' and reltoastrelid != 0;
+
+reset search_path;
+
diff --git a/src/test/regress/sql/gtt_prepare.sql b/src/test/regress/sql/gtt_prepare.sql
new file mode 100644
index 0000000..042d9e6
--- /dev/null
+++ b/src/test/regress/sql/gtt_prepare.sql
@@ -0,0 +1,15 @@
+
+CREATE SCHEMA IF NOT EXISTS gtt;
+
+set search_path=gtt,sys;
+
+create global temp table gtt1(a int primary key, b text);
+
+create global temp table gtt2(a int primary key, b text) on commit delete rows;
+
+create global temp table gtt3(a int primary key, b text) on commit PRESERVE rows;
+
+create global temp table gtt_t_kenyon(id int,vname varchar(48),remark text) on commit PRESERVE rows;
+
+reset search_path;
+

---

From: "曾文旌(义从)" <[email-masked]>
Date: Thu, 07 Nov 2019 17:30:27 +0800

> 2019年11月7日 上午12:08，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
> 1.  I have ported you patch to the latest Postgres version (my patch is attached).
> 2. You patch is supporting only B-Tree index for GTT. All other indexes (hash, gin, gist, brin,...) are not currently supported.
Currently I only support btree index.
I noticed that your patch supports more index types, which is where I'd like to work with you.

> Why do we need GTT at all? Mostly because we need to access temporary data in more than one backend. Otherwise we can just use normal table.
> If temp table is expected to be larger enough, so that we need to create index for it, then it is hard to believe that it will be needed only in one backend.
> 
> May be the assumption is that all indexes has to be created before GTT start to be used.
Yes, Currently, GTT's index is only supported and created in an empty table state, and other sessions are not using it.
There has two improvements pointer:
1 Index can create on GTT(A) when the GTT(A)  in the current session is not empty, requiring the GTT table to be empty in the other session.
Index_build needs to be done in the current session just like a normal table. This improvement is relatively easy.

2 Index can create on GTT(A)  when more than one session are using this GTT(A).
Because when I'm done creating an index of the GTT in this session and setting it to be an valid index, it's not true for the GTT in other sessions.
Indexes on gtt in other sessions require "rebuild_index" before using it. 
I don't have a better solution right now, maybe you have some suggestions.

> But right now this check is not working correctly in any case - if you insert some data into the table, then
> postgres=# insert into gtt values (generate_series(1,100000), generate_series(1,100000));
> INSERT 0 100000
> ERROR:  can not create index when have one or more backend attached this global temp table
> 
> <global_temporary_table_v1-pg13.patch>

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 7 Nov 2019 10:40:19 +0100

čt 7. 11. 2019 v 10:30 odesílatel 曾文旌(义从) <[email-masked]>
napsal:

> index for it, then it is hard to believe that it will be needed only in one
> backend.
> >
> Yes, Currently, GTT's index is only supported and created in an empty
> table state, and other sessions are not using it.
> 1 Index can create on GTT(A) when the GTT(A)  in the current session is
> not empty, requiring the GTT table to be empty in the other session.
> Index_build needs to be done in the current session just like a normal
> table. This improvement is relatively easy.
>
> Because when I'm done creating an index of the GTT in this session and
> setting it to be an valid index, it's not true for the GTT in other
> sessions.

I think so DDL operations can be implemented in some reduced form - so DDL
are active only for one session, and for other sessions are invisible.
Important is state of GTT object on session start.

For example ALTER TABLE DROP COLUMN can has very fatal impact on other
sessions. So I think the best of GTT can be pattern - the structure of GTT
table is immutable for any session that doesn't do DDL operations.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Thu, 07 Nov 2019 19:49:29 +0800

> 2019年11月7日 下午5:30，曾文旌(义从) <[email-masked]> 写道：
> 
> 
> 
>> 2019年11月7日 上午12:08，Konstantin Knizhnik <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> 
>> 
This part of the improvement has been completed.
New patch is attached.

>> ERROR:  can not create index when have one or more backend attached this global temp table
Index can create on GTT(A) when the GTT(A)  in the current session is not empty now.
But still requiring the GTT table to be empty in the other session.

Zeng Wenjing

---

From: "曾文旌(义从)" <[email-masked]>
Date: Thu, 07 Nov 2019 20:17:50 +0800

> 2019年11月7日 下午5:40，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> čt 7. 11. 2019 v 10:30 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
> I think so DDL operations can be implemented in some reduced form - so DDL are active only for one session, and for other sessions are invisible. Important is state of GTT object on session start. 
> 
> For example ALTER TABLE DROP COLUMN can has very fatal impact on other sessions. So I think the best of GTT can be pattern - the structure of GTT table is immutable for any session that doesn't do DDL operations.
Yes, Those ddl that need to rewrite data files will have this problem.
This is why I disabled alter GTT in the current version.
It can be improved, such as Alter GTT can also be allowed when only the current session is in use.
Users can also choose to kick off other sessions that are using gtt, then do alter GTT.
I provide a function(pg_gtt_attached_pid(relation, schema)) to query which session a GTT is being used by.

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 7 Nov 2019 13:29:36 +0100

čt 7. 11. 2019 v 13:17 odesílatel 曾文旌(义从) <[email-masked]>
napsal:

>> > If temp table is expected to be larger enough, so that we need to
>> create index for it, then it is hard to believe that it will be needed only
>> in one backend.
>> >

I think so it is acceptable solution for some first steps, but I cannot to
imagine so this behave can be good for production usage. But can be good
enough for some time.

Regards

Pavel

Users can also choose to kick off other sessions that are using gtt, then
> do alter GTT.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Thu, 7 Nov 2019 19:32:44 +0300

> I don't have a better solution right now, maybe you have some suggestions.
It is possible to create index on demand:

Buffer
_bt_getbuf(Relation rel, BlockNumber blkno, int access)
{
     Buffer        buf;

     if (blkno != P_NEW)
     {
         /* Read an existing block of the relation */
         buf = ReadBuffer(rel, blkno);
         /* Session temporary relation may be not yet initialized for 
this backend. */
         if (blkno == BTREE_METAPAGE && 
GlobalTempRelationPageIsNotInitialized(rel, BufferGetPage(buf)))
         {
             Relation heap = RelationIdGetRelation(rel->rd_index->indrelid);
             ReleaseBuffer(buf);
             DropRelFileNodeLocalBuffers(rel->rd_node, MAIN_FORKNUM, blkno);
             btbuild(heap, rel, BuildIndexInfo(rel));
             RelationClose(heap);
             buf = ReadBuffer(rel, blkno);
             LockBuffer(buf, access);
         }
         else
         {
             LockBuffer(buf, access);
             _bt_checkpage(rel, buf);
         }
     }
     ...

This code initializes B-Tree and load data in it when GTT index is 
access and is not initialized yet.
It looks a little bit hacker but it works.

I also wonder why you are keeping information about GTT in shared 
memory. Looks like the only information we really need to share is 
table's metadata.
But it is already shared though catalog. All other GTT related 
information is private to backend so I do not see reasons to place it in 
shared memory.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Fri, 08 Nov 2019 15:50:42 +0800

> 2019年11月8日 上午12:32，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
>         /* Session temporary relation may be not yet initialized for this backend. */
>         if (blkno == BTREE_METAPAGE && GlobalTempRelationPageIsNotInitialized(rel, BufferGetPage(buf)))
>         {
In my opinion, it is not a good idea to trigger a btbuild with a select or DML, the cost of which depends on the amount of data in the GTT.

> 
> 
> This code initializes B-Tree and load data in it when GTT index is access and is not initialized yet.
> I also wonder why you are keeping information about GTT in shared memory. Looks like the only information we really need to share is table's metadata.
> But it is already shared though catalog. All other GTT related information is private to backend so I do not see reasons to place it in shared memory.
The shared hash structure tracks which backend has initialized the GTT storage in order to implement the DDL of the GTT.
As for GTT, there is only one definition(include index on GTT), but each backend may have one data.
For the implementation of drop GTT, I assume that all data and definitions need to be deleted.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 8 Nov 2019 15:57:13 +0300

> In my opinion, it is not a good idea to trigger a btbuild with a select or DML, the cost of which depends on the amount of data in the GTT.
IMHO it is better than returning error.
Also index will be used only if cost of plan with index will be 
considered better than cost of plan without index. If you do not have 
index, then you have to scan the whole table.
Time of such scan is comparable with time of building index.

Yes, I agree that indexes for GTT are used to be created together with 
table itself before it is used by any application.
But if later DBA recognized that efficient execution of queries requires 
some more indexes,
it will be strange and dangerous to prevent him from adding such index 
until all clients which have accessed this table will drop their 
connections.
Also maintaining in shared memory information about attached backends 
seems to be overkill.

> The shared hash structure tracks which backend has initialized the GTT storage in order to implement the DDL of the GTT.
Sorry, I do not understand this argument.
DDL is performed on shared metadata present in global catalog.
Standard postgres invalidation mechanism is used to notify all backends 
about schema changes.
Why do we need to maintain some extra information in shared memory.
Can you give me example of DLL which does't work without such shared hash?

> For the implementation of drop GTT, I assume that all data and definitions need to be deleted.

Data of dropped GTT is removed on normal backend termination or cleaned 
up at server restart in case of abnormal shutdown (as it is done for 
local temp tables).
I have not used any shared control structures for GTT in my 
implementation and that is why I wonder why do you need it and what are 
the expected problems with my
implementation?

---

From: "曾文旌(义从)" <[email-masked]>
Date: Fri, 08 Nov 2019 23:06:17 +0800

My comments for global_private_temp-4.patch

good side:
1 Lots of  index type on GTT. I think we need support for all kinds of indexes.
2 serial column on GTT.
3 INHERITS GTT.
4 PARTITION GTT.

I didn't choose to support them in the first release, but you did.

Other side:
1 case: create global temp table gtt2(a int primary key, b text) on commit delete rows;
I think you've lost the meaning of the on commit delete rows clause.
After the GTT is created, the other sessions feel that this is an on commit PRESERVE rows GTT.

2 truncate gtt, mybe this is a bug in DropRelFileNodeBuffers.
GTT's local buffer is not released.
Case:
postgres=# insert into gtt2 values(1,'xx');
INSERT 0 1
postgres=# truncate gtt2;
TRUNCATE TABLE
postgres=# insert into gtt2 values(1,'xx');
ERROR:  unexpected data beyond EOF in block 0 of relation base/13579/t3_16384
HINT:  This has been seen to occur with buggy kernels; consider updating your system.

3  lock type of truncate GTT.
I don't think it's a good idea to hold a big lock with truncate GTT, because it only needs to process private data.

4 GTT's ddl Those ddl that need to rewrite data files may need attention.
We have discussed in the previous email. This is why I used shared hash to track the GTT file.

5 There will be problems with DDL that will change relfilenode. Such as cluster GTT ,vacuum full GTT.
A session completes vacuum full gtt(a), and other sessions will immediately start reading and writing new storage files and existing data is also lost.
I disable them in my current version.

6 drop GTT
I think drop GTT should clean up all storage files and definitions. How do you think?

7 MVCC visibility clog clean
GTT data visibility rules, like regular tables, so GTT also need clog.
We need to avoid the clog that GTT needs to be cleaned up. 
At the same time, GTT does not do autovacuum, and retaining "too old data" will cause wraparound data loss.
I have given a solution in my design.

Zeng Wenjing

> 2019年11月1日 下午11:15，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
>> Users doesn't do ANALYZE on temp tables in 99%. It's true. But second fact is so users has lot of problems. It's very similar to wrong statistics on persistent tables. When data are small, then it is not problem for users, although from my perspective it's not optimal. When data are not small, then the problem can be brutal. Temporary tables are not a exception. And users and developers are people - we know only about fatal problems. There are lot of unoptimized queries, but because the problem is not fatal, then it is not reason for report it. And lot of people has not any idea how fast the databases can be. The knowledges of  users and app developers are sad book.
>> 
>> Pavel
> 
> <global_private_temp-4.patch>

---

From: Konstantin Knizhnik <[email-masked]>
Date: Mon, 11 Nov 2019 18:19:21 +0300

> My comments for global_private_temp-4.patch

Thank you very much for inspecting my patch.
> commit PRESERVE rows GTT.

Yes, there was bug in my implementation of ON COMMIT DELETE ROWS for GTT.
It is fixed in global_private_temp-6.patch

> HINT:  This has been seen to occur with buggy kernels; consider 

Yes another bug, also fixed in new version of the patch.

> I don't think it's a good idea to hold a big lock with truncate GTT, 
> because it only needs to process private data.

Sorry, I do not understand which lock you are talking about.
I have not introduced any special locks for GTT.

> hash to track the GTT file.

You are right.
But instead of prohibiting ALTER TABLE at all for GTT, we can check
that there are no other backends using it.
I do not think that we should maintain some hash in shared memory to 
check it.
As far as ALTER TABLE is rare and slow operation in any case, we can 
just check presence of GTT files
created by other backends.
I have implemented this check in global_private_temp-6.patch

> A session completes vacuum full gtt(a), and other sessions will 
> immediately start reading and writing new storage files and existing 
> data is also lost.

Thank you for noticing it.
Autovacuum full should really be prohibited for GTT.

> I think drop GTT should clean up all storage files and definitions. 
Storage files will be cleaned in any case on backend termination.
Certainly if backend creates  and deletes huge number of GTT in the 
loop, it can cause space exhaustion.
But it seems to be very strange pattern of GTT usage.

> I have given a solution in my design.
But why do we need some special handling of visibility rules for GTT 
comparing with normal (local) temp tables?
Them are also not proceeded by autovacuum?

In principle, I have also implemented special visibility rules for GTT, 
but only for the case when them
are accessed at replica. And it is not included in this patch, because 
everybody think that access to GTT
replica should be considered in separate patch.

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 5 Jan 2020 21:06:03 +0100

Hi,

I think we need to do something with having two patches aiming to add
global temporary tables:

[1] https://commitfest.postgresql.org/26/2349/

[2] https://commitfest.postgresql.org/26/2233/

As a reviewer I have no idea which of the threads to look at - certainly
not without reading both threads, which I doubt anyone will really do.
The reviews and discussions are somewhat intermixed between those two
threads, which makes it even more confusing.

I think we should agree on a minimal patch combining the necessary/good
bits from the various patches, and terminate one of the threads (i.e.
mark it as rejected or RWF). And we need to do that now, otherwise
there's about 0% chance of getting this into v13.

In general, I agree with the sentiment Rober expressed in [1] - the
patch needs to be as small as possible, not adding "nice to have"
features (like support for parallel queries - I very much doubt just
using shared instead of local buffers is enough to make it work.)

regards

-- 
Tomas Vondra                  http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

---

From: "曾文旌(义从)" <[email-masked]>
Date: Mon, 06 Jan 2020 13:04:15 +0800

In the previous communication

1 we agreed on the general direction
1.1 gtt use local (private) buffer
1.2 no replica access in first version

2 We feel that gtt needs to maintain statistics, but there is no agreement on what it will be done.

3 Still no one commented on GTT's transaction information processing, they include
3.1 Should gtt's frozenxid need to be care?
3.2 gtt’s clog clean
3.3 How to deal with "too old" gtt data

I suggest we discuss further, reach an agreement, and merge the two patches to one.

Wenjing

> 2020年1月6日 上午4:06，Tomas Vondra <[email-masked]> 写道：
> 
> Hi,
>

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 6 Jan 2020 12:01:19 +0100

>1.2 no replica access in first version

OK, good.

>agreement on what it will be done.

I certainly agree GTT needs to maintain statistics, otherwise it'll lead
to poor query plans. AFAIK the current patch stores the info in a hash
table in a backend private memory, and I don't see how else to do that
(e.g. storing it in a catalog would cause catalog bloat).

FWIW this is a reasons why I think just using shared buffers (instead of
local ones) is not sufficient to support parallel queriesl as proposed
by Alexander. The workers would not know the stats, breaking planning of
queries in PARALLEL SAFE plpgsql functions etc.

>3.3 How to deal with "too old" gtt data

No idea what to do about this.

>I suggest we discuss further, reach an agreement, and merge the two patches to one.

OK, cool. Thanks for the clarification.

regards

---

From: Dean Rasheed <[email-masked]>
Date: Mon, 6 Jan 2020 12:17:43 +0000

On Mon, 6 Jan 2020 at 11:01, Tomas Vondra <[email-masked]> wrote:
> to poor query plans.

+1

> AFAIK the current patch stores the info in a hash

It sounds like it needs a pair of system GTTs to hold the table and
column statistics for other GTTs. One would probably have the same
columns as pg_statistic, and the other just the relevant columns from
pg_class. I can see it being useful for the user to be able to see
these stats, so perhaps they could be UNIONed into the existing stats
view.

Regards,
Dean

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 6 Jan 2020 13:52:34 +0100

>these stats, so perhaps they could be UNIONed into the existing stats

Hmmm, yeah. A "temporary catalog" (not sure if it can work exactly the
same as GTT) storing pg_statistics data for GTTs might work, I think. It
would not have the catalog bloat issue, which is good.

I still think we'd need to integrate this with the regular pg_statistic
catalogs somehow, so that people don't have to care about two things. I
mean, extensions like hypopg do use pg_statistic data to propose indexes
etc. and it would be nice if we don't make them more complicated.

Not sure why we'd need a temporary version of pg_class, though?

regards

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 6 Jan 2020 14:50:49 +0100

+1

Pavel

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 08 Jan 2020 15:03:09 +0800

> 2020年1月6日 下午8:17，Dean Rasheed <[email-masked]> 写道：
> 
The current patch provides several functions as extension(pg_gtt) for read gtt statistics. 
Next I can move them to the kernel and let the view pg_stats can see gtt’s statistics.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Thu, 9 Jan 2020 14:17:08 +0300

> I suggest we discuss further, reach an agreement, and merge the two patches to one.

I also hope that we should come to the common solution for GTT.
If we do not try to address parallel execution issues and access to temp 
tables at replicas (and I agreed
that it should be avoided in first version of the patch), then GTT patch 
becomes quite small.

The most complex and challenged task is to support GTT for all kind of 
indexes. Unfortunately I can not proposed some good universal solution 
for it.
Just patching all existed indexes implementation seems to be the only 
choice.

Statistic is another important case.
But once again I do not completely understand why we want to address all 
this issues with statistic in first version of the patch? It contradicts 
to the idea to make this patch as small as possible.
Also it seems to me that everybody agreed that users very rarely create 
indexes for temp tables and explicitly analyze them.
So I think GTT will be useful even with limited support of statistic. In 
my version statistics for GTT is provided by pushing correspondent 
information to backend's cache for pg_statistic table.
Also I provided pg_temp_statistic view for inspecting it by users. The 
idea to make pg_statistic a view which combines statistic of normal and 
temporary tables is overkill from my point of view.

I do not understand why do we need to maintain hash with some extra 
information for GTT in backends memory (as it was done in Wenjing patch).
Also idea to use create extension for accessing this information seems 
to be dubious.

-- 
Konstantin Knizhnik
Postgres Professional:http://www.postgrespro.com
The Russian Postgres Company

---

From: Konstantin Knizhnik <[email-masked]>
Date: Thu, 9 Jan 2020 18:07:46 +0300

> queries in PARALLEL SAFE plpgsql functions etc.

I do not think that "all or nothing" approach is so good for software 
development as for database transactions.
Yes, if we have function in PL/pgSQL which performs queries om temporary 
tables, then
parallel workers may build inefficient plan for this queries due to lack 
of statistics.
 From my point of view this is not a pitfall of GTT but result of lack 
of global plan cache in Postgres. And it should be fixed not at GTT level.

Also I never see real use cases with such functions, even in the systems 
which using hard temporary tables and stored procedures.
But there are many other real problems with temp tables  (except already 
mentioned in this thread).
In PgPro/EE we have fixes for some of them, for example:

1. Do not reserve space in the file for temp relations. Right now append 
of relation cause writing zero page to the disk by mdextend.
It cause useless disk IO for temp tables which in most cases fit in 
memory and should not be written at disk.

2. Implicitly perform analyze of temp table intermediately after storing 
data in it. Usually tables are analyzed by autovacuum in background.
But it doesn't work for temp tables which are not processes by 
autovacuum and are accessed immediately after filling them with data and 
lack of statistic  may cause
building very inefficient plan. We have online_analyze extension which 
force analyze of the table after appending some bulk of data to it.
It can be used for normal table but most of all it is useful for temp 
relations.

Unlike hypothetical example with parallel safe function working with 
temp tables,
this are real problems observed by some of our customers.
Them are applicable both to local and global temp tables and this is why 
I do not want to discuss them in context of GTT.

> No idea what to do about this.

I wonder what is the specific of GTT here?
The same problem takes place for normal (local) temp tables, doesn't it?

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 9 Jan 2020 17:30:37 +0100

On Thu, Jan 09, 2020 at 06:07:46PM +0300, Konstantin Knizhnik wrote:
>>>1 we agreed on the general direction 1.1 gtt use local (private)
>>>buffer 1.2 no replica access in first version
>>>
>>
>>OK, good.
>>
>>I certainly agree GTT needs to maintain statistics, otherwise it'll
>>lead to poor query plans. AFAIK the current patch stores the info in a
>>hash table in a backend private memory, and I don't see how else to do
>>that (e.g. storing it in a catalog would cause catalog bloat).
>>
>>FWIW this is a reasons why I think just using shared buffers (instead
>>of local ones) is not sufficient to support parallel queriesl as
>>proposed by Alexander. The workers would not know the stats, breaking
>>planning of queries in PARALLEL SAFE plpgsql functions etc.
>
>

Well, sure. I'm not saying we need to have a perfect solution in v1. I'm
saying if we have two choices:

(1) Use shared buffers even if it means the parallel query plan may be
     arbitrarily bad.

(2) Use private buffers, even if it means no parallel queries with temp
     tables.

Then I'm voting for (2) because it's less likely to break down. I can
imagine allowing parallel queries with GTT when there's no risk of
having to plan in the worker, but that's not there yet.

If we can come up with a reasonable solution for the parallel case, we
can enable it later.

>Yes, if we have function in PL/pgSQL which performs queries om 
>temporary tables, then
>parallel workers may build inefficient plan for this queries due to 
>lack of statistics.

IMHO that's a pretty awful deficiency, because it essentially means
users may need to disable parallelism for such queries. Which means
we'll get complaints from users, and we'll have to come up with some
sort of solution. I'd rather not be in that position.

>of global plan cache in Postgres. And it should be fixed not at GTT 

That doesn't give us free pass to just ignore the issue. Even if it
really was due to a lack of global plan cache, the fact is we don't have
that feature, so we have a problem. I mean, if you need infrastructure
that is not available, you either have to implement that infrastructure
or make it work properly without it.

>Also I never see real use cases with such functions, even in the 
>systems which using hard temporary tables and stored procedures.
>But there are many other real problems with temp tables  (except 
>already mentioned in this thread).

Oh, I'm sure there are pretty large plpgsql applications, and I'd be
surprised if at least some of those were not affected. And I'm sure
there are apps using UDF to do all sorts of stuff (e.g. I wonder if
PostGIS would have this issue - IIRC it's using SPI etc.).

The question is whether we should consider existing apps affected,
because they are using the regular temporary tables and not GTT. So
unless they switch to GTT there is no regression ...

But even in that case I don't think it's a good idea to accept this as
an acceptable limitation. I admit one of the reasons why I think that
may be that statistics and planning are my areas of interest, so I'm not
quite willing to accept incomplete stuff as OK.

>1. Do not reserve space in the file for temp relations. Right now 
>append of relation cause writing zero page to the disk by mdextend.
>2. Implicitly perform analyze of temp table intermediately after 
>storing data in it. Usually tables are analyzed by autovacuum in 
>background.
>autovacuum and are accessed immediately after filling them with data 
>and lack of statistic  may cause
>Them are applicable both to local and global temp tables and this is 
>why I do not want to discuss them in context of GTT.
>

I think those are both interesting issues worth fixing, but I don't
think it makes the issue discussed here less important.

>The same problem takes place for normal (local) temp tables, doesn't it?

Not sure. TBH I'm not sure I understand what the issue actually is.

regards

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 9 Jan 2020 17:48:29 +0100

On Thu, Jan 09, 2020 at 02:17:08PM +0300, Konstantin Knizhnik wrote:
>If we do not try to address parallel execution issues and access to 
>temp tables at replicas (and I agreed
>that it should be avoided in first version of the patch), then GTT 
>patch becomes quite small.
>

Well, that was kinda my goal - making the patch as small as possible by
eliminating bits that are contentious or where we don't know the
solution (like planning for parallel queries).

>Just patching all existed indexes implementation seems to be the only 

I haven't looked at the indexing issue closely, but IMO we need to
ensure that every session sees/uses only indexes on GTT that were
defined before the seesion started using the table.

Can't we track which indexes a particular session sees, somehow?

>But once again I do not completely understand why we want to address 
>all this issues with statistic in first version of the patch?

I think the question is which "issues with statistic" you mean. I'm sure
we can ignore some of them, e.g. the one with parallel workers not
having any stats (assuming we consider functions using GTT to be
parallel restricted).

>It contradicts to the idea to make this patch as small as possible.

Well, there's "making patch as small as possible" vs. "patch behaving
correctly" trade-off ;-)

>Also it seems to me that everybody agreed that users very rarely 
>create indexes for temp tables and explicitly analyze them.

I certainly *disagree* with this.

We often see temporary tables as a fix or misestimates in complex
queries, and/or as a replacement for CTEs with statistics/indexes. In
fact it's a pretty valuable tool when helping customers with complex
queries affected by poor estimates.

>So I think GTT will be useful even with limited support of statistic. 
>In my version statistics for GTT is provided by pushing correspondent 

I think someone pointed out pushing stuff directly into the cache is
rather problematic, but I don't recall the details.

>idea to make pg_statistic a view which combines statistic of normal 
>and temporary tables is overkill from my point of view.
>

I think the extension was more a PoC rather than a final solution.

regards

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 10 Jan 2020 11:47:42 +0300

On 09.01.2020 19:48, Tomas Vondra wrote:
>
>> The most complex and challenged task is to support GTT for all kind 
>> of indexes. Unfortunately I can not proposed some good universal 
>> solution for it.

Why? It contradicts with behavior of normal tables.
Assume that you have active clients and at some point of time DBA 
recognizes that them are spending to much time in scanning some GTT.
It cab create index for this GTT but if existed client will not be able 
to use this index, then we need somehow make this clients to restart 
their sessions?
In my patch I have implemented building indexes for GTT on demand: if 
accessed index on GTT is not yet initialized, then it is filled with 
local data.
> having any stats (assuming we consider functions using GTT to be

If we do not use shared buffers for GTT then parallel processing of GTT 
is not possible at all, so there is no problem with statistic for 
parallel workers.

> rather problematic, but I don't recall the details.
I have not encountered any problems, so if you can point me on what is 
wrong with this approach, I will think about alternative solution.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 10 Jan 2020 15:24:34 +0300

> Not sure. TBH I'm not sure I understand what the issue actually is. 

Just open session, create temporary table and insert some data in it.
Then in other session run 2^31 transactions (at my desktop it takes 
about 2 hours).
As far as temp tables are not proceeded by vacuum, database is stalled:

  ERROR:  database is not accepting commands to avoid wraparound data 
loss in database "postgres"

It seems to be quite dubious behavior and it is strange to me that 
nobody complains about it.
We discuss  many issues related with temp tables (statistic, parallel 
queries,...) which seems to be less critical.

But this problem is not specific to GTT - it can be reproduced with 
normal (local) temp tables.
This is why I wonder why do we need to solve it in GTT patch.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Sat, 11 Jan 2020 22:00:44 +0800

Hi all

This is the latest patch

The updates are as follows:
1. Support global temp Inherit table global temp partition table
2. Support serial column in GTT
3. Provide views pg_gtt_relstats pg_gtt_stats for GTT’s statistics
4. Provide view pg_gtt_attached_pids to manage GTT
5. Provide function pg_list_gtt_relfrozenxids() to manage GTT
6. Alter GTT or rename GTT is allowed under some conditions

Please give me feedback.

Wenjing

---

From: Pavel Stehule <[email-masked]>
Date: Sat, 11 Jan 2020 21:27:19 +0100

Hi

> 6. Alter GTT or rename GTT is allowed under some conditions

I tested the functionality

1. i think so "ON COMMIT PRESERVE ROWS" should be default mode (like local
temp tables).

I tested some simple scripts

test01.sql

CREATE TEMP TABLE foo(a int, b int);
INSERT INTO foo SELECT random()*100, random()*1000 FROM
generate_series(1,1000);
ANALYZE foo;
SELECT sum(a), sum(b) FROM foo;
DROP TABLE foo; -- simulate disconnect

after 100 sec, the table pg_attribute has 3.2MB
and 64 tps, 6446 transaction

test02.sql

INSERT INTO foo SELECT random()*100, random()*1000 FROM
generate_series(1,1000);
ANALYZE foo;
SELECT sum(a), sum(b) FROM foo;
DELETE FROM foo; -- simulate disconnect

after 100 sec, 1688 tps, 168830 transactions

So performance is absolutely different as we expected.

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 12 Jan 2020 02:14:01 +0100

>This is why I wonder why do we need to solve it in GTT patch.

Yeah, I think that's out of scope for GTT patch. Once we solve it for
plain temporary tables, we'll solve it for GTT too.

regards

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 12 Jan 2020 02:51:09 +0100

On Fri, Jan 10, 2020 at 11:47:42AM +0300, Konstantin Knizhnik wrote:
>>>The most complex and challenged task is to support GTT for all 
>>>kind of indexes. Unfortunately I can not proposed some good 
>>>universal solution for it.
>>>Just patching all existed indexes implementation seems to be the 
>>>only choice.
>>>
>>
>It cab create index for this GTT but if existed client will not be 
>able to use this index, then we need somehow make this clients to 
>restart their sessions?

Yes, I know the behavior would be different from behavior for regular
tables. And yes, it would not allow fixing slow queries in sessions
without interrupting those sessions.

I proposed just ignoring those new indexes because it seems much simpler
than alternative solutions that I can think of, and it's not like those
other solutions don't have other issues.

For example, I've looked at the "on demand" building as implemented in
global_private_temp-8.patch, I kinda doubt adding a bunch of index build
calls into various places in index code seems somewht suspicious.

* brinbuild is added to brinRevmapInitialize, which is meant to
   initialize state for scanning. It seems wrong to build the index we're
   scanning from this function (layering and all that).

* btbuild is called from _bt_getbuf. That seems a bit ... suspicious?

... and so on for other index types. Also, what about custom indexes
implemented in extensions? It seems a bit strange each of them has to
support this separately.

IMHO if this really is the right solution, we need to make it work for
existing indexes without having to tweak them individually. Why don't we
track a flag whether an index on GTT was initialized in a given session,
and if it was not then call the build function before calling any other
function from the index AM? 

But let's talk about other issues caused by "on demand" build. Imagine
you have 50 sessions, each using the same GTT with a GB of per-session
data. Now you create a new index on the GTT, which forces the sessions
to build it's "local" index. Those builds will use maintenance_work_mem
each, so 50 * m_w_m. I doubt that's expected/sensible.

So I suggest we start by just ignoring the *new* indexes, and improve
this in the future (by building the indexes on demand or whatever).

>>>But once again I do not completely understand why we want to 
>>>address all this issues with statistic in first version of the 
>>>patch?
>>
>If we do not use shared buffers for GTT then parallel processing of 
>GTT is not possible at all, so there is no problem with statistic for 
>parallel workers.
>

Right.

>wrong with this approach, I will think about alternative solution.

I meant this comment by Robert:

https://www.postgresql.org/message-id/CA%2BTgmoZFWaND4PpT_CJbeu6VZGZKi2rrTuSTL-Ykd97fexTN-w%40mail.gmail.com

regards

---

From: Konstantin Knizhnik <[email-masked]>
Date: Mon, 13 Jan 2020 11:08:40 +0300

> other solutions don't have other issues.

Quit opposite: prohibiting sessions to see indexes created before 
session start to use GTT requires more efforts. We need to somehow 
maintain and check GTT first access time.

> calls into various places in index code seems somewht suspicious.

We in any case has to initialize GTT indexes on demand even if we 
prohibit usages of indexes created after first access by session to GTT.
So the difference is only in one thing: should we just initialize empty 
index or populate it with local data (if rules for index usability are 
the same for GTT as for normal tables).
 From implementation point of view there is no big difference. Actually 
building index in standard way is even simpler than constructing empty 
index. Originally I have implemented
first approach (I just forgot to consider case when GTT was already user 
by a session). Then I rewrited it using second approach and patch even 
became simpler.

> * btbuild is called from _bt_getbuf. That seems a bit ... suspicious?

As I already mentioned - support of indexes for GTT is one of the most 
challenged things in my patch.
I didn't find good and universal solution. So I agreed that call of 
btbuild from _bt_getbuf may be considered as suspicious.
I will be pleased if you or sombody else can propose better elternative 
and not only for B-Tree, but for all other indexes.

But as I already wrote above, prohibiting session to used indexes 
created after first access to GTT doesn't solve the problem.
For normal tables (and for local temp tables) indexes are initialized at 
the time of their creation.
With GTT it doesn't work, because each session has its own local data of 
GTT.
We should either initialize/build index on demand (when it is first 
accessed), either at the moment of session start initialize indexes for 
all existed GTTs.
Last options seem to be much worser from my point of view: there may me 
huge number of GTT and session may not need to access GTT at all.
> implemented in extensions? It seems a bit strange each of them has to

I have already complained about it: my patch supports GTT for all 
built-in indexes, but custom indexes has to handle it themselves.
Looks like to provide some generic solution we need to extend index API, 
providing two diffrent operations: creation and initialization.
But extending index API is very critical change... And also it doesn't 
solve the problem with all existed extensions: them in any case have
to be rewritten to implement new API version in order to support GTT.
> each, so 50 * m_w_m. I doubt that's expected/sensible.

I do not see principle difference here with scenario when 50 sessions 
create (local) temp table,
populate it with GB of data and create index for it.

> this in the future (by building the indexes on demand or whatever).

Sorry, but still do not agree with this suggestions:
- it doesn't simplify things
- it makes behavior of GTT incompatible with normal tables.
- it doesn't prevent some bad or unexpected behavior which can't be 
currently reproduced with normal (local) temp tables.

>> I have not encountered any problems, so if you can point me on what 
>> is wrong with this approach, I will think about alternative solution.
>>
>
"if any code tried to access the statistics directly from the table, 
rather than via the caches".

Currently optimizer is accessing statistic though caches. So this 
approach works. If somebody will rewrite optimizer or provide own custom 
optimizer in extension which access statistic directly
then it we really be a problem. But I wonder why bypassing catalog cache 
may be needed.

Moreover, if we implement alternative solution - for example make 
pg_statistic a view which combines results for normal tables and GTT, 
then existed optimizer has to be rewritten
because it can not access statistic in the way it is doing now. And 
there will be all problem with all existed extensions which are 
accessing statistic in most natural way - through system cache.

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 13 Jan 2020 17:32:53 +0100

On Mon, Jan 13, 2020 at 11:08:40AM +0300, Konstantin Knizhnik wrote:
>>>>>Just patching all existed indexes implementation seems to be 
>>>>>the only choice.
>>>>>
>>>>
>>>In my patch I have implemented building indexes for GTT on demand: 
>>>if accessed index on GTT is not yet initialized, then it is filled 
>>>with local data.
>>

Hmmm, OK. I'd expect such check to be much simpler than the on-demand
index building, but I admit I haven't tried implementing either of those
options.

>So the difference is only in one thing: should we just initialize 
>empty index or populate it with local data (if rules for index 
>usability are the same for GTT as for normal tables).
>first approach (I just forgot to consider case when GTT was already 
>user by a session). Then I rewrited it using second approach and patch 
>even became simpler.
>
>>
>I will be pleased if you or sombody else can propose better 
>elternative and not only for B-Tree, but for all other indexes.
>
>For normal tables (and for local temp tables) indexes are initialized 
>at the time of their creation.
>With GTT it doesn't work, because each session has its own local data 
>of GTT.
>accessed), either at the moment of session start initialize indexes 
>for all existed GTTs.
>Last options seem to be much worser from my point of view: there may 
>me huge number of GTT and session may not need to access GTT at all.
>>
>Looks like to provide some generic solution we need to extend index 
>API, providing two diffrent operations: creation and initialization.

Why not to allow creating only indexes implementing this new API method
(on GTT)?

>populate it with GB of data and create index for it.

I'd say the high memory consumption is pretty significant.

>>>I have not encountered any problems, so if you can point me on 
>>>what is wrong with this approach, I will think about alternative 
>>>solution.
>>>
>>
>approach works. If somebody will rewrite optimizer or provide own 
>custom optimizer in extension which access statistic directly
>then it we really be a problem. But I wonder why bypassing catalog 
>cache may be needed.
>

I don't know, but it seems extensions like hypopg do it.

>accessing statistic in most natural way - through system cache.

Perhaps. I don't know enough about this part of the code to have a
strong opinion.

regards

---

From: Julien Rouhaud <[email-masked]>
Date: Mon, 13 Jan 2020 21:12:38 +0100

> I don't know, but it seems extensions like hypopg do it.

AFAIR, hypopg only opens pg_statistic to use its tupledesc when creating
statistics on hypothetical partitions, but it should otherwise never reads or
need plain pg_statistic rows.

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 13 Jan 2020 22:03:01 +0100

>need plain pg_statistic rows.

Ah, OK! Thanks for the clarification. I knew it does something with the
catalog, didn't realize it only gets the descriptor.

regards

---

From: "曾文旌(义从)" <[email-masked]>
Date: Tue, 14 Jan 2020 21:09:34 +0800

Thank you for review my patch.

> 2020年1月12日 上午4:27，Pavel Stehule <[email-masked]> 写道：
> 
> Hi
> 
> so 11. 1. 2020 v 15:00 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> Hi all
> 
> This is the latest patch
> 
> 1. i think so "ON COMMIT PRESERVE ROWS" should be default mode (like local temp tables).
makes sense, I will fix it.

> INSERT INTO foo SELECT random()*100, random()*1000 FROM generate_series(1,1000);
> ANALYZE foo;
> INSERT INTO foo SELECT random()*100, random()*1000 FROM generate_series(1,1000);
> ANALYZE foo;
> From my perspective, this functionality is great.
Yes, frequent ddl causes catalog bloat, GTT avoids this problem.

> 
> Todo:
> 
> pg_table_size function doesn't work
Do you mean that function pg_table_size() need get the storage space used by the one GTT in the entire db(include all session) .

> 
> Regards
> 
> Pavel
> 
> 
> Wenjing
> 
> 
> 
> 
> 
>> 2020年1月6日 上午4:06，Tomas Vondra <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> Hi,
>> 
>> [1] https://commitfest.postgresql.org/26/2349/ <https://commitfest.postgresql.org/26/2349/>
>> 
>> [2] https://commitfest.postgresql.org/26/2233/ <https://commitfest.postgresql.org/26/2233/>
>> 
>> Tomas Vondra                  http://www.2ndQuadrant.com <http://www.2ndquadrant.com/>

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 14 Jan 2020 14:20:17 +0100

út 14. 1. 2020 v 14:09 odesílatel 曾文旌(义从) <[email-masked]>
napsal:

> Do you mean that function pg_table_size() need get the storage space used
> by the one GTT in the entire db(include all session) .
>

It's question how much GTT tables should be similar to classic tables. But
the reporting in psql should to work \dt+, \l+, \di+

>> <http://www.2ndquadrant.com/>

---

From: "曾文旌(义从)" <[email-masked]>
Date: Tue, 14 Jan 2020 22:15:11 +0800

> 2020年1月12日 上午9:14，Tomas Vondra <[email-masked]> 写道：
> 
>> Then in other session run 2^31 transactions (at my desktop it takes about 2 hours).
>>  ERROR:  database is not accepting commands to avoid wraparound data loss in database "postgres"
>> 
>> It seems to be quite dubious behavior and it is strange to me that nobody complains about it.
>> We discuss  many issues related with temp tables (statistic, parallel queries,...) which seems to be less critical.
>> 
>> But this problem is not specific to GTT - it can be reproduced with normal (local) temp tables.
1. The core problem is that the data contains transaction information (xid), which needs to be vacuum(freeze) regularly to avoid running out of xid.
The autovacuum supports vacuum regular table but local temp does not. autovacuum also does not support GTT.

2. However, the difference between the local temp table and the global temp table(GTT) is that
a) For local temp table: one table hava one piece of data. the frozenxid of one local temp table is store in the catalog(pg_class). 
b) For global temp table: each session has a separate copy of data, one GTT may contain maxbackend frozenxid.
and I don't think it's a good idea to keep frozenxid of GTT in the catalog(pg_class). 
It becomes a question: how to handle GTT transaction information?

I agree that problem 1 should be completely solved by a some feature, such as local transactions. It is definitely not included in the GTT patch.

But, I think we need to ensure the durability of GTT data. For example, data in GTT cannot be lost due to the clog being cleaned up. It belongs to problem 2.

Wenjing

---

From: "曾文旌(义从)" <[email-masked]>
Date: Tue, 14 Jan 2020 22:16:12 +0800

> 2020年1月14日 下午9:20，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> út 14. 1. 2020 v 14:09 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
>> 2020年1月12日 上午4:27，Pavel Stehule <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> Hi
>> 
> It's question how much GTT tables should be similar to classic tables. But the reporting in psql should to work \dt+, \l+, \di+
Get it, I will fix it.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 15 Jan 2020 21:10:25 +0800

> 2020年1月13日 下午4:08，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
>>>>> The most complex and challenged task is to support GTT for all kind of indexes. Unfortunately I can not proposed some good universal solution for it.
>>>>> Just patching all existed indexes implementation seems to be the only choice.
>>>>> 
>>>> 
>>> Assume that you have active clients and at some point of time DBA recognizes that them are spending to much time in scanning some GTT.
>>> It cab create index for this GTT but if existed client will not be able to use this index, then we need somehow make this clients to restart their sessions?
>>> In my patch I have implemented building indexes for GTT on demand: if accessed index on GTT is not yet initialized, then it is filled with local data.
>> 
> Quit opposite: prohibiting sessions to see indexes created before session start to use GTT requires more efforts. We need to somehow maintain and check GTT first access time.
> 
>> 
> We in any case has to initialize GTT indexes on demand even if we prohibit usages of indexes created after first access by session to GTT.
> So the difference is only in one thing: should we just initialize empty index or populate it with local data (if rules for index usability are the same for GTT as for normal tables).
> From implementation point of view there is no big difference. Actually building index in standard way is even simpler than constructing empty index. Originally I have implemented
> first approach (I just forgot to consider case when GTT was already user by a session). Then I rewrited it using second approach and patch even became simpler.
> 
>> 
> As I already mentioned - support of indexes for GTT is one of the most challenged things in my patch.
> I didn't find good and universal solution. So I agreed that call of btbuild from _bt_getbuf may be considered as suspicious.
> I will be pleased if you or sombody else can propose better elternative and not only for B-Tree, but for all other indexes.
> 
> But as I already wrote above, prohibiting session to used indexes created after first access to GTT doesn't solve the problem.
> For normal tables (and for local temp tables) indexes are initialized at the time of their creation.
> With GTT it doesn't work, because each session has its own local data of GTT.
> We should either initialize/build index on demand (when it is first accessed), either at the moment of session start initialize indexes for all existed GTTs.
> Last options seem to be much worser from my point of view: there may me huge number of GTT and session may not need to access GTT at all.
>> 
> I have already complained about it: my patch supports GTT for all built-in indexes, but custom indexes has to handle it themselves.
> Looks like to provide some generic solution we need to extend index API, providing two diffrent operations: creation and initialization.
> But extending index API is very critical change... And also it doesn't solve the problem with all existed extensions: them in any case have
> I do not see principle difference here with scenario when 50 sessions create (local) temp table,
I think the problem is that when one session completes the creation of the index on GTT,
it will trigger the other sessions build own local index of GTT in a centralized time.
This will consume a lot of hardware resources (cpu io memory) in a short time, 
and even the database service becomes slow, because 50 sessions are building index.
I think this is not what we expected.

> - it doesn't prevent some bad or unexpected behavior which can't be currently reproduced with normal (local) temp tables.
From a user perspective, this proposal is reasonable.
From an implementation perspective, the same GTT index needs to maintain different states (valid or invalid) in different sessions, 
which seems difficult to do in the current framework.

So in my first version, I chose to complete all index creation before using GTT.
I think this will satisfy most use cases.

>>> I have not encountered any problems, so if you can point me on what is wrong with this approach, I will think about alternative solution.
>>> 
>> 
> "if any code tried to access the statistics directly from the table, rather than via the caches".
> 
> Currently optimizer is accessing statistic though caches. So this approach works. If somebody will rewrite optimizer or provide own custom optimizer in extension which access statistic directly
> then it we really be a problem. But I wonder why bypassing catalog cache may be needed.
> 
> Moreover, if we implement alternative solution - for example make pg_statistic a view which combines results for normal tables and GTT, then existed optimizer has to be rewritten
> because it can not access statistic in the way it is doing now. And there will be all problem with all existed extensions which are accessing statistic in most natural way - through system cache.
> 
> 
> 
> -- 
> Konstantin Knizhnik

---

From: Konstantin Knizhnik <[email-masked]>
Date: Thu, 16 Jan 2020 10:23:33 +0300

> I think this is not what we expected.

First of all creating index for GTT ni one session doesn't immediately 
initiate building indexes in all other sessions.
Indexes are built on demand. If session is not using this GTT any more, 
then index for it will not build at all.
And if GTT is really are actively used by all sessions, then building 
index and using it for constructing optimal execution plan is better,
then continue to  use sequential scan and read all GTT data from the disk.

And as I already mentioned I do not see some principle difference in 
aspect of resource consumptions comparing with current usage of local 
temp tables.
If we have have many sessions, each creating temp table, populating it 
with data and building index for it, then we will
observe the same CPU utilization and memory resource consumption as in 
case of using GTT and creating index for it.

Sorry, but I still not convinced by your and Tomas arguments.
Yes, building GTT index may cause high memory consumption 
(maintenance_work_mem * n_backends).
But such consumption can be  observed also without GTT and it has to be 
taken in account when choosing value for maintenance_work_mem.
But from my point of view it is much more important to make behavior of 
GTT as much compatible with normal tables as possible.
Also from database administration point of view, necessity to restart 
sessions to make then use new indexes seems to be very strange and 
inconvenient.
Alternatively DBA can address the problem with high memory consumption 
by adjusting maintenance_work_mem, so this solution is more flexible.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Mon, 20 Jan 2020 01:04:38 +0800

> It's question how much GTT tables should be similar to classic tables. But the reporting in psql should to work \dt+, \l+, \di+

I have fixed this problem.

Please let me know where I need to improve.

Thanks

Wenjing

---

From: Erik Rijkers <[email-masked]>
Date: Sun, 19 Jan 2020 18:32:58 +0100

On 2020-01-19 18:04, 曾文旌(义从) wrote:
>> út 14. 1. 2020 v 14:09 odesílatel 曾文旌(义从) <[email-masked] 
>> <mailto:[email-masked]>> napsal:

>> [global_temporary_table_v4-pg13.patch ]

Hi,

This patch doesn't quiet apply for me:

patching file src/backend/access/common/reloptions.c
patching file src/backend/access/gist/gistutil.c
patching file src/backend/access/hash/hash.c
Hunk #1 succeeded at 149 (offset 3 lines).
patching file src/backend/access/heap/heapam_handler.c
patching file src/backend/access/heap/vacuumlazy.c
patching file src/backend/access/nbtree/nbtpage.c
patching file src/backend/access/table/tableam.c
patching file src/backend/access/transam/xlog.c
patching file src/backend/catalog/Makefile
Hunk #1 FAILED at 44.
1 out of 1 hunk FAILED -- saving rejects to file 
src/backend/catalog/Makefile.rej
[...]
    (The rest applies without errors)

src/backend/catalog/Makefile.rej contains:

------------------------
--- src/backend/catalog/Makefile
+++ src/backend/catalog/Makefile
@@ -44,6 +44,8 @@ OBJS = \
  	storage.o \
  	toasting.o

+OBJS += storage_gtt.o
+

------------------------

Can you have a look?

thanks,

Erik Rijkers

---

From: "曾文旌(义从)" <[email-masked]>
Date: Tue, 21 Jan 2020 00:27:17 +0800

> 2020年1月20日 上午1:32，Erik Rijkers <[email-masked]> 写道：
> 
> 1 out of 1 hunk FAILED -- saving rejects to file src/backend/catalog/Makefile.rej
> [...]
I updated the code and remade the patch.
Please give me feedback if you have any more questions.

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 21 Jan 2020 06:43:32 +0100

Hi

I have a free time this evening, so I will check this patch

I have a one question

Isbn't possible to fill the rd_rel structure too, so this branching can be
reduced?

Regards

Pavel

---

From: "曾文旌(义从)" <[email-masked]>
Date: Tue, 21 Jan 2020 16:45:53 +0800

> 1. i think so "ON COMMIT PRESERVE ROWS" should be default mode (like local temp tables).

ON COMMIT PRESERVE ROWS is default mode now.

Wenjing

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 21 Jan 2020 19:51:17 +0100

> ON COMMIT PRESERVE ROWS is default mode now.

Thank you

* I tried to create global temp table with index. When I tried to drop this
table (and this table was used by second instance), then I got error message

postgres=# drop table foo;
ERROR:  can not drop index when other backend attached this global temp
table

It is expected, but it is not too much user friendly. Is better to check if
you can drop table, then lock it, and then drop all objects.

* tab complete can be nice for CREATE GLOBAL TEMP table

\dt+ \di+ doesn't work correctly, or maybe I don't understand to the
implementation.

I see same size in all sessions. Global temp tables shares same files?

Regards

Pavel

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 22 Jan 2020 13:29:50 +0800

> 2020年1月21日 下午1:43，Pavel Stehule <[email-masked]> 写道：
> 
> Hi
> 
> Isbn't possible to fill the rd_rel structure too, so this branching can be reduced?
I'll make some improvements to optimize this part of the code.

> 
> Regards
> 
> Pavel
> 
> po 20. 1. 2020 v 17:27 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
> > 2020年1月20日 上午1:32，Erik Rijkers <[email-masked] <mailto:[email-masked]>> 写道：
> > 
> >>> 2020年1月14日 下午9:20，Pavel Stehule <[email-masked] <mailto:[email-masked]>> 写道：
> >>> út 14. 1. 2020 v 14:09 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]> <mailto:[email-masked] <mailto:[email-masked]>>> napsal:
> > 
> > include $(top_srcdir)/src/backend/common.mk <http://common.mk/>
> > ------------------------
> > 
> > Can you have a look?

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 22 Jan 2020 14:16:10 +0800

> 2020年1月22日 上午2:51，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> út 21. 1. 2020 v 9:46 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
> * I tried to create global temp table with index. When I tried to drop this table (and this table was used by second instance), then I got error message
> 
> ERROR:  can not drop index when other backend attached this global temp table
> 
> It is expected, but it is not too much user friendly. Is better to check if you can drop table, then lock it, and then drop all objects.
I don't understand what needs to be improved. Could you describe it in detail?

> * tab complete can be nice for CREATE GLOBAL TEMP table
Yes, I will improve it.
> 
> \dt+ \di+ doesn't work correctly, or maybe I don't understand to the implementation.
> 

postgres=# create table t(a int primary key);
CREATE TABLE
postgres=# create global temp table gt(a int primary key);
CREATE TABLE
postgres=# insert into t values(generate_series(1,10000));
INSERT 0 10000
postgres=# insert into gt values(generate_series(1,10000));
INSERT 0 10000

postgres=# \dt+
                            List of relations
 Schema | Name | Type  |    Owner    | Persistence |  Size  | Description 
--------+------+-------+-------------+-------------+--------+-------------
 public | gt   | table | wenjing.zwj | session     | 384 kB | 
 public | t    | table | wenjing.zwj | permanent   | 384 kB | 
(2 rows)

postgres=# \di+
                                  List of relations
 Schema |  Name   | Type  |    Owner    | Table | Persistence |  Size  | Description 
--------+---------+-------+-------------+-------+-------------+--------+-------------
 public | gt_pkey | index | wenjing.zwj | gt    | session     | 240 kB | 
 public | t_pkey  | index | wenjing.zwj | t     | permanent   | 240 kB | 
(2 rows)

> I see same size in all sessions. Global temp tables shares same files?
No, they use their own files.
But \dt+ \di+ counts the total file sizes in all sessions for each GTT.

Wenjing

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 22 Jan 2020 07:31:00 +0100

st 22. 1. 2020 v 7:16 odesílatel 曾文旌(义从) <[email-masked]>
napsal:

> * I tried to create global temp table with index. When I tried to drop
> this table (and this table was used by second instance), then I got error
> message
>
> It is expected, but it is not too much user friendly. Is better to check
> if you can drop table, then lock it, and then drop all objects.
>

the error messages should be some like

can not drop table when other backend attached this global temp table.

It is little bit messy, when you try to drop table and you got message
about index

>  Schema | Name | Type  |    Owner    | Persistence |  Size  | Description

I think so it is wrong. The data are independent and the sizes should be
independent too

---

From: "曾文旌(义从)" <[email-masked]>
Date: Fri, 24 Jan 2020 00:22:31 +0800

> 2020年1月22日 下午2:31，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> st 22. 1. 2020 v 7:16 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
>> 2020年1月22日 上午2:51，Pavel Stehule <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> 
>> 
> It is little bit messy, when you try to drop table and you got message about index
It has been repaired in global_temporary_table_v7-pg13.patch

>> * tab complete can be nice for CREATE GLOBAL TEMP table
It has been repaired in global_temporary_table_v7-pg13.patch

> I think so it is wrong. The data are independent and the sizes should be independent too
It has been repaired in global_temporary_table_v7-pg13.patch.

Wenjing

---

From: "曾文旌(义从)" <[email-masked]>
Date: Fri, 24 Jan 2020 00:28:05 +0800

> 2020年1月22日 下午1:29，曾文旌(义从) <[email-masked]> 写道：
> 
> 
> 
>> 2020年1月21日 下午1:43，Pavel Stehule <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> Hi
>> 
I'm trying to improve this part of the implementation in global_temporary_table_v7-pg13.patch
Please check my patch and give me feedback.

Thanks

Wenjing

---

From: Pavel Stehule <[email-masked]>
Date: Thu, 23 Jan 2020 18:21:42 +0100

> Please check my patch and give me feedback.
It is looking better, still there are some strange things (I didn't tested
functionality yet)

@@ -3313,6 +3336,10 @@ RelationBuildLocalRelation(const char *relname,

+ rel->rd_islocaltemp = true;  <<<<<<< if this is valid, then the name of
field "rd_islocaltemp" is not probably best

regards

Pavel

>> >>> út 14. 1. 2020 v 14:09 odesílatel 曾文旌(义从) <
>> [email-masked] <mailto:[email-masked]>> napsal:
>> >

---

From: Robert Haas <[email-masked]>
Date: Thu, 23 Jan 2020 15:47:10 -0500

> other solutions don't have other issues.

+1.

> calls into various places in index code seems somewht suspicious.

+1. I can't imagine that's a safe or sane thing to do.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 24 Jan 2020 11:20:09 +0300

> Please check my patch and give me feedback.

Below is my short review of the patch:

+    /*
+            RELOPT_KIND_HEAP | RELOPT_KIND_PARTITIONED,

The comment seems to be confusing: it says about AccessExclusiveLock but 
actually uses ShareUpdateExclusiveLock.

- Assert(TransactionIdIsNormal(onerel->rd_rel->relfrozenxid));
-    Assert(MultiXactIdIsValid(onerel->rd_rel->relminmxid));
+    Assert((RELATION_IS_GLOBAL_TEMP(onerel) && 
onerel->rd_rel->relfrozenxid == InvalidTransactionId) ||
+        (!RELATION_IS_GLOBAL_TEMP(onerel) && 
TransactionIdIsNormal(onerel->rd_rel->relfrozenxid)));
+    Assert((RELATION_IS_GLOBAL_TEMP(onerel) && 
onerel->rd_rel->relminmxid == InvalidMultiXactId) ||
+        (!RELATION_IS_GLOBAL_TEMP(onerel) && 
MultiXactIdIsValid(onerel->rd_rel->relminmxid)));

It is actually equivalent to:

Assert(RELATION_IS_GLOBAL_TEMP(onerel) ^ 
TransactionIdIsNormal(onerel->rd_rel->relfrozenxid);
Assert(RELATION_IS_GLOBAL_TEMP(onerel) ^ 
MultiXactIdIsValid(onerel->rd_rel->relminmxid));

I wonder why do we need some special check for GTT here.
 From my point of view cleanup at startup of local storage of temp 
tables should be performed in the same way for local and global temp tables.

Why do we need to do it for GTT?
Did you check that there will be no problems with GTT in case of XID 
wraparound?
Right now if you create temp table and keep session open, then it will 
block XID wraparound.

+    /* We allow to drop global temp table only this session use it */
+    if (RELATION_IS_GLOBAL_TEMP(rel))
+    {
+        if (is_other_backend_use_gtt(rel->rd_node))
+            elog(ERROR, "can not drop relation when other backend 
attached this global temp table");
+    }
+

Here we once again introduce incompatibility with normal (permanent) tables.
Assume that DBA or programmer need to change format of GTT. But there 
are some active sessions which have used this GTT sometime in the past.
We will not be able to drop this GTT until all this sessions are terminated.
I do not think that it is acceptable behaviour.

What are the reasons of using RowExclusiveLock for GTT instead of 
AccessExclusiveLock?
Yes, GTT data is access only by one backend so no locking here seems to 
be needed at all.
But I wonder what are the motivations/benefits of using weaker lock 
level here?
There should be no conflicts in any case...

+        /* We allow to create index on global temp table only this 
session use it */
+        if (is_other_backend_use_gtt(heapRelation->rd_node))
+            elog(ERROR, "can not create index when have other backend 
attached this global temp table");
+

The same argument as in case of dropping GTT: I do not think that 
prohibiting DLL operations on GTT used by more than one backend is bad idea.

+                 errmsg("referenced relation \"%s\" is not a global 
temp table",

Why do we need to prohibit foreign key constraint on GTT?

+    /*
+                (errmsg("global temp table oldest FrozenXid is far in 
the past"),
+                 errhint("please truncate them or kill those sessions 
that use them.")));

As far as I understand, content of GTT will never be processes by 
autovacuum.
So who will update frozenxid of GTT?
I see that up_gtt_relstats is invoked when:
- index is created on GTT
- GTT is truncated
- GTT is vacuumed
So unless GTT is explicitly vacuumed by user, its GTT is and them will 
not be taken in account
when computing new frozen xid value. Autovacumm will produce this 
warnings (which will ton be visible by end user and only appended to the 
log).
And at some moment of time wrap around happen and if there still some 
old active GTT, we will get incorrect results.

-- 

Konstantin Knizhnik

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 24 Jan 2020 11:39:40 +0300

> +1. I can't imagine that's a safe or sane thing to do.

As far as you know there are two versions of GTT implementations now.
And we are going to merge them into single patch.
But there are some principle question concerning provided functionality 
which has to be be discussed:
should we prohibit DDL on GTT if there are more than one sessions using 
it. It includes creation/dropping indexes, dropping table, altering table...

If the answer is "yes", then the question whether to populate new 
indexes with data is no relevant at all, because such situation will not 
be possible.
But in this case we will get incompatible behavior with normal 
(permanent) tables and it seems to be very inconvenient from DBA point 
of view:
it will be necessary to enforce all clients to close their sessions to 
perform some DDL manipulations with GTT.
Some DDLs will be very difficult to implement if GTT is used by more 
than one backend, for example altering table schema.

My current solution is to allow creation/droping index on GTT and 
dropping table itself, while prohibit alter schema at all for GTT.
Wenjing's approach is to prohibit any DDL if GTT is used by more than 
one backend.

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 24 Jan 2020 10:09:32 +0100

> Wenjing's approach is to prohibit any DDL if GTT is used by more than

When I create index on GTT in one session, then I don't expect creating
same index in all other sessions that uses same GTT.

But I can imagine to creating index on GTT enforces index in current
session, and for other sessions this index will be invalid to end of
session.

Regards

Pavel

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 24 Jan 2020 12:43:10 +0300

On 24.01.2020 12:09, Pavel Stehule wrote:
>
>
> pá 24. 1. 2020 v 9:39 odesílatel Konstantin Knizhnik 
>     > <[email-masked]
>     <mailto:[email-masked]>> wrote:
> When I create index on GTT in one session, then I don't expect 
> creating same index in all other sessions that uses same GTT.
>

So there are three possible alternatives:

1. Prohibit index creation of GTT when it used by more than once session.
2. Create index and populate them with data in all sessions using this GTT.
3. Create index only in current session and do not allow to use it in 
all other sessions already using this GTT (but definitely allow to use 
it in new sessions).

1 is Wenjing's approach, 2 - is my approach, 3 - is your suggestion :)

I can construct the following table with pro/cons of each approach:

Approach
	Compatibility with normal table
	User (DBA) friendly
	Complexity of implementation
	Consistency
1
	-
	1: requires restart of all sessions to perform operation
	2: requires global cache of GTT
	3/: /no man, no problem
2
	+
	3: if index is created then it is actually needed, isn't it? 	1: use 
existed functionality to create index
	2: if alter schema is prohibited
3
	-
	2: requires restart of all sessions to use created index
	3: requires some mechanism for prohibiting index created after first 
session access to GTT
	1: can perform DDL but do no see effect of it

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 24 Jan 2020 13:15:13 +0100

pá 24. 1. 2020 v 10:43 odesílatel Konstantin Knizhnik <
[email-masked]> napsal:

> 3. Create index only in current session and do not allow to use it in all
> other sessions already using this GTT (but definitely allow to use it in
> new sessions).
>
> 3*: *no man, no problem
You will see a effect of DDL in current session (where you did the change),
all other sessions should to live without any any change do reconnect or to
RESET connect

I don't like 2 - when I do index on global temp table, I don't would to
wait on indexing on all other sessions. These operations should be
maximally independent.

Regards

Pavel

---

From: Konstantin Knizhnik <[email-masked]>
Date: Fri, 24 Jan 2020 16:17:17 +0300

On 24.01.2020 15:15, Pavel Stehule wrote:
> You will see a effect of DDL in current session (where you did the 
> change), all other sessions should to live without any any change do 
> reconnect or to RESET connect
>
Why? I found this requirement quit unnatural and contradicting to the 
behavior of normal tables.
Actually one of motivation for adding global tempo tables to Postgres is 
to provide compatibility with Oracle.
Although I know that Oracle design decisions were never considered as  
axioms by Postgres community,
but ni case of GTT design I think that we should take in account Oracle 
approach.
And GTT in Oracle behaves exactly as in my implementation:

https://www.oracletutorial.com/oracle-basics/oracle-global-temporary-table/

It is not clear from this documentation whether index created for GTT in 
one session can be used in another session which already has some data 
in this GTT.
But I did experiment with install Oracle server and  can confirm that 
actually works in this way.

So I do not understand why do we need to complicate our GTT 
implementation in order to prohibit useful functionality and introduce 
inconsistency between behavior of normal and global temp tables.

> I don't like 2 - when I do index on global temp table, I don't would 
> to wait on indexing on all other sessions. These operations should be 
> maximally independent.
>

Nobody suggest to wait building index in all sessions.
Indexes will be constructed on demand when session access this table.
If session will no access this table at all, then index will never be 
constructed.

Once again: logic of dealing with indexes in GTT is very simple.
For normal tables, indexes are initialized at the tame when them are 
created.
For GTT it is not true. We have to initialize index on demand when it is 
accessed first time in session.

So it has to be handled in any way.
The question is only whether we should allow creation of index for table 
already populated with some data?
Actually doesn't require some additional efforts. We can use existed 
build_index function which initialize index and populates it with data.
So the solution proposed for me is most natural, convenient and simplest 
solution at the same time. And compatible with Oracle.

---

From: Pavel Stehule <[email-masked]>
Date: Fri, 24 Jan 2020 20:39:17 +0100

pá 24. 1. 2020 v 14:17 odesílatel Konstantin Knizhnik <
[email-masked]> napsal:

> one session can be used in another session which already has some data in
> this GTT.
> So I do not understand why do we need to complicate our GTT implementation
> in order to prohibit useful functionality and introduce inconsistency
> between behavior of normal and global temp tables.
>
>
>

> solution at the same time. And compatible with Oracle.

I cannot to evaluate your proposal, and I am sure, so you know more about
this code.

There is a question if we can allow to build local temp index on global
temp table. It is different situation. When I work with global properties
personally I prefer total asynchronous implementation of any DDL operations
for other than current session. When it is true, then I have not any
objection. For me, good enough design of any DDL can be based on catalog
change without forcing to living tables.

I see following disadvantage of your proposal. See scenario

1. I have two sessions

A - small GTT with active owner
B - big GTT with some active application.

session A will do new index - it is fast, but if creating index is forced
on B on demand (when B was touched), then this operation have to wait after
index will be created.

So I afraid build a index on other sessions on GTT when GTT tables in other
sessions will not be empty.

Regards

Pavel

---

From: "曾文旌(义从)" <[email-masked]>
Date: Sat, 25 Jan 2020 23:15:48 +0800

Thank you for review patch.

> 2020年1月24日 下午4:20，Konstantin Knizhnik <[email-masked]> 写道：
> 
> 
> 
> The comment seems to be confusing: it says about AccessExclusiveLock but actually uses ShareUpdateExclusiveLock.
There is a problem with the comment description, I will fix it.

> Assert(RELATION_IS_GLOBAL_TEMP(onerel) ^ TransactionIdIsNormal(onerel->rd_rel->relfrozenxid);
> Assert(RELATION_IS_GLOBAL_TEMP(onerel) ^ MultiXactIdIsValid(onerel->rd_rel->relminmxid));
Yes, Thank you for your points out, It's simpler.

> From my point of view cleanup at startup of local storage of temp tables should be performed in the same way for local and global temp tables.
After oom kill, In autovacuum, the Isolated local temp table will be cleaned like orphan temporary tables. The definition of local temp table is deleted with the storage file. 
But GTT can not do that. So we have the this implementation in my patch.
If you have other solutions, please let me know.

> Did you check that there will be no problems with GTT in case of XID wraparound?
> Right now if you create temp table and keep session open, then it will block XID wraparound.
In my design
1 Because different sessions have different transaction information, I choose to store the transaction information of GTT in MyProc,not catalog.
2 About the XID wraparound problem, the reason is the design of the temp table storage(local temp table and global temp table) that makes it can not to do vacuum by autovacuum. 
It should be completely solve at the storage level.

> Assume that DBA or programmer need to change format of GTT. But there are some active sessions which have used this GTT sometime in the past.
In fact, The dba can still complete the DDL of the GTT.
I've provided a set of functions for this case.
If the dba needs to modify a GTT A(or drop GTT or create index on GTT), he needs to do:
1 Use the pg_gtt_attached_pids view to list the pids for the session that is using the GTT A.
2 Use pg_terminate_backend(pid)terminate they except itself.
3 Do alter GTT A.

> What are the reasons of using RowExclusiveLock for GTT instead of AccessExclusiveLock?
> Yes, GTT data is access only by one backend so no locking here seems to be needed at all.
> But I wonder what are the motivations/benefits of using weaker lock level here?
1 Truncate GTT deletes only the data in the session, so no need use high-level lock.
2 I think it still needs to be block by DDL of GTT, which is why I use RowExclusiveLock.

> +            elog(ERROR, "can not create index when have other backend attached this global temp table");
> +
> 
> The same argument as in case of dropping GTT: I do not think that prohibiting DLL operations on GTT used by more than one backend is bad idea.
The idea was to give the GTT almost all the features of a regular table with few code changes.
The current version DBA can still do all DDL for GTT, I've already described.

> Why do we need to prohibit foreign key constraint on GTT?
It may be possible to support FK on GTT in later versions. Before that, I need to check some code.

> As far as I understand, content of GTT will never be processes by autovacuum.
> So unless GTT is explicitly vacuumed by user, its GTT is and them will not be taken in account 
> when computing new frozen xid value. Autovacumm will produce this warnings (which will ton be visible by end user and only appended to the log).
> And at some moment of time wrap around happen and if there still some old active GTT, we will get incorrect results.
I have already described my point in previous emails.

For problem 2
If we ignore the frozenxid of GTT, when vacuum truncates the clog that GTT need, the GTT data in some sessions is completely lost.
Perhaps we could consider let aotuvacuum terminate those sessions that contain "too old" data, 
But It's not very friendly, so I didn't choose to implement it in the first version.
Maybe you have a better idea.

Wenjing

---

From: Konstantin Knizhnik <[email-masked]>
Date: Mon, 27 Jan 2020 12:11:29 +0300

On 24.01.2020 22:39, Pavel Stehule wrote:
> I cannot to evaluate your proposal, and I am sure, so you know more 
> about this code.
>
> There is a question if we can allow to build local temp index on 
> global temp table. It is different situation. When I work with global 
> properties personally I prefer total asynchronous implementation of 
> any DDL operations for other than current session. When it is true, 
> then I have not any objection. For me, good enough design of any DDL 
> can be based on catalog change without forcing to living tables.
>

 From my point of view there are two difference uses cases of temp tables:
1. Backend needs some private data source which is specific to this 
session and has no relation with activities of other sessions.
2. We need a table  containing private session data, but which is used 
in the same way by all database users.

In the first case current Postgres temp tables works well (if we forget 
for a moment about all known issues related with temp tables).
Global temp tables are used to address the second scenario.  Assume that 
we write some stored procedure or implement some business logic  outside 
database and
what to perform some complex analtic query which requires tepmp table 
for storing intermediate results. In this case we can create GTT with 
all needed index at the moment of database initialization
and do not perform any extra DDL during query execution. If will prevent 
catalog bloating and makes execution of query more efficient.

I do not see any reasons to allow build local indexes for global table. 
Yes,it can happen that some session will have small amount of data in 
particular GTT and another - small amount of data in this table. But if 
access pattern is the same  (and nature of GTT assumes it), then index 
in either appreciate, either useless in both cases.

> session A will do new index - it is fast, but if creating index is 
> forced on B on demand (when B was touched), then this operation have 
> to wait after index will be created.
>
> So I afraid build a index on other sessions on GTT when GTT tables in 
> other sessions will not be empty.

Yes, it is true. But is is not the most realistic scenario from my point 
of view.
As I explained above, GTT should be used when we need temporary storage 
accessed in the same way by all clients.
If (as with normal tables) at some moment of time DBA realizes, that 
efficient execution of some queries needs extra indexes,
then it should be able to do it. It is very inconvenient and unnatural 
to prohibit DBA to do it until all sessions using this GTT are closed 
(it may never happen)
or require all sessions to restart to be able to use this index.

So it is possible to imagine different scenarios of working with GTTs.
But from my point of view the only non-contradictory model of their 
behavior is to make it compatible with normal tables.
And do not forget about compatibility with Oracle. Simplifying of 
porting existed applications from Oracle to Postgres  may be the
main motivation of adding GTT to Postgres. And making them incompatible 
with Oracle will be very strange.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Mon, 27 Jan 2020 12:38:13 +0300

On 25.01.2020 18:15, 曾文旌(义从) wrote:
> After oom kill, In autovacuum, the Isolated local temp table will be 
> cleaned like orphan temporary tables. The definition of local temp 
> table is deleted with the storage file.
I wonder if it is possible that autovacuum or some other Postgres 
process is killed by OOM and postmaster is not noticing it can doens't 
restart Postgres instance?
as far as I know, crash of any process connected to Postgres shared 
memory (and autovacuum definitely has such connection) cause Postgres 
restart.

> In my design
> 1 Because different sessions have different transaction information, I 
> choose to store the transaction information of GTT in MyProc,not catalog.
> 2 About the XID wraparound problem, the reason is the design of the 
> temp table storage(local temp table and global temp table) that makes 
> it can not to do vacuum by autovacuum.

My point of view is that vacuuming of temp tables is common problem for 
local and global temp tables.
So it has to be addressed in the common way and so we should not try to 
fix this problem only for GTT.

> 2 Use pg_terminate_backend(pid)terminate they except itself.
IMHO forced terminated of client sessions is not acceptable solution.
And it is not an absolutely necessary requirement.
So from my point of view we should not add such limitations to GTT design.

> 2 I think it still needs to be block by DDL of GTT, which is why I use 

Sorry, I do not understand your arguments: we do not need exclusive lock 
because we drop only local (private) data
but we need some kind of lock. I agree with 1) and not 2).

>> +            elog(ERROR, "can not create index when have other 
>> backend attached this global temp table");
>> +
>>

I absolutely agree with you that GTT should be given the same features 
as regular tables.
The irony is that this most natural and convenient behavior is most easy 
to implement without putting some extra restrictions.
Just let indexes for GTT be constructed on demand. It it can be done 
using the same function used for regular index creation.

> that, I need to check some code.

Ok,  may be approach to prohibit everything except minimally required 
functionality  is safe and reliable.
But frankly speaking I prefer different approach: if I do not see any 
contradictions of new feature with existed operations
and it is passing tests, then we should  not prohibit this operations 
for new feature.

> 1. The core problem is that the data contains transaction information 
> (xid), which needs to be vacuum(freeze) regularly to avoid running out 
> of xid.
> a) For local temp table: one table hava one piece of data. the 
> frozenxid of one local temp table is store in the catalog(pg_class).
> b) For global temp table: each session has a separate copy of data, 
> one GTT may contain maxbackend frozenxid.
> I agree that problem 1 should be completely solved by a some feature, 
> such as local transactions. It is definitely not included in the GTT 
> patch.
> But, I think we need to ensure the durability of GTT data. For 
> example, data in GTT cannot be lost due to the clog being cleaned up. 
> It belongs to problem 2.
>
> For problem 2
> If we ignore the frozenxid of GTT, when vacuum truncates the clog that 
> GTT need, the GTT data in some sessions is completely lost.

Sorry, I do not have better idea.
I prefer not to address this problem in first version of the patch at all.
fozen_xid of temp table is never changed unless user explicitly invoke 
vacuum on it.
I do not think that anybody is doing it (because it accentually contains 
temporary data which is not expected to live long time.
Certainly it is possible to imagine situation when session use GTT to 
store some local data which is valid during all session life time (which 
can be large enough).
But I am not sure that it is popular scenario.

---

From: Pavel Stehule <[email-masked]>
Date: Mon, 27 Jan 2020 20:44:13 +0100

po 27. 1. 2020 v 10:11 odesílatel Konstantin Knizhnik <
[email-masked]> napsal:

> 2. We need a table  containing private session data, but which is used in
> the same way by all database users.
>
> what to perform some complex analtic query which requires tepmp table for
> storing intermediate results. In this case we can create GTT with all
> needed index at the moment of database initialization
> access pattern is the same  (and nature of GTT assumes it), then index in
> either appreciate, either useless in both cases.
>
>
>
>
> then it should be able to do it. It is very inconvenient and unnatural to
> prohibit DBA to do it until all sessions using this GTT are closed (it may
> never happen)
> And do not forget about compatibility with Oracle. Simplifying of porting
> existed applications from Oracle to Postgres  may be the

I don't think so compatibility with Oracle is valid point in this case. We
need GTT, but the mechanism of index building should be designed for
Postgres, and for users.

Maybe the method proposed by you can be activated by some option like
CREATE INDEX IMMEDIATELY FOR ALL SESSION. When you use GTT without index,
then
it should to work some time more, and if you use short life sessions, then
index build can be last or almost last operation over table and can be
suboptimal.

Anyway, this behave can be changed later without bigger complications - and
now I am have strong opinion to prefer don't allow to any DDL (with index
creation) on any active GTT in other sessions.
Probably your proposal - build indexes on other sessions when GTT is
touched can share code with just modify metadata and wait on session reset
or GTT reset

Usually it is not hard problem to refresh sessions, and what I know when
you update plpgsql code, it is best practice to refresh session early.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 29 Jan 2020 00:01:17 +0800

> 2020年1月24日 上午4:47，Robert Haas <[email-masked]> 写道：
> 
I complete the implementation of this feature.
When a session x create an index idx_a on GTT A then
For session x, idx_a is valid when after create index.
For session y, before session x create index done, GTT A has some data, then index_a is invalid.
For session z, before session x create index done, GTT A has no data, then index_a is valid.

> +1. I can't imagine that's a safe or sane thing to do.

Opinion by Pavel
+	rel->rd_islocaltemp = true;  <<<<<<< if this is valid, then the name of field "rd_islocaltemp" is not probably best
I renamed rd_islocaltemp

Opinion by Konstantin Knizhnik
1 Fixed comments
2 Fixed assertion

Please help me review.

Wenjing

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 28 Jan 2020 17:40:14 +0100

> +1. I can't imagine that's a safe or sane thing to do.

I don't see any change?

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 29 Jan 2020 01:12:01 +0800

> 2020年1月29日 上午12:40，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> út 28. 1. 2020 v 17:01 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
>> 2020年1月24日 上午4:47，Robert Haas <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> <[email-masked] <mailto:[email-masked]>> wrote:
>> EnterpriseDB: http://www.enterprisedb.com <http://www.enterprisedb.com/>
Rename rd_islocaltemp to rd_istemp  in global_temporary_table_v8-pg13.patch

Wenjing

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 28 Jan 2020 18:13:31 +0100

> in global_temporary_table_v8-pg13.patch

ok :)

Pavel

---

From: Pavel Stehule <[email-masked]>
Date: Tue, 28 Jan 2020 18:54:50 +0100

út 28. 1. 2020 v 18:13 odesílatel Pavel Stehule <[email-masked]>
napsal:

>>> + rel->rd_islocaltemp = true;  <<<<<<< if this is valid, then the name
>>> of field "rd_islocaltemp" is not probably best
>>> I renamed rd_islocaltemp
>>>
>>
>> I don't see any change?
>>

I found a bug

postgres=# create global temp table x(a int);
CREATE TABLE
postgres=# insert into x values(1);
INSERT 0 1
postgres=# create index on x (a);
CREATE INDEX
postgres=# create index on x((a + 1));
CREATE INDEX
postgres=# analyze x;
WARNING:  oid 16468 not a relation
ANALYZE

other behave looks well for me.

Regards

Pavel

---

From: Konstantin Knizhnik <[email-masked]>
Date: Wed, 29 Jan 2020 11:12:59 +0300

On 27.01.2020 22:44, Pavel Stehule wrote:
>
> I don't think so compatibility with Oracle is valid point in this 
> case. We need GTT, but the mechanism of index building should be 
> designed for Postgres, and for users.
>
> CREATE INDEX IMMEDIATELY FOR ALL SESSION. When you use GTT without 
> index, then
> it should to work some time more, and if you use short life sessions, 
> then index build can be last or almost last operation over table and 
> can be suboptimal.
>
> Anyway, this behave can be changed later without bigger complications 
> - and now I am have strong opinion to prefer don't allow to any DDL 
> (with index creation) on any active GTT in other sessions.
> touched can share code with just modify metadata and wait on session 
> reset or GTT reset
>
Well, compatibility with Oracle was never treated as important argument 
in this group:)
But I hope that you agree that it real argument against your proposal.
Much more important argument is incompatibility with behavior of regular 
table.
If you propose such incompatibility, then you should have some very 
strong arguments for such behavior which will definitely confuse users.

But I heard only two arguments:

1. Concurrent building of indexes by all backends may consume much 
memory (n_backends * maintenance_work_mem) and consume a lot of disk/CPU 
resources.

First of all it is not completely true. Indexes will be created on 
demand when GTT will be accessed and chance that all sessions will 
become building indexes simultaneously is very small.

But what will happen if we prohibit access to this index for existed 
sessions? If we need index for GTT, then most likely it is used for joins.
If there is no index, then optimizer has to choose some other plan to 
perform this join. For example use hash join. Hash join also requires 
memory,
so if all backends will perform such join simultaneously, then them 
consume (n_backends * work_mem) memory.
Yes, work_mem is used to be smaller than maintenance_work_mem. But in 
any case DBA has a choice to adjust this parameters to avoid this problem.
And in case of your proposal (prohibit access to this index) you give 
him no choice to optimize query execution in existed sessions.

Also if all sessions will simultaneously perform sequential scan of GTT 
instead of building index for it, then them will read the same amount of 
data and consume comparable CPU time.
So prohibiting access to the indexes will not save us from high 
resources consumption if all existed sessions are really actively 
working with this GTT.

2. GTT in one session can contains large amount of data and we need 
index for it, but small amount of data in another session and we do not 
need index for it.

Such situation definitely can happen. But it contradicts to the main 
assumption of GTT use case (that it is accessed in the same way by all 
sessions).
Also I may be agree with this argument if you propose to create indexes 
locally for each sessions.
But your proposal is to prohibit access to the index to the sessions 
which already have populated GTT with data but allow it for sessions 
which have not accessed this GTT yet.
So if some session stores some data in GTT after index was created, then 
it will build index for it, doesn't matter whether size of table is 
small or large.
Why do we make an exception for sessions which already have data in GTT 
in this case?

So from my point of view both arguments are doubtful and can not explain 
why rules of index usability for GTT should be different from regular 
tables.

> Usually it is not hard problem to refresh sessions, and what I know 
> when you update plpgsql code, it is best practice to refresh session 
> early.
>

I know may systems where session is established once client is connected 
to the system and not closed until client is disconnected.
And any attempt to force termination of the session will cause 
application errors which are not expected by the client.

Sorry, I think that it is principle point in discussion concerning GTT 
design.
Implementation of GTT can be changed in future, but it is bad if 
behavior of GTT will be changed.
It is not clear for me why from the very beginning we should provide 
inconsistent behavior which is even more difficult to implement than 
behavior compatible with regular tables.
And say that in the future it can be changed...

Sorry, but I do not consider proposals to create indexes locally for 
each session (i.e. global tables but private indexes) or use some 
special complicated SQL syntax constructions like
CREATE INDEX IMMEDIATELY FOR ALL SESSION as some real alternatives which 
have to be discussed.

---

From: "曾文旌(义从)" <[email-masked]>
Date: Wed, 29 Jan 2020 21:06:44 +0800

> 2020年1月29日 上午1:54，Pavel Stehule <[email-masked]> 写道：
> 
> 
> 
> út 28. 1. 2020 v 18:13 odesílatel Pavel Stehule <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
> út 28. 1. 2020 v 18:12 odesílatel 曾文旌(义从) <[email-masked] <mailto:[email-masked]>> napsal:
> 
> 
>> 2020年1月29日 上午12:40，Pavel Stehule <[email-masked] <mailto:[email-masked]>> 写道：
>> 
>> 
>> 
Thanks for review.

The index expression need to store statistics on index, I missed it and I'll fix it later.

Wenjing

---

From: Robert Haas <[email-masked]>
Date: Wed, 29 Jan 2020 08:43:57 -0500

On Mon, Jan 27, 2020 at 4:11 AM Konstantin Knizhnik
<[email-masked]> wrote:
> I do not see any reasons to allow build local indexes for global table. Yes,it can happen that some session will have small amount of data in particular GTT and another - small amount of data in this table. But if access pattern is the same  (and nature of GTT assumes it), then index in either appreciate, either useless in both cases.

I agree. I think allowing different backends to have different indexes
is overly complicated.

Regarding another point that was raised, I think it's not a good idea
to prohibit DDL on global temporary tables altogether. It should be
fine to change things when no sessions are using the GTT. Going
further and allowing changes when there are attached sessions would be
nice, but I think we shouldn't try. Getting this feature committed is
going to be a huge amount of work with even a minimal feature set;
complicating the problem by adding what are essentially new
DDL-concurrency features on top of the basic feature seems very much
unwise.

---

From: Robert Haas <[email-masked]>
Date: Wed, 29 Jan 2020 08:48:26 -0500

> Rename rd_islocaltemp to rd_istemp  in global_temporary_table_v8-pg13.patch

In view of commit 6919b7e3294702adc39effd16634b2715d04f012, I think
that this has approximately a 0% chance of being acceptable. If you're
setting a field in a way that is inconsistent with the current use of
the field, you're probably doing it wrong, because the field has an
existing purpose to which new code must conform. And if you're not
doing that, then you don't need to rename it.

---

From: Robert Haas <[email-masked]>
Date: Wed, 29 Jan 2020 09:47:52 -0500

On Wed, Jan 29, 2020 at 3:13 AM Konstantin Knizhnik
<[email-masked]> wrote:
> 1. Concurrent building of indexes by all backends may consume much memory (n_backends * maintenance_work_mem) and consume a lot of disk/CPU resources.
> 2. GTT in one session can contains large amount of data and we need index for it, but small amount of data in another session and we do not need index for it.

You seem to be ignoring the fact that two committers told you this
probably wasn't safe.

Perhaps your view is that those people made no argument, and therefore
you don't have to respond to it. But the onus is not on somebody else
to tell you why a completely novel idea is not safe. The onus is on
you to analyze it in detail and prove that it is safe. What you need
to show is that there is no code anywhere in the system which will be
confused by an index springing into existence at whatever time you're
creating it.

One problem is that there are various backend-local data structures in
the relcache, the planner, and the executor that remember information
about indexes, and that may not respond well to having more indexes
show up unexpectedly. On the one hand, they might crash; on the other
hand, they might ignore the new index when they shouldn't. Another
problem is that the code which creates indexes might fail or misbehave
when run in an environment different from the one in which it
currently runs. I haven't really studied your code, so I don't know
exactly what it does, but for example it would be really bad to try to
build an index while holding a buffer lock, both because it might
cause (low-probability) undetected deadlocks and also because it might
block another process that wants that buffer lock in a
non-interruptible wait state for a long time.

Now, maybe you can make an argument that you only create indexes at
points in the query that are "safe." But I am skeptical, because of
this example:

rhaas=# create table foo (a int primary key, b text, c text, d text);
CREATE TABLE
rhaas=# create function blump() returns trigger as $$begin create
index on foo (b); return new; end$$ language plpgsql;
CREATE FUNCTION
rhaas=# create trigger thud before insert on foo execute function blump();
CREATE TRIGGER
rhaas=# insert into foo (a) select generate_series(1,10);
ERROR:  cannot CREATE INDEX "foo" because it is being used by active
queries in this session
CONTEXT:  SQL statement "create index on foo (b)"
PL/pgSQL function blump() line 1 at SQL statement

That prohibition is there for some reason. Someone did not just decide
to arbitrarily prohibit it. A CREATE INDEX command run in that context
won't run afoul of many of the things that might be problems in other
places -- e.g. there won't be a buffer lock held. Yet, despite the
fact that a trigger context is safe for executing a wide variety of
user-defined code, this particular operation is not allowed here. That
is the sort of thing that should worry you.

At any rate, even if this somehow were or could be made safe,
on-the-fly index creation is a feature that cannot and should not be
combined with a patch to implement global temporary tables. Surely, it
will require a lot of study and work to get the details right. And so
will GTT. As I said in the other email I wrote, this feature is hard
enough without adding this kind of thing to it. There's a reason why I
never got around to implementing this ten years ago when I did
unlogged tables; I was intending that to be a precursor to the GTT
work. I found that it was too hard and I gave up. I'm glad to see
people trying again, but the idea that we can afford to add in extra
features, or frankly that either of the dueling patches on this thread
are close to committable, is just plain wrong.

---

From: Konstantin Knizhnik <[email-masked]>
Date: Wed, 29 Jan 2020 18:30:44 +0300

> are close to committable, is just plain wrong.

Sorry, I really didn't consider statements containing word "probably" as 
arguments.
But I agree with you: it is task of developer of new feature to prove 
that proposed approach is safe rather than of reviewers to demonstrate 
that it is unsafe.
Can I provide such proof now? I afraid that not.
But please consider two arguments:

1. Index for GTT in any case has to be initialized on demand. In case of 
regular tables index is initialized at the moment of its creation. In 
case of GTT it doesn't work.
So we should somehow detect that accessed index is not initialized and 
perform lazy initialization of the index.
The only difference with the approach proposed by Pavel  (allow index 
for empty GTT but prohibit it for GTT filled with data) is whether we 
also need to populate index with data or not.
I can imagine that implicit initialization of index in read-only query 
(select) can be unsafe and cause some problems. I have not encountered 
such problems yet after performing many tests with GTTs, but certainly I 
have not covered all possible scenarios (not sure that it is possible at 
all).
But I do not understand how populating  index with data can add some 
extra unsafety.

So I can not prove that building index for GTT on demand is safe, but it 
is not more unsafe than initialization of index on demand which is 
required in any case.

2. Actually I do not propose some completely new approach. I try to 
provide behavior with is compatible with regular tables.
If you create index for regular table, then it can be used in all 
sessions, right?
And all "various backend-local data structures in the relcache, the 
planner, and the executor that remember information about indexes"
have to be properly updated.  It is done using invalidation mechanism. 
The same mechanism is used in case of DDL operations with GTT, because 
we change system catalog.

So my point here is that creation index of GTT is almost the same as 
creation of index for regular tables and the same mechanism will be used 
to provide correctness of this operation.

---

From: Pavel Stehule <[email-masked]>
Date: Wed, 29 Jan 2020 18:08:35 +0100

> If you create index for regular table, then it can be used in all

I don't understand to this point. Regular tables shares data, shares files.
You cannot to separate it. More - you have to uses relatively aggressive
locks to be this operation safe.

Nothing from these points are valid for GTT.

Regards

Pavel