From d6769e94c11184e6d3b34f1561a67411073076e5 Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 29 Sep 2026 13:01:10 -0300 Subject: [PATCH v2] 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 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/catversion.h | 2 +- src/include/catalog/pg_constraint.h | 1 + 3 files changed, 23 insertions(+), 15 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/catversion.h b/src/include/catalog/catversion.h index 6f3e526de96..d636e7a7f30 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202609152 +#define CATALOG_VERSION_NO 202609291 #endif 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