From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Fri, 17 Jul 2026 12:11:36 +0530 Subject: [PATCH v1] Cache a partition index's immediate parent OID in the relcache get_partition_ancestors() scans pg_inherits once per hierarchy level on every call. In the INSERT ... ON CONFLICT arbiter-mapping loop in ExecInitPartitionInfo() this runs once per leaf index; the existing XXX comment there flagged it as slow. Add get_partition_index_ancestors(Relation indexRel), which caches the immediate partition-parent OID of an already-open, locked partition index on its relcache entry (rd_partparent, populated lazily) and walks higher levels with the existing catalog-scanning helper. Only the leaf index's parent is cached, using the relation the caller already holds a lock on, so no unlocked relcache opens are introduced. Supported changes to index ancestry already invalidate the affected index relcache entry. ATTACH and DETACH update the child index's pg_class row, while index_concurrently_swap() updates both pg_class rows as it transfers the pg_inherits link; creation and drop replace the relation altogether. Reset the cached parent during both full relcache builds and RelationReloadIndexInfo(), the minimal reload used for open indexes. Add a regression that warms the cache, moves a partition between two partitioned tables, and verifies the reverse invalidation across a savepoint rollback. --- src/backend/catalog/partition.c | 76 +++++++++++++++++++ src/backend/executor/execPartition.c | 11 ++- src/backend/utils/cache/relcache.c | 6 ++ src/include/catalog/partition.h | 1 + src/include/utils/rel.h | 5 ++ src/test/regress/expected/insert_conflict.out | 28 +++++++ src/test/regress/sql/insert_conflict.sql | 30 ++++++++ 7 files changed, 151 insertions(+), 6 deletions(-) diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c index 28f3cade6ff..166a544b2cf 100644 --- a/src/backend/catalog/partition.c +++ b/src/backend/catalog/partition.c @@ -27,6 +27,7 @@ #include "optimizer/optimizer.h" #include "rewrite/rewriteManip.h" #include "utils/fmgroids.h" +#include "utils/lsyscache.h" #include "utils/partcache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -35,6 +36,7 @@ static Oid get_partition_parent_worker(Relation inhRel, Oid relid, bool *detach_pending); static void get_partition_ancestors_worker(Relation inhRel, Oid relid, List **ancestors); +static Oid RelationGetPartitionParent(Relation rel); /* * get_partition_parent @@ -167,6 +169,80 @@ get_partition_ancestors_worker(Relation inhRel, Oid relid, List **ancestors) get_partition_ancestors_worker(inhRel, parentOid, ancestors); } +/* + * get_partition_index_ancestors + * Obtain ancestors using the immediate-parent OID cached on 'indexRel'. + * + * The caller must already have opened and locked 'indexRel'. Higher levels + * use the regular catalog walker, because opening their relcache entries by + * OID without holding relation locks would be unsafe. + */ +List * +get_partition_index_ancestors(Relation indexRel) +{ + List *result; + Oid parentOid; + + Assert(indexRel->rd_rel->relkind == RELKIND_INDEX); + + parentOid = RelationGetPartitionParent(indexRel); + if (!OidIsValid(parentOid)) + return NIL; + + result = list_make1_oid(parentOid); + if (get_rel_relispartition(parentOid)) + result = list_concat(result, get_partition_ancestors(parentOid)); + + return result; +} + +/* + * RelationGetPartitionParent + * Return the OID of the immediate partition parent of 'rel', caching it + * on the relcache entry. + * + * Returns InvalidOid when 'rel' is not a partition or has no parent. + * + * The cached value is reset by both full relcache rebuilds and the minimal + * reload used for open indexes. Changes to an index partition's pg_inherits + * link either create or drop the index, or update its pg_class row and cause + * a relcache invalidation. + */ +static Oid +RelationGetPartitionParent(Relation rel) +{ + Relation inhRel; + Oid parentOid; + bool detach_pending; + + if (rel->rd_partparentvalid) + return rel->rd_partparent; + + if (!rel->rd_rel->relispartition) + { + rel->rd_partparent = InvalidOid; + rel->rd_partparentvalid = true; + return InvalidOid; + } + + inhRel = table_open(InheritsRelationId, AccessShareLock); + parentOid = get_partition_parent_worker(inhRel, RelationGetRelid(rel), + &detach_pending); + table_close(inhRel, AccessShareLock); + + /* + * Only leaf partition indexes reach here, and an index's pg_inherits row + * is never marked detach-pending: DETACH PARTITION CONCURRENTLY sets that + * flag on the table partition's row, not on its indexes (see + * MarkInheritDetached). So the immediate parent link is always live. + */ + Assert(!detach_pending); + + rel->rd_partparent = parentOid; + rel->rd_partparentvalid = true; + return parentOid; +} + /* * index_get_partition * Return the OID of index of the given partition that is a child diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index 33ec5bfde4c..196ad46d77d 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -786,14 +786,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, * This will have the effect of also treating that second * index as arbiter. * - * XXX get_partition_ancestors scans pg_inherits, which is not - * only slow, but also means the catalog snapshot can get - * invalidated each time through the loop (cf. - * GetNonHistoricCatalogSnapshot). Consider a syscache or - * some other way to cache? + * Use the already-open index relation so its immediate parent + * can be cached on the relcache entry, avoiding repeated + * pg_inherits scans in this loop. */ indexoid = RelationGetRelid(leaf_part_rri->ri_IndexRelationDescs[listidx]); - ancestors = get_partition_ancestors(indexoid); + ancestors = + get_partition_index_ancestors(leaf_part_rri->ri_IndexRelationDescs[listidx]); INJECTION_POINT("exec-init-partition-after-get-partition-ancestors", NULL); if (ancestors != NIL && diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 19c4ff6e75e..6713734f320 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -1218,6 +1218,8 @@ retry: relation->rd_partcheck = NIL; relation->rd_partcheckvalid = false; relation->rd_partcheckcxt = NULL; + relation->rd_partparent = InvalidOid; + relation->rd_partparentvalid = false; /* * initialize access method information @@ -2318,6 +2320,8 @@ RelationReloadIndexInfo(Relation relation) RelationGetRelid(relation)); relp = (Form_pg_class) GETSTRUCT(pg_class_tuple); memcpy(relation->rd_rel, relp, CLASS_TUPLE_SIZE); + relation->rd_partparent = InvalidOid; + relation->rd_partparentvalid = false; /* Reload reloptions in case they changed */ if (relation->rd_options) pfree(relation->rd_options); @@ -6500,6 +6504,8 @@ load_relcache_init_file(bool shared) rel->rd_partcheck = NIL; rel->rd_partcheckvalid = false; rel->rd_partcheckcxt = NULL; + rel->rd_partparent = InvalidOid; + rel->rd_partparentvalid = false; rel->rd_indexprs = NIL; rel->rd_indpred = NIL; rel->rd_exclops = NULL; diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h index a93cf081dd2..6b7064b8ecf 100644 --- a/src/include/catalog/partition.h +++ b/src/include/catalog/partition.h @@ -21,6 +21,7 @@ extern Oid get_partition_parent(Oid relid, bool even_if_detached); extern List *get_partition_ancestors(Oid relid); +extern List *get_partition_index_ancestors(Relation indexRel); extern Oid index_get_partition(Relation partition, Oid indexId); extern List *map_partition_varattnos(List *expr, int fromrel_varno, Relation to_rel, Relation from_rel); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 89c159b133f..0a7a5a5927b 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -148,6 +148,11 @@ typedef struct RelationData bool rd_partcheckvalid; /* true if list has been computed */ MemoryContext rd_partcheckcxt; /* private cxt for rd_partcheck, if any */ + /* data managed by RelationGetPartitionParent: */ + Oid rd_partparent; /* OID of immediate partition parent, or + * InvalidOid; valid only if rd_partparentvalid */ + bool rd_partparentvalid; /* true if rd_partparent has been computed */ + /* data managed by RelationGetIndexList: */ List *rd_indexlist; /* list of OIDs of indexes on relation */ Oid rd_pkindex; /* OID of (deferrable?) primary key, if any */ diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out index 34e2e7ee355..fdabb23ca13 100644 --- a/src/test/regress/expected/insert_conflict.out +++ b/src/test/regress/expected/insert_conflict.out @@ -1042,6 +1042,34 @@ insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do selec (1 row) drop table parted_conflict_test; +-- check that a cached index parent is invalidated when its partition is +-- detached and attached to another partitioned table +create table reparent_conflict_a (i int primary key) partition by range (i); +create table reparent_conflict_b (i int primary key) partition by range (i); +create table reparent_conflict_child partition of reparent_conflict_a + for values from (0) to (10); +insert into reparent_conflict_a values (1) + on conflict (i) do update set i = excluded.i; +begin; +alter table reparent_conflict_a detach partition reparent_conflict_child; +alter table reparent_conflict_b attach partition reparent_conflict_child + for values from (0) to (10); +insert into reparent_conflict_b values (1) + on conflict (i) do update set i = excluded.i; +commit; +-- check the reverse invalidation when reparenting is rolled back +begin; +savepoint reparent_conflict_savepoint; +alter table reparent_conflict_b detach partition reparent_conflict_child; +alter table reparent_conflict_a attach partition reparent_conflict_child + for values from (0) to (10); +insert into reparent_conflict_a values (1) + on conflict (i) do update set i = excluded.i; +rollback to reparent_conflict_savepoint; +insert into reparent_conflict_b values (1) + on conflict (i) do update set i = excluded.i; +commit; +drop table reparent_conflict_a, reparent_conflict_b; -- test behavior of inserting a conflicting tuple into an intermediate -- partitioning level create table parted_conflict (a int primary key, b text) partition by range (a); diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql index a5a84d1d4b8..03a5198b1ac 100644 --- a/src/test/regress/sql/insert_conflict.sql +++ b/src/test/regress/sql/insert_conflict.sql @@ -596,6 +596,36 @@ insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do selec drop table parted_conflict_test; +-- check that a cached index parent is invalidated when its partition is +-- detached and attached to another partitioned table +create table reparent_conflict_a (i int primary key) partition by range (i); +create table reparent_conflict_b (i int primary key) partition by range (i); +create table reparent_conflict_child partition of reparent_conflict_a + for values from (0) to (10); +insert into reparent_conflict_a values (1) + on conflict (i) do update set i = excluded.i; +begin; +alter table reparent_conflict_a detach partition reparent_conflict_child; +alter table reparent_conflict_b attach partition reparent_conflict_child + for values from (0) to (10); +insert into reparent_conflict_b values (1) + on conflict (i) do update set i = excluded.i; +commit; + +-- check the reverse invalidation when reparenting is rolled back +begin; +savepoint reparent_conflict_savepoint; +alter table reparent_conflict_b detach partition reparent_conflict_child; +alter table reparent_conflict_a attach partition reparent_conflict_child + for values from (0) to (10); +insert into reparent_conflict_a values (1) + on conflict (i) do update set i = excluded.i; +rollback to reparent_conflict_savepoint; +insert into reparent_conflict_b values (1) + on conflict (i) do update set i = excluded.i; +commit; +drop table reparent_conflict_a, reparent_conflict_b; + -- test behavior of inserting a conflicting tuple into an intermediate -- partitioning level create table parted_conflict (a int primary key, b text) partition by range (a); -- 2.43.0