Skip to content

Commit

Permalink
Avoid creating duplicate cached plans for inherited FK constraints.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tglsfdc committed Mar 10, 2021
1 parent b124363 commit c3ffe34
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 4 deletions.
67 changes: 63 additions & 4 deletions src/backend/utils/adt/ri_triggers.c
Expand Up @@ -101,7 +101,10 @@ typedef struct RI_ConstraintInfo
{
Oid constraint_id; /* OID of pg_constraint entry (hash key) */
bool valid; /* successfully initialized? */
uint32 oidHashValue; /* hash value of pg_constraint OID */
Oid constraint_root_id; /* OID of topmost ancestor constraint;
* same as constraint_id if not inherited */
uint32 oidHashValue; /* hash value of constraint_id */
uint32 rootHashValue; /* hash value of constraint_root_id */
NameData conname; /* name of the FK constraint */
Oid pk_relid; /* referenced relation */
Oid fk_relid; /* referencing relation */
Expand Down Expand Up @@ -207,6 +210,7 @@ static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname,
static const RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger,
Relation trig_rel, bool rel_is_pk);
static const RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid);
static Oid get_ri_constraint_root(Oid constrOid);
static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes,
RI_QueryKey *qkey, Relation fk_rel, Relation pk_rel);
static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
Expand Down Expand Up @@ -1892,7 +1896,7 @@ ri_GenerateQualCollation(StringInfo buf, Oid collation)
* Construct a hashtable key for a prepared SPI plan of an FK constraint.
*
* key: output argument, *key is filled in based on the other arguments
* riinfo: info from pg_constraint entry
* riinfo: info derived from pg_constraint entry
* constr_queryno: an internal number identifying the query type
* (see RI_PLAN_XXX constants at head of file)
* ----------
Expand All @@ -1902,10 +1906,27 @@ ri_BuildQueryKey(RI_QueryKey *key, const RI_ConstraintInfo *riinfo,
int32 constr_queryno)
{
/*
* Inherited constraints with a common ancestor can share ri_query_cache
* entries for all query types except RI_PLAN_CHECK_LOOKUPPK_FROM_PK.
* Except in that case, the query processes the other table involved in
* the FK constraint (i.e., not the table on which the trigger has been
* fired), and so it will be the same for all members of the inheritance
* tree. So we may use the root constraint's OID in the hash key, rather
* than the constraint's own OID. This avoids creating duplicate SPI
* plans, saving lots of work and memory when there are many partitions
* with similar FK constraints.
*
* (Note that we must still have a separate RI_ConstraintInfo for each
* constraint, because partitions can have different column orders,
* resulting in different pk_attnums[] or fk_attnums[] array contents.)
*
* We assume struct RI_QueryKey contains no padding bytes, else we'd need
* to use memset to clear them.
*/
key->constr_id = riinfo->constraint_id;
if (constr_queryno != RI_PLAN_CHECK_LOOKUPPK_FROM_PK)
key->constr_id = riinfo->constraint_root_id;
else
key->constr_id = riinfo->constraint_id;
key->constr_queryno = constr_queryno;
}

Expand Down Expand Up @@ -2051,8 +2072,15 @@ ri_LoadConstraintInfo(Oid constraintOid)

/* And extract data */
Assert(riinfo->constraint_id == constraintOid);
if (OidIsValid(conForm->conparentid))
riinfo->constraint_root_id =
get_ri_constraint_root(conForm->conparentid);
else
riinfo->constraint_root_id = constraintOid;
riinfo->oidHashValue = GetSysCacheHashValue1(CONSTROID,
ObjectIdGetDatum(constraintOid));
riinfo->rootHashValue = GetSysCacheHashValue1(CONSTROID,
ObjectIdGetDatum(riinfo->constraint_root_id));
memcpy(&riinfo->conname, &conForm->conname, sizeof(NameData));
riinfo->pk_relid = conForm->confrelid;
riinfo->fk_relid = conForm->conrelid;
Expand Down Expand Up @@ -2082,6 +2110,30 @@ ri_LoadConstraintInfo(Oid constraintOid)
return riinfo;
}

/*
* get_ri_constraint_root
* Returns the OID of the constraint's root parent
*/
static Oid
get_ri_constraint_root(Oid constrOid)
{
for (;;)
{
HeapTuple tuple;
Oid constrParentOid;

tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for constraint %u", constrOid);
constrParentOid = ((Form_pg_constraint) GETSTRUCT(tuple))->conparentid;
ReleaseSysCache(tuple);
if (!OidIsValid(constrParentOid))
break; /* we reached the root constraint */
constrOid = constrParentOid;
}
return constrOid;
}

/*
* Callback for pg_constraint inval events
*
Expand Down Expand Up @@ -2117,7 +2169,14 @@ InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue)
RI_ConstraintInfo *riinfo = dlist_container(RI_ConstraintInfo,
valid_link, iter.cur);

if (hashvalue == 0 || riinfo->oidHashValue == hashvalue)
/*
* We must invalidate not only entries directly matching the given
* hash value, but also child entries, in case the invalidation
* affects a root constraint.
*/
if (hashvalue == 0 ||
riinfo->oidHashValue == hashvalue ||
riinfo->rootHashValue == hashvalue)
{
riinfo->valid = false;
/* Remove invalidated entries from the list, too */
Expand Down
18 changes: 18 additions & 0 deletions src/test/regress/expected/foreign_key.out
Expand Up @@ -2470,3 +2470,21 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
-- test that ri_Check_Pk_Match() scans the correct partition for a deferred
-- ON DELETE/UPDATE NO ACTION constraint
CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
DROP SCHEMA fkpart10 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
16 changes: 16 additions & 0 deletions src/test/regress/sql/foreign_key.sql
Expand Up @@ -1738,3 +1738,19 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;

-- test that ri_Check_Pk_Match() scans the correct partition for a deferred
-- ON DELETE/UPDATE NO ACTION constraint
CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
DROP SCHEMA fkpart10 CASCADE;

0 comments on commit c3ffe34

Please sign in to comment.