From 32e86de0e36a9b9544426ab91c6af57d029d078c Mon Sep 17 00:00:00 2001
From: charsyam <charsyam@naver.com>
Date: Sun, 12 Apr 2026 16:20:35 +0900
Subject: [PATCH] Reduce pg_class scans in GRANT/REVOKE ON ALL TABLES IN SCHEMA

When processing GRANT/REVOKE ... ON ALL TABLES IN SCHEMA,
objectsInSchemaToOids() called getRelationsInNamespace() five times,
once per relkind (RELATION, VIEW, MATVIEW, FOREIGN_TABLE,
PARTITIONED_TABLE).  pg_class does have an index on
(relname, relnamespace), but there is no index matching
(relnamespace, relkind), so each of those per-relkind calls falls
back to a full heap scan via table_beginscan_catalog() -- i.e. the
catalog is scanned five times in total.

Introduce getRelationsInNamespaceMulti(), which performs a single
heap scan filtered by relnamespace and distributes matching tuples
into per-relkind buckets supplied by the caller.  Relkind filtering
is done in C after each tuple is read, which is trivially cheap.
The OBJECT_TABLE case uses the helper; OBJECT_SEQUENCE and
OBJECT_PROPGRAPH keep calling getRelationsInNamespace() unchanged
because they only need a single relkind and benefit from the second
ScanKey.

Behavior is preserved:

* Result order is identical.  The underlying pg_class heap (and
  therefore its physical scan order) is the same regardless of how
  we filter, so each bucket ends up holding exactly the OIDs that
  the corresponding per-relkind heap scan would have produced, in
  the same order.  Concatenating the buckets in the original
  relkind order reproduces the previous list tuple-for-tuple.
  This was verified empirically by comparing, on a schema with
  mixed relkinds, the OID sequence produced by the old UNION-ALL
  pattern against the new single-scan + bucketed pattern; the
  sequences are identical element by element.

* MVCC semantics are, if anything, a bit stricter: all relkinds
  are now collected under a single catalog snapshot rather than
  five.

* Locking is unchanged in kind -- AccessShareLock on pg_class is
  still taken, just once instead of five times.

A simple benchmark (10,000 tables in one schema, pg_class ~10,452
rows) shows a consistent ~15% reduction in end-to-end time of
GRANT/REVOKE SELECT ON ALL TABLES IN SCHEMA:

  GRANT : 88.2 ms -> 75.9 ms
  REVOKE: 134.9 ms -> 115.7 ms

The absolute savings are small because the bulk of the time in
these commands is spent updating per-relation ACL tuples, not
scanning pg_class.  For schemas with only a handful of relations
the effect is not measurable.  The change is therefore a targeted
improvement for deployments with very large catalogs
(multi-tenant / partition-heavy systems) that frequently run ALL
TABLES IN SCHEMA grants.
---
 src/backend/catalog/aclchk.c | 90 ++++++++++++++++++++++++++++++++----
 1 file changed, 80 insertions(+), 10 deletions(-)

diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..a68f0a989d4 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -125,6 +125,11 @@ static List *objectNamesToOids(ObjectType objtype, List *objnames,
 							   bool is_grant);
 static List *objectsInSchemaToOids(ObjectType objtype, List *nspnames);
 static List *getRelationsInNamespace(Oid namespaceId, char relkind);
+/* Single-scan helper over pg_class for multiple relkinds in one namespace */
+static void getRelationsInNamespaceMulti(Oid namespaceId,
+										 const char *relkinds,
+										 int nkinds,
+										 List **buckets);
 static void expand_col_privileges(List *colnames, Oid table_oid,
 								  AclMode this_privileges,
 								  AclMode *col_privileges,
@@ -797,16 +802,25 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 		switch (objtype)
 		{
 			case OBJECT_TABLE:
-				objs = getRelationsInNamespace(namespaceId, RELKIND_RELATION);
-				objects = list_concat(objects, objs);
-				objs = getRelationsInNamespace(namespaceId, RELKIND_VIEW);
-				objects = list_concat(objects, objs);
-				objs = getRelationsInNamespace(namespaceId, RELKIND_MATVIEW);
-				objects = list_concat(objects, objs);
-				objs = getRelationsInNamespace(namespaceId, RELKIND_FOREIGN_TABLE);
-				objects = list_concat(objects, objs);
-				objs = getRelationsInNamespace(namespaceId, RELKIND_PARTITIONED_TABLE);
-				objects = list_concat(objects, objs);
+				{
+					const char kinds[] = {
+						RELKIND_RELATION,
+						RELKIND_VIEW,
+						RELKIND_MATVIEW,
+						RELKIND_FOREIGN_TABLE,
+						RELKIND_PARTITIONED_TABLE
+					};
+					List *buckets[lengthof(kinds)];
+					int i;
+
+					for (i = 0; i < (int) lengthof(kinds); i++)
+						buckets[i] = NIL;
+
+					getRelationsInNamespaceMulti(namespaceId, kinds, lengthof(kinds), buckets);
+
+					for (i = 0; i < (int) lengthof(kinds); i++)
+						objects = list_concat(objects, buckets[i]);
+				}
 				break;
 			case OBJECT_SEQUENCE:
 				objs = getRelationsInNamespace(namespaceId, RELKIND_SEQUENCE);
@@ -907,6 +921,62 @@ getRelationsInNamespace(Oid namespaceId, char relkind)
 	return relations;
 }
 
+/*
+ * getRelationsInNamespaceMulti
+ *
+ * Perform a single heap scan over pg_class for the given namespace, and
+ * distribute matching tuples into per-relkind buckets provided by the
+ * caller.  There is no pg_class index matching (relnamespace, relkind),
+ * so the previous per-relkind variant also resorted to a full heap scan;
+ * this helper simply collapses N such scans into one.
+ *
+ * Order preservation: within each bucket, entries appear in the order
+ * they were encountered during the heap scan.  Because the underlying
+ * heap (and thus its physical scan order) is the same regardless of
+ * how we filter, each bucket ends up holding the same OIDs in the same
+ * relative order as a separate per-relkind heap scan would have
+ * produced.  Concatenating the buckets in the caller's requested
+ * relkind order therefore reproduces the list that the previous code
+ * built from N separate getRelationsInNamespace() calls, tuple for
+ * tuple.
+ */
+static void
+getRelationsInNamespaceMulti(Oid namespaceId, const char *relkinds, int nkinds, List **buckets)
+{
+	ScanKeyData key;
+	Relation	rel;
+	TableScanDesc scan;
+	HeapTuple	tuple;
+	int			i;
+
+	/* Open pg_class once and scan by namespace; filter relkind in-code */
+	ScanKeyInit(&key,
+				Anum_pg_class_relnamespace,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(namespaceId));
+
+	rel = table_open(RelationRelationId, AccessShareLock);
+	scan = table_beginscan_catalog(rel, 1, &key);
+
+	while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+	{
+		Form_pg_class cform = (Form_pg_class) GETSTRUCT(tuple);
+		char		rk = cform->relkind;
+
+		for (i = 0; i < nkinds; i++)
+		{
+			if (rk == relkinds[i])
+			{
+				buckets[i] = lappend_oid(buckets[i], cform->oid);
+				break;
+			}
+		}
+	}
+
+	table_endscan(scan);
+	table_close(rel, AccessShareLock);
+}
+
 
 /*
  * ALTER DEFAULT PRIVILEGES statement
-- 
2.50.1 (Apple Git-155)

