From 6c3f2705fc58d4f30b6d9455e803456b0da71a27 Mon Sep 17 00:00:00 2001 From: Manu 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 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