Avoid creating duplicate cached plans for inherited FK constraints.
authorTom Lane
Wed, 10 Mar 2021 19:22:31 +0000 (14:22 -0500)
committerTom Lane
Wed, 10 Mar 2021 19:22:31 +0000 (14:22 -0500)
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

src/backend/utils/adt/ri_triggers.c
src/test/regress/expected/foreign_key.out
src/test/regress/sql/foreign_key.sql

index 6e3a41062fcc49a40cbe0ca867b332d29747d65b..09a2ad28814d94a26f0e98f88173cf439a8cdb07 100644 (file)
@@ -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 */
@@ -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,
@@ -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)
  * ----------
@@ -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;
 }
 
@@ -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;
@@ -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
  *
@@ -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 */
index 07bd5b6434f9718221e2e6f3629c27047096fc5e..7386f4d6359a94ea0bb0c6415bdbf49f156c0b2c 100644 (file)
@@ -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
index c5c9011afcb861ca3afe8cf056062807b6ab18ee..67aa20435d20abfafa52be0cfb4b782b0a96a73e 100644 (file)
@@ -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;