Re: plan cache overhead on plpgsql expression - Mailing list pgsql-hackers

From Tom Lane
Subject Re: plan cache overhead on plpgsql expression
Date
Msg-id 10353.1585247879@sss.pgh.pa.us
Whole thread Raw
In response to Re: plan cache overhead on plpgsql expression  (Tom Lane <tgl@sss.pgh.pa.us>)
Responses Re: plan cache overhead on plpgsql expression  (Andres Freund <andres@anarazel.de>)
List pgsql-hackers
I wrote:
> I had a thought about a possibly-cleaner way to do this.  We could invent
> a resowner function, say ResourceOwnerReleaseAllPlanCacheRefs, that
> explicitly releases all plancache pins it knows about.  So plpgsql
> would not call the regular ResourceOwnerRelease entry point at all,
> but just call that and then ResourceOwnerDelete, again relying on the
> assertions therein to catch anything not released.

Here's a version that does it like that.  This does seem marginally
nicer than the other way.  I have a feeling that at some point we'll
want to expose resowners' contents more generally, but I'm not quite
sure what the use-cases will be, so I don't want to design that now.

Also, I studied the question of DO blocks' eval_estate + resowner
more carefully, and eventually concluded that the way it's being
done is okay --- it doesn't leak memory, as I'd first suspected.
But it's surely underdocumented, so I added some comments about it.
I also concluded as part of that study that it's probably best if
we *do* make the resowner parentage different in the two cases
after all.  So this has the "shared" resowner as a child of
TopTransactionResourceOwner after all (which means we don't need
to delete it explicitly), while a DO block's private resowner is
standalone (so it needs an explicit deletion).

Testing that reminded me of the other regression test failure I'd seen
when I first tried to do it: select_parallel.sql shows a WARNING about
a plancache leak in a parallel worker process.  When I looked into the
reason for that, it turned out that some cowboy has split
XACT_EVENT_COMMIT into XACT_EVENT_COMMIT and
XACT_EVENT_PARALLEL_COMMIT (where the latter is used in parallel
workers) without bothering to fix the collateral damage to plpgsql.
So plpgsql_xact_cb isn't doing any cleanup in parallel workers, and
hasn't been for a couple of releases.  The bad effects of that are
probably limited given that the worker process will exit after
committing, but I still think that that part of this patch is a bug
fix that needs to be back-patched.  (Just looking at what
FreeExecutorState does, I wonder whether jit_release_context has any
side-effects that are visible outside the process?  But I bet I can
make a test case that shows issues even without JIT, based on the
failure to call ExprContext shutdown callbacks.)

Anyway, I think this is committable now.

            regards, tom lane

diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index 70a9c34..193df8a 100644
--- a/src/pl/plpgsql/src/Makefile
+++ b/src/pl/plpgsql/src/Makefile
@@ -33,8 +33,8 @@ DATA = plpgsql.control plpgsql--1.0.sql
 REGRESS_OPTS = --dbname=$(PL_TESTDB)

 REGRESS = plpgsql_call plpgsql_control plpgsql_copy plpgsql_domain \
-    plpgsql_record plpgsql_cache plpgsql_transaction plpgsql_trap \
-    plpgsql_trigger plpgsql_varprops
+    plpgsql_record plpgsql_cache plpgsql_simple plpgsql_transaction \
+    plpgsql_trap plpgsql_trigger plpgsql_varprops

 # where to find gen_keywordlist.pl and subsidiary files
 TOOLSDIR = $(top_srcdir)/src/tools
diff --git a/src/pl/plpgsql/src/expected/plpgsql_simple.out b/src/pl/plpgsql/src/expected/plpgsql_simple.out
new file mode 100644
index 0000000..5a2fefa
--- /dev/null
+++ b/src/pl/plpgsql/src/expected/plpgsql_simple.out
@@ -0,0 +1,68 @@
+--
+-- Tests for plpgsql's handling of "simple" expressions
+--
+-- Check that changes to an inline-able function are handled correctly
+create function simplesql(int) returns int language sql
+as 'select $1';
+create function simplecaller() returns int language plpgsql
+as $$
+declare
+  sum int := 0;
+begin
+  for n in 1..10 loop
+    sum := sum + simplesql(n);
+    if n = 5 then
+      create or replace function simplesql(int) returns int language sql
+      as 'select $1 + 100';
+    end if;
+  end loop;
+  return sum;
+end$$;
+select simplecaller();
+ simplecaller
+--------------
+          555
+(1 row)
+
+-- Check that changes in search path are dealt with correctly
+create schema simple1;
+create function simple1.simpletarget(int) returns int language plpgsql
+as $$begin return $1; end$$;
+create function simpletarget(int) returns int language plpgsql
+as $$begin return $1 + 100; end$$;
+create or replace function simplecaller() returns int language plpgsql
+as $$
+declare
+  sum int := 0;
+begin
+  for n in 1..10 loop
+    sum := sum + simpletarget(n);
+    if n = 5 then
+      set local search_path = 'simple1';
+    end if;
+  end loop;
+  return sum;
+end$$;
+select simplecaller();
+ simplecaller
+--------------
+          555
+(1 row)
+
+-- try it with non-volatile functions, too
+alter function simple1.simpletarget(int) immutable;
+alter function simpletarget(int) immutable;
+select simplecaller();
+ simplecaller
+--------------
+          555
+(1 row)
+
+-- make sure flushing local caches changes nothing
+\c -
+select simplecaller();
+ simplecaller
+--------------
+          555
+(1 row)
+
diff --git a/src/pl/plpgsql/src/sql/plpgsql_simple.sql b/src/pl/plpgsql/src/sql/plpgsql_simple.sql
new file mode 100644
index 0000000..8a95768
--- /dev/null
+++ b/src/pl/plpgsql/src/sql/plpgsql_simple.sql
@@ -0,0 +1,61 @@
+--
+-- Tests for plpgsql's handling of "simple" expressions
+--
+
+-- Check that changes to an inline-able function are handled correctly
+create function simplesql(int) returns int language sql
+as 'select $1';
+
+create function simplecaller() returns int language plpgsql
+as $$
+declare
+  sum int := 0;
+begin
+  for n in 1..10 loop
+    sum := sum + simplesql(n);
+    if n = 5 then
+      create or replace function simplesql(int) returns int language sql
+      as 'select $1 + 100';
+    end if;
+  end loop;
+  return sum;
+end$$;
+
+select simplecaller();
+
+
+-- Check that changes in search path are dealt with correctly
+create schema simple1;
+
+create function simple1.simpletarget(int) returns int language plpgsql
+as $$begin return $1; end$$;
+
+create function simpletarget(int) returns int language plpgsql
+as $$begin return $1 + 100; end$$;
+
+create or replace function simplecaller() returns int language plpgsql
+as $$
+declare
+  sum int := 0;
+begin
+  for n in 1..10 loop
+    sum := sum + simpletarget(n);
+    if n = 5 then
+      set local search_path = 'simple1';
+    end if;
+  end loop;
+  return sum;
+end$$;
+
+select simplecaller();
+
+-- try it with non-volatile functions, too
+alter function simple1.simpletarget(int) immutable;
+alter function simpletarget(int) immutable;
+
+select simplecaller();
+
+-- make sure flushing local caches changes nothing
+\c -
+
+select simplecaller();
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index dbae18d..8e27b03 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -1278,6 +1278,160 @@ ReleaseCachedPlan(CachedPlan *plan, bool useResOwner)
 }

 /*
+ * CachedPlanAllowsSimpleValidityCheck: can we use CachedPlanIsSimplyValid?
+ *
+ * This function, together with CachedPlanIsSimplyValid, provides a fast path
+ * for revalidating "simple" generic plans.  The core requirement to be simple
+ * is that the plan must not require taking any locks, which translates to
+ * not touching any tables; this happens to match up well with an important
+ * use-case in PL/pgSQL.  This function tests whether that's true, along
+ * with checking some other corner cases that we'd rather not bother with
+ * handling in the fast path.  (Note that it's still possible for such a plan
+ * to be invalidated, for example due to a change in a function that was
+ * inlined into the plan.)
+ *
+ * This must only be called on known-valid generic plans (eg, ones just
+ * returned by GetCachedPlan).  If it returns true, the caller may re-use
+ * the cached plan as long as CachedPlanIsSimplyValid returns true; that
+ * check is much cheaper than the full revalidation done by GetCachedPlan.
+ * Nonetheless, no required checks are omitted.
+ */
+bool
+CachedPlanAllowsSimpleValidityCheck(CachedPlanSource *plansource,
+                                    CachedPlan *plan)
+{
+    ListCell   *lc;
+
+    /* Sanity-check that the caller gave us a validated generic plan. */
+    Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
+    Assert(plan->magic == CACHEDPLAN_MAGIC);
+    Assert(plansource->is_valid);
+    Assert(plan->is_valid);
+    Assert(plan == plansource->gplan);
+
+    /* We don't support oneshot plans here. */
+    if (plansource->is_oneshot)
+        return false;
+    Assert(!plan->is_oneshot);
+
+    /*
+     * If the plan is dependent on RLS considerations, or it's transient,
+     * reject.  These things probably can't ever happen for table-free
+     * queries, but for safety's sake let's check.
+     */
+    if (plansource->dependsOnRLS)
+        return false;
+    if (plan->dependsOnRole)
+        return false;
+    if (TransactionIdIsValid(plan->saved_xmin))
+        return false;
+
+    /*
+     * Reject if AcquirePlannerLocks would have anything to do.  This is
+     * simplistic, but there's no need to inquire any more carefully; indeed,
+     * for current callers it shouldn't even be possible to hit any of these
+     * checks.
+     */
+    foreach(lc, plansource->query_list)
+    {
+        Query       *query = lfirst_node(Query, lc);
+
+        if (query->commandType == CMD_UTILITY)
+            return false;
+        if (query->rtable || query->cteList || query->hasSubLinks)
+            return false;
+    }
+
+    /*
+     * Reject if AcquireExecutorLocks would have anything to do.  This is
+     * probably unnecessary given the previous check, but let's be safe.
+     */
+    foreach(lc, plan->stmt_list)
+    {
+        PlannedStmt *plannedstmt = lfirst_node(PlannedStmt, lc);
+        ListCell   *lc2;
+
+        if (plannedstmt->commandType == CMD_UTILITY)
+            return false;
+
+        /*
+         * We have to grovel through the rtable because it's likely to contain
+         * an RTE_RESULT relation, rather than being totally empty.
+         */
+        foreach(lc2, plannedstmt->rtable)
+        {
+            RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc2);
+
+            if (rte->rtekind == RTE_RELATION)
+                return false;
+        }
+    }
+
+    /*
+     * Okay, it's simple.  Note that what we've primarily established here is
+     * that no locks need be taken before checking the plan's is_valid flag.
+     */
+    return true;
+}
+
+/*
+ * CachedPlanIsSimplyValid: quick check for plan still being valid
+ *
+ * This function must not be used unless CachedPlanAllowsSimpleValidityCheck
+ * previously said it was OK.
+ *
+ * If the plan is valid, and "owner" is not NULL, record a refcount on
+ * the plan in that resowner before returning.  It is caller's responsibility
+ * to be sure that a refcount is held on any plan that's being actively used.
+ *
+ * The code here is unconditionally safe as long as the only use of this
+ * CachedPlanSource is in connection with the particular CachedPlan pointer
+ * that's passed in.  If the plansource were being used for other purposes,
+ * it's possible that its generic plan could be invalidated and regenerated
+ * while the current caller wasn't looking, and then there could be a chance
+ * collision of address between this caller's now-stale plan pointer and the
+ * actual address of the new generic plan.  For current uses, that scenario
+ * can't happen; but with a plansource shared across multiple uses, it'd be
+ * advisable to also save plan->generation and verify that that still matches.
+ */
+bool
+CachedPlanIsSimplyValid(CachedPlanSource *plansource, CachedPlan *plan,
+                        ResourceOwner owner)
+{
+    /*
+     * Careful here: since the caller doesn't necessarily hold a refcount on
+     * the plan to start with, it's possible that "plan" is a dangling
+     * pointer.  Don't dereference it until we've verified that it still
+     * matches the plansource's gplan (which is either valid or NULL).
+     */
+    Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
+
+    /*
+     * Has cache invalidation fired on this plan?  We can check this right
+     * away since there are no locks that we'd need to acquire first.
+     */
+    if (!plansource->is_valid || plan != plansource->gplan || !plan->is_valid)
+        return false;
+
+    Assert(plan->magic == CACHEDPLAN_MAGIC);
+
+    /* Is the search_path still the same as when we made it? */
+    Assert(plansource->search_path != NULL);
+    if (!OverrideSearchPathMatchesCurrent(plansource->search_path))
+        return false;
+
+    /* It's still good.  Bump refcount if requested. */
+    if (owner)
+    {
+        ResourceOwnerEnlargePlanCacheRefs(owner);
+        plan->refcount++;
+        ResourceOwnerRememberPlanCacheRef(owner, plan);
+    }
+
+    return true;
+}
+
+/*
  * CachedPlanSetParentContext: move a CachedPlanSource to a new memory context
  *
  * This can only be applied to unsaved plans; once saved, a plan always
diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c
index 3c39e48..8bc2c4e 100644
--- a/src/backend/utils/resowner/resowner.c
+++ b/src/backend/utils/resowner/resowner.c
@@ -679,6 +679,30 @@ ResourceOwnerReleaseInternal(ResourceOwner owner,
 }

 /*
+ * ResourceOwnerReleaseAllPlanCacheRefs
+ *        Release the plancache references (only) held by this owner.
+ *
+ * We might eventually add similar functions for other resource types,
+ * but for now, only this is needed.
+ */
+void
+ResourceOwnerReleaseAllPlanCacheRefs(ResourceOwner owner)
+{
+    ResourceOwner save;
+    Datum        foundres;
+
+    save = CurrentResourceOwner;
+    CurrentResourceOwner = owner;
+    while (ResourceArrayGetAny(&(owner->planrefarr), &foundres))
+    {
+        CachedPlan *res = (CachedPlan *) DatumGetPointer(foundres);
+
+        ReleaseCachedPlan(res, true);
+    }
+    CurrentResourceOwner = save;
+}
+
+/*
  * ResourceOwnerDelete
  *        Delete an owner object and its descendants.
  *
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 6a5953c..037edd0 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -20,6 +20,8 @@
 #include "nodes/params.h"
 #include "tcop/cmdtag.h"
 #include "utils/queryenvironment.h"
+#include "utils/resowner.h"
+

 /* Forward declaration, to avoid including parsenodes.h here */
 struct RawStmt;
@@ -220,6 +222,12 @@ extern CachedPlan *GetCachedPlan(CachedPlanSource *plansource,
                                  QueryEnvironment *queryEnv);
 extern void ReleaseCachedPlan(CachedPlan *plan, bool useResOwner);

+extern bool CachedPlanAllowsSimpleValidityCheck(CachedPlanSource *plansource,
+                                                CachedPlan *plan);
+extern bool CachedPlanIsSimplyValid(CachedPlanSource *plansource,
+                                    CachedPlan *plan,
+                                    ResourceOwner owner);
+
 extern CachedExpression *GetCachedExpression(Node *expr);
 extern void FreeCachedExpression(CachedExpression *cexpr);

diff --git a/src/include/utils/resowner.h b/src/include/utils/resowner.h
index 850bf69..878f39c 100644
--- a/src/include/utils/resowner.h
+++ b/src/include/utils/resowner.h
@@ -71,6 +71,7 @@ extern void ResourceOwnerRelease(ResourceOwner owner,
                                  ResourceReleasePhase phase,
                                  bool isCommit,
                                  bool isTopLevel);
+extern void ResourceOwnerReleaseAllPlanCacheRefs(ResourceOwner owner);
 extern void ResourceOwnerDelete(ResourceOwner owner);
 extern ResourceOwner ResourceOwnerGetParent(ResourceOwner owner);
 extern void ResourceOwnerNewParent(ResourceOwner owner,
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index d3ad4fa..6f8ae2c 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -84,6 +84,13 @@ typedef struct
  * has its own simple-expression EState, which is cleaned up at exit from
  * plpgsql_inline_handler().  DO blocks still use the simple_econtext_stack,
  * though, so that subxact abort cleanup does the right thing.
+ *
+ * (However, if a DO block executes COMMIT or ROLLBACK, then exec_stmt_commit
+ * or exec_stmt_rollback will unlink it from the DO's simple-expression EState
+ * and create a new shared EState that will be used thenceforth.  The original
+ * EState will be cleaned up when we get back to plpgsql_inline_handler.  This
+ * is a bit ugly, but it isn't worth doing better, since scenarios like this
+ * can't result in indefinite accumulation of state trees.)
  */
 typedef struct SimpleEcontextStackEntry
 {
@@ -96,6 +103,15 @@ static EState *shared_simple_eval_estate = NULL;
 static SimpleEcontextStackEntry *simple_econtext_stack = NULL;

 /*
+ * In addition to the shared simple-eval EState, we have a shared resource
+ * owner that holds refcounts on the CachedPlans for any "simple" expressions
+ * we have evaluated in the current transaction.  This allows us to avoid
+ * continually grabbing and releasing a plan refcount when a simple expression
+ * is used over and over.
+ */
+static ResourceOwner shared_simple_eval_resowner = NULL;
+
+/*
  * Memory management within a plpgsql function generally works with three
  * contexts:
  *
@@ -314,7 +330,8 @@ static int    exec_stmt_set(PLpgSQL_execstate *estate,
 static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
                                  PLpgSQL_function *func,
                                  ReturnSetInfo *rsi,
-                                 EState *simple_eval_estate);
+                                 EState *simple_eval_estate,
+                                 ResourceOwner simple_eval_resowner);
 static void exec_eval_cleanup(PLpgSQL_execstate *estate);

 static void exec_prepare_plan(PLpgSQL_execstate *estate,
@@ -440,16 +457,19 @@ static char *format_preparedparamsdata(PLpgSQL_execstate *estate,
  *
  * This is also used to execute inline code blocks (DO blocks).  The only
  * difference that this code is aware of is that for a DO block, we want
- * to use a private simple_eval_estate, which is created and passed in by
- * the caller.  For regular functions, pass NULL, which implies using
- * shared_simple_eval_estate.  (When using a private simple_eval_estate,
+ * to use a private simple_eval_estate and a private simple_eval_resowner,
+ * which are created and passed in by the caller.  For regular functions,
+ * pass NULL, which implies using shared_simple_eval_estate and
+ * shared_simple_eval_resowner.  (When using a private simple_eval_estate,
  * we must also use a private cast hashtable, but that's taken care of
  * within plpgsql_estate_setup.)
  * ----------
  */
 Datum
 plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
-                      EState *simple_eval_estate, bool atomic)
+                      EState *simple_eval_estate,
+                      ResourceOwner simple_eval_resowner,
+                      bool atomic)
 {
     PLpgSQL_execstate estate;
     ErrorContextCallback plerrcontext;
@@ -460,7 +480,7 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
      * Setup the execution state
      */
     plpgsql_estate_setup(&estate, func, (ReturnSetInfo *) fcinfo->resultinfo,
-                         simple_eval_estate);
+                         simple_eval_estate, simple_eval_resowner);
     estate.atomic = atomic;

     /*
@@ -897,7 +917,7 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
     /*
      * Setup the execution state
      */
-    plpgsql_estate_setup(&estate, func, NULL, NULL);
+    plpgsql_estate_setup(&estate, func, NULL, NULL, NULL);
     estate.trigdata = trigdata;

     /*
@@ -1135,7 +1155,7 @@ plpgsql_exec_event_trigger(PLpgSQL_function *func, EventTriggerData *trigdata)
     /*
      * Setup the execution state
      */
-    plpgsql_estate_setup(&estate, func, NULL, NULL);
+    plpgsql_estate_setup(&estate, func, NULL, NULL, NULL);
     estate.evtrigdata = trigdata;

     /*
@@ -2319,6 +2339,7 @@ exec_stmt_call(PLpgSQL_execstate *estate, PLpgSQL_stmt_call *stmt)
          * some internal state.
          */
         estate->simple_eval_estate = NULL;
+        estate->simple_eval_resowner = NULL;
         plpgsql_create_econtext(estate);
     }

@@ -3874,7 +3895,8 @@ static void
 plpgsql_estate_setup(PLpgSQL_execstate *estate,
                      PLpgSQL_function *func,
                      ReturnSetInfo *rsi,
-                     EState *simple_eval_estate)
+                     EState *simple_eval_estate,
+                     ResourceOwner simple_eval_resowner)
 {
     HASHCTL        ctl;

@@ -3965,6 +3987,11 @@ plpgsql_estate_setup(PLpgSQL_execstate *estate,
         estate->cast_hash = shared_cast_hash;
         estate->cast_hash_context = shared_cast_context;
     }
+    /* likewise for the simple-expression resource owner */
+    if (simple_eval_resowner)
+        estate->simple_eval_resowner = simple_eval_resowner;
+    else
+        estate->simple_eval_resowner = shared_simple_eval_resowner;

     /*
      * We start with no stmt_mcontext; one will be created only if needed.
@@ -4825,6 +4852,7 @@ exec_stmt_commit(PLpgSQL_execstate *estate, PLpgSQL_stmt_commit *stmt)
     }

     estate->simple_eval_estate = NULL;
+    estate->simple_eval_resowner = NULL;
     plpgsql_create_econtext(estate);

     return PLPGSQL_RC_OK;
@@ -4847,6 +4875,7 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
     }

     estate->simple_eval_estate = NULL;
+    estate->simple_eval_resowner = NULL;
     plpgsql_create_econtext(estate);

     return PLPGSQL_RC_OK;
@@ -6059,8 +6088,6 @@ loop_exit:
  * someone might redefine a SQL function that had been inlined into the simple
  * expression.  That cannot cause a simple expression to become non-simple (or
  * vice versa), but we do have to handle replacing the expression tree.
- * Fortunately it's normally inexpensive to call SPI_plan_get_cached_plan for
- * a simple expression.
  *
  * Note: if pass-by-reference, the result is in the eval_mcontext.
  * It will be freed when exec_eval_cleanup is done.
@@ -6076,7 +6103,7 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
 {
     ExprContext *econtext = estate->eval_econtext;
     LocalTransactionId curlxid = MyProc->lxid;
-    CachedPlan *cplan;
+    ParamListInfo paramLI;
     void       *save_setup_arg;
     bool        need_snapshot;
     MemoryContext oldcontext;
@@ -6090,29 +6117,92 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
     /*
      * If expression is in use in current xact, don't touch it.
      */
-    if (expr->expr_simple_in_use && expr->expr_simple_lxid == curlxid)
+    if (unlikely(expr->expr_simple_in_use) &&
+        expr->expr_simple_lxid == curlxid)
         return false;

     /*
-     * Revalidate cached plan, so that we will notice if it became stale. (We
-     * need to hold a refcount while using the plan, anyway.)  If replanning
-     * is needed, do that work in the eval_mcontext.
+     * Check to see if the cached plan has been invalidated.  If not, and this
+     * is the first use in the current transaction, save a plan refcount in
+     * the simple-expression resowner.
      */
-    oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
-    cplan = SPI_plan_get_cached_plan(expr->plan);
-    MemoryContextSwitchTo(oldcontext);
+    if (likely(CachedPlanIsSimplyValid(expr->expr_simple_plansource,
+                                       expr->expr_simple_plan,
+                                       (expr->expr_simple_plan_lxid != curlxid ?
+                                        estate->simple_eval_resowner : NULL))))
+    {
+        /*
+         * It's still good, so just remember that we have a refcount on the
+         * plan in the current transaction.  (If we already had one, this
+         * assignment is a no-op.)
+         */
+        expr->expr_simple_plan_lxid = curlxid;
+    }
+    else
+    {
+        /* Need to replan */
+        CachedPlan *cplan;

-    /*
-     * We can't get a failure here, because the number of CachedPlanSources in
-     * the SPI plan can't change from what exec_simple_check_plan saw; it's a
-     * property of the raw parsetree generated from the query text.
-     */
-    Assert(cplan != NULL);
+        /*
+         * If we have a valid refcount on some previous version of the plan,
+         * release it, so we don't leak plans intra-transaction.
+         */
+        if (expr->expr_simple_plan_lxid == curlxid)
+        {
+            ResourceOwner saveResourceOwner = CurrentResourceOwner;

-    /* If it got replanned, update our copy of the simple expression */
-    if (cplan->generation != expr->expr_simple_generation)
-    {
+            CurrentResourceOwner = estate->simple_eval_resowner;
+            ReleaseCachedPlan(expr->expr_simple_plan, true);
+            CurrentResourceOwner = saveResourceOwner;
+            expr->expr_simple_plan = NULL;
+            expr->expr_simple_plan_lxid = InvalidLocalTransactionId;
+        }
+
+        /* Do the replanning work in the eval_mcontext */
+        oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
+        cplan = SPI_plan_get_cached_plan(expr->plan);
+        MemoryContextSwitchTo(oldcontext);
+
+        /*
+         * We can't get a failure here, because the number of
+         * CachedPlanSources in the SPI plan can't change from what
+         * exec_simple_check_plan saw; it's a property of the raw parsetree
+         * generated from the query text.
+         */
+        Assert(cplan != NULL);
+
+        /*
+         * These tests probably can't fail either, but if they do, cope by
+         * declaring the plan to be non-simple.  On success, we'll acquire a
+         * refcount on the new plan, stored in simple_eval_resowner.
+         */
+        if (CachedPlanAllowsSimpleValidityCheck(expr->expr_simple_plansource,
+                                                cplan) &&
+            CachedPlanIsSimplyValid(expr->expr_simple_plansource, cplan,
+                                    estate->simple_eval_resowner))
+        {
+            /* Remember that we have the refcount */
+            expr->expr_simple_plan = cplan;
+            expr->expr_simple_plan_lxid = curlxid;
+        }
+        else
+        {
+            /* Release SPI_plan_get_cached_plan's refcount */
+            ReleaseCachedPlan(cplan, true);
+            /* Mark expression as non-simple, and fail */
+            expr->expr_simple_expr = NULL;
+            return false;
+        }
+
+        /*
+         * SPI_plan_get_cached_plan acquired a plan refcount stored in the
+         * active resowner.  We don't need that anymore, so release it.
+         */
+        ReleaseCachedPlan(cplan, true);
+
+        /* Extract desired scalar expression from cached plan */
         exec_save_simple_expr(expr, cplan);
+
         /* better recheck r/w safety, as it could change due to inlining */
         if (expr->rwparam >= 0)
             exec_check_rw_parameter(expr, expr->rwparam);
@@ -6128,16 +6218,24 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
      * Set up ParamListInfo to pass to executor.  For safety, save and restore
      * estate->paramLI->parserSetupArg around our use of the param list.
      */
-    save_setup_arg = estate->paramLI->parserSetupArg;
+    paramLI = estate->paramLI;
+    save_setup_arg = paramLI->parserSetupArg;

-    econtext->ecxt_param_list_info = setup_param_list(estate, expr);
+    /*
+     * We can skip using setup_param_list() in favor of just doing this
+     * unconditionally, because there's no need for the optimization of
+     * possibly setting ecxt_param_list_info to NULL; we've already forced use
+     * of a generic plan.
+     */
+    paramLI->parserSetupArg = (void *) expr;
+    econtext->ecxt_param_list_info = paramLI;

     /*
      * Prepare the expression for execution, if it's not been done already in
      * the current transaction.  (This will be forced to happen if we called
      * exec_save_simple_expr above.)
      */
-    if (expr->expr_simple_lxid != curlxid)
+    if (unlikely(expr->expr_simple_lxid != curlxid))
     {
         oldcontext = MemoryContextSwitchTo(estate->simple_eval_estate->es_query_cxt);
         expr->expr_simple_state =
@@ -6185,7 +6283,7 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,

     econtext->ecxt_param_list_info = NULL;

-    estate->paramLI->parserSetupArg = save_setup_arg;
+    paramLI->parserSetupArg = save_setup_arg;

     if (need_snapshot)
         PopActiveSnapshot();
@@ -6193,11 +6291,6 @@ exec_eval_simple_expr(PLpgSQL_execstate *estate,
     MemoryContextSwitchTo(oldcontext);

     /*
-     * Now we can release our refcount on the cached plan.
-     */
-    ReleaseCachedPlan(cplan, true);
-
-    /*
      * That's it.
      */
     return true;
@@ -7984,10 +8077,35 @@ exec_simple_check_plan(PLpgSQL_execstate *estate, PLpgSQL_expr *expr)
     /* Can't fail, because we checked for a single CachedPlanSource above */
     Assert(cplan != NULL);

-    /* Share the remaining work with replan code path */
-    exec_save_simple_expr(expr, cplan);
+    /*
+     * Verify that plancache.c thinks the plan is simple enough to use
+     * CachedPlanIsSimplyValid.  Given the restrictions above, it's unlikely
+     * that this could fail, but if it does, just treat plan as not simple.
+     */
+    if (CachedPlanAllowsSimpleValidityCheck(plansource, cplan))
+    {
+        /*
+         * OK, use CachedPlanIsSimplyValid to save a refcount on the plan in
+         * the simple-expression resowner.  This shouldn't fail either, but if
+         * somehow it does, again we can cope by treating plan as not simple.
+         */
+        if (CachedPlanIsSimplyValid(plansource, cplan,
+                                    estate->simple_eval_resowner))
+        {
+            /* Remember that we have the refcount */
+            expr->expr_simple_plansource = plansource;
+            expr->expr_simple_plan = cplan;
+            expr->expr_simple_plan_lxid = MyProc->lxid;
+
+            /* Share the remaining work with the replan code path */
+            exec_save_simple_expr(expr, cplan);
+        }
+    }

-    /* Release our plan refcount */
+    /*
+     * Release the plan refcount obtained by SPI_plan_get_cached_plan.  (This
+     * refcount is held by the wrong resowner, so we can't just repurpose it.)
+     */
     ReleaseCachedPlan(cplan, true);
 }

@@ -8060,7 +8178,6 @@ exec_save_simple_expr(PLpgSQL_expr *expr, CachedPlan *cplan)
      * current transaction".
      */
     expr->expr_simple_expr = tle_expr;
-    expr->expr_simple_generation = cplan->generation;
     expr->expr_simple_state = NULL;
     expr->expr_simple_in_use = false;
     expr->expr_simple_lxid = InvalidLocalTransactionId;
@@ -8196,7 +8313,7 @@ exec_set_found(PLpgSQL_execstate *estate, bool state)
  *
  * We may need to create a new shared_simple_eval_estate too, if there's not
  * one already for the current transaction.  The EState will be cleaned up at
- * transaction end.
+ * transaction end.  Ditto for shared_simple_eval_resowner.
  */
 static void
 plpgsql_create_econtext(PLpgSQL_execstate *estate)
@@ -8208,8 +8325,11 @@ plpgsql_create_econtext(PLpgSQL_execstate *estate)
      * one already in the current transaction.  The EState is made a child of
      * TopTransactionContext so it will have the right lifespan.
      *
-     * Note that this path is never taken when executing a DO block; the
-     * required EState was already made by plpgsql_inline_handler.
+     * Note that this path is never taken when beginning a DO block; the
+     * required EState was already made by plpgsql_inline_handler.  However,
+     * if the DO block executes COMMIT or ROLLBACK, then we'll come here and
+     * make a shared EState to use for the rest of the DO block.  That's OK;
+     * see the comments above struct SimpleEcontextStackEntry.
      */
     if (estate->simple_eval_estate == NULL)
     {
@@ -8225,6 +8345,18 @@ plpgsql_create_econtext(PLpgSQL_execstate *estate)
     }

     /*
+     * Likewise for the simple-expression resource owner.
+     */
+    if (estate->simple_eval_resowner == NULL)
+    {
+        if (shared_simple_eval_resowner == NULL)
+            shared_simple_eval_resowner =
+                ResourceOwnerCreate(TopTransactionResourceOwner,
+                                    "PL/pgSQL simple expressions");
+        estate->simple_eval_resowner = shared_simple_eval_resowner;
+    }
+
+    /*
      * Create a child econtext for the current function.
      */
     estate->eval_econtext = CreateExprContext(estate->simple_eval_estate);
@@ -8270,29 +8402,40 @@ plpgsql_destroy_econtext(PLpgSQL_execstate *estate)
  * plpgsql_xact_cb --- post-transaction-commit-or-abort cleanup
  *
  * If a simple-expression EState was created in the current transaction,
- * it has to be cleaned up.
+ * it has to be cleaned up.  The same for the simple-expression resowner.
  */
 void
 plpgsql_xact_cb(XactEvent event, void *arg)
 {
     /*
-     * If we are doing a clean transaction shutdown, free the EState (so that
-     * any remaining resources will be released correctly). In an abort, we
-     * expect the regular abort recovery procedures to release everything of
-     * interest.
+     * If we are doing a clean transaction shutdown, free the EState and tell
+     * the resowner to release whatever plancache references it has, so that
+     * all remaining resources will be released correctly.  (We don't need to
+     * actually delete the resowner here; deletion of the
+     * TopTransactionResourceOwner will take care of that.)
+     *
+     * In an abort, we expect the regular abort recovery procedures to release
+     * everything of interest, so just clear our pointers.
      */
-    if (event == XACT_EVENT_COMMIT || event == XACT_EVENT_PREPARE)
+    if (event == XACT_EVENT_COMMIT ||
+        event == XACT_EVENT_PARALLEL_COMMIT ||
+        event == XACT_EVENT_PREPARE)
     {
         simple_econtext_stack = NULL;

         if (shared_simple_eval_estate)
             FreeExecutorState(shared_simple_eval_estate);
         shared_simple_eval_estate = NULL;
+        if (shared_simple_eval_resowner)
+            ResourceOwnerReleaseAllPlanCacheRefs(shared_simple_eval_resowner);
+        shared_simple_eval_resowner = NULL;
     }
-    else if (event == XACT_EVENT_ABORT)
+    else if (event == XACT_EVENT_ABORT ||
+             event == XACT_EVENT_PARALLEL_ABORT)
     {
         simple_econtext_stack = NULL;
         shared_simple_eval_estate = NULL;
+        shared_simple_eval_resowner = NULL;
     }
 }

diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c
index b434818..97cf849 100644
--- a/src/pl/plpgsql/src/pl_handler.c
+++ b/src/pl/plpgsql/src/pl_handler.c
@@ -262,7 +262,9 @@ plpgsql_call_handler(PG_FUNCTION_ARGS)
             retval = (Datum) 0;
         }
         else
-            retval = plpgsql_exec_function(func, fcinfo, NULL, !nonatomic);
+            retval = plpgsql_exec_function(func, fcinfo,
+                                           NULL, NULL,
+                                           !nonatomic);
     }
     PG_FINALLY();
     {
@@ -297,6 +299,7 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
     PLpgSQL_function *func;
     FmgrInfo    flinfo;
     EState       *simple_eval_estate;
+    ResourceOwner simple_eval_resowner;
     Datum        retval;
     int            rc;

@@ -323,22 +326,33 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
     flinfo.fn_oid = InvalidOid;
     flinfo.fn_mcxt = CurrentMemoryContext;

-    /* Create a private EState for simple-expression execution */
+    /*
+     * Create a private EState and resowner for simple-expression execution.
+     * Notice that these are NOT tied to transaction-level resources; they
+     * must survive any COMMIT/ROLLBACK the DO block executes, since we will
+     * unconditionally try to clean them up below.  (Hence, be wary of adding
+     * anything that could fail between here and the PG_TRY block.)
+     */
     simple_eval_estate = CreateExecutorState();
+    simple_eval_resowner =
+        ResourceOwnerCreate(NULL, "PL/pgSQL DO block simple expressions");

     /* And run the function */
     PG_TRY();
     {
-        retval = plpgsql_exec_function(func, fake_fcinfo, simple_eval_estate, codeblock->atomic);
+        retval = plpgsql_exec_function(func, fake_fcinfo,
+                                       simple_eval_estate,
+                                       simple_eval_resowner,
+                                       codeblock->atomic);
     }
     PG_CATCH();
     {
         /*
          * We need to clean up what would otherwise be long-lived resources
          * accumulated by the failed DO block, principally cached plans for
-         * statements (which can be flushed with plpgsql_free_function_memory)
-         * and execution trees for simple expressions, which are in the
-         * private EState.
+         * statements (which can be flushed by plpgsql_free_function_memory),
+         * execution trees for simple expressions, which are in the private
+         * EState, and cached-plan refcounts held by the private resowner.
          *
          * Before releasing the private EState, we must clean up any
          * simple_econtext_stack entries pointing into it, which we can do by
@@ -351,8 +365,10 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
                            GetCurrentSubTransactionId(),
                            0, NULL);

-        /* Clean up the private EState */
+        /* Clean up the private EState and resowner */
         FreeExecutorState(simple_eval_estate);
+        ResourceOwnerReleaseAllPlanCacheRefs(simple_eval_resowner);
+        ResourceOwnerDelete(simple_eval_resowner);

         /* Function should now have no remaining use-counts ... */
         func->use_count--;
@@ -366,8 +382,10 @@ plpgsql_inline_handler(PG_FUNCTION_ARGS)
     }
     PG_END_TRY();

-    /* Clean up the private EState */
+    /* Clean up the private EState and resowner */
     FreeExecutorState(simple_eval_estate);
+    ResourceOwnerReleaseAllPlanCacheRefs(simple_eval_resowner);
+    ResourceOwnerDelete(simple_eval_resowner);

     /* Function should now have no remaining use-counts ... */
     func->use_count--;
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index 69df330..1af2595 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -231,12 +231,21 @@ typedef struct PLpgSQL_expr

     /* fields for "simple expression" fast-path execution: */
     Expr       *expr_simple_expr;    /* NULL means not a simple expr */
-    int            expr_simple_generation; /* plancache generation we checked */
     Oid            expr_simple_type;    /* result type Oid, if simple */
     int32        expr_simple_typmod; /* result typmod, if simple */
     bool        expr_simple_mutable;    /* true if simple expr is mutable */

     /*
+     * If the expression was ever determined to be simple, we remember its
+     * CachedPlanSource and CachedPlan here.  If expr_simple_plan_lxid matches
+     * current LXID, then we hold a refcount on expr_simple_plan in the
+     * current transaction.  Otherwise we need to get one before re-using it.
+     */
+    CachedPlanSource *expr_simple_plansource;    /* extracted from "plan" */
+    CachedPlan *expr_simple_plan;    /* extracted from "plan" */
+    LocalTransactionId expr_simple_plan_lxid;
+
+    /*
      * if expr is simple AND prepared in current transaction,
      * expr_simple_state and expr_simple_in_use are valid. Test validity by
      * seeing if expr_simple_lxid matches current LXID.  (If not,
@@ -1082,8 +1091,9 @@ typedef struct PLpgSQL_execstate
      */
     ParamListInfo paramLI;

-    /* EState to use for "simple" expression evaluation */
+    /* EState and resowner to use for "simple" expression evaluation */
     EState       *simple_eval_estate;
+    ResourceOwner simple_eval_resowner;

     /* lookup table to use for executing type casts */
     HTAB       *cast_hash;
@@ -1268,6 +1278,7 @@ extern void _PG_init(void);
 extern Datum plpgsql_exec_function(PLpgSQL_function *func,
                                    FunctionCallInfo fcinfo,
                                    EState *simple_eval_estate,
+                                   ResourceOwner simple_eval_resowner,
                                    bool atomic);
 extern HeapTuple plpgsql_exec_trigger(PLpgSQL_function *func,
                                       TriggerData *trigdata);
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 5ff7824..2ec2301 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -126,6 +126,11 @@
  * namespaceUser is the userid the path has been computed for.
  *
  * Note: all data pointed to by these List variables is in TopMemoryContext.
+ *
+ * activePathGeneration is incremented whenever the effective values of
+ * activeSearchPath/activeCreationNamespace/activeTempCreationPending change.
+ * This can be used to quickly detect whether any change has happened since
+ * a previous examination of the search path state.
  */

 /* These variables define the actually active state: */
@@ -138,6 +143,9 @@ static Oid    activeCreationNamespace = InvalidOid;
 /* if true, activeCreationNamespace is wrong, it should be temp namespace */
 static bool activeTempCreationPending = false;

+/* current generation counter; make sure this is never zero */
+static uint64 activePathGeneration = 1;
+
 /* These variables are the values last derived from namespace_search_path: */

 static List *baseSearchPath = NIL;
@@ -3373,6 +3381,7 @@ GetOverrideSearchPath(MemoryContext context)
         schemas = list_delete_first(schemas);
     }
     result->schemas = schemas;
+    result->generation = activePathGeneration;

     MemoryContextSwitchTo(oldcxt);

@@ -3393,12 +3402,18 @@ CopyOverrideSearchPath(OverrideSearchPath *path)
     result->schemas = list_copy(path->schemas);
     result->addCatalog = path->addCatalog;
     result->addTemp = path->addTemp;
+    result->generation = path->generation;

     return result;
 }

 /*
  * OverrideSearchPathMatchesCurrent - does path match current setting?
+ *
+ * This is tested over and over in some common code paths, and in the typical
+ * scenario where the active search path seldom changes, it'll always succeed.
+ * We make that case fast by keeping a generation counter that is advanced
+ * whenever the active search path changes.
  */
 bool
 OverrideSearchPathMatchesCurrent(OverrideSearchPath *path)
@@ -3408,6 +3423,10 @@ OverrideSearchPathMatchesCurrent(OverrideSearchPath *path)

     recomputeNamespacePath();

+    /* Quick out if already known equal to active path. */
+    if (path->generation == activePathGeneration)
+        return true;
+
     /* We scan down the activeSearchPath to see if it matches the input. */
     lc = list_head(activeSearchPath);

@@ -3440,6 +3459,13 @@ OverrideSearchPathMatchesCurrent(OverrideSearchPath *path)
     }
     if (lc)
         return false;
+
+    /*
+     * Update path->generation so that future tests will return quickly, so
+     * long as the active search path doesn't change.
+     */
+    path->generation = activePathGeneration;
+
     return true;
 }

@@ -3510,6 +3536,14 @@ PushOverrideSearchPath(OverrideSearchPath *newpath)
     activeCreationNamespace = entry->creationNamespace;
     activeTempCreationPending = false;    /* XXX is this OK? */

+    /*
+     * We always increment activePathGeneration when pushing/popping an
+     * override path.  In current usage, these actions always change the
+     * effective path state, so there's no value in checking to see if it
+     * didn't change.
+     */
+    activePathGeneration++;
+
     MemoryContextSwitchTo(oldcxt);
 }

@@ -3551,6 +3585,9 @@ PopOverrideSearchPath(void)
         activeCreationNamespace = baseCreationNamespace;
         activeTempCreationPending = baseTempCreationPending;
     }
+
+    /* As above, the generation always increments. */
+    activePathGeneration++;
 }


@@ -3707,6 +3744,7 @@ recomputeNamespacePath(void)
     ListCell   *l;
     bool        temp_missing;
     Oid            firstNS;
+    bool        pathChanged;
     MemoryContext oldcxt;

     /* Do nothing if an override search spec is active. */
@@ -3814,18 +3852,31 @@ recomputeNamespacePath(void)
         oidlist = lcons_oid(myTempNamespace, oidlist);

     /*
-     * Now that we've successfully built the new list of namespace OIDs, save
-     * it in permanent storage.
+     * We want to detect the case where the effective value of the base search
+     * path variables didn't change.  As long as we're doing so, we can avoid
+     * copying the OID list unncessarily.
      */
-    oldcxt = MemoryContextSwitchTo(TopMemoryContext);
-    newpath = list_copy(oidlist);
-    MemoryContextSwitchTo(oldcxt);
+    if (baseCreationNamespace == firstNS &&
+        baseTempCreationPending == temp_missing &&
+        equal(oidlist, baseSearchPath))
+    {
+        pathChanged = false;
+    }
+    else
+    {
+        pathChanged = true;
+
+        /* Must save OID list in permanent storage. */
+        oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+        newpath = list_copy(oidlist);
+        MemoryContextSwitchTo(oldcxt);

-    /* Now safe to assign to state variables. */
-    list_free(baseSearchPath);
-    baseSearchPath = newpath;
-    baseCreationNamespace = firstNS;
-    baseTempCreationPending = temp_missing;
+        /* Now safe to assign to state variables. */
+        list_free(baseSearchPath);
+        baseSearchPath = newpath;
+        baseCreationNamespace = firstNS;
+        baseTempCreationPending = temp_missing;
+    }

     /* Mark the path valid. */
     baseSearchPathValid = true;
@@ -3836,6 +3887,16 @@ recomputeNamespacePath(void)
     activeCreationNamespace = baseCreationNamespace;
     activeTempCreationPending = baseTempCreationPending;

+    /*
+     * Bump the generation only if something actually changed.  (Notice that
+     * what we compared to was the old state of the base path variables; so
+     * this does not deal with the situation where we have just popped an
+     * override path and restored the prior state of the base path.  Instead
+     * we rely on the override-popping logic to have bumped the generation.)
+     */
+    if (pathChanged)
+        activePathGeneration++;
+
     /* Clean up. */
     pfree(rawname);
     list_free(namelist);
@@ -4054,6 +4115,8 @@ AtEOXact_Namespace(bool isCommit, bool parallel)
         activeSearchPath = baseSearchPath;
         activeCreationNamespace = baseCreationNamespace;
         activeTempCreationPending = baseTempCreationPending;
+        /* Always bump generation --- see note in recomputeNamespacePath */
+        activePathGeneration++;
     }
 }

@@ -4109,6 +4172,8 @@ AtEOSubXact_Namespace(bool isCommit, SubTransactionId mySubid,
         overrideStack = list_delete_first(overrideStack);
         list_free(entry->searchPath);
         pfree(entry);
+        /* Always bump generation --- see note in recomputeNamespacePath */
+        activePathGeneration++;
     }

     /* Activate the next level down. */
@@ -4118,6 +4183,12 @@ AtEOSubXact_Namespace(bool isCommit, SubTransactionId mySubid,
         activeSearchPath = entry->searchPath;
         activeCreationNamespace = entry->creationNamespace;
         activeTempCreationPending = false;    /* XXX is this OK? */
+
+        /*
+         * It's probably unnecessary to bump generation here, but this should
+         * not be a performance-critical case, so better to be over-cautious.
+         */
+        activePathGeneration++;
     }
     else
     {
@@ -4125,6 +4196,12 @@ AtEOSubXact_Namespace(bool isCommit, SubTransactionId mySubid,
         activeSearchPath = baseSearchPath;
         activeCreationNamespace = baseCreationNamespace;
         activeTempCreationPending = baseTempCreationPending;
+
+        /*
+         * If we popped an override stack entry, then we already bumped the
+         * generation above.  If we did not, then the above assignments did
+         * nothing and we need not bump the generation.
+         */
     }
 }

@@ -4264,6 +4341,7 @@ InitializeSearchPath(void)
         activeSearchPath = baseSearchPath;
         activeCreationNamespace = baseCreationNamespace;
         activeTempCreationPending = baseTempCreationPending;
+        activePathGeneration++; /* pro forma */
     }
     else
     {
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 3e3a192..2456c08 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -49,12 +49,17 @@ typedef enum TempNamespaceStatus

 /*
  *    Structure for xxxOverrideSearchPath functions
+ *
+ * The generation counter is private to namespace.c and shouldn't be touched
+ * by other code.  It can be initialized to zero if necessary (that means
+ * "not known equal to the current active path").
  */
 typedef struct OverrideSearchPath
 {
     List       *schemas;        /* OIDs of explicitly named schemas */
     bool        addCatalog;        /* implicitly prepend pg_catalog? */
     bool        addTemp;        /* implicitly prepend temp schema? */
+    uint64        generation;        /* for quick detection of equality to active */
 } OverrideSearchPath;

 /*

pgsql-hackers by date:

Previous
From: Robert Haas
Date:
Subject: Re: backup manifests
Next
From: Erik Rijkers
Date:
Subject: Re: proposal \gcsv