diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2015-06-03 11:58:47 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2015-06-03 11:59:10 -0400 |
commit | 3f59be836c555fa679bbe0ec76de50a8b5cb23e0 (patch) | |
tree | 1d12706f003e66bb5bed5ce03f5119e1b3b169e8 /src/backend/optimizer/util/pathnode.c | |
parent | 37013621f3b0e296aa71b812ca9d46871ead95e2 (diff) | |
download | postgresql-3f59be836c555fa679bbe0ec76de50a8b5cb23e0.tar.gz postgresql-3f59be836c555fa679bbe0ec76de50a8b5cb23e0.zip |
Fix planner's cost estimation for SEMI/ANTI joins with inner indexscans.
When the inner side of a nestloop SEMI or ANTI join is an indexscan that
uses all the join clauses as indexquals, it can be presumed that both
matched and unmatched outer rows will be processed very quickly: for
matched rows, we'll stop after fetching one row from the indexscan, while
for unmatched rows we'll have an indexscan that finds no matching index
entries, which should also be quick. The planner already knew about this,
but it was nonetheless charging for at least one full run of the inner
indexscan, as a consequence of concerns about the behavior of materialized
inner scans --- but those concerns don't apply in the fast case. If the
inner side has low cardinality (many matching rows) this could make an
indexscan plan look far more expensive than it actually is. To fix,
rearrange the work in initial_cost_nestloop/final_cost_nestloop so that we
don't add the inner scan cost until we've inspected the indexquals, and
then we can add either the full-run cost or just the first tuple's cost as
appropriate.
Experimentation with this fix uncovered another problem: add_path and
friends were coded to disregard cheap startup cost when considering
parameterized paths. That's usually okay (and desirable, because it thins
the path herd faster); but in this fast case for SEMI/ANTI joins, it could
result in throwing away the desired plain indexscan path in favor of a
bitmap scan path before we ever get to the join costing logic. In the
many-matching-rows cases of interest here, a bitmap scan will do a lot more
work than required, so this is a problem. To fix, add a per-relation flag
consider_param_startup that works like the existing consider_startup flag,
but applies to parameterized paths, and set it for relations that are the
inside of a SEMI or ANTI join.
To make this patch reasonably safe to back-patch, care has been taken to
avoid changing the planner's behavior except in the very narrow case of
SEMI/ANTI joins with inner indexscans. There are places in
compare_path_costs_fuzzily and add_path_precheck that are not terribly
consistent with the new approach, but changing them will affect planner
decisions at the margins in other cases, so we'll leave that for a
HEAD-only fix.
Back-patch to 9.3; before that, the consider_startup flag didn't exist,
meaning that the second aspect of the patch would be too invasive.
Per a complaint from Peter Holzer and analysis by Tomas Vondra.
Diffstat (limited to 'src/backend/optimizer/util/pathnode.c')
-rw-r--r-- | src/backend/optimizer/util/pathnode.c | 70 |
1 files changed, 36 insertions, 34 deletions
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 7f7aa24bb83..5075c8752a5 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -138,19 +138,17 @@ compare_fractional_path_costs(Path *path1, Path *path2, * total cost, we just say that their costs are "different", since neither * dominates the other across the whole performance spectrum. * - * If consider_startup is false, then we don't care about keeping paths with - * good startup cost, so we'll never return COSTS_DIFFERENT. - * - * This function also includes special hacks to support a policy enforced - * by its sole caller, add_path(): paths that have any parameterization - * cannot win comparisons on the grounds of having cheaper startup cost, - * since we deem only total cost to be of interest for a parameterized path. - * (Unparameterized paths are more common, so we check for this case last.) + * This function also enforces a policy rule that paths for which the relevant + * one of parent->consider_startup and parent->consider_param_startup is false + * cannot win comparisons on the grounds of good startup cost, so we never + * return COSTS_DIFFERENT when that is true for the total-cost loser. */ static PathCostComparison -compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor, - bool consider_startup) +compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor) { +#define CONSIDER_PATH_STARTUP_COST(p) \ + ((p)->param_info == NULL ? (p)->parent->consider_startup : (p)->parent->consider_param_startup) + /* * Check total cost first since it's more likely to be different; many * paths have zero startup cost. @@ -158,9 +156,8 @@ compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor, if (path1->total_cost > path2->total_cost * fuzz_factor) { /* path1 fuzzily worse on total cost */ - if (consider_startup && - path2->startup_cost > path1->startup_cost * fuzz_factor && - path1->param_info == NULL) + if (CONSIDER_PATH_STARTUP_COST(path1) && + path2->startup_cost > path1->startup_cost * fuzz_factor) { /* ... but path2 fuzzily worse on startup, so DIFFERENT */ return COSTS_DIFFERENT; @@ -171,9 +168,8 @@ compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor, if (path2->total_cost > path1->total_cost * fuzz_factor) { /* path2 fuzzily worse on total cost */ - if (consider_startup && - path1->startup_cost > path2->startup_cost * fuzz_factor && - path2->param_info == NULL) + if (CONSIDER_PATH_STARTUP_COST(path2) && + path1->startup_cost > path2->startup_cost * fuzz_factor) { /* ... but path1 fuzzily worse on startup, so DIFFERENT */ return COSTS_DIFFERENT; @@ -181,8 +177,13 @@ compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor, /* else path1 dominates */ return COSTS_BETTER1; } - /* fuzzily the same on total cost */ - /* (so we may as well compare startup cost, even if !consider_startup) */ + + /* + * Fuzzily the same on total cost (so we might as well compare startup + * cost, even when that would otherwise be uninteresting; but + * parameterized paths aren't allowed to win this way, we'd rather move on + * to other comparison heuristics) + */ if (path1->startup_cost > path2->startup_cost * fuzz_factor && path2->param_info == NULL) { @@ -197,6 +198,8 @@ compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor, } /* fuzzily the same on both costs */ return COSTS_EQUAL; + +#undef CONSIDER_PATH_STARTUP_COST } /* @@ -212,11 +215,11 @@ compare_path_costs_fuzzily(Path *path1, Path *path2, double fuzz_factor, * * The cheapest_parameterized_paths list collects all parameterized paths * that have survived the add_path() tournament for this relation. (Since - * add_path ignores pathkeys and startup cost for a parameterized path, - * these will be paths that have best total cost or best row count for their - * parameterization.) cheapest_parameterized_paths always includes the - * cheapest-total unparameterized path, too, if there is one; the users of - * that list find it more convenient if that's included. + * add_path ignores pathkeys for a parameterized path, these will be paths + * that have best cost or best row count for their parameterization.) + * cheapest_parameterized_paths always includes the cheapest-total + * unparameterized path, too, if there is one; the users of that list find + * it more convenient if that's included. * * This is normally called only after we've finished constructing the path * list for the rel node. @@ -361,14 +364,15 @@ set_cheapest(RelOptInfo *parent_rel) * cases do arise, so we make the full set of checks anyway. * * There are two policy decisions embedded in this function, along with - * its sibling add_path_precheck: we treat all parameterized paths as - * having NIL pathkeys, and we ignore their startup costs, so that they - * compete only on parameterization, total cost and rowcount. This is to - * reduce the number of parameterized paths that are kept. See discussion - * in src/backend/optimizer/README. + * its sibling add_path_precheck. First, we treat all parameterized paths + * as having NIL pathkeys, so that they cannot win comparisons on the + * basis of sort order. This is to reduce the number of parameterized + * paths that are kept; see discussion in src/backend/optimizer/README. * - * Another policy that is enforced here is that we only consider cheap - * startup cost to be interesting if parent_rel->consider_startup is true. + * Second, we only consider cheap startup cost to be interesting if + * parent_rel->consider_startup is true for an unparameterized path, or + * parent_rel->consider_param_startup is true for a parameterized one. + * Again, this allows discarding useless paths sooner. * * The pathlist is kept sorted by total_cost, with cheaper paths * at the front. Within this routine, that's simply a speed hack: @@ -433,8 +437,7 @@ add_path(RelOptInfo *parent_rel, Path *new_path) * Do a fuzzy cost comparison with 1% fuzziness limit. (XXX does this * percentage need to be user-configurable?) */ - costcmp = compare_path_costs_fuzzily(new_path, old_path, 1.01, - parent_rel->consider_startup); + costcmp = compare_path_costs_fuzzily(new_path, old_path, 1.01); /* * If the two paths compare differently for startup and total cost, @@ -501,8 +504,7 @@ add_path(RelOptInfo *parent_rel, Path *new_path) accept_new = false; /* old dominates new */ else if (compare_path_costs_fuzzily(new_path, old_path, - 1.0000000001, - parent_rel->consider_startup) == COSTS_BETTER1) + 1.0000000001) == COSTS_BETTER1) remove_old = true; /* new dominates old */ else accept_new = false; /* old equals or |