From 044d49760e16fdc876d582d83830293b829022e3 Mon Sep 17 00:00:00 2001 From: Teletele Date: Fri, 9 Oct 2026 14:47:18 +0800 Subject: [PATCH v1] Reanalyze cached queries that reference ENRs Ephemeral named relation metadata belongs to the current query environment and can change without catalog invalidation. In particular, transition tables contain the rows affected by the current statement, while a cached RangeTblEntry retains the row estimate from the initial analysis. Even a custom plan therefore uses stale cardinality estimates. Mark cached query sources that have a raw parse tree and reference named tuplestores for reanalysis before reuse. Use the existing invalidation path to repeat analysis, rewriting, and planning with the current query environment. Detect references in nested and utility-wrapped queries. Keep the existing behavior for one-shot and pre-analyzed query sources. Queries without ENR references continue to use the normal plan cache. --- src/backend/utils/cache/plancache.c | 71 +++++++++++++++++++++++++++++ src/include/utils/plancache.h | 1 + 2 files changed, 72 insertions(+) diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index a1b406cee29..a418a689dd3 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -18,6 +18,9 @@ * and then planning is performed as normal. We also force re-analysis and * re-planning if the active search_path is different from the previous time * or, if RLS is involved, if the user changes or the RLS environment changes. + * Queries with a raw parse tree that reference ENRs are also re-analyzed on + * each use, because their metadata comes from the current query environment + * rather than the catalogs. * * Note that if the sinval was a result of user DDL actions, parse analysis * could throw an error, for example if a column referenced by the query is @@ -95,6 +98,7 @@ static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_l static void ReleaseGenericPlan(CachedPlanSource *plansource); static bool StmtPlanRequiresRevalidation(CachedPlanSource *plansource); static bool BuildingPlanRequiresSnapshot(CachedPlanSource *plansource); +static bool QueryHasENRWalker(Node *node, void *context); static List *RevalidateCachedQuery(CachedPlanSource *plansource, QueryEnvironment *queryEnv); static bool CheckCachedPlan(CachedPlanSource *plansource); @@ -243,6 +247,7 @@ CreateCachedPlan(const RawStmt *raw_parse_tree, plansource->rewriteRoleId = InvalidOid; plansource->rewriteRowSecurity = false; plansource->dependsOnRLS = false; + plansource->requiresENRReanalysis = false; plansource->gplan = NULL; plansource->is_oneshot = false; plansource->is_complete = false; @@ -342,6 +347,7 @@ CreateOneShotCachedPlan(RawStmt *raw_parse_tree, plansource->rewriteRoleId = InvalidOid; plansource->rewriteRowSecurity = false; plansource->dependsOnRLS = false; + plansource->requiresENRReanalysis = false; plansource->gplan = NULL; plansource->is_oneshot = true; plansource->is_complete = false; @@ -457,6 +463,10 @@ CompleteCachedPlan(CachedPlanSource *plansource, &plansource->relationOids, &plansource->invalItems, &plansource->dependsOnRLS); + /* Only raw parse sources can refresh ENR metadata by re-analysis. */ + if (plansource->raw_parse_tree != NULL) + plansource->requiresENRReanalysis = + QueryHasENRWalker((Node *) querytree_list, NULL); /* Update RLS info as well. */ plansource->rewriteRoleId = GetUserId(); @@ -675,6 +685,46 @@ BuildingPlanRequiresSnapshot(CachedPlanSource *plansource) return false; } +/* + * ENR metadata is specific to the current query environment, so queries that + * reference ENRs need re-analysis before reuse. Cache this property when + * each analyzed query tree is built to avoid another tree walk when executing + * ordinary queries. + */ +static bool +QueryHasENRWalker(Node *node, void *context) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + /* + * EXPLAIN, DECLARE CURSOR, and CREATE TABLE AS can wrap ENR queries + * in utilityStmt, which query_tree_walker does not visit. + */ + if (query->commandType == CMD_UTILITY) + { + query = UtilityContainsQuery(query->utilityStmt); + return QueryHasENRWalker((Node *) query, context); + } + return query_tree_walker(query, QueryHasENRWalker, + context, QTW_EXAMINE_RTES_BEFORE); + } + if (IsA(node, RangeTblEntry)) + { + RangeTblEntry *rte = (RangeTblEntry *) node; + + /* + * Leave recursion into RTE contents to query_tree_walker to avoid + * visiting subqueries twice. + */ + return rte->rtekind == RTE_NAMEDTUPLESTORE; + } + return expression_tree_walker(node, QueryHasENRWalker, context); +} + /* * RevalidateCachedQuery: ensure validity of analyzed-and-rewritten query tree. * @@ -742,6 +792,22 @@ RevalidateCachedQuery(CachedPlanSource *plansource, plansource->rewriteRowSecurity != row_security)) plansource->is_valid = false; + /* + * ENR metadata can change between executions without catalog invalidation + * messages. For example, a trigger's transition table contains the rows + * affected by the current statement. Re-analysis is needed to obtain + * this metadata from queryEnv: discarding just the execution plan would + * still use the old row estimate stored in the cached RTE. The same + * query-environment dependency applies to named tuplestores registered by + * other SPI callers. + */ + if (plansource->is_valid && plansource->requiresENRReanalysis) + { + plansource->is_valid = false; + if (plansource->gplan) + plansource->gplan->is_valid = false; + } + /* * If the query is currently valid, acquire locks on the referenced * objects; then check again. We need to do it this way to cover the race @@ -774,6 +840,7 @@ RevalidateCachedQuery(CachedPlanSource *plansource, plansource->query_list = NIL; plansource->relationOids = NIL; plansource->invalItems = NIL; + plansource->requiresENRReanalysis = false; plansource->search_path = NULL; /* @@ -913,6 +980,9 @@ RevalidateCachedQuery(CachedPlanSource *plansource, &plansource->relationOids, &plansource->invalItems, &plansource->dependsOnRLS); + if (plansource->raw_parse_tree != NULL) + plansource->requiresENRReanalysis = + QueryHasENRWalker((Node *) qlist, NULL); /* Update RLS info as well. */ plansource->rewriteRoleId = GetUserId(); @@ -1743,6 +1813,7 @@ CopyCachedPlan(CachedPlanSource *plansource) newsource->rewriteRoleId = plansource->rewriteRoleId; newsource->rewriteRowSecurity = plansource->rewriteRowSecurity; newsource->dependsOnRLS = plansource->dependsOnRLS; + newsource->requiresENRReanalysis = plansource->requiresENRReanalysis; newsource->gplan = NULL; diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index a0355e79c28..ca9b77b2f7c 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -129,6 +129,7 @@ typedef struct CachedPlanSource Oid rewriteRoleId; /* Role ID we did rewriting for */ bool rewriteRowSecurity; /* row_security used during rewrite */ bool dependsOnRLS; /* is rewritten query specific to the above? */ + bool requiresENRReanalysis; /* refresh current ENR metadata */ /* If we have a generic plan, this is a reference-counted link to it: */ struct CachedPlan *gplan; /* generic plan, or NULL if not valid */ /* Some state flags: */ -- 2.43.0