From 1881125433760cb52c54188463cd6a3222aa0bc9 Mon Sep 17 00:00:00 2001 From: Teletele Date: Fri, 9 Oct 2026 16:58:36 +0800 Subject: [PATCH v2] Reanalyze cached queries when ENR metadata changes Ephemeral named relation metadata comes from the current query environment and can change without catalog invalidation. A cached RangeTblEntry keeps the row estimate from parse analysis, so even a custom plan can use stale cardinality estimates for a trigger's transition tables. Record the named tuplestore RTEs in cached query sources that have a raw parse tree. Before reuse, compare their relation binding and row estimates with the current query environment. Reuse the existing invalidation path when an ENR is missing or its metadata differs, so analysis, rewriting, and planning use the current metadata. Matching metadata preserves both the analyzed query and its cached plan. Keep pointers to these RTEs in the query context to avoid walking query trees on each execution, including executions of queries without ENRs. Rebuild the dependency list when copying a cached query source. Handle nested queries and queries wrapped in utility statements. Conservatively reanalyze ENRs with directly supplied tuple descriptors, since their structural changes are not covered by catalog invalidation. One-shot and pre-analyzed-only sources retain the existing behavior. --- src/backend/utils/cache/plancache.c | 103 ++++++++++++++++++++++++++++ src/include/utils/plancache.h | 1 + 2 files changed, 104 insertions(+) diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index a1b406cee29..f5ca5a53321 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 if + * their metadata changes, because it 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,9 @@ 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 ExtractENRDependenciesWalker(Node *node, void *context); +static bool ENRMetadataChanged(CachedPlanSource *plansource, + QueryEnvironment *queryEnv); static List *RevalidateCachedQuery(CachedPlanSource *plansource, QueryEnvironment *queryEnv); static bool CheckCachedPlan(CachedPlanSource *plansource); @@ -238,6 +244,7 @@ CreateCachedPlan(const RawStmt *raw_parse_tree, plansource->query_list = NIL; plansource->relationOids = NIL; plansource->invalItems = NIL; + plansource->enrRtes = NIL; plansource->search_path = NULL; plansource->query_context = NULL; plansource->rewriteRoleId = InvalidOid; @@ -337,6 +344,7 @@ CreateOneShotCachedPlan(RawStmt *raw_parse_tree, plansource->query_list = NIL; plansource->relationOids = NIL; plansource->invalItems = NIL; + plansource->enrRtes = NIL; plansource->search_path = NULL; plansource->query_context = NULL; plansource->rewriteRoleId = InvalidOid; @@ -457,6 +465,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) + ExtractENRDependenciesWalker((Node *) querytree_list, + &plansource->enrRtes); /* Update RLS info as well. */ plansource->rewriteRoleId = GetUserId(); @@ -675,6 +687,81 @@ BuildingPlanRequiresSnapshot(CachedPlanSource *plansource) return false; } +/* + * Collect pointers to ENR RTEs in the cached query trees. Keeping these in + * query_context lets us check ENR metadata before reuse without walking the + * query trees again. Queries without ENRs have no such work on reuse. + */ +static bool +ExtractENRDependenciesWalker(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 ExtractENRDependenciesWalker((Node *) query, context); + } + return query_tree_walker(query, ExtractENRDependenciesWalker, + context, QTW_EXAMINE_RTES_BEFORE); + } + if (IsA(node, RangeTblEntry)) + { + RangeTblEntry *rte = (RangeTblEntry *) node; + List **enrRtes = (List **) context; + + if (rte->rtekind == RTE_NAMEDTUPLESTORE) + *enrRtes = lappend(*enrRtes, rte); + + /* + * Leave recursion into RTE contents to query_tree_walker to avoid + * visiting subqueries twice. + */ + return false; + } + return expression_tree_walker(node, ExtractENRDependenciesWalker, context); +} + +/* + * ENR metadata is supplied by the current query environment and can change + * without catalog invalidation. Compare it with the metadata captured by + * parse analysis; the tuplestore itself can change without affecting planning. + */ +static bool +ENRMetadataChanged(CachedPlanSource *plansource, QueryEnvironment *queryEnv) +{ + ListCell *lc; + + foreach(lc, plansource->enrRtes) + { + RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); + EphemeralNamedRelationMetadata enr; + + enr = get_visible_ENR_metadata(queryEnv, rte->enrname); + if (enr == NULL || enr->enrtype != ENR_NAMED_TUPLESTORE || + enr->reliddesc != rte->relid || enr->enrtuples != rte->enrtuples) + return true; + + /* + * A catalog relation's descriptor is protected by the normal catalog + * invalidation machinery. A directly supplied TupleDesc has no such + * protection, so conservatively re-analyze in that case. + */ + if (!OidIsValid(enr->reliddesc)) + return true; + } + + return false; +} + /* * RevalidateCachedQuery: ensure validity of analyzed-and-rewritten query tree. * @@ -742,6 +829,15 @@ RevalidateCachedQuery(CachedPlanSource *plansource, plansource->rewriteRowSecurity != row_security)) plansource->is_valid = false; + /* + * A transition table's row count can change with each statement. When + * ENR metadata changes, discard the analyzed query as well as its plan: + * replanning alone would still use the old row estimate in the cached + * RTE. The invalidation path below also releases any generic plan. + */ + if (plansource->is_valid && ENRMetadataChanged(plansource, queryEnv)) + plansource->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 +870,7 @@ RevalidateCachedQuery(CachedPlanSource *plansource, plansource->query_list = NIL; plansource->relationOids = NIL; plansource->invalItems = NIL; + plansource->enrRtes = NIL; plansource->search_path = NULL; /* @@ -913,6 +1010,8 @@ RevalidateCachedQuery(CachedPlanSource *plansource, &plansource->relationOids, &plansource->invalItems, &plansource->dependsOnRLS); + if (plansource->raw_parse_tree != NULL) + ExtractENRDependenciesWalker((Node *) qlist, &plansource->enrRtes); /* Update RLS info as well. */ plansource->rewriteRoleId = GetUserId(); @@ -1737,6 +1836,10 @@ CopyCachedPlan(CachedPlanSource *plansource) newsource->query_list = copyObject(plansource->query_list); newsource->relationOids = copyObject(plansource->relationOids); newsource->invalItems = copyObject(plansource->invalItems); + /* The dependencies must point into the copied query trees. */ + if (plansource->enrRtes != NIL) + ExtractENRDependenciesWalker((Node *) newsource->query_list, + &newsource->enrRtes); if (plansource->search_path) newsource->search_path = CopySearchPathMatcher(plansource->search_path); newsource->query_context = querytree_context; diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index a0355e79c28..5da43945d5a 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -123,6 +123,7 @@ typedef struct CachedPlanSource List *query_list; /* list of Query nodes, or NIL if not valid */ List *relationOids; /* OIDs of relations the queries depend on */ List *invalItems; /* other dependencies, as PlanInvalItems */ + List *enrRtes; /* ENR RTEs in query_list, for metadata checks */ struct SearchPathMatcher *search_path; /* search_path used for parsing * and planning */ MemoryContext query_context; /* context holding the above, or NULL */ -- 2.43.0