I wrote: > So what this seems to mean is that for SEMI/ANTI join cases, we have to > postpone all of the inner scan cost determination to final_cost_nestloop, > so that we can do this differently depending on whether > has_indexed_join_quals() is true. That's a little bit annoying because it > will mean we take the shortcut exit less often; but since SEMI/ANTI joins > aren't that common, it's probably not going to be a big planning time hit.
Attached is a draft patch for that. It fixes the problem for me: Nested Loop Semi Join (cost=0.99..9.09 rows=1 width=74) (actual time=0.591..1.554 rows=2 loops=1) -> Index Scan using term_facttablename_columnname_idx on term t (cost=0.55..8.57 rows=1 width=74) (actual time=0.022..0.025 rows=2 loops=1) Index Cond: (((facttablename)::text = 'facttable_stat_fta4'::text) AND ((columnname)::text = 'berechnungsart'::text)) -> Index Only Scan using facttable_stat_fta4_berechnungsart_idx on facttable_stat_fta4 f (cost=0.43..143244.98 rows=5015134 width=2) (actual time=0.759..0.759 rows=1 loops=2) Index Cond: (berechnungsart = (t.term)::text) Heap Fetches: 0 Planning time: 0.545 ms Execution time: 1.615 ms > Not sure yet about your other point about the indexscan getting rejected > too soon. That doesn't seem to be happening for me, at least not in HEAD. I do see something of the sort if I turn off enable_indexonlyscan. Not sure about a good fix for that aspect. It may not be terribly critical, since AFAICS index-only scans generally ought to apply in these cases. regards, tom lane
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index ac865be..0d302f6 100644 *** a/src/backend/optimizer/path/costsize.c --- b/src/backend/optimizer/path/costsize.c *************** cost_group(Path *path, PlannerInfo *root *** 1767,1773 **** * estimate and getting a tight lower bound. We choose to not examine the * join quals here, since that's by far the most expensive part of the * calculations. The end result is that CPU-cost considerations must be ! * left for the second phase. * * 'workspace' is to be filled with startup_cost, total_cost, and perhaps * other data to be used by final_cost_nestloop --- 1767,1774 ---- * estimate and getting a tight lower bound. We choose to not examine the * join quals here, since that's by far the most expensive part of the * calculations. The end result is that CPU-cost considerations must be ! * left for the second phase; and for SEMI/ANTI joins, we must also postpone ! * incorporation of the inner path's run cost. * * 'workspace' is to be filled with startup_cost, total_cost, and perhaps * other data to be used by final_cost_nestloop *************** initial_cost_nestloop(PlannerInfo *root, *** 1815,1858 **** if (jointype == JOIN_SEMI || jointype == JOIN_ANTI) { - double outer_matched_rows; - Selectivity inner_scan_frac; - /* * SEMI or ANTI join: executor will stop after first match. * ! * For an outer-rel row that has at least one match, we can expect the ! * inner scan to stop after a fraction 1/(match_count+1) of the inner ! * rows, if the matches are evenly distributed. Since they probably ! * aren't quite evenly distributed, we apply a fuzz factor of 2.0 to ! * that fraction. (If we used a larger fuzz factor, we'd have to ! * clamp inner_scan_frac to at most 1.0; but since match_count is at ! * least 1, no such clamp is needed now.) ! * ! * A complicating factor is that rescans may be cheaper than first ! * scans. If we never scan all the way to the end of the inner rel, ! * it might be (depending on the plan type) that we'd never pay the ! * whole inner first-scan run cost. However it is difficult to ! * estimate whether that will happen, so be conservative and always ! * charge the whole first-scan cost once. ! */ ! run_cost += inner_run_cost; ! ! outer_matched_rows = rint(outer_path_rows * semifactors->outer_match_frac); ! inner_scan_frac = 2.0 / (semifactors->match_count + 1.0); ! ! /* Add inner run cost for additional outer tuples having matches */ ! if (outer_matched_rows > 1) ! run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac; ! ! /* ! * The cost of processing unmatched rows varies depending on the ! * details of the joinclauses, so we leave that part for later. */ /* Save private data for final_cost_nestloop */ ! workspace->outer_matched_rows = outer_matched_rows; ! workspace->inner_scan_frac = inner_scan_frac; } else { --- 1816,1831 ---- if (jointype == JOIN_SEMI || jointype == JOIN_ANTI) { /* * SEMI or ANTI join: executor will stop after first match. * ! * Getting decent estimates requires inspection of the join quals, ! * which we choose to postpone to final_cost_nestloop. */ /* Save private data for final_cost_nestloop */ ! workspace->inner_run_cost = inner_run_cost; ! workspace->inner_rescan_run_cost = inner_rescan_run_cost; } else { *************** initial_cost_nestloop(PlannerInfo *root, *** 1869,1875 **** workspace->total_cost = startup_cost + run_cost; /* Save private data for final_cost_nestloop */ workspace->run_cost = run_cost; - workspace->inner_rescan_run_cost = inner_rescan_run_cost; } /* --- 1842,1847 ---- *************** final_cost_nestloop(PlannerInfo *root, N *** 1893,1899 **** double inner_path_rows = inner_path->rows; Cost startup_cost = workspace->startup_cost; Cost run_cost = workspace->run_cost; - Cost inner_rescan_run_cost = workspace->inner_rescan_run_cost; Cost cpu_per_tuple; QualCost restrict_qual_cost; double ntuples; --- 1865,1870 ---- *************** final_cost_nestloop(PlannerInfo *root, N *** 1912,1953 **** if (!enable_nestloop) startup_cost += disable_cost; ! /* cost of source data */ if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI) { - double outer_matched_rows = workspace->outer_matched_rows; - Selectivity inner_scan_frac = workspace->inner_scan_frac; - /* * SEMI or ANTI join: executor will stop after first match. */ ! /* Compute number of tuples processed (not number emitted!) */ ntuples = outer_matched_rows * inner_path_rows * inner_scan_frac; /* ! * For unmatched outer-rel rows, there are two cases. If the inner ! * path is an indexscan using all the joinquals as indexquals, then an ! * unmatched row results in an indexscan returning no rows, which is ! * probably quite cheap. We estimate this case as the same cost to ! * return the first tuple of a nonempty scan. Otherwise, the executor ! * will have to scan the whole inner rel; not so cheap. */ if (has_indexed_join_quals(path)) { run_cost += (outer_path_rows - outer_matched_rows) * inner_rescan_run_cost / inner_path_rows; /* ! * We won't be evaluating any quals at all for these rows, so * don't add them to ntuples. */ } else { run_cost += (outer_path_rows - outer_matched_rows) * inner_rescan_run_cost; ntuples += (outer_path_rows - outer_matched_rows) * inner_path_rows; } --- 1883,1983 ---- if (!enable_nestloop) startup_cost += disable_cost; ! /* cost of inner-relation source data (we already dealt with outer rel) */ if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI) { /* * SEMI or ANTI join: executor will stop after first match. */ + Cost inner_run_cost = workspace->inner_run_cost; + Cost inner_rescan_run_cost = workspace->inner_rescan_run_cost; + double outer_matched_rows; + Selectivity inner_scan_frac; ! /* ! * For an outer-rel row that has at least one match, we can expect the ! * inner scan to stop after a fraction 1/(match_count+1) of the inner ! * rows, if the matches are evenly distributed. Since they probably ! * aren't quite evenly distributed, we apply a fuzz factor of 2.0 to ! * that fraction. (If we used a larger fuzz factor, we'd have to ! * clamp inner_scan_frac to at most 1.0; but since match_count is at ! * least 1, no such clamp is needed now.) ! */ ! outer_matched_rows = rint(outer_path_rows * semifactors->outer_match_frac); ! inner_scan_frac = 2.0 / (semifactors->match_count + 1.0); ! ! /* ! * Compute number of tuples processed (not number emitted!). First, ! * account for successfully-matched outer rows. ! */ ntuples = outer_matched_rows * inner_path_rows * inner_scan_frac; /* ! * Now we need to estimate the actual costs of scanning the inner ! * relation, which may be quite a bit less than N times inner_run_cost ! * due to early scan stops. We consider two cases. If the inner path ! * is an indexscan using all the joinquals as indexquals, then an ! * unmatched outer row results in an indexscan returning no rows, ! * which is probably quite cheap. Otherwise, the executor will have ! * to scan the whole inner rel for an unmatched row; not so cheap. */ if (has_indexed_join_quals(path)) { + /* + * Successfully-matched outer rows will only require scanning + * inner_scan_frac of the inner relation. In this case, we don't + * need to charge the full inner_run_cost even when that's more + * than inner_rescan_run_cost, because we can assume that none of + * the inner scans ever scan the whole inner relation. So it's + * okay to assume that all the inner scan executions can be + * fractions of the full cost, even if materialization is reducing + * the rescan cost. At this writing, it's impossible to get here + * for a materialized inner scan, so inner_run_cost and + * inner_rescan_run_cost will be the same anyway; but just in + * case, use inner_run_cost for the first matched tuple and + * inner_rescan_run_cost for additional ones. + */ + run_cost += inner_run_cost * inner_scan_frac; + if (outer_matched_rows > 1) + run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac; + + /* + * Add the cost of inner-scan executions for unmatched outer rows. + * We estimate this as the same cost as returning the first tuple + * of a nonempty scan. We consider that these are all rescans, + * since we used inner_run_cost once already. + */ run_cost += (outer_path_rows - outer_matched_rows) * inner_rescan_run_cost / inner_path_rows; /* ! * We won't be evaluating any quals at all for unmatched rows, so * don't add them to ntuples. */ } else { + /* + * Here, a complicating factor is that rescans may be cheaper than + * first scans. If we never scan all the way to the end of the + * inner rel, it might be (depending on the plan type) that we'd + * never pay the whole inner first-scan run cost. However it is + * difficult to estimate whether that will happen (and it could + * not happen if there are any unmatched outer rows!), so be + * conservative and always charge the whole first-scan cost once. + */ + run_cost += inner_run_cost; + + /* Add inner run cost for additional outer tuples having matches */ + if (outer_matched_rows > 1) + run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac; + + /* Add inner run cost for unmatched outer tuples */ run_cost += (outer_path_rows - outer_matched_rows) * inner_rescan_run_cost; + + /* And count the unmatched join tuples as being processed */ ntuples += (outer_path_rows - outer_matched_rows) * inner_path_rows; } diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h index 279051e..706b6d1 100644 *** a/src/include/nodes/relation.h --- b/src/include/nodes/relation.h *************** typedef struct JoinCostWorkspace *** 1719,1730 **** Cost run_cost; /* non-startup cost components */ /* private for cost_nestloop code */ Cost inner_rescan_run_cost; - double outer_matched_rows; - Selectivity inner_scan_frac; /* private for cost_mergejoin code */ - Cost inner_run_cost; double outer_rows; double inner_rows; double outer_skip_rows; --- 1719,1728 ---- Cost run_cost; /* non-startup cost components */ /* private for cost_nestloop code */ + Cost inner_run_cost; /* also used by cost_mergejoin code */ Cost inner_rescan_run_cost; /* private for cost_mergejoin code */ double outer_rows; double inner_rows; double outer_skip_rows;
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers