From 06432869639827d50958f372758a44fd26946268 Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Sun, 26 Jul 2026 11:39:07 +0000 Subject: [PATCH v9 13/21] Consider foreign keys when estimating filter selectivity When estimating the filter selectivity, consider foreign keys, similarly to calc_joinrel_size_estimate. This change is incomplete - it only works for filters built directly on a base relation, not for joins. bloom_build_side_join_ratio fails to consider the FK when calculating the join size. While it calls get_foreign_key_join_selectivity, that only handles FK between the base relation (where we try to push the filter) and the join the filter is built on. We'd have to deduce how to split the build relids (in reverse to how we build them), which seems unnecessary. Instead, we should calculate the join size incrementally during the bottom-up phase, just like during the regular join planning phase. --- src/backend/optimizer/path/allpaths.c | 74 ++++++++++++++++++++++++--- src/backend/optimizer/path/costsize.c | 7 +-- src/include/optimizer/cost.h | 5 ++ src/test/regress/expected/join.out | 10 ++-- 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 160ab49d3c1..66ea735c0b6 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -1068,6 +1068,7 @@ bloom_build_side_join_ratio(PlannerInfo *root, RelOptInfo *rel, int *dsu; double nrows = 1.0; Selectivity clausesel; + Selectivity fkselec; SpecialJoinInfo sjinfo; ListCell *lc; int i; @@ -1249,7 +1250,21 @@ bloom_build_side_join_ratio(PlannerInfo *root, RelOptInfo *rel, * use the same thing here? */ init_dummy_sjinfo(&sjinfo, rel->relids, build_relids); - clausesel = clauselist_selectivity(root, clauses, 0, JOIN_INNER, &sjinfo); + + /* + * Just like calc_joinrel_size_estimate (and also + * find_interesting_bloom_filters for the per-relation estimates), match + * the clauses to foreign keys and estimate those using FK semantics. + * Without this, joins matching a multi-column foreign key get badly + * underestimated, because each of the join clauses applies the + * selectivity of the restrictions on the referenced relation. + */ + fkselec = get_foreign_key_join_selectivity(root, rel->relids, build_relids, + &sjinfo, &clauses); + + clausesel = fkselec * clauselist_selectivity(root, clauses, 0, + JOIN_INNER, &sjinfo); + CLAMP_PROBABILITY(clausesel); bms_free(setrelids); @@ -1318,7 +1333,7 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) if (rinfo->parent_ec != NULL) continue; - candidates = lappend(candidates, rinfo->clause); + candidates = lappend(candidates, rinfo); } /* @@ -1345,6 +1360,7 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) foreach(lc, candidates) { Node *clause = (Node *) lfirst(lc); + RestrictInfo *rinfo = NULL; Node *left; Node *right; Node *ownerexpr; @@ -1353,9 +1369,16 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) Relids build_relids; int buildrel; - /* strip RestrictInfo (see comment above) */ + /* + * Strip RestrictInfo (see comment above), but remember it - we need + * it to match the clauses to foreign keys when estimating the filter + * selectivity below. + */ if (IsA(clause, RestrictInfo)) - clause = (Node *) ((RestrictInfo *) clause)->clause; + { + rinfo = (RestrictInfo *) clause; + clause = (Node *) rinfo->clause; + } /* * Only care about (Expr op Expr) clauses. We know one side has to be @@ -1404,13 +1427,26 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) Assert(root->simple_rel_array[buildrel]->reloptkind == RELOPT_BASEREL); - clauses_by_rel[buildrel] = lappend(clauses_by_rel[buildrel], clause); + /* all the candidate clauses come from RestrictInfos */ + Assert(rinfo != NULL); + + clauses_by_rel[buildrel] = lappend(clauses_by_rel[buildrel], rinfo); has_rel[buildrel] = true; } /* * Pre-compute the per-build-relation semijoin selectivity. * + * A build relation may be joined by multiple clauses (e.g. when the join + * matches a multi-column key). Simply multiplying the per-clause + * selectivities assumes the columns are independent, which is badly wrong + * for such joins - it applies the selectivity of restrictions on the + * build relation once per join clause. So we first match the clauses to + * foreign keys (the same way join size estimates do in + * calc_joinrel_size_estimate), and estimate those using FK semantics. + * Only the clauses not matched to any foreign key are then estimated the + * regular way. + * * XXX I think we could calculate the selectivity only for semijoin-like * joins (semijoin, inner join), and ignore the rest. We don't even need * to calculate interesting filters for those cases. @@ -1420,6 +1456,10 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) { SpecialJoinInfo sjinfo; Relids build_relids; + List *worklist; + List *clauses = NIL; + Selectivity fkselec; + ListCell *lc2; /* ignore relations without join clauses */ if (!has_rel[i]) @@ -1429,8 +1469,28 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) init_dummy_sjinfo(&sjinfo, rel->relids, build_relids); sjinfo.jointype = JOIN_SEMI; - sel_by_rel[i] = clauselist_selectivity(root, clauses_by_rel[i], 0, - JOIN_SEMI, &sjinfo); + /* + * Estimate clauses matching a foreign key using FK semantics. This + * removes the matched clauses from the worklist (which is a copy, the + * original list is left alone). + */ + worklist = clauses_by_rel[i]; + fkselec = get_foreign_key_join_selectivity(root, rel->relids, + build_relids, &sjinfo, + &worklist); + + /* strip RestrictInfo from the remaining clauses (see comment above) */ + foreach(lc2, worklist) + clauses = lappend(clauses, ((RestrictInfo *) lfirst(lc2))->clause); + + sel_by_rel[i] = fkselec * clauselist_selectivity(root, clauses, 0, + JOIN_SEMI, &sjinfo); + + CLAMP_PROBABILITY(sel_by_rel[i]); + + list_free(clauses); + if (worklist != clauses_by_rel[i]) + list_free(worklist); bms_free(build_relids); } diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index f650c7c3d19..49aa541d400 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -227,11 +227,6 @@ static double calc_joinrel_size_estimate(PlannerInfo *root, double inner_rows, SpecialJoinInfo *sjinfo, List *restrictlist); -static Selectivity get_foreign_key_join_selectivity(PlannerInfo *root, - Relids outer_relids, - Relids inner_relids, - SpecialJoinInfo *sjinfo, - List **restrictlist); static Cost append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers); static void set_rel_width(PlannerInfo *root, RelOptInfo *rel); @@ -5848,7 +5843,7 @@ calc_joinrel_size_estimate(PlannerInfo *root, * able to get a better answer when the pg_statistic stats are missing or out * of date. */ -static Selectivity +Selectivity get_foreign_key_join_selectivity(PlannerInfo *root, Relids outer_relids, Relids inner_relids, diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index 9daa6ded3f8..dff97783e15 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -209,6 +209,11 @@ extern double get_parameterized_joinrel_size(PlannerInfo *root, Path *inner_path, SpecialJoinInfo *sjinfo, List *restrict_clauses); +extern Selectivity get_foreign_key_join_selectivity(PlannerInfo *root, + Relids outer_relids, + Relids inner_relids, + SpecialJoinInfo *sjinfo, + List **restrictlist); extern void set_joinrel_size_estimates(PlannerInfo *root, RelOptInfo *rel, RelOptInfo *outer_rel, RelOptInfo *inner_rel, diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index d7b8412320d..0879638b187 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -9591,16 +9591,16 @@ select * from fkest f1 QUERY PLAN --------------------------------------------------------------- Hash Join - Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10)) + Hash Cond: ((f1.x = f2.x) AND (f1.x10 = f2.x10b)) -> Nested Loop - -> Seq Scan on fkest f2 + -> Seq Scan on fkest f1 Filter: (x100 = 2) - Bloom Filter 1: keys=(x, x10b) + Bloom Filter 1: keys=(x, x10) -> Index Scan using fkest_x_x10_x100_idx on fkest f3 - Index Cond: (x = f2.x) + Index Cond: (x = f1.x) -> Hash Bloom Filter 1 - -> Seq Scan on fkest f1 + -> Seq Scan on fkest f2 Filter: (x100 = 2) (12 rows) -- 2.50.1 (Apple Git-155)