From f5077990fee5f9692d3f4779aa51fc4b93839883 Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Wed, 26 Aug 2026 12:24:09 -0700 Subject: [PATCH v2 08/17] Allow a costing pass to examine clauses under a second join type A clause caches one selectivity for JOIN_INNER and one for whatever other join type it is examined under, which suffices because a clause belongs to a single join. A pass that costs clauses under a join type of its own choosing would both read estimates meant for the real join and leave its own behind for that join to find, so the cache is bypassed for the duration of the pass. --- src/backend/optimizer/path/clausesel.c | 43 +++++++++++++++++++++++--- src/include/nodes/pathnodes.h | 3 ++ src/include/optimizer/optimizer.h | 2 ++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c index 25c4d177ad9..e12dedd2781 100644 --- a/src/backend/optimizer/path/clausesel.c +++ b/src/backend/optimizer/path/clausesel.c @@ -723,11 +723,16 @@ clause_selectivity_ext(PlannerInfo *root, * result won't change if we are switching the input relations or * considering a unique-ified case, so we only need one cache variable * for all non-JOIN_INNER cases. + * + * A speculative costing pass examines the clause under a join type of + * its own choosing, so it bypasses the cache entirely; see + * begin_speculative_costing(). */ - if (varRelid == 0 || - rinfo->num_base_rels == 0 || - (rinfo->num_base_rels == 1 && - bms_is_member(varRelid, rinfo->clause_relids))) + if (root->speculative_costing == 0 && + (varRelid == 0 || + rinfo->num_base_rels == 0 || + (rinfo->num_base_rels == 1 && + bms_is_member(varRelid, rinfo->clause_relids)))) { /* Cacheable --- do we already have the result? */ if (jointype == JOIN_INNER) @@ -975,3 +980,33 @@ clause_selectivity_ext(PlannerInfo *root, return s1; } + +/* + * begin_speculative_costing + * Enter a costing pass that examines clauses under a join type of its own + * choosing. + * + * A clause holds one cached selectivity for JOIN_INNER and one for whatever + * other join type it is examined under, which suffices because a clause + * belongs to a single join. Such a pass would both read estimates meant for + * the real join and leave its own behind for that join to find, so between + * here and end_speculative_costing() the cache is bypassed. An OR clause + * caches per-subclause selectivities in sub-RestrictInfos of its own, which a + * list of clauses would miss. + */ +void +begin_speculative_costing(PlannerInfo *root) +{ + root->speculative_costing++; +} + +/* + * end_speculative_costing + * Leave the costing pass entered by begin_speculative_costing(). + */ +void +end_speculative_costing(PlannerInfo *root) +{ + Assert(root->speculative_costing > 0); + root->speculative_costing--; +} diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 4dd8cc88486..96953d947fa 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -660,6 +660,9 @@ struct PlannerInfo /* true if a planner extension may replan this subquery */ bool assumeReplanning; + /* nesting depth of speculative costing; see clausesel.c */ + int speculative_costing; + /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no * RTE_GROUP RTE. diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h index cb6241e2bdd..deb4f922578 100644 --- a/src/include/optimizer/optimizer.h +++ b/src/include/optimizer/optimizer.h @@ -66,6 +66,8 @@ extern Selectivity clauselist_selectivity_ext(PlannerInfo *root, JoinType jointype, SpecialJoinInfo *sjinfo, bool use_extended_stats); +extern void begin_speculative_costing(PlannerInfo *root); +extern void end_speculative_costing(PlannerInfo *root); /* in path/costsize.c: */