Commit ad1c36b0 authored by Tom Lane's avatar Tom Lane

Fix foreign-key selectivity estimation in the presence of constants.

get_foreign_key_join_selectivity() looks for join clauses that equate
the two sides of the FK constraint.  However, if we have a query like
"WHERE fktab.a = pktab.a and fktab.a = 1", it won't find any such join
clause, because equivclass.c replaces the given clauses with "fktab.a
= 1 and pktab.a = 1", which can be enforced at the scan level, leaving
nothing to be done for column "a" at the join level.

We can fix that expectation without much trouble, but then a new problem
arises: applying the foreign-key-based selectivity rule produces a
rowcount underestimate, because we're effectively double-counting the
selectivity of the "fktab.a = 1" clause.  So we have to cancel that
selectivity out of the estimate.

To fix, refactor process_implied_equality() so that it can pass back the
new RestrictInfo to its callers in equivclass.c, allowing the generated
"fktab.a = 1" clause to be saved in the EquivalenceClass's ec_derives
list.  Then it's not much trouble to dig out the relevant RestrictInfo
when we need to adjust an FK selectivity estimate.  (While at it, we
can also remove the expensive use of initialize_mergeclause_eclasses()
to set up the new RestrictInfo's left_ec and right_ec pointers.
The equivclass.c code can set those basically for free.)

This seems like clearly a bug fix, but I'm hesitant to back-patch it,
first because there's some API/ABI risk for extensions and second because
we're usually loath to destabilize plan choices in stable branches.

Per report from Sigrid Ehrenreich.

Discussion: https://postgr.es/m/1019549.1603770457@sss.pgh.pa.us
Discussion: https://postgr.es/m/AM6PR02MB5287A0ADD936C1FA80973E72AB190@AM6PR02MB5287.eurprd02.prod.outlook.com
parent ce7f772c
......@@ -2352,6 +2352,7 @@ _outForeignKeyOptInfo(StringInfo str, const ForeignKeyOptInfo *node)
WRITE_ATTRNUMBER_ARRAY(confkey, node->nkeys);
WRITE_OID_ARRAY(conpfeqop, node->nkeys);
WRITE_INT_FIELD(nmatched_ec);
WRITE_INT_FIELD(nconst_ec);
WRITE_INT_FIELD(nmatched_rcols);
WRITE_INT_FIELD(nmatched_ri);
/* for compactness, just print the number of matches per column: */
......
......@@ -5066,9 +5066,16 @@ get_foreign_key_join_selectivity(PlannerInfo *root,
* remove back into the worklist.
*
* Since the matching clauses are known not outerjoin-delayed, they
* should certainly have appeared in the initial joinclause list. If
* we didn't find them, they must have been matched to, and removed
* by, some other FK in a previous iteration of this loop. (A likely
* would normally have appeared in the initial joinclause list. If we
* didn't find them, there are two possibilities:
*
* 1. If the FK match is based on an EC that is ec_has_const, it won't
* have generated any join clauses at all. We discount such ECs while
* checking to see if we have "all" the clauses. (Below, we'll adjust
* the selectivity estimate for this case.)
*
* 2. The clauses were matched to some other FK in a previous
* iteration of this loop, and thus removed from worklist. (A likely
* case is that two FKs are matched to the same EC; there will be only
* one EC-derived clause in the initial list, so the first FK will
* consume it.) Applying both FKs' selectivity independently risks
......@@ -5078,8 +5085,9 @@ get_foreign_key_join_selectivity(PlannerInfo *root,
* Later we might think of a reasonable way to combine the estimates,
* but for now, just punt, since this is a fairly uncommon situation.
*/
if (list_length(removedlist) !=
(fkinfo->nmatched_ec + fkinfo->nmatched_ri))
if (removedlist == NIL ||
list_length(removedlist) !=
(fkinfo->nmatched_ec - fkinfo->nconst_ec + fkinfo->nmatched_ri))
{
worklist = list_concat(worklist, removedlist);
continue;
......@@ -5138,9 +5146,48 @@ get_foreign_key_join_selectivity(PlannerInfo *root,
fkselec *= 1.0 / ref_tuples;
}
/*
* If any of the FK columns participated in ec_has_const ECs, then
* equivclass.c will have generated "var = const" restrictions for
* each side of the join, thus reducing the sizes of both input
* relations. Taking the fkselec at face value would amount to
* double-counting the selectivity of the constant restriction for the
* referencing Var. Hence, look for the restriction clause(s) that
* were applied to the referencing Var(s), and divide out their
* selectivity to correct for this.
*/
if (fkinfo->nconst_ec > 0)
{
for (int i = 0; i < fkinfo->nkeys; i++)
{
EquivalenceClass *ec = fkinfo->eclass[i];
if (ec && ec->ec_has_const)
{
EquivalenceMember *em = fkinfo->fk_eclass_member[i];
RestrictInfo *rinfo = find_derived_clause_for_ec_member(ec,
em);
if (rinfo)
{
Selectivity s0;
s0 = clause_selectivity(root,
(Node *) rinfo,
0,
jointype,
sjinfo);
if (s0 > 0)
fkselec /= s0;
}
}
}
}
}
*restrictlist = worklist;
CLAMP_PROBABILITY(fkselec);
return fkselec;
}
......
......@@ -840,10 +840,8 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
* scanning of the quals and before Path construction begins.
*
* We make no attempt to avoid generating duplicate RestrictInfos here: we
* don't search ec_sources for matches, nor put the created RestrictInfos
* into ec_derives. Doing so would require some slightly ugly changes in
* initsplan.c's API, and there's no real advantage, because the clauses
* generated here can't duplicate anything we will generate for joins anyway.
* don't search ec_sources or ec_derives for matches. It doesn't really
* seem worth the trouble to do so.
*/
void
generate_base_implied_equalities(PlannerInfo *root)
......@@ -969,6 +967,7 @@ generate_base_implied_equalities_const(PlannerInfo *root,
{
EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc);
Oid eq_op;
RestrictInfo *rinfo;
Assert(!cur_em->em_is_child); /* no children yet */
if (cur_em == const_em)
......@@ -982,7 +981,7 @@ generate_base_implied_equalities_const(PlannerInfo *root,
ec->ec_broken = true;
break;
}
process_implied_equality(root, eq_op, ec->ec_collation,
rinfo = process_implied_equality(root, eq_op, ec->ec_collation,
cur_em->em_expr, const_em->em_expr,
bms_copy(ec->ec_relids),
bms_union(cur_em->em_nullable_relids,
......@@ -990,6 +989,23 @@ generate_base_implied_equalities_const(PlannerInfo *root,
ec->ec_min_security,
ec->ec_below_outer_join,
cur_em->em_is_const);
/*
* If the clause didn't degenerate to a constant, fill in the correct
* markings for a mergejoinable clause, and save it in ec_derives. (We
* will not re-use such clauses directly, but selectivity estimation
* may consult the list later. Note that this use of ec_derives does
* not overlap with its use for join clauses, since we never generate
* join clauses from an ec_has_const eclass.)
*/
if (rinfo && rinfo->mergeopfamilies)
{
/* it's not redundant, so don't set parent_ec */
rinfo->left_ec = rinfo->right_ec = ec;
rinfo->left_em = cur_em;
rinfo->right_em = const_em;
ec->ec_derives = lappend(ec->ec_derives, rinfo);
}
}
}
......@@ -1028,6 +1044,7 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
{
EquivalenceMember *prev_em = prev_ems[relid];
Oid eq_op;
RestrictInfo *rinfo;
eq_op = select_equality_operator(ec,
prev_em->em_datatype,
......@@ -1038,7 +1055,7 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
ec->ec_broken = true;
break;
}
process_implied_equality(root, eq_op, ec->ec_collation,
rinfo = process_implied_equality(root, eq_op, ec->ec_collation,
prev_em->em_expr, cur_em->em_expr,
bms_copy(ec->ec_relids),
bms_union(prev_em->em_nullable_relids,
......@@ -1046,6 +1063,21 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
ec->ec_min_security,
ec->ec_below_outer_join,
false);
/*
* If the clause didn't degenerate to a constant, fill in the
* correct markings for a mergejoinable clause. We don't put it
* in ec_derives however; we don't currently need to re-find such
* clauses, and we don't want to clutter that list with non-join
* clauses.
*/
if (rinfo && rinfo->mergeopfamilies)
{
/* it's not redundant, so don't set parent_ec */
rinfo->left_ec = rinfo->right_ec = ec;
rinfo->left_em = prev_em;
rinfo->right_em = cur_em;
}
}
prev_ems[relid] = cur_em;
}
......@@ -2151,6 +2183,10 @@ exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2)
* we ignore that fine point here.) This is much like exprs_known_equal,
* except that we insist on the comparison operator matching the eclass, so
* that the result is definite not approximate.
*
* On success, we also set fkinfo->eclass[colno] to the matching eclass,
* and set fkinfo->fk_eclass_member[colno] to the eclass member for the
* referencing Var.
*/
EquivalenceClass *
match_eclasses_to_foreign_key_col(PlannerInfo *root,
......@@ -2180,8 +2216,8 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
{
EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes,
i);
bool item1member = false;
bool item2member = false;
EquivalenceMember *item1_em = NULL;
EquivalenceMember *item2_em = NULL;
ListCell *lc2;
/* Never match to a volatile EC */
......@@ -2206,12 +2242,12 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
/* Match? */
if (var->varno == var1varno && var->varattno == var1attno)
item1member = true;
item1_em = em;
else if (var->varno == var2varno && var->varattno == var2attno)
item2member = true;
item2_em = em;
/* Have we found both PK and FK column in this EC? */
if (item1member && item2member)
if (item1_em && item2_em)
{
/*
* Succeed if eqop matches EC's opfamilies. We could test
......@@ -2221,7 +2257,11 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
if (opfamilies == NIL) /* compute if we didn't already */
opfamilies = get_mergejoin_opfamilies(eqop);
if (equal(opfamilies, ec->ec_opfamilies))
{
fkinfo->eclass[colno] = ec;
fkinfo->fk_eclass_member[colno] = item2_em;
return ec;
}
/* Otherwise, done with this EC, move on to the next */
break;
}
......@@ -2230,6 +2270,37 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
return NULL;
}
/*
* find_derived_clause_for_ec_member
* Search for a previously-derived clause mentioning the given EM.
*
* The eclass should be an ec_has_const EC, of which the EM is a non-const
* member. This should ensure there is just one derived clause mentioning
* the EM (and equating it to a constant).
* Returns NULL if no such clause can be found.
*/
RestrictInfo *
find_derived_clause_for_ec_member(EquivalenceClass *ec,
EquivalenceMember *em)
{
ListCell *lc;
Assert(ec->ec_has_const);
Assert(!em->em_is_const);
foreach(lc, ec->ec_derives)
{
RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
/*
* generate_base_implied_equalities_const will have put non-const
* members on the left side of derived clauses.
*/
if (rinfo->left_em == em)
return rinfo;
}
return NULL;
}
/*
* add_child_rel_equivalences
......
This diff is collapsed.
......@@ -567,9 +567,11 @@ get_relation_foreign_keys(PlannerInfo *root, RelOptInfo *rel,
memcpy(info->conpfeqop, cachedfk->conpfeqop, sizeof(info->conpfeqop));
/* zero out fields to be filled by match_foreign_keys_to_quals */
info->nmatched_ec = 0;
info->nconst_ec = 0;
info->nmatched_rcols = 0;
info->nmatched_ri = 0;
memset(info->eclass, 0, sizeof(info->eclass));
memset(info->fk_eclass_member, 0, sizeof(info->fk_eclass_member));
memset(info->rinfos, 0, sizeof(info->rinfos));
root->fkey_list = lappend(root->fkey_list, info);
......
......@@ -889,10 +889,13 @@ typedef struct ForeignKeyOptInfo
/* Derived info about whether FK's equality conditions match the query: */
int nmatched_ec; /* # of FK cols matched by ECs */
int nconst_ec; /* # of these ECs that are ec_has_const */
int nmatched_rcols; /* # of FK cols matched by non-EC rinfos */
int nmatched_ri; /* total # of non-EC rinfos matched to FK */
/* Pointer to eclass matching each column's condition, if there is one */
struct EquivalenceClass *eclass[INDEX_MAX_KEYS];
/* Pointer to eclass member for the referencing Var, if there is one */
struct EquivalenceMember *fk_eclass_member[INDEX_MAX_KEYS];
/* List of non-EC RestrictInfos matching each column's condition */
List *rinfos[INDEX_MAX_KEYS];
} ForeignKeyOptInfo;
......
......@@ -149,6 +149,8 @@ extern bool exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2);
extern EquivalenceClass *match_eclasses_to_foreign_key_col(PlannerInfo *root,
ForeignKeyOptInfo *fkinfo,
int colno);
extern RestrictInfo *find_derived_clause_for_ec_member(EquivalenceClass *ec,
EquivalenceMember *em);
extern void add_child_rel_equivalences(PlannerInfo *root,
AppendRelInfo *appinfo,
RelOptInfo *parent_rel,
......
......@@ -77,7 +77,7 @@ extern void create_lateral_join_info(PlannerInfo *root);
extern List *deconstruct_jointree(PlannerInfo *root);
extern void distribute_restrictinfo_to_rels(PlannerInfo *root,
RestrictInfo *restrictinfo);
extern void process_implied_equality(PlannerInfo *root,
extern RestrictInfo *process_implied_equality(PlannerInfo *root,
Oid opno,
Oid collation,
Expr *item1,
......
......@@ -5843,6 +5843,56 @@ select t1.b, ss.phv from join_ut1 t1 left join lateral
drop table join_pt1;
drop table join_ut1;
--
-- test estimation behavior with multi-column foreign key and constant qual
--
begin;
create table fkest (x integer, x10 integer, x10b integer, x100 integer);
insert into fkest select x, x/10, x/10, x/100 from generate_series(1,1000) x;
create unique index on fkest(x, x10, x100);
analyze fkest;
explain (costs off)
select * from fkest f1
join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
join fkest f3 on f1.x = f3.x
where f1.x100 = 2;
QUERY PLAN
-----------------------------------------------------------
Nested Loop
-> Hash Join
Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-> Seq Scan on fkest f2
Filter: (x100 = 2)
-> Hash
-> Seq Scan on fkest f1
Filter: (x100 = 2)
-> Index Scan using fkest_x_x10_x100_idx on fkest f3
Index Cond: (x = f1.x)
(10 rows)
alter table fkest add constraint fk
foreign key (x, x10b, x100) references fkest (x, x10, x100);
explain (costs off)
select * from fkest f1
join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
join fkest f3 on f1.x = f3.x
where f1.x100 = 2;
QUERY PLAN
-----------------------------------------------------
Hash Join
Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-> Hash Join
Hash Cond: (f3.x = f2.x)
-> Seq Scan on fkest f3
-> Hash
-> Seq Scan on fkest f2
Filter: (x100 = 2)
-> Hash
-> Seq Scan on fkest f1
Filter: (x100 = 2)
(11 rows)
rollback;
--
-- test that foreign key join estimation performs sanely for outer joins
--
begin;
......
......@@ -1975,6 +1975,35 @@ select t1.b, ss.phv from join_ut1 t1 left join lateral
drop table join_pt1;
drop table join_ut1;
--
-- test estimation behavior with multi-column foreign key and constant qual
--
begin;
create table fkest (x integer, x10 integer, x10b integer, x100 integer);
insert into fkest select x, x/10, x/10, x/100 from generate_series(1,1000) x;
create unique index on fkest(x, x10, x100);
analyze fkest;
explain (costs off)
select * from fkest f1
join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
join fkest f3 on f1.x = f3.x
where f1.x100 = 2;
alter table fkest add constraint fk
foreign key (x, x10b, x100) references fkest (x, x10, x100);
explain (costs off)
select * from fkest f1
join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
join fkest f3 on f1.x = f3.x
where f1.x100 = 2;
rollback;
--
-- test that foreign key join estimation performs sanely for outer joins
--
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment