From 6c3f2705fc58d4f30b6d9455e803456b0da71a27 Mon Sep 17 00:00:00 2001
From: Manu <manuelreyesbravo@gmail.com>
Date: Tue, 29 Sep 2026 13:01:10 -0300
Subject: [PATCH v3] Index pg_constraint.confrelid to avoid seqscans in
 partition DDL

CloneFkReferenced() collects the foreign keys that reference a
partitioned table by looking for pg_constraint rows whose confrelid is
the table.  pg_constraint had no index on confrelid, so this was a
sequential scan of the whole catalog.

That was cheap while pg_constraint stayed small.  Since not-null
constraints gained pg_constraint rows (commit 14e87ffa5c5), the catalog
holds a row per not-null column, and the scan's cost now grows with the
total number of constraints in the database.  ATTACH PARTITION and
CREATE TABLE ... PARTITION OF, which call CloneFkReferenced()
unconditionally, became noticeably slow on large schemas.

Add a btree index on pg_constraint.confrelid and use it in the three
places that scan the catalog by confrelid:

  - CloneFkReferenced()          (ATTACH PARTITION / PARTITION OF)
  - GetParentedForeignKeyRefs()  (DETACH PARTITION)
  - ATPrepChangePersistence()    (ALTER TABLE ... SET UNLOGGED)

Only foreign keys set confrelid, so the first two scan on confrelid
alone and filter contype in the loop; the third already did.  With ~1M
not-null constraints, ATTACH drops from ~25 ms to ~0.3 ms per partition
and stays flat, and each of the other two paths drops from one catalog
seqscan to an index scan.

Catalog indexes cannot be partial, so the index covers every row,
including the confrelid = 0 majority; a confrelid <> 0 partial index
would be far smaller but is not supported by the bootstrap and
CatalogIndexInsert paths.

Reported-by: Bernhard Wonisch <bernhard.wonisch@gmx.at>
Discussion: https://postgr.es/m/trinity-08d3329b-f6b2-4c11-91d7-2198e50bf4fc-1790681922354@trinity-msg-rest-gmx-gmx-live-58cc8f554d-c56lm
---
 src/backend/commands/tablecmds.c    | 35 +++++++++++++++++------------
 src/include/catalog/pg_constraint.h |  1 +
 2 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..d4708507788 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11355,16 +11355,21 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 	ScanKeyInit(&key[0],
 				Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
 				F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel)));
-	ScanKeyInit(&key[1],
-				Anum_pg_constraint_contype, BTEqualStrategyNumber,
-				F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
-	/* This is a seqscan, as we don't have a usable index ... */
-	scan = systable_beginscan(pg_constraint, InvalidOid, true,
-							  NULL, 2, key);
+	/*
+	 * Look this up through the index on confrelid rather than seqscanning all
+	 * of pg_constraint.  That scan grew expensive once not-null constraints
+	 * started to have pg_constraint rows, making its cost scale with the total
+	 * number of constraints in the database.  Only foreign keys set confrelid,
+	 * so filtering on contype in the loop is just belt-and-suspenders.
+	 */
+	scan = systable_beginscan(pg_constraint, ConstraintConfRelidIndexId, true,
+							  NULL, 1, key);
 	while ((tuple = systable_getnext(scan)) != NULL)
 	{
 		Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
 
+		if (constrForm->contype != CONSTRAINT_FOREIGN)
+			continue;
 		clone = lappend_oid(clone, constrForm->oid);
 	}
 	systable_endscan(scan);
@@ -19562,8 +19567,8 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
 
 	/*
-	 * Scan conrelid if changing to permanent, else confrelid.  This also
-	 * determines whether a useful index exists.
+	 * Scan conrelid if changing to permanent, else confrelid.  Both columns
+	 * are indexed, so this also determines which index to use.
 	 */
 	ScanKeyInit(&skey[0],
 				toLogged ? Anum_pg_constraint_conrelid :
@@ -19571,7 +19576,8 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 				BTEqualStrategyNumber, F_OIDEQ,
 				ObjectIdGetDatum(RelationGetRelid(rel)));
 	scan = systable_beginscan(pg_constraint,
-							  toLogged ? ConstraintRelidTypidNameIndexId : InvalidOid,
+							  toLogged ? ConstraintRelidTypidNameIndexId :
+							  ConstraintConfRelidIndexId,
 							  true, NULL, 1, skey);
 
 	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
@@ -22672,16 +22678,17 @@ GetParentedForeignKeyRefs(Relation partition)
 	ScanKeyInit(&key[0],
 				Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
 				F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition)));
-	ScanKeyInit(&key[1],
-				Anum_pg_constraint_contype, BTEqualStrategyNumber,
-				F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
 
-	/* XXX This is a seqscan, as we don't have a usable index */
-	scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key);
+	/* Use the index on confrelid; only FKs set it, so filter contype below */
+	scan = systable_beginscan(pg_constraint, ConstraintConfRelidIndexId, true,
+							  NULL, 1, key);
 	while ((tuple = systable_getnext(scan)) != NULL)
 	{
 		Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
 
+		if (constrForm->contype != CONSTRAINT_FOREIGN)
+			continue;
+
 		/*
 		 * We only need to process constraints that are part of larger ones.
 		 */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index e8d27546ed9..47d57f29488 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -185,6 +185,7 @@ DECLARE_UNIQUE_INDEX(pg_constraint_conrelid_contypid_conname_index, 2665, Constr
 DECLARE_INDEX(pg_constraint_contypid_index, 2666, ConstraintTypidIndexId, pg_constraint, btree(contypid oid_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_constraint_oid_index, 2667, ConstraintOidIndexId, pg_constraint, btree(oid oid_ops));
 DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, pg_constraint, btree(conparentid oid_ops));
+DECLARE_INDEX(pg_constraint_confrelid_index, 9370, ConstraintConfRelidIndexId, pg_constraint, btree(confrelid oid_ops));
 
 MAKE_SYSCACHE(CONSTROID, pg_constraint_oid_index, 16);
 
-- 
2.55.0

