0005-Global-temporary-tables-disable-parallel-query-and-a.patch
text/x-patch
Filename: 0005-Global-temporary-tables-disable-parallel-query-and-a.patch
Type: text/x-patch
Part: 4
Message:
Re: Global temporary tables
Patch
Format: format-patch
Series: patch 0005
Subject: Global temporary tables: disable parallel query and autovacuum
| File | + | − |
|---|---|---|
| src/backend/catalog/storage_gtt.c | 39 | 0 |
| src/backend/optimizer/path/allpaths.c | 15 | 9 |
| src/backend/optimizer/plan/planner.c | 4 | 3 |
| src/backend/optimizer/util/clauses.c | 20 | 0 |
| src/backend/postmaster/autovacuum.c | 11 | 2 |
From d67c6d671cfb26198bebca52820d68986f7b27fb Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Fri, 13 Mar 2026 12:18:50 -0400
Subject: [PATCH 05/12] Global temporary tables: disable parallel query and
autovacuum
GTTs use per-session local buffers that parallel workers cannot access,
for the same reason as regular temporary tables. Disable parallel
query for GTT scans in set_rel_consider_parallel() and disable parallel
CREATE INDEX in plan_create_index_workers().
Also skip GTTs in autovacuum's table scanning loops, since autovacuum
runs in a separate backend and cannot access any session's local GTT
storage.
The planner-side exclusion only covers GTTs used as parallel
baserels; a function mislabeled PARALLEL SAFE can still reach a GTT
from inside a worker. GttInitSessionStorage therefore errors out
cleanly in parallel workers (mirroring the temp-table check in
InitLocalBuffers), and in the parallel-mode leader when first-time
storage initialization would be required, instead of tripping
RelationCreateStorage's assertion or scribbling on the leader's temp
namespace from a worker.
---
src/backend/catalog/storage_gtt.c | 39 +++++++++++++++++++++++++++
src/backend/optimizer/path/allpaths.c | 24 ++++++++++-------
src/backend/optimizer/plan/planner.c | 7 ++---
src/backend/optimizer/util/clauses.c | 20 ++++++++++++++
src/backend/postmaster/autovacuum.c | 13 +++++++--
5 files changed, 89 insertions(+), 14 deletions(-)
diff --git a/src/backend/catalog/storage_gtt.c b/src/backend/catalog/storage_gtt.c
index 3aa1d20f156..bd4540b59b1 100644
--- a/src/backend/catalog/storage_gtt.c
+++ b/src/backend/catalog/storage_gtt.c
@@ -22,6 +22,7 @@
#include "postgres.h"
#include "access/amapi.h"
+#include "access/parallel.h"
#include "access/relation.h"
#include "access/table.h"
#include "access/tableam.h"
@@ -184,6 +185,33 @@ GttInitSessionStorage(Relation relation)
bool found;
Oid relid = RelationGetRelid(relation);
+ /*
+ * A parallel worker must never create or register per-session storage:
+ * the storage map is backend-local (a worker cannot see the leader's
+ * entries or dirty local buffers), so any worker-side materialization
+ * would corrupt or duplicate the leader's session state. But a worker
+ * may legitimately need only the relation's catalog metadata -- e.g.
+ * pg_get_expr() opens the relation to deparse a column default while
+ * pg_dump runs under debug_parallel_query. Point the relcache entry at
+ * the session-local file in the leader's temp namespace, exactly as an
+ * untouched GTT looks in the leader, but without recording anything in
+ * the storage map. No file is created here; if the leader never
+ * materialized the relation, reads short-circuit to empty. The planner
+ * never makes a GTT a parallel baserel and DML is never parallelized, so
+ * a worker never actually scans or writes one.
+ */
+ if (IsParallelWorker())
+ {
+ if (OidIsValid(relation->rd_rel->reltablespace))
+ relation->rd_locator.spcOid = relation->rd_rel->reltablespace;
+ else
+ relation->rd_locator.spcOid = MyDatabaseTableSpace;
+ relation->rd_locator.dbOid = MyDatabaseId;
+ relation->rd_locator.relNumber = relation->rd_rel->relfilenode;
+ relation->rd_backend = ProcNumberForTempRelations();
+ return;
+ }
+
ensure_gtt_hash();
entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
@@ -339,6 +367,17 @@ GttEnsureSessionStorage(Relation relation)
if (entry->storage_created)
return;
+ /*
+ * RelationCreateStorage cannot run in parallel mode (it couldn't update
+ * pendingSyncHash), and a worker must never materialize state in the
+ * leader's temp namespace; relcache builds in workers are already
+ * rejected in GttInitSessionStorage.
+ */
+ if (IsInParallelMode())
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TRANSACTION_STATE),
+ errmsg("cannot initialize global temporary table storage during a parallel operation"));
+
RelationCreateStorage(entry->locator, RELPERSISTENCE_GLOBAL_TEMP, true);
entry->storage_created = true;
entry->storage_subid = GetCurrentSubTransactionId();
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index c134594a21a..c40b71c902d 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -664,16 +664,22 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
/*
* Currently, parallel workers can't access the leader's temporary
- * tables. We could possibly relax this if we wrote all of its
- * local buffers at the start of the query and made no changes
- * thereafter (maybe we could allow hint bit changes), and if we
- * taught the workers to read them. Writing a large number of
- * temporary buffers could be expensive, though, and we don't have
- * the rest of the necessary infrastructure right now anyway. So
- * for now, bail out if we see a temporary table.
+ * tables, nor a global temporary table's per-session data. We
+ * could possibly relax this if we wrote all of its local buffers
+ * at the start of the query and made no changes thereafter (maybe
+ * we could allow hint bit changes), and if we taught the workers
+ * to read them. Writing a large number of temporary buffers
+ * could be expensive, though, and we don't have the rest of the
+ * necessary infrastructure right now anyway. So for now, bail
+ * out if we see a temporary or global temporary table.
*/
- if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
- return;
+ {
+ char relpersist = get_rel_persistence(rte->relid);
+
+ if (relpersist == RELPERSISTENCE_TEMP ||
+ relpersist == RELPERSISTENCE_GLOBAL_TEMP)
+ return;
+ }
/*
* Table sampling can be pushed down to workers if the sample
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index f4689e7c9f8..964dc4a0fa0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -7347,11 +7347,12 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)
/*
* Determine if it's safe to proceed.
*
- * Currently, parallel workers can't access the leader's temporary tables.
- * Furthermore, any index predicate or index expressions must be parallel
- * safe.
+ * Currently, parallel workers can't access the leader's temporary tables,
+ * nor a global temporary table's per-session data. Furthermore, any
+ * index predicate or index expressions must be parallel safe.
*/
if (heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+ RelationIsGlobalTemp(heap) ||
!is_parallel_safe(root, (Node *) RelationGetIndexExpressions(index)) ||
!is_parallel_safe(root, (Node *) RelationGetIndexPredicate(index)))
{
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 01997e22266..8d98d5f8b43 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -965,6 +965,7 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
else if (IsA(node, Query))
{
Query *query = (Query *) node;
+ ListCell *lc;
/* SELECT FOR UPDATE/SHARE must be treated as unsafe */
if (query->rowMarks != NULL)
@@ -973,6 +974,25 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context)
return true;
}
+ /*
+ * A global temporary table has per-session, backend-local storage
+ * that parallel workers cannot see, and that storage may have to be
+ * created on first access -- which is impossible in parallel mode.
+ * Referencing one therefore makes the query parallel-unsafe, so that
+ * parallel mode is never imposed on it (e.g. by debug_parallel_query).
+ */
+ foreach(lc, query->rtable)
+ {
+ RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
+
+ if (rte->rtekind == RTE_RELATION &&
+ get_rel_persistence(rte->relid) == RELPERSISTENCE_GLOBAL_TEMP)
+ {
+ context->max_hazard = PROPARALLEL_UNSAFE;
+ return true;
+ }
+ }
+
/* Recurse into subselects */
return query_tree_walker(query,
max_parallel_hazard_walker,
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index e9aaf24c1be..7452518c386 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2073,6 +2073,13 @@ do_autovacuum(void)
continue;
}
+ /*
+ * Global temporary tables have per-session local storage that
+ * autovacuum cannot access. Skip them.
+ */
+ if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ continue;
+
/* Fetch reloptions and the pgstat entry for this table */
relopts = extract_autovac_opts(tuple, pg_class_desc);
@@ -2147,9 +2154,11 @@ do_autovacuum(void)
AutoVacuumScores scores;
/*
- * We cannot safely process other backends' temp tables, so skip 'em.
+ * We cannot safely process other backends' temp tables or GTTs (which
+ * have per-session local storage), so skip them.
*/
- if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+ if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+ classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
continue;
relid = classForm->oid;
--
2.43.0