From 694c64bc50cd11f1e41eb110b3bcf0997edb50be Mon Sep 17 00:00:00 2001 From: Evdokimov Ilia Date: Mon, 28 Sep 2026 15:49:01 +0500 Subject: [PATCH v4 1/2] Account for NULLs in FK-based join selectivity get_foreign_key_join_selectivity() assumes that each referencing row matches exactly one row in the referenced table. That's not true for rows having a NULL in any of the referencing columns, which match nothing, so the join size could be overestimated considerably: by 2x when half of the referencing values are NULL. The regular clause-based estimate in eqjoinsel() does take the null fraction into account. Derate the FK-based selectivity by the fraction of referencing rows with a NULL in some FK column. The XXX comment there gave two reasons for not doing so, which are handled as follows. Columns constrained by a strict restriction clause of the referencing rel are skipped, since their NULLs are already excluded from its row count. For multi-column FKs, take the largest per-column null fraction: that's exact when the columns are NULL together, as is typical, and a lower bound otherwise. --- src/backend/optimizer/path/costsize.c | 70 +++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 7bbddb8bee4..cd494d29cbb 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -88,12 +88,14 @@ #include "access/amapi.h" #include "access/htup_details.h" #include "access/tsmapi.h" +#include "catalog/pg_statistic.h" #include "executor/executor.h" #include "executor/nodeAgg.h" #include "executor/nodeHash.h" #include "executor/nodeMemoize.h" #include "miscadmin.h" #include "nodes/makefuncs.h" +#include "nodes/multibitmapset.h" #include "nodes/nodeFuncs.h" #include "nodes/tidbitmap.h" #include "optimizer/clauses.h" @@ -108,6 +110,7 @@ #include "utils/lsyscache.h" #include "utils/selfuncs.h" #include "utils/spccache.h" +#include "utils/syscache.h" #include "utils/tuplesort.h" @@ -198,6 +201,8 @@ static Selectivity get_foreign_key_join_selectivity(PlannerInfo *root, Relids inner_relids, SpecialJoinInfo *sjinfo, List **restrictlist); +static Selectivity fkey_referencing_nullfrac(PlannerInfo *root, + ForeignKeyOptInfo *fkinfo); static Cost append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers); static void set_rel_width(PlannerInfo *root, RelOptInfo *rel); @@ -6051,15 +6056,9 @@ get_foreign_key_join_selectivity(PlannerInfo *root, /* * Finally we get to the payoff: estimate selectivity using the * knowledge that each referencing row will match exactly one row in - * the referenced table. - * - * XXX that's not true in the presence of nulls in the referencing - * column(s), so in principle we should derate the estimate for those. - * However (1) if there are any strict restriction clauses for the - * referencing column(s) elsewhere in the query, derating here would - * be double-counting the null fraction, and (2) it's not very clear - * how to combine null fractions for multiple referencing columns. So - * we do nothing for now about correcting for nulls. + * the referenced table. That's not true for referencing rows with + * nulls in the FK columns, which match nothing; we derate the + * estimate for those below. * * XXX another point here is that if either side of an FK constraint * is an inheritance parent, we estimate as though the constraint @@ -6102,6 +6101,9 @@ get_foreign_key_join_selectivity(PlannerInfo *root, fkselec *= 1.0 / ref_tuples; } + /* Referencing rows with nulls in the FK columns have no match */ + fkselec *= 1.0 - fkey_referencing_nullfrac(root, fkinfo); + /* * If any of the FK columns participated in ec_has_const ECs, then * equivclass.c will have generated "var = const" restrictions for @@ -6147,6 +6149,56 @@ get_foreign_key_join_selectivity(PlannerInfo *root, return fkselec; } +/* + * fkey_referencing_nullfrac + * Estimate the fraction of the referencing rel's rows that have a null + * in at least one of the FK columns. + * + * Columns constrained by a strict restriction clause of the referencing rel + * are skipped: their nulls are already excluded from the rel's row count, so + * counting them again here would underestimate the join size. For the + * remaining columns we take the largest null fraction. That's exact when + * the columns are null together, as is typical for multi-column FKs, and + * otherwise it's a lower bound. + */ +static Selectivity +fkey_referencing_nullfrac(PlannerInfo *root, ForeignKeyOptInfo *fkinfo) +{ + RelOptInfo *con_rel = find_base_rel(root, fkinfo->con_relid); + RangeTblEntry *rte = planner_rt_fetch(fkinfo->con_relid, root); + List *nonnullable_vars = NIL; + Selectivity nullfrac = 0.0; + + foreach_node(RestrictInfo, rinfo, con_rel->baserestrictinfo) + nonnullable_vars = + mbms_add_members(nonnullable_vars, + find_nonnullable_vars((Node *) rinfo->clause)); + + for (int i = 0; i < fkinfo->nkeys; i++) + { + AttrNumber attno = fkinfo->conkey[i]; + HeapTuple tup; + + if (mbms_is_member(fkinfo->con_relid, + attno - FirstLowInvalidHeapAttributeNumber, + nonnullable_vars)) + continue; + + tup = SearchSysCache3(STATRELATTINH, + ObjectIdGetDatum(rte->relid), + Int16GetDatum(attno), + BoolGetDatum(rte->inh)); + if (HeapTupleIsValid(tup)) + { + nullfrac = Max(nullfrac, + ((Form_pg_statistic) GETSTRUCT(tup))->stanullfrac); + ReleaseSysCache(tup); + } + } + + return nullfrac; +} + /* * set_subquery_size_estimates * Set the size estimates for a base relation that is a subquery. -- 2.43.0