Hi,
For SEMI/ANTI joins the selectivity is existence-based - whether an
outer row has at least one match - so it cannot be written as the
inner-join pair sum. It is estimated as follows.
First, for each side a single key distribution is built from the
COALESCE branches. Each branch is weighted by the probability of
reaching it (the product of the earlier branches' null fractions), so
the merged frequency of a value is:
F(value) = P(reach branch 1) * freq(value in branch 1)
+ P(reach branch 2) * freq(value in branch 2)
+ ...
Each side also carries its null fraction and distinct count.
Then the two keys are combined once:
sel = matchfreq + uncertainfrac * uncertain
matchfreq - total frequency of outer MCV values that also appear
on the inner side;
uncertain - the outer non-NULL mass the MCV does not cover, (1 -
outer_nullfrac) - seen;
uncertainfrac - the estimated match share of that tail,
clamp(inner_ndistinct / outer_ndistinct),
or 0.5 when a distinct count is unknown.
Finally sel is capped at 1 - outer_nullfrac, since a semi-join keeps
at most the non-NULL outer rows. This is the SEMI selectivity; the
join-size code inverts it for ANTI.
Best regards,
Egor Savelev,
Tantor Labs
ср, 23 сент. 2026 г. в 07:35, Ilia Evdokimov <[email protected]>:
>
> While reviewing v6, I noticed try_coalesce_eq() in eqjoinsel() returns
> before the function ever looks at sjinfo->jointype, so SEMI/ANTI joins
> get the same formula meant for INNER/LEFT/FULL. I'm not sure it is true.
> Shouldn't the fat path be restricted to INNER/LEFT/FULL until SEMI/ANTI
> get their own existence-base treatment?
>
> --
> Best regards,
> Ilia Evdokimov,
> Tantor Labs LLC,
> https://tantorlabs.com/
>
From 06e5c1db0610549382b0f4008cd850a28dfe6eeb Mon Sep 17 00:00:00 2001
From: prankware <[email protected]>
Date: Wed, 30 Sep 2026 17:59:17 +0300
Subject: [PATCH v7] Coalesce eqsel eqjoinsel
---
src/backend/utils/adt/selfuncs.c | 821 +++++++++++++++++++++++++++++--
1 file changed, 791 insertions(+), 30 deletions(-)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index e27ec9e5c25..de8feec3edb 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -304,6 +304,634 @@ eqsel(PG_FUNCTION_ARGS)
PG_RETURN_FLOAT8((float8) eqsel_internal(fcinfo, false));
}
+/*
+ * Extract the estimable branches of a CoalesceExpr. Strips RelabelType,
+ * skips NULL-constant branches, and stops at the first non-NULL Const
+ * (setting *terminates_with_const). Returns false if the expression
+ * is constant or no branches remain.
+ */
+static bool
+match_coalesce_join_side(Node *side,
+ List **stripped_args,
+ bool *terminates_with_const)
+{
+ CoalesceExpr *c;
+ ListCell *lc;
+ Node *firstarg;
+ List *result = NIL;
+
+ *terminates_with_const = false;
+ *stripped_args = NIL;
+
+ if (side == NULL || !IsA(side, CoalesceExpr))
+ return false;
+
+ c = (CoalesceExpr *) side;
+
+ firstarg = (Node *) linitial(c->args);
+
+ while (firstarg && IsA(firstarg, RelabelType))
+ firstarg = (Node *) ((RelabelType *) firstarg)->arg;
+ if (firstarg == NULL || IsA(firstarg, Const))
+ return false;
+
+ foreach(lc, c->args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+
+ /* strip RelabelType */
+ while (arg && IsA(arg, RelabelType))
+ arg = (Node *) ((RelabelType *) arg)->arg;
+
+ if (arg == NULL)
+ {
+ list_free(result);
+ return false;
+ }
+
+ if (IsA(arg, Const))
+ {
+ if (((Const *) arg)->consttype != c->coalescetype)
+ {
+ list_free(result);
+ return false;
+ }
+
+ /* NULL Const is skipped */
+ if (!((Const *) arg)->constisnull)
+ {
+ result = lappend(result, arg);
+ *terminates_with_const = true;
+ break;
+ }
+ }
+ else
+ {
+ if (exprType(arg) != c->coalescetype)
+ {
+ list_free(result);
+ return false;
+ }
+ result = lappend(result, arg);
+ }
+ }
+
+ if (result == NIL)
+ return false;
+
+ *stripped_args = result;
+ return true;
+}
+
+/*
+ * Fill prefix_probs[i] = Prod_{j<i} stanullfrac(args[j]) for a stripped
+ * COALESCE arg list, and set *side_nullfrac to the null fraction of the whole
+ * side: the expression is NULL only when every branch is NULL, so this is the
+ * product of all the branches' null fractions (a non-NULL Const branch is never
+ * NULL and makes the side never NULL). Returns false if a non-Const arg lacks
+ * statistics.
+ */
+static bool
+get_coalesce_prefix_probs(PlannerInfo *root, List *args, double *prefix_probs,
+ double *side_nullfrac)
+{
+ ListCell *lc;
+ int i = 0;
+ double prefix = 1.0;
+
+ foreach(lc, args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+
+ prefix_probs[i++] = prefix;
+
+ /* a Const terminates the list */
+ if (IsA(arg, Const))
+ {
+ /* a non-NULL Const is never NULL, so the side never is */
+ if (!((Const *) arg)->constisnull)
+ prefix = 0.0;
+ break;
+ }
+
+ {
+ VariableStatData vd;
+ double p_i;
+
+ examine_variable(root, arg, 0, &vd);
+ if (!HeapTupleIsValid(vd.statsTuple))
+ {
+ ReleaseVariableStats(vd);
+ return false;
+ }
+ p_i = ((Form_pg_statistic) GETSTRUCT(vd.statsTuple))->stanullfrac;
+ ReleaseVariableStats(vd);
+
+ if (p_i < 0.0 || p_i > 1.0)
+ return false;
+
+ prefix *= p_i;
+ }
+ }
+
+ *side_nullfrac = prefix;
+ return true;
+}
+
+/*
+ * Merge one value (frequency freq) into a key's MCV accumulator, combining
+ * equal values. The value is copied so it survives the caller freeing the
+ * stats slot it came from.
+ */
+static void
+coalesce_merge_value(Datum **values, double **freqs, int *nvalues, int *maxv,
+ Datum value, double freq, FmgrInfo *eqproc, Oid collation,
+ int16 typlen, bool typbyval)
+{
+ int i;
+
+ for (i = 0; i < *nvalues; i++)
+ {
+ if (DatumGetBool(FunctionCall2Coll(eqproc, collation, value, (*values)[i])))
+ {
+ (*freqs)[i] += freq;
+ return;
+ }
+ }
+ if (*nvalues >= *maxv)
+ {
+ *maxv *= 2;
+ *values = (Datum *) repalloc(*values, sizeof(Datum) * (*maxv));
+ *freqs = (double *) repalloc(*freqs, sizeof(double) * (*maxv));
+ }
+ (*values)[*nvalues] = datumCopy(value, typbyval, typlen);
+ (*freqs)[*nvalues] = freq;
+ (*nvalues)++;
+}
+
+/*
+ * Synthesize a COALESCE key's MCV list, NULL fraction and distinct-count from
+ * its branches, for one decomposed side. Each branch is weighted by the
+ * probability of reaching it (product of earlier branches' nullfracs). Returns
+ * false if a column branch lacks statistics.
+ */
+static bool
+build_coalesce_key_mcv(PlannerInfo *root, List *args, Oid keytype,
+ FmgrInfo *eqproc, Oid collation,
+ Datum **values_out, double **freqs_out, int *nvalues_out,
+ double *nullfrac_out, double *ndistinct_out,
+ bool *ndistinct_isdefault_out)
+{
+ int nargs = list_length(args);
+ double *prefix = (double *) palloc(sizeof(double) * nargs);
+ double nullfrac;
+ int maxv = 8;
+ int nvalues = 0;
+ Datum *values = (Datum *) palloc(sizeof(Datum) * maxv);
+ double *freqs = (double *) palloc(sizeof(double) * maxv);
+ double ndistinct = 0.0;
+ bool ndistinct_default = false;
+ int16 typlen;
+ bool typbyval;
+ ListCell *lc;
+ int i = 0;
+
+ /*
+ * Reuse the shared branch walk: prefix[i] is the probability of reaching
+ * branch i, nullfrac is the key's overall NULL fraction, and it also
+ * verifies that every column branch has statistics.
+ */
+ if (!get_coalesce_prefix_probs(root, args, prefix, &nullfrac))
+ {
+ pfree(prefix);
+ pfree(values);
+ pfree(freqs);
+ return false;
+ }
+
+ get_typlenbyval(keytype, &typlen, &typbyval);
+
+ foreach(lc, args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+ double w = prefix[i++];
+
+ if (w < 1.0e-12)
+ break;
+
+ if (IsA(arg, Const))
+ {
+ Const *c = (Const *) arg;
+
+ if (c->constisnull)
+ continue;
+
+ coalesce_merge_value(&values, &freqs, &nvalues, &maxv,
+ c->constvalue, w, eqproc, collation,
+ typlen, typbyval);
+ ndistinct += 1.0;
+ break;
+ }
+ else
+ {
+ VariableStatData vd;
+ AttStatsSlot sslot;
+ double ndistinct_b;
+ bool isdef;
+
+ examine_variable(root, arg, 0, &vd);
+
+ if (get_attstatsslot(&sslot, vd.statsTuple,
+ STATISTIC_KIND_MCV, InvalidOid,
+ ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS))
+ {
+ int k;
+
+ for (k = 0; k < sslot.nvalues; k++)
+ coalesce_merge_value(&values, &freqs, &nvalues, &maxv,
+ sslot.values[k],
+ w * sslot.numbers[k],
+ eqproc, collation, typlen, typbyval);
+ free_attstatsslot(&sslot);
+ }
+
+ ndistinct_b = get_variable_numdistinct(&vd, &isdef);
+ if (ndistinct_b > 0.0)
+ ndistinct += ndistinct_b;
+ ndistinct_default |= isdef;
+
+ ReleaseVariableStats(vd);
+ }
+ }
+
+ pfree(prefix);
+
+ *nullfrac_out = nullfrac;
+ *values_out = values;
+ *freqs_out = freqs;
+ *nvalues_out = nvalues;
+ *ndistinct_out = ndistinct;
+ *ndistinct_isdefault_out = ndistinct_default;
+ return true;
+}
+
+/*
+ * Estimate SEMI/ANTI selectivity for an equality join with a COALESCE key
+ * (either side may be a plain single-branch expression): the fraction of outer
+ * rows with a matching inner row. Returns false (caller uses eqjoinsel_semi)
+ * when the sides can't be oriented or lack stats; size code inverts for ANTI.
+ */
+static bool
+coalesce_semi_estimate(PlannerInfo *root, Oid operator, Oid collation,
+ Node *left, Node *right, List *left_args,
+ List *right_args, SpecialJoinInfo *sjinfo,
+ double *selec_out)
+{
+ bool left_inner = bms_is_subset(pull_varnos(root, left),
+ sjinfo->syn_righthand);
+ bool right_inner = bms_is_subset(pull_varnos(root, right),
+ sjinfo->syn_righthand);
+ List *outer_args;
+ List *inner_args;
+ Oid keytype = exprType(left);
+ FmgrInfo eqproc;
+ Datum *outer_values,
+ *inner_values;
+ double *outer_freqs,
+ *inner_freqs;
+ int outer_nvalues,
+ inner_nvalues;
+ double outer_nullfrac,
+ inner_nullfrac,
+ outer_ndistinct,
+ inner_ndistinct;
+ bool outer_nd_default,
+ inner_nd_default;
+ MemoryContext tmpctx;
+ MemoryContext oldctx;
+ bool ok = false;
+
+ if (left_inner == right_inner)
+ return false;
+ if (left_inner)
+ {
+ inner_args = left_args;
+ outer_args = right_args;
+ }
+ else
+ {
+ inner_args = right_args;
+ outer_args = left_args;
+ }
+
+ tmpctx = AllocSetContextCreate(CurrentMemoryContext,
+ "coalesce semijoin",
+ ALLOCSET_SMALL_SIZES);
+ oldctx = MemoryContextSwitchTo(tmpctx);
+
+ fmgr_info(get_opcode(operator), &eqproc);
+
+ if (build_coalesce_key_mcv(root, outer_args, keytype, &eqproc, collation,
+ &outer_values, &outer_freqs, &outer_nvalues,
+ &outer_nullfrac, &outer_ndistinct, &outer_nd_default) &&
+ build_coalesce_key_mcv(root, inner_args, keytype, &eqproc, collation,
+ &inner_values, &inner_freqs, &inner_nvalues,
+ &inner_nullfrac, &inner_ndistinct, &inner_nd_default))
+ {
+ double matchfreq = 0.0,
+ seen = 0.0,
+ uncertain,
+ uncertainfrac,
+ sel;
+ int i,
+ j;
+
+ /* matchfreq: outer MCV mass whose value also exists on the inner */
+ for (i = 0; i < outer_nvalues; i++)
+ {
+ seen += outer_freqs[i];
+ for (j = 0; j < inner_nvalues; j++)
+ {
+ if (DatumGetBool(FunctionCall2Coll(&eqproc, collation,
+ outer_values[i], inner_values[j])))
+ {
+ matchfreq += outer_freqs[i];
+ break;
+ }
+ }
+ }
+
+ /* uncertain: outer non-NULL mass its MCV didn't cover */
+ uncertain = (1.0 - outer_nullfrac) - seen;
+ if (uncertain < 0.0)
+ uncertain = 0.0;
+
+ /*
+ * share of that tail assumed to match: clamp(nd_in / nd_out), else
+ * 0.5
+ */
+ if (!outer_nd_default && !inner_nd_default && outer_ndistinct > 0.0)
+ {
+ uncertainfrac = inner_ndistinct / outer_ndistinct;
+ if (uncertainfrac > 1.0)
+ uncertainfrac = 1.0;
+ else if (uncertainfrac < 0.0)
+ uncertainfrac = 0.0;
+ }
+ else
+ uncertainfrac = 0.5;
+
+ sel = matchfreq + uncertainfrac * uncertain;
+
+ /* a semi-join keeps at most the non-NULL outer rows */
+ if (sel > 1.0 - outer_nullfrac)
+ sel = 1.0 - outer_nullfrac;
+ CLAMP_PROBABILITY(sel);
+
+ *selec_out = sel;
+ ok = true;
+ }
+
+ MemoryContextSwitchTo(oldctx);
+ MemoryContextDelete(tmpctx);
+ return ok;
+}
+
+/*
+ * Common guts of eqsel/eqjoinsel when one or both operands are CoalesceExpr.
+ * Computes:
+ *
+ * sel(COALESCE(l_1,...,l_M) = COALESCE(r_1,...,r_N)) =
+ * Sum_{i,j} P_L(reach i) * P_R(reach j) * sel(l_i = r_j)
+ *
+ * where P(reach i) = Prod_{j<i} stanullfrac(arg_j). A non-COALESCE operand
+ * is treated as a single-element list. For <> (negate), the equality
+ * selectivity is turned into "1 - eq - nullfrac", where nullfrac is the
+ * fraction of rows where the whole clause is NULL.
+ */
+static bool
+try_coalesce_eq(PG_FUNCTION_ARGS, bool negate, bool is_eqjoin, double *selec_out)
+{
+ PlannerInfo *root = (PlannerInfo *) PG_GETARG_POINTER(0);
+ Oid operator = PG_GETARG_OID(1);
+ List *args = (List *) PG_GETARG_POINTER(2);
+ SpecialJoinInfo *sjinfo = is_eqjoin ?
+ (SpecialJoinInfo *) PG_GETARG_POINTER(4) : NULL;
+ int eqsel_varRelid = is_eqjoin ? 0 : PG_GETARG_INT32(3);
+ Oid collation = PG_GET_COLLATION();
+ Node *left;
+ Node *right;
+ List *left_args = NIL;
+ List *right_args = NIL;
+ bool left_is_coalesce;
+ bool right_is_coalesce;
+ bool left_term_const = false;
+ bool right_term_const = false;
+ double *left_prefix;
+ double *right_prefix;
+ double left_nullfrac;
+ double right_nullfrac;
+ double acc_selec = 0.0;
+ ListCell *llc;
+ int li;
+
+ /*
+ * For <> we estimate with the corresponding = operator and negate the
+ * result below. Switch to that operator up front so the whole
+ * decomposition works with equality.
+ */
+ if (negate)
+ {
+ operator = get_negator(operator);
+ if (!OidIsValid(operator))
+ return false;
+ }
+
+ if (list_length(args) != 2)
+ return false;
+
+ left = (Node *) linitial(args);
+ right = (Node *) lsecond(args);
+
+ left_is_coalesce = match_coalesce_join_side(left, &left_args,
+ &left_term_const);
+ right_is_coalesce = match_coalesce_join_side(right, &right_args,
+ &right_term_const);
+
+ if (!left_is_coalesce && !right_is_coalesce)
+ return false;
+
+ /*
+ * One side is a CoalesceExpr that we declined to decompose. Give up
+ * rather than wrap it as a single opaque branch below, which would add a
+ * bogus term to the sum; let the caller estimate the clause normally.
+ */
+ if ((left_is_coalesce && !right_is_coalesce && IsA(right, CoalesceExpr)) ||
+ (right_is_coalesce && !left_is_coalesce && IsA(left, CoalesceExpr)))
+ {
+ list_free(left_args);
+ list_free(right_args);
+ return false;
+ }
+
+ if (!left_is_coalesce)
+ {
+ left_args = list_make1(left);
+ left_term_const = IsA(left, Const);
+ }
+ if (!right_is_coalesce)
+ {
+ right_args = list_make1(right);
+ right_term_const = IsA(right, Const);
+ }
+
+ /*
+ * SEMI/ANTI selectivity is existence-based and cannot be written as the
+ * inner-join sum below, so estimate it separately. Any other
+ * non-INNER/LEFT/FULL join type doesn't want the inner sum either, so
+ * give up and let the standard estimator handle it.
+ */
+ if (is_eqjoin && sjinfo)
+ {
+ JoinType jointype = sjinfo->jointype;
+
+ if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
+ {
+ double sel;
+ bool ok;
+
+ ok = coalesce_semi_estimate(root, operator, collation,
+ left, right, left_args, right_args,
+ sjinfo, &sel);
+ list_free(left_args);
+ list_free(right_args);
+ if (ok)
+ {
+ *selec_out = sel;
+ return true;
+ }
+ return false;
+ }
+ else if (jointype != JOIN_INNER &&
+ jointype != JOIN_LEFT &&
+ jointype != JOIN_FULL)
+ {
+ list_free(left_args);
+ list_free(right_args);
+ return false;
+ }
+ }
+
+ left_prefix = (double *) palloc(sizeof(double) * list_length(left_args));
+ right_prefix = (double *) palloc(sizeof(double) * list_length(right_args));
+
+ if (!get_coalesce_prefix_probs(root, left_args, left_prefix, &left_nullfrac) ||
+ !get_coalesce_prefix_probs(root, right_args, right_prefix, &right_nullfrac))
+ {
+ pfree(left_prefix);
+ pfree(right_prefix);
+ list_free(left_args);
+ list_free(right_args);
+ return false;
+ }
+
+ li = 0;
+ foreach(llc, left_args)
+ {
+ Node *larg = (Node *) lfirst(llc);
+ bool lconst = IsA(larg, Const);
+ ListCell *rlc;
+ int ri = 0;
+
+ if (left_prefix[li] < 1.0e-12)
+ break;
+
+ foreach(rlc, right_args)
+ {
+ Node *rarg = (Node *) lfirst(rlc);
+ bool rconst = IsA(rarg, Const);
+ Selectivity contrib = 0.0;
+
+ if (left_prefix[li] * right_prefix[ri] < 1.0e-12)
+ break;
+
+ if (lconst && rconst)
+ {
+ Const *lc = (Const *) larg;
+ Const *rc = (Const *) rarg;
+
+ /*
+ * Both branches are constants, so the equality is constant
+ * too: a match is selectivity 1.0, a mismatch 0.0. A NULL
+ * input makes the strict operator never return true, i.e.
+ * 0.0.
+ */
+ if (!(lc->constisnull || rc->constisnull) &&
+ DatumGetBool(OidFunctionCall2Coll(get_opcode(operator),
+ collation,
+ lc->constvalue,
+ rc->constvalue)))
+ contrib = 1.0;
+ }
+ else
+ {
+ List *sub_args = list_make2(copyObject(larg),
+ copyObject(rarg));
+
+ if (!is_eqjoin || lconst || rconst)
+ {
+ contrib = DatumGetFloat8(DirectFunctionCall4Coll(eqsel,
+ collation,
+ PointerGetDatum(root),
+ ObjectIdGetDatum(operator),
+ PointerGetDatum(sub_args),
+ Int32GetDatum(eqsel_varRelid)));
+ }
+ else
+ {
+ contrib = DatumGetFloat8(DirectFunctionCall5Coll(eqjoinsel,
+ collation,
+ PointerGetDatum(root),
+ ObjectIdGetDatum(operator),
+ PointerGetDatum(sub_args),
+ Int16GetDatum(JOIN_INNER),
+ PointerGetDatum(sjinfo)));
+ }
+
+ list_free(sub_args);
+ }
+
+ CLAMP_PROBABILITY(contrib);
+ acc_selec += left_prefix[li] * right_prefix[ri] * contrib;
+
+ ri++;
+ }
+
+ li++;
+ }
+
+ /*
+ * acc_selec is now the equality selectivity. For <> return "1 - eq -
+ * nullfrac", where the clause is NULL whenever either side is NULL.
+ */
+ if (negate)
+ {
+ double clause_nullfrac;
+
+ clause_nullfrac = 1.0 - (1.0 - left_nullfrac) * (1.0 - right_nullfrac);
+ acc_selec = 1.0 - acc_selec - clause_nullfrac;
+ }
+
+ pfree(left_prefix);
+ pfree(right_prefix);
+ list_free(left_args);
+ list_free(right_args);
+
+ CLAMP_PROBABILITY(acc_selec);
+ *selec_out = acc_selec;
+ return true;
+}
+
/*
* Common code for eqsel() and neqsel()
*/
@@ -320,6 +948,9 @@ eqsel_internal(PG_FUNCTION_ARGS, bool negate)
bool varonleft;
double selec;
+ if (try_coalesce_eq(fcinfo, negate, false, &selec))
+ return selec;
+
/*
* When asked about <>, we do the estimation using the corresponding =
* operator, then convert to <> via "1.0 - eq_selectivity - nullfrac".
@@ -2421,6 +3052,9 @@ eqjoinsel(PG_FUNCTION_ARGS)
bool join_is_reversed;
RelOptInfo *inner_rel;
+ if (try_coalesce_eq(fcinfo, false, true, &selec))
+ PG_RETURN_FLOAT8((float8) selec);
+
get_join_variables(root, args, sjinfo,
&vardata1, &vardata2, &join_is_reversed);
@@ -4377,6 +5011,155 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner,
return otherclauses;
}
+/*
+ * Most-common-value frequency for vardata. Falls back to 1/ntuples when
+ * only a histogram slot is present. Returns 0.0 if no statistics are
+ * available.
+ */
+static void
+get_variable_mcv_freq(VariableStatData *vardata, Selectivity *mcv_freq)
+{
+ AttStatsSlot sslot;
+
+ *mcv_freq = 0.0;
+
+ if (!HeapTupleIsValid(vardata->statsTuple))
+ return;
+
+ if (get_attstatsslot(&sslot, vardata->statsTuple,
+ STATISTIC_KIND_MCV, InvalidOid,
+ ATTSTATSSLOT_NUMBERS))
+ {
+ if (sslot.nnumbers > 0)
+ *mcv_freq = sslot.numbers[0];
+ free_attstatsslot(&sslot);
+ }
+ else if (get_attstatsslot(&sslot, vardata->statsTuple,
+ STATISTIC_KIND_HISTOGRAM, InvalidOid,
+ 0))
+ {
+ /* no MCVs but histogram present: column is likely unique */
+ if (vardata->rel && vardata->rel->tuples > 0)
+ *mcv_freq = 1.0 / vardata->rel->tuples;
+ }
+}
+
+/*
+ * Estimate bucket stats for a CoalesceExpr hashkey when examine_variable()
+ * returned a default ndistinct. Uses per-branch ndistinct and mcv_freq,
+ * weighted by null fall-through probability.
+ */
+static bool
+hash_bucket_stats_coalesce_dispatch(PlannerInfo *root,
+ Node *hashkey,
+ double nbuckets,
+ Selectivity *mcv_freq,
+ Selectivity *bucketsize_frac)
+{
+ List *stripped_args;
+ bool terminates_with_const;
+ double *prefix;
+ double side_nullfrac;
+ int nargs;
+ int i;
+ ListCell *lc;
+ double nd_mix = 0.0;
+ Selectivity mcv_mix = 0.0;
+ double rel_rows_proxy = 0.0;
+ double rel_tuples_proxy = 0.0;
+ double estfract;
+
+ if (!match_coalesce_join_side(hashkey, &stripped_args, &terminates_with_const))
+ return false;
+
+ nargs = list_length(stripped_args);
+ prefix = (double *) palloc(sizeof(double) * nargs);
+
+ if (!get_coalesce_prefix_probs(root, stripped_args, prefix, &side_nullfrac))
+ {
+ pfree(prefix);
+ list_free(stripped_args);
+ return false;
+ }
+
+ i = 0;
+ foreach(lc, stripped_args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+
+ if (IsA(arg, Const))
+ {
+ mcv_mix = Max(mcv_mix, prefix[i]);
+ if (prefix[i] > 0.0)
+ nd_mix += 1.0;
+ }
+ else
+ {
+ VariableStatData vd;
+ double nd_i;
+ bool isdefault;
+ Selectivity mcv_i;
+
+ examine_variable(root, arg, 0, &vd);
+ nd_i = get_variable_numdistinct(&vd, &isdefault);
+
+ if (isdefault)
+ {
+ ReleaseVariableStats(vd);
+ pfree(prefix);
+ list_free(stripped_args);
+ return false;
+ }
+
+ get_variable_mcv_freq(&vd, &mcv_i);
+
+ nd_mix = Max(nd_mix, nd_i);
+ mcv_mix = Max(mcv_mix, prefix[i] * mcv_i);
+
+ if (i == 0 && vd.rel && vd.rel->tuples > 0)
+ {
+ rel_rows_proxy = vd.rel->rows;
+ rel_tuples_proxy = vd.rel->tuples;
+ }
+
+ ReleaseVariableStats(vd);
+ }
+
+ i++;
+ }
+
+ pfree(prefix);
+ list_free(stripped_args);
+
+ if (rel_tuples_proxy > 0.0)
+ {
+ nd_mix *= rel_rows_proxy / rel_tuples_proxy;
+ nd_mix = clamp_row_est(nd_mix);
+ }
+
+ if (nd_mix <= 0.0)
+ return false;
+
+ if (nd_mix > nbuckets)
+ estfract = 1.0 / nbuckets;
+ else
+ estfract = 1.0 / nd_mix;
+
+ CLAMP_PROBABILITY(mcv_mix);
+ *mcv_freq = Max(*mcv_freq, mcv_mix);
+ CLAMP_PROBABILITY(*mcv_freq);
+ estfract = Max(estfract, *mcv_freq);
+
+ if (estfract < 1.0e-6)
+ estfract = 1.0e-6;
+ else if (estfract > 1.0)
+ estfract = 1.0;
+
+ *bucketsize_frac = (Selectivity) estfract;
+
+ return true;
+}
+
/*
* Estimate hash bucket statistics when the specified expression is used
* as a hash key for the given number of buckets.
@@ -4430,43 +5213,21 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets,
double estfract,
ndistinct;
bool isdefault;
- AttStatsSlot sslot;
examine_variable(root, hashkey, 0, &vardata);
- /* Initialize *mcv_freq to "unknown" */
- *mcv_freq = 0.0;
-
- /* Look up the frequency of the most common value, if available */
- if (HeapTupleIsValid(vardata.statsTuple))
- {
- if (get_attstatsslot(&sslot, vardata.statsTuple,
- STATISTIC_KIND_MCV, InvalidOid,
- ATTSTATSSLOT_NUMBERS))
- {
- /*
- * The first MCV stat is for the most common value.
- */
- if (sslot.nnumbers > 0)
- *mcv_freq = sslot.numbers[0];
- free_attstatsslot(&sslot);
- }
- else if (get_attstatsslot(&sslot, vardata.statsTuple,
- STATISTIC_KIND_HISTOGRAM, InvalidOid,
- 0))
- {
- /*
- * If there are no recorded MCVs, but we do have a histogram, then
- * assume that ANALYZE determined that the column is unique.
- */
- if (vardata.rel && vardata.rel->tuples > 0)
- *mcv_freq = 1.0 / vardata.rel->tuples;
- }
- }
+ get_variable_mcv_freq(&vardata, mcv_freq);
/* Get number of distinct values */
ndistinct = get_variable_numdistinct(&vardata, &isdefault);
+ if (isdefault && hash_bucket_stats_coalesce_dispatch(root, hashkey, nbuckets,
+ mcv_freq, bucketsize_frac))
+ {
+ ReleaseVariableStats(vardata);
+ return;
+ }
+
/*
* If ndistinct isn't real, punt. We normally return 0.1, but if the
* mcv_freq is known to be even higher than that, use it instead.
base-commit: 5c52858ecdc052b2a9fc2a7f30e338ec6777c4ae
--
2.43.0