8000 Fix planner's cost estimation for SEMI/ANTI joins with inner indexscans. · luisacoding/postgres@3f59be8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3f59be8

Browse files
committed
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.
1 parent 3701362 commit 3f59be8

File tree

7 files changed

+175
-86
lines changed

7 files changed

+175
-86
lines changed

src/backend/nodes/outfuncs.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,6 +1844,7 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node)
18441844
WRITE_FLOAT_FIELD(rows, "%.0f");
18451845
WRITE_INT_FIELD(width);
18461846
WRITE_BOOL_FIELD(consider_startup);
1847+
WRITE_BOOL_FIELD(consider_param_startup);
18471848
WRITE_NODE_FIELD(reltargetlist);
18481849
WRITE_NODE_FIELD(pathlist);
18491850
WRITE_NODE_FIELD(ppilist);

src/backend/optimizer/README

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ a nestloop that provides parameters to the lower join's inputs). While we
795795
do not ignore merge joins entirely, joinpath.c does not fully explore the
796796
space of potential merge joins with parameterized inputs. Also, add_path
797797
treats parameterized paths as having no pathkeys, so that they compete
798-
only on total cost and rowcount; they don't get preference for producing a
798+
only on cost and rowcount; they don't get preference for producing a
799799
special sort order. This creates additional bias against merge joins,
800800
since we might discard a path that could have been useful for performing
801801
a merge without an explicit sort step. Since a parameterized path must
@@ -804,6 +804,13 @@ uninteresting, these choices do not affect any requirement for the final
804804
output order of a query --- they only make it harder to use a merge join
805805
at a lower level. The savings in planning work justifies that.
806806

807+
Similarly, parameterized paths do not normally get preference in add_path
808+
for having cheap startup cost; that's seldom of much value when on the
809+
inside of a nestloop, so it seems not worth keeping extra paths solely for
810+
that. An exception occurs for parameterized paths fo 8000 r the RHS relation of
811+
a SEMI or ANTI join: in those cases, we can stop the inner scan after the
812+
first match, so it's primarily startup not total cost that we care about.
813+
807814

808815
LATERAL subqueries
809816
------------------

src/backend/optimizer/path/allpaths.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ set_rel_pathlist_hook_type set_rel_pathlist_hook = NULL;
6161
join_search_hook_type join_search_hook = NULL;
6262

6363

64+
static void set_base_rel_consider_startup(PlannerInfo *root);
6465
static void set_base_rel_sizes(PlannerInfo *root);
6566
static void set_base_rel_pathlists(PlannerInfo *root);
6667
static void set_rel_size(PlannerInfo *root, RelOptInfo *rel,
@@ -152,6 +153,9 @@ make_one_rel(PlannerInfo *root, List *joinlist)
152153
root->all_baserels = bms_add_member(root->all_baserels, brel->relid);
153154
}
154155

156+
/* Mark base rels as to whether we care about fast-start plans */
157+
set_base_rel_consider_startup(root);
158+
155159
/*
156160
* Generate access paths for the base rels.
157161
*/
@@ -171,6 +175,49 @@ make_one_rel(PlannerInfo *root, List *joinlist)
171175
return rel;
172176
}
173177

178+
/*
179+
* set_base_rel_consider_startup
180+
* Set the consider_[param_]startup flags for each base-relation entry.
181+
*
182+
* For the moment, we only deal with consider_param_startup here; because the
183+
* logic for consider_startup is pretty trivial and is the same for every base
184+
* relation, we just let build_simple_rel() initialize that flag correctly to
185+
* start with. If that logic ever gets more complicated it would probably
186+
* be better to move it here.
187+
*/
188+
static void
189+
set_base_rel_consider_startup(PlannerInfo *root)
190+
{
191+
/*
192+
* Since parameterized paths can only be used on the inside of a nestloop
193+
* join plan, there is usually little value in considering fast-start
194+
* plans for them. However, for relations that are on the RHS of a SEMI
195+
* or ANTI join, a fast-start plan can be useful because we're only going
196+
* to care about fetching one tuple anyway.
197+
*
198+
* To minimize growth of planning time, we currently restrict this to
199+
* cases where the RHS is a single base relation, not a join; there is no
200+
* provision for consider_param_startup to get set at all on joinrels.
201+
* Also we don't worry about appendrels. costsize.c's costing rules for
202+
* nestloop semi/antijoins don't consider such cases either.
203+
*/ F438
204+
ListCell *lc;
205+
206+
foreach(lc, root->join_info_list)
207+
{
208+
SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) lfirst(lc);
209+
int varno;
210+
211+
if ((sjinfo->jointype == JOIN_SEMI || sjinfo->jointype == JOIN_ANTI) &&
212+
bms_get_singleton_member(sjinfo->syn_righthand, &varno))
213+
{
214+
RelOptInfo *rel = find_base_rel(root, varno);
215+
216+
rel->consider_param_startup = true;
217+
}
218+
}
219+
}
220+
174221
/*
175222
* set_base_rel_sizes
176223
* Set the size estimates (rows and widths) for each base-relation entry.

src/backend/optimizer/path/costsize.c

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,7 +1767,8 @@ cost_group(Path *path, PlannerInfo *root,
17671767
* estimate and getting a tight lower bound. We choose to not examine the
17681768
* join quals here, since that's by far the most expensive part of the
17691769
* calculations. The end result is that CPU-cost considerations must be
1770-
* left for the second phase.
1770+
* left for the second phase; and for SEMI/ANTI joins, we must also postpone
1771+
* incorporation of the inner path's run cost.
17711772
*
17721773
* 'workspace' is to be filled with startup_cost, total_cost, and perhaps
17731774
* other data to be used by final_cost_nestloop
@@ -1815,44 +1816,16 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
18151816

18161817
if (jointype == JOIN_SEMI || jointype == JOIN_ANTI)
18171818
{
1818-
double outer_matched_rows;
1819-
Selectivity inner_scan_frac;
1820-
18211819
/*
18221820
* SEMI or ANTI join: executor will stop after first match.
18231821
*
1824-
* For an outer-rel row that has at least one match, we can expect the
1825-
* inner scan to stop after a fraction 1/(match_count+1) of the inner
1826-
* rows, if the matches are evenly distributed. Since they probably
1827-
* aren't quite evenly distributed, we apply a fuzz factor of 2.0 to
1828-
* that fraction. (If we used a larger fuzz factor, we'd have to
1829-
* clamp inner_scan_frac to at most 1.0; but since match_count is at
1830-
* least 1, no such clamp is needed now.)
1831-
*
1832-
* A complicating factor is that rescans may be cheaper than first
1833-
* scans. If we never scan all the way to the end of the inner rel,
1834-
* it might be (depending on the plan type) that we'd never pay the
1835-
* whole inner first-scan run cost. However it is difficult to
1836-
* estimate whether that will happen, so be conservative and always
1837-
* charge the whole first-scan cost once.
1838-
*/
1839-
run_cost += inner_run_cost;
1840-
1841-
outer_matched_rows = rint(outer_path_rows * semifactors->outer_match_frac);
1842-
inner_scan_frac = 2.0 / (semifactors->match_count + 1.0);
1843-
1844-
/* Add inner run cost for additional outer tuples having matches */
1845-
if (outer_matched_rows > 1)
1846-
run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac;
1847-
1848-
/*
1849-
* The cost of processing unmatched rows varies depending on the
1850-
* details of the joinclauses, so we leave that part for later.
1822+
* Getting decent estimates requires inspection of the join quals,
1823+
* which we choose to postpone to final_cost_nestloop.
18511824
*/
18521825

18531826
/* Save private data for final_cost_nestloop */
1854-
workspace->outer_matched_rows = outer_matched_rows;
1855-
workspace->inner_scan_frac = inner_scan_frac;
1827+
workspace->inner_run_cost = inner_run_cost;
1828+
workspace->inner_rescan_run_cost = inner_rescan_run_cost;
18561829
}
18571830
else
18581831
{
@@ -1869,7 +1842,6 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace,
18691842
workspace->total_cost = startup_cost + run_cost;
18701843
/* Save private data for final_cost_nestloop */
18711844
workspace->run_cost = run_cost;
1872-
workspace->inner_rescan_run_cost = inner_rescan_run_cost;
18731845
}
18741846

18751847
/*
@@ -1893,7 +1865,6 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
18931865
double inner_path_rows = inner_path->rows;
18941866
Cost startup_cost = workspace->startup_cost;
18951867
Cost run_cost = workspace->run_cost;
1896-
Cost inner_rescan_run_cost = workspace->inner_rescan_run_cost;
18971868
Cost cpu_per_tuple;
18981869
QualCost restrict_qual_cost;
18991870
double ntuples;
@@ -1912,42 +1883,101 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
19121883
if (!enable_nestloop)
19131884
startup_cost += disable_cost;
19141885

1915-
/* cost of source data */
1886+
/* cost of inner-relation source data (we already dealt with outer rel) */
19161887

19171888
if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI)
19181889
{
1919-
double outer_matched_rows = workspace->outer_matched_rows;
1920-
Selectivity inner_scan_frac = workspace->inner_scan_frac;
1921-
19221890
/*
19231891
* SEMI or ANTI join: executor will stop after first match.
19241892
*/
1893+
Cost inner_run_cost = workspace->inner_run_cost;
1894+
Cost inner_rescan_run_cost = workspace->inner_rescan_run_cost;
1895+
double outer_matched_rows;
1896+
Selectivity inner_scan_frac;
19251897

1926-
/* Compute number of tuples processed (not number emitted!) */
1898+
/*
1899+
* For an outer-rel row that has at least one match, we can expect the
1900+
* inner scan to stop after a fraction 1/(match_count+1) of the inner
1901+
* rows, if the matches are evenly distributed. Since they probably
1902+
* aren't quite evenly distributed, we apply a fuzz factor of 2.0 to
1903+
* that fraction. (If we used a larger fuzz factor, we'd have to
1904+
* clamp inner_scan_frac to at most 1.0; but since match_count is at
1905+
* least 1, no such clamp is needed now.)
1906+
*/
1907+
outer_matched_rows = rint(outer_path_rows * semifactors->outer_match_frac);
1908+
inner_scan_frac = 2.0 / (semifactors->match_count + 1.0);
1909+
1910+
/*
1911+
* Compute number of tuples processed (not number emitted!). First,
1912+
* account for successfully-matched outer rows.
1913+
*/
19271914
ntuples = outer_matched_rows * inner_path_rows * inner_scan_frac;
19281915

19291916
/*
1930-
* For unmatched outer-rel rows, there are two cases. If the inner
1931-
* path is an indexscan using all the joinquals as indexquals, then an
1932-
* unmatched row results in an indexscan returning no rows, which is
1933-
* probably quite cheap. We estimate this case as the same cost to
1934-
* return the first tuple of a nonempty scan. Otherwise, the executor
1935-
* will have to scan the whole inner rel; not so cheap.
1917+
* Now we need to estimate the actual costs of scanning the inner
1918+
* relation, which may be quite a bit less than N times inner_run_cost
1919+
* due to early scan stops. We consider two cases. If the inner path
1920+
* is an indexscan using all the joinquals as indexquals, then an
1921+
* unmatched outer row results in an indexscan returning no rows,
1922+
* which is probably quite cheap. Otherwise, the executor will have
1923+
* to scan the whole inner rel for an unmatched row; not so cheap.
19361924
*/
19371925
if (has_indexed_join_quals(path))
19381926
{
1927+
/*
1928+
* Successfully-matched outer rows will only require scanning
1929+
* inner_scan_frac of the inner relation. In this case, we don't
1930+
* need to charge the full inner_run_cost even when that's more
1931+
* than inner_rescan_run_cost, because we can assume that none of
1932+
* the inner scans ever scan the whole inner relation. So it's
1933+
* okay to assume that all the inner scan executions can be
1934+
* fractions of the full cost, even if materialization is reducing
1935+
* the rescan cost. At this writing, it's impossible to get here
1936+
* for a materialized inner scan, so inner_run_cost and
1937+
* inner_rescan_run_cost will be the same anyway; but just in
1938+
* case, use inner_run_cost for the first matched tuple and
1939+
* inner_rescan_run_cost for additional ones.
1940+
*/
1941+
run_cost += inner_run_cost * inner_scan_frac;
1942+
if (outer_matched_rows > 1)
1943+
run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac;
1944+
1945+
/*
1946+
* Add the cost of inner-scan executions for unmatched outer rows.
1947+
* We estimate this as the same cost as returning the first tuple
1948+
* of a nonempty scan. We consider that these are all rescans,
1949+
* since we used inner_run_cost once already.
1950+
*/
19391951
run_cost += (outer_path_rows - outer_matched_rows) *
19401952
inner_rescan_run_cost / inner_path_rows;
19411953

19421954
/*
1943-
* We won't be evaluating any quals at all for these rows, so
1955+
* We won't be evaluating any quals at all for unmatched rows, so
19441956
* don't add them to ntuples.
19451957
*/
19461958
}
19471959
else
19481960
{
1961+
/*
1962+
* Here, a complicating factor is that rescans may be cheaper than
1963+
* first scans. If we never scan all the way to the end of the
1964+
* inner rel, it might be (depending on the plan type) that we'd
1965+
* never pay the whole inner first-scan run cost. However it is
1966+
* difficult to estimate whether that will happen (and it could
1967+
* not happen if there are any unmatched outer rows!), so be
1968+
* conservative and always charge the whole first-scan cost once.
1969+
*/
1970+
run_cost += inner_run_cost;
1971+
1972+
/* Add inner run cost for additional outer tuples having matches */
1973+
if (outer_matched_rows > 1)
1974+
run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac;
1975+
1976+
/* Add inner run cost for unmatched outer tuples */
19491977
run_cost += (outer_path_rows - outer_matched_rows) *
19501978
inner_rescan_run_cost;
1979+
1980+
/* And count the unmatched join tuples as being processed */
19511981
ntuples += (outer_path_rows - outer_matched_rows) *
19521982
inner_path_rows;
19531983
}

0 commit comments

Comments
 (0)
0