Skip to content

Commit c3ffe34

Browse files
committed
Avoid creating duplicate cached plans for inherited FK constraints.
When a foreign key constraint is applied to a partitioned table, each leaf partition inherits a similar FK constraint. We were processing all of those constraints independently, meaning that in large partitioning trees we'd build up large collections of cached FK-checking query plans. However, in all cases but one, the generated queries are actually identical for all members of the inheritance tree (because, in most cases, the query only mentions the topmost table of the other side of the FK relationship). So we can share a single cached plan among all the partitions, saving memory, not to mention time to build and maintain the cached plans. Keisuke Kuroda and Amit Langote Discussion: https://postgr.es/m/cab4b85d-9292-967d-adf2-be0d803c3e23@nttcom.co.jp_1
1 parent b124363 commit c3ffe34

File tree

3 files changed

+97
-4
lines changed

3 files changed

+97
-4
lines changed

src/backend/utils/adt/ri_triggers.c

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ typedef struct RI_ConstraintInfo
101101
{
102102
Oid constraint_id; /* OID of pg_constraint entry (hash key) */
103103
bool valid; /* successfully initialized? */
104-
uint32 oidHashValue; /* hash value of pg_constraint OID */
104+
Oid constraint_root_id; /* OID of topmost ancestor constraint;
105+
* same as constraint_id if not inherited */
106+
uint32 oidHashValue; /* hash value of constraint_id */
107+
uint32 rootHashValue; /* hash value of constraint_root_id */
105108
NameData conname; /* name of the FK constraint */
106109
Oid pk_relid; /* referenced relation */
107110
Oid fk_relid; /* referencing relation */
@@ -207,6 +210,7 @@ static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname,
207210
static const RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger,
208211
Relation trig_rel, bool rel_is_pk);
209212
static const RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid);
213+
static Oid get_ri_constraint_root(Oid constrOid);
210214
static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes,
211215
RI_QueryKey *qkey, Relation fk_rel, Relation pk_rel);
212216
static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -1892,7 +1896,7 @@ ri_GenerateQualCollation(StringInfo buf, Oid collation)
18921896
* Construct a hashtable key for a prepared SPI plan of an FK constraint.
18931897
*
18941898
* key: output argument, *key is filled in based on the other arguments
1895-
* riinfo: info from pg_constraint entry
1899+
* riinfo: info derived from pg_constraint entry
18961900
* constr_queryno: an internal number identifying the query type
18971901
* (see RI_PLAN_XXX constants at head of file)
18981902
* ----------
@@ -1902,10 +1906,27 @@ ri_BuildQueryKey(RI_QueryKey *key, const RI_ConstraintInfo *riinfo,
19021906
int32 constr_queryno)
19031907
{
19041908
/*
1909+
* Inherited constraints with a common ancestor can share ri_query_cache
1910+
* entries for all query types except RI_PLAN_CHECK_LOOKUPPK_FROM_PK.
1911+
* Except in that case, the query processes the other table involved in
1912+
* the FK constraint (i.e., not the table on which the trigger has been
1913+
* fired), and so it will be the same for all members of the inheritance
1914+
* tree. So we may use the root constraint's OID in the hash key, rather
1915+
* than the constraint's own OID. This avoids creating duplicate SPI
1916+
* plans, saving lots of work and memory when there are many partitions
1917+
* with similar FK constraints.
1918+
*
1919+
* (Note that we must still have a separate RI_ConstraintInfo for each
1920+
* constraint, because partitions can have different column orders,
1921+
* resulting in different pk_attnums[] or fk_attnums[] array contents.)
1922+
*
19051923
* We assume struct RI_QueryKey contains no padding bytes, else we'd need
19061924
* to use memset to clear them.
19071925
*/
1908-
key->constr_id = riinfo->constraint_id;
1926+
if (constr_queryno != RI_PLAN_CHECK_LOOKUPPK_FROM_PK)
1927+
key->constr_id = riinfo->constraint_root_id;
1928+
else
1929+
key->constr_id = riinfo->constraint_id;
19091930
key->constr_queryno = constr_queryno;
19101931
}
19111932

@@ -2051,8 +2072,15 @@ ri_LoadConstraintInfo(Oid constraintOid)
20512072

20522073
/* And extract data */
20532074
Assert(riinfo->constraint_id == constraintOid);
2075+
if (OidIsValid(conForm->conparentid))
2076+
riinfo->constraint_root_id =
2077+
get_ri_constraint_root(conForm->conparentid);
2078+
else
2079+
riinfo->constraint_root_id = constraintOid;
20542080
riinfo->oidHashValue = GetSysCacheHashValue1(CONSTROID,
20552081
ObjectIdGetDatum(constraintOid));
2082+
riinfo->rootHashValue = GetSysCacheHashValue1(CONSTROID,
2083+
ObjectIdGetDatum(riinfo->constraint_root_id));
20562084
memcpy(&riinfo->conname, &conForm->conname, sizeof(NameData));
20572085
riinfo->pk_relid = conForm->confrelid;
20582086
riinfo->fk_relid = conForm->conrelid;
@@ -2082,6 +2110,30 @@ ri_LoadConstraintInfo(Oid constraintOid)
20822110
return riinfo;
20832111
}
20842112

2113+
/*
2114+
* get_ri_constraint_root
2115+
* Returns the OID of the constraint's root parent
2116+
*/
2117+
static Oid
2118+
get_ri_constraint_root(Oid constrOid)
2119+
{
2120+
for (;;)
2121+
{
2122+
HeapTuple tuple;
2123+
Oid constrParentOid;
2124+
2125+
tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
2126+
if (!HeapTupleIsValid(tuple))
2127+
elog(ERROR, "cache lookup failed for constraint %u", constrOid);
2128+
constrParentOid = ((Form_pg_constraint) GETSTRUCT(tuple))->conparentid;
2129+
ReleaseSysCache(tuple);
2130+
if (!OidIsValid(constrParentOid))
2131+
break; /* we reached the root constraint */
2132+
constrOid = constrParentOid;
2133+
}
2134+
return constrOid;
2135+
}
2136+
20852137
/*
20862138
* Callback for pg_constraint inval events
20872139
*
@@ -2117,7 +2169,14 @@ InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue)
21172169
RI_ConstraintInfo *riinfo = dlist_container(RI_ConstraintInfo,
21182170
valid_link, iter.cur);
21192171

2120-
if (hashvalue == 0 || riinfo->oidHashValue == hashvalue)
2172+
/*
2173+
* We must invalidate not only entries directly matching the given
2174+
* hash value, but also child entries, in case the invalidation
2175+
* affects a root constraint.
2176+
*/
2177+
if (hashvalue == 0 ||
2178+
riinfo->oidHashValue == hashvalue ||
2179+
riinfo->rootHashValue == hashvalue)
21212180
{
21222181
riinfo->valid = false;
21232182
/* Remove invalidated entries from the list, too */

src/test/regress/expected/foreign_key.out

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,3 +2470,21 @@ DROP SCHEMA fkpart9 CASCADE;
24702470
NOTICE: drop cascades to 2 other objects
24712471
DETAIL: drop cascades to table fkpart9.pk
24722472
drop cascades to table fkpart9.fk
2473+
-- test that ri_Check_Pk_Match() scans the correct partition for a deferred
2474+
-- ON DELETE/UPDATE NO ACTION constraint
2475+
CREATE SCHEMA fkpart10
2476+
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
2477+
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
2478+
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
2479+
CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
2480+
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
2481+
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
2482+
BEGIN;
2483+
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
2484+
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
2485+
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
2486+
COMMIT;
2487+
DROP SCHEMA fkpart10 CASCADE;
2488+
NOTICE: drop cascades to 2 other objects
2489+
DETAIL: drop cascades to table fkpart10.tbl1
2490+
drop cascades to table fkpart10.tbl2

src/test/regress/sql/foreign_key.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1738,3 +1738,19 @@ DELETE FROM fkpart9.pk WHERE a=35;
17381738
SELECT * FROM fkpart9.pk;
17391739
SELECT * FROM fkpart9.fk;
17401740
DROP SCHEMA fkpart9 CASCADE;
1741+
1742+
-- test that ri_Check_Pk_Match() scans the correct partition for a deferred
1743+
-- ON DELETE/UPDATE NO ACTION constraint
1744+
CREATE SCHEMA fkpart10
1745+
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
1746+
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
1747+
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
1748+
CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
1749+
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
1750+
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
1751+
BEGIN;
1752+
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
1753+
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
1754+
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
1755+
COMMIT;
1756+
DROP SCHEMA fkpart10 CASCADE;

0 commit comments

Comments
 (0)