From 7c1794729b586a826b046c2ee1edfd4a193c5cd7 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Thu, 23 Jul 2026 09:42:34 -0300 Subject: [PATCH v8 13/13] Rework hashjoin Bloom filter selection , combination and costing. Candidate selection no longer applies a count cap. find_interesting_bloom_filters() used to keep only the bloom_filter_pushdown_max most selective candidates; it now returns every candidate that clears bloom_filter_pushdown_threshold. A filter that looks weak on its own can still be valuable combined with others, or to a consumer that can use it for reasons the raw selectivity estimate can't see. Combinations now apply all candidates at once instead of enumerating subsets. Previously each non-empty subset of the interesting filters produced its own path (up to 2^n-1 extra paths per base scan). Following the "apply all candidates simultaneously" heuristic from the bottom-up Bloom filter paper, find_bloom_filter_combinations() now builds a single combination: it takes the candidates most selective first and adds each one whose build side does not overlap the build sides already chosen (overlapping build sides are sourced from joins sharing relations, so their selectivities are not independent), stopping at bloom_filter_pushdown_max filters. bloom_filter_pushdown_max is thus repurposed from "how many candidates to keep" to "how many filters to apply in a single scan", which also bounds the per-tuple probe cost. The number of extra paths is now linear in the base paths rather than exponential in the candidates. Filtering is no longer modeled as free. Filter-aware scan paths are built through the real create_*_path constructors, and a new apply_expected_filters() helper charges one cpu_operator_cost per filter per tuple on top of reducing the row estimate, so a filter is only chosen when it is actually cheaper. The scan constructors gain a filters argument for this. IndexPath is the exception: create_filtered_scan_path() clones the source IndexPath, reusing its indexclauses and pathkeys rather than re-deriving them, and applies the same cost/row adjustment. The interesting-filter list is cached on the RelOptInfo and computed once per base relation. Core no longer builds filter-aware CustomPaths itself. A provider that wants them builds them from its set_rel_pathlist_hook using the now-exported find_bloom_filter_combinations() and apply_expected_filters(). test_bloom_customscan is updated accordingly and gains a test combining two filters. Because filtering now carries a real cost and a single combined filter set is offered per scan, several regression plans drop or reorder filters on small relations; the expected output is updated to match. --- src/backend/optimizer/path/allpaths.c | 317 ++++++++++-------- src/backend/optimizer/path/costsize.c | 14 +- src/backend/optimizer/path/indxpath.c | 4 +- src/backend/optimizer/path/tidpath.c | 8 +- src/backend/optimizer/plan/planner.c | 2 +- src/backend/optimizer/util/pathnode.c | 121 ++++--- src/backend/utils/misc/guc_parameters.dat | 4 +- src/backend/utils/misc/postgresql.conf.sample | 4 +- src/include/nodes/pathnodes.h | 10 + src/include/optimizer/pathnode.h | 15 +- src/include/optimizer/paths.h | 1 + .../expected/test_bloom_customscan.out | 92 ++++- .../sql/test_bloom_customscan.sql | 51 ++- .../test_bloom_customscan.c | 74 ++-- src/test/regress/expected/graph_table.out | 2 +- src/test/regress/expected/join.out | 191 +++++------ src/test/regress/expected/merge.out | 16 +- src/test/regress/expected/misc_functions.out | 14 +- src/test/regress/expected/returning.out | 8 +- src/test/regress/expected/updatable_views.out | 4 +- 20 files changed, 577 insertions(+), 375 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 160ab49d3c1..d9e681ef9ae 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -876,7 +876,7 @@ set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte) return; /* Consider sequential scan */ - add_path(rel, create_seqscan_path(root, rel, required_outer, 0)); + add_path(rel, create_seqscan_path(root, rel, required_outer, 0, NIL)); /* If appropriate, consider parallel sequential scan */ if (rel->consider_parallel && required_outer == NULL) @@ -903,7 +903,7 @@ create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel) return; /* Add an unordered partial path based on a parallel sequential scan. */ - add_partial_path(rel, create_seqscan_path(root, rel, NULL, parallel_workers)); + add_partial_path(rel, create_seqscan_path(root, rel, NULL, parallel_workers, NIL)); } /* @@ -1268,9 +1268,10 @@ bloom_build_side_join_ratio(PlannerInfo *root, RelOptInfo *rel, * fraction is estimated as the semijoin selectivity of those clauses. * * A candidate is "interesting" only if it is expected to eliminate at least - * bloom_filter_pushdown_threshold of the rel's tuples. We keep at most - * bloom_filter_pushdown_max of the most selective candidates, and return them - * as a list of ExpectedFilter nodes. + * bloom_filter_pushdown_threshold of the rel's tuples. Every candidate that + * clears that bar is returned as an ExpectedFilter node; deciding which of + * them to actually combine into a scan path (and how many) is left to + * find_bloom_filter_combinations(). * * XXX This needs to be careful to not interfere with the general selectivity * estimation, performed by clauselist_selectivity(). We'll estimate the filter @@ -1305,6 +1306,15 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) if (rel->reloptkind != RELOPT_BASEREL) return NIL; + /* + * Return the cached result if we've already computed the interesting + * filters for this rel. Both core path generation and a CustomScan + * provider may ask for them, and the enumeration/selectivity work below + * is not free, so we do it only once per base relation. + */ + if (rel->bloom_filters_valid) + return rel->bloom_filters; + /* Collect candidate hashjoinable equality clauses for this rel. */ candidates = generate_implied_equalities_for_all_columns(root, rel, bloom_em_matches_anybarevar, @@ -1622,34 +1632,13 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) } /* - * We have collected all potentially intresting filters. Evaluate - * selectivity of each group and keep only the most interesting filters. - * Filters have to eliminate at least bloom_filter_pushdown_threshold - * tuples, and we keep only bloom_filter_pushdown_max most selective ones. - * - * We only connsider a limited number of interesting filters, to prevent - * path explosion. If we found too many, keep only the most selective ones - * (with smallest surviving fraction of tuples), to bound the number of - * generated paths. - * - * XXX This also aligns with good join orders - those tend to perform the - * most selective joins first. So we get to build the filters soon, even - * if the hashjoin optimization is not disabled. + * Return every candidate that clears bloom_filter_pushdown_threshold. + * Which of them to combine into a scan path, and how many, is decided by + * find_bloom_filter_combinations(); a filter that is weak on its own can + * still be useful combined with others, or to a smart consumer. */ - while (list_length(result) > bloom_filter_pushdown_max) - { - ExpectedFilter *worst = NULL; - ListCell *lcw; - - foreach(lcw, result) - { - ExpectedFilter *f = (ExpectedFilter *) lfirst(lcw); - - if (worst == NULL || f->selectivity > worst->selectivity) - worst = f; - } - result = list_delete_ptr(result, worst); - } + rel->bloom_filters = result; + rel->bloom_filters_valid = true; return result; } @@ -1659,59 +1648,40 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel) * Generate additional scan paths that anticipate one or more pushed-down * Bloom filters. * - * For each non-empty subset of the interesting filters, we clone every eligible - * existing scan path, reducing its row estimate by the combined selectivity and - * attaching the corresponding ExpectedFilter nodes. + * For each combination of the interesting filters (see + * find_bloom_filter_combinations), we build a new path via the real + * constructor for that path's type, passing the filters in so the + * constructor's apply_expected_filters() call charges a per-tuple probe cost. + * + * IndexPath is the one exception: it goes through create_filtered_scan_path, + * which clones the source path, because create_index_path() would re-derive + * its indexclauses/pathkeys from scratch for no benefit (see the comment on + * create_filtered_scan_path). * * These paths are kept alongside the regular paths (add_path keeps paths with * differing expected_filters) and are consumed by join path generation; * set_cheapest never selects them. * - * XXX We must not clone paths that already have expected filters. - * - * XXX The cloning is a rather dirty way to copy paths. It does not readjust the - * cost in a reasonable way. For example custom scans could do something smart - * with the filters, so it should have a chance to deal with that. A cleaner - * solution might be to actually pass the filters to the various "create" - * function, like create_seqscan_path/... For CustomScan nodes we can probably - * do most of this in the set_rel_pathlist_hook, somewhere. Maybe that needs - * some helper methods, though. And maybe it will need to pass some of the info - * through the callbacks? Not sure, someone has to try that. - * - * XXX This may need some major changes to work with custom scans. Right now we - * only consider filters exactly matching the hash keys, so if the hashjoin is - * on (t1.a = t2.a AND t1.b = t2.b), then the filter will be on (a,b). But a - * custom scan may prefer "split" filters on each column independently. We'd - * need a way for the custom scan to indicate that, and we'd need to apply this - * only to the "matching" scan paths (and not to any other scan paths). But - * we only look at the paths after selecting the "interesting" filters, so we'd - * need to rethink that - we'd need to make the "interesting" filters specific - * to a path, or something like that. + * CustomPath is not handled here at all. Core cannot safely copy or construct + * a CustomPath, so a provider that wants filter-aware CustomPaths must build + * them itself, inside its own set_rel_pathlist_hook, by calling + * find_bloom_filter_combinations() directly and costing the result however it + * likes -- with real smart costing, or by calling apply_expected_filters() on + * its own freshly-built CustomPath if it doesn't want to be smart. */ static void generate_expected_filter_paths(PlannerInfo *root, RelOptInfo *rel) { - List *filters; + List *combinations; List *basepaths = NIL; - int nfilters; - uint32 combo; - ListCell *lc; - - filters = find_interesting_bloom_filters(root, rel); - if (filters == NIL) - return; - - nfilters = list_length(filters); /* * Snapshot the existing unparameterized, non-partial scan paths of a * supported type. We must snapshot before calling add_path(), which * mutates rel->pathlist. */ - foreach(lc, rel->pathlist) + foreach_ptr(Path, path, rel->pathlist) { - Path *path = (Path *) lfirst(lc); - /* XXX Is parameterization really a problem? Always? */ if (path->param_info != NULL || path->expected_filters != NIL) continue; @@ -1725,19 +1695,6 @@ generate_expected_filter_paths(PlannerInfo *root, RelOptInfo *rel) case T_TidRangePath: basepaths = lappend(basepaths, path); break; - case T_CustomPath: - - /* - * A base-relation CustomScan can receive a pushed-down - * filter, but only if the provider advertised that it knows - * how to consume one (it probes the filter itself inside its - * scan loop; see find_bloom_filter_recipient in createplan.c - * and ExecInitCustomScan). Providers that don't opt in, like - * heap, are unaffected. - */ - if (((CustomPath *) path)->flags & CUSTOMPATH_SUPPORT_BLOOM_FILTERS) - basepaths = lappend(basepaths, path); - break; default: break; } @@ -1746,79 +1703,155 @@ generate_expected_filter_paths(PlannerInfo *root, RelOptInfo *rel) if (basepaths == NIL) return; + combinations = find_bloom_filter_combinations(root, rel); + if (combinations == NIL) + return; + /* - * Generate all combinations of the interesting filters. We do that by - * iterating 1 to (2^n-1), which generates all bitmask in between. Those - * are the subsets. - * - * XXX This is a good demonstration why we need to keep the number of - * filters low - * - * XXX Maybe we should also stop adding filters once the other filters - * already eliminate enought tuples. Say, we know F1 alone eliminates 99% - * tuples. Does it make sense to also consider [F1,F2]? Probably not. We - * could track "maximum" sets, and reject combinations containing one of - * those. We'd need to generate sets of increasing size, the iteration - * does not do that. But that's not hard. + * For each combination, build a fresh path from each eligible base path + * via that base path's real constructor (except IndexPath, see the + * function comment above). */ - for (combo = 1; combo < ((uint32) 1 << nfilters); combo++) + foreach_ptr(List, combo, combinations) { - List *subset = NIL; - Relids used = NULL; - bool overlap = false; - int i = 0; - ListCell *lcf; - - foreach(lcf, filters) + foreach_ptr(Path, base, basepaths) { - if (combo & ((uint32) 1 << i)) - subset = lappend(subset, lfirst(lcf)); - i++; - } + Path *newpath = NULL; - /* - * Skip combinations that mix filters with overlapping build sides. - * Such filters would be sourced from joins that share build - * relations, so their selectivities are not independent and applying - * them together at the same scan wrong - the result would be correct, - * but the estimates would get smaller. - */ - foreach(lcf, subset) - { - ExpectedFilter *f = (ExpectedFilter *) lfirst(lcf); - - if (bms_overlap(used, f->build_relids)) + switch (nodeTag(base)) { - overlap = true; - break; + case T_Path: + if (base->pathtype == T_SeqScan) + newpath = create_seqscan_path(root, rel, NULL, + base->parallel_workers, + combo); + else if (base->pathtype == T_SampleScan) + newpath = create_samplescan_path(root, rel, NULL, + combo); + break; + case T_IndexPath: + newpath = create_filtered_scan_path(root, base, combo); + break; + case T_BitmapHeapPath: + newpath = (Path *) + create_bitmap_heap_path(root, rel, + ((BitmapHeapPath *) base)->bitmapqual, + NULL, 1.0, + base->parallel_workers, + combo); + break; + case T_TidPath: + newpath = (Path *) + create_tidscan_path(root, rel, + ((TidPath *) base)->tidquals, + NULL, combo); + break; + case T_TidRangePath: + newpath = (Path *) + create_tidrangescan_path(root, rel, + ((TidRangePath *) base)->tidrangequals, + NULL, base->parallel_workers, + combo); + break; + default: + break; } - used = bms_add_members(used, f->build_relids); - } - bms_free(used); - - if (overlap) - { - list_free(subset); - continue; - } - /* - * All filtered paths for this combo share the same expected_filters - * list. That's safe: the list is never modified, and add_path() only - * ever frees the Path node itself, not its expected_filters. - */ - foreach(lc, basepaths) - { - Path *base = (Path *) lfirst(lc); - Path *newpath; - - newpath = create_filtered_scan_path(root, base, subset); if (newpath != NULL) add_path(rel, newpath); } } } +/* + * expected_filter_selectivity_cmp + * list_sort comparator ordering ExpectedFilters by ascending selectivity, + * i.e. the most selective filter (smallest surviving fraction) first. + */ +static int +expected_filter_selectivity_cmp(const ListCell *a, const ListCell *b) +{ + Selectivity sa = ((ExpectedFilter *) lfirst(a))->selectivity; + Selectivity sb = ((ExpectedFilter *) lfirst(b))->selectivity; + + if (sa < sb) + return -1; + if (sa > sb) + return 1; + return 0; +} + +/* + * find_bloom_filter_combinations + * Find the interesting Bloom filters rel could receive from a hash join + * above it (see find_interesting_bloom_filters), and return the combination + * of them worth building a path for, as a list of lists of ExpectedFilter. + * + * Following the "apply all candidates simultaneously" heuristic (Heuristic 4 + * of the bottom-up Bloom filter paper), we build a single combination that + * applies as many candidates as possible at once. We take the interesting + * filters most selective first and add each one whose build side does not + * overlap the build sides already chosen, stopping once + * bloom_filter_pushdown_max filters have been collected. + * + * Overlapping build sides are skipped because such filters would be sourced + * from joins sharing build relations, so their selectivities are not + * independent and applying them together would under-estimate the surviving + * rows. On such a conflict we keep the more selective filter, since it sorts + * first. bloom_filter_pushdown_max bounds how many filters are applied at + * once, which bounds the per-tuple probe cost. + * + * The result is a list of combinations (each a list of ExpectedFilter); this + * heuristic produces at most one combination. + * + * Note: A CustomScan provider can call it directly from its own + * set_rel_pathlist_hook (see generate_expected_filter_paths above). + */ +List * +find_bloom_filter_combinations(PlannerInfo *root, RelOptInfo *rel) +{ + List *filters = find_interesting_bloom_filters(root, rel); + List *sorted; + List *combo = NIL; + Relids used = NULL; + ListCell *lc; + + if (filters == NIL) + return NIL; + + /* + * Consider the most selective filters first, so that on a build-side + * conflict we keep the one that discards more rows, and so that the + * bloom_filter_pushdown_max cap retains the most useful filters. Sort a + * copy: "filters" is cached on the RelOptInfo and must not be reordered. + */ + sorted = list_copy(filters); + list_sort(sorted, expected_filter_selectivity_cmp); + + foreach(lc, sorted) + { + ExpectedFilter *f = (ExpectedFilter *) lfirst(lc); + + if (list_length(combo) >= bloom_filter_pushdown_max) + break; + + /* skip a filter whose build side overlaps one already chosen */ + if (bms_overlap(used, f->build_relids)) + continue; + + combo = lappend(combo, f); + used = bms_add_members(used, f->build_relids); + } + + list_free(sorted); + bms_free(used); + + if (combo == NIL) + return NIL; + + return list_make1(combo); +} + /* * set_tablesample_rel_size * Set size estimates for a sampled relation @@ -1877,7 +1910,7 @@ set_tablesample_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry * required_outer = rel->lateral_relids; /* Consider sampled scan */ - path = create_samplescan_path(root, rel, required_outer); + path = create_samplescan_path(root, rel, required_outer, NIL); /* * If the sampling method does not support repeatable scans, we must avoid @@ -5893,7 +5926,7 @@ create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel, return; add_partial_path(rel, (Path *) create_bitmap_heap_path(root, rel, - bitmapqual, rel->lateral_relids, 1.0, parallel_workers)); + bitmapqual, rel->lateral_relids, 1.0, parallel_workers, NIL)); } /* diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index f650c7c3d19..c270e871914 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -176,11 +176,11 @@ bool enable_async_append = true; double bloom_filter_pushdown_threshold = 0.3; /* - * Upper bound on the number of distinct interesting Bloom filters considered - * for a single scan relation. This bounds the number of additional paths - * generated per scan (the planner enumerates non-empty subsets of the - * interesting filters, i.e. up to 2^bloom_filter_pushdown_max - 1 extra - * paths per base scan path). + * Upper bound on the number of interesting Bloom filters that may be + * combined into a single filter-aware scan path variant for a base + * relation. This does not limit how many candidate filters are considered + * (see bloom_filter_pushdown_threshold) -- only how large a combination of + * them the planner is willing to build a path for. */ int bloom_filter_pushdown_max = 3; @@ -189,8 +189,8 @@ int bloom_filter_pushdown_max = 3; * side. Bloom filters over larger joins are unlikely to be worthwhile and * enumerating them would inflate planning time, so we keep this small. This * bounds the *size* of each candidate build side, which is distinct from - * bloom_filter_pushdown_max that bounds how many interesting filters are - * ultimately kept per probe relation. + * bloom_filter_pushdown_max, which bounds how many filters are applied + * together in a single scan. */ int bloom_filter_pushdown_max_build_relids = 3; diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index 3f5d4fa3182..a6c9a41ead1 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -344,7 +344,7 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) bitmapqual = choose_bitmap_and(root, rel, bitindexpaths); bpath = create_bitmap_heap_path(root, rel, bitmapqual, - rel->lateral_relids, 1.0, 0); + rel->lateral_relids, 1.0, 0, NIL); add_path(rel, (Path *) bpath); /* create a partial bitmap heap path */ @@ -411,7 +411,7 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel) required_outer = PATH_REQ_OUTER(bitmapqual); loop_count = get_loop_count(root, rel->relid, required_outer); bpath = create_bitmap_heap_path(root, rel, bitmapqual, - required_outer, loop_count, 0); + required_outer, loop_count, 0, NIL); add_path(rel, (Path *) bpath); } } diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c index 18a3654720b..83b261542ea 100644 --- a/src/backend/optimizer/path/tidpath.c +++ b/src/backend/optimizer/path/tidpath.c @@ -468,7 +468,7 @@ BuildParameterizedTidPaths(PlannerInfo *root, RelOptInfo *rel, List *clauses) required_outer = bms_del_member(required_outer, rel->relid); add_path(rel, (Path *) create_tidscan_path(root, rel, tidquals, - required_outer)); + required_outer, NIL)); } } @@ -520,7 +520,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) Relids required_outer = rel->lateral_relids; add_path(rel, (Path *) create_tidscan_path(root, rel, tidquals, - required_outer)); + required_outer, NIL)); /* * When the qual is CurrentOfExpr, the path that we just added is the @@ -554,7 +554,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) add_path(rel, (Path *) create_tidrangescan_path(root, rel, tidrangequals, required_outer, - 0)); + 0, NIL)); /* If appropriate, consider parallel tid range scan. */ if (rel->consider_parallel && required_outer == NULL) @@ -569,7 +569,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) rel, tidrangequals, required_outer, - parallel_workers)); + parallel_workers, NIL)); } } diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 6de8ffa8b03..eb853d267b6 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -7179,7 +7179,7 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid) comparisonCost = 2.0 * (indexExprCost.startup + indexExprCost.per_tuple); /* Estimate the cost of seq scan + sort */ - seqScanPath = create_seqscan_path(root, rel, NULL, 0); + seqScanPath = create_seqscan_path(root, rel, NULL, 0, NIL); cost_sort(&seqScanAndSortPath, root, NIL, seqScanPath->disabled_nodes, seqScanPath->total_cost, rel->tuples, rel->reltarget->width, diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 2e7c9c8bb18..3ed10474469 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -459,72 +459,63 @@ expected_filters_selectivity(List *filters) return sel; } +/* + * apply_expected_filters + * Adjust an already-costed path to account for probing the given set of + * Bloom filters against every tuple it produces, and record them as the + * path's expected_filters. + * + * This is the generic, not-smart costing fallback: it assumes each filter + * costs one hash-probe-equivalent (cpu_operator_cost) per input tuple, on + * top of shrinking the row estimate by the filters' combined selectivity. + * Callers that can do something smarter (e.g. a CustomScan skipping whole + * row groups via a zone map) should cost themselves instead of using this. + */ +void +apply_expected_filters(Path *path, List *filters) +{ + if (filters == NIL) + return; + + path->total_cost += list_length(filters) * cpu_operator_cost * path->rows; + path->rows = clamp_row_est(path->rows * expected_filters_selectivity(filters)); + path->expected_filters = filters; +} + /* * create_filtered_scan_path - * Build a copy of a base-relation scan path that additionally expects the - * given set of pushed-down Bloom filters. + * Build a copy of an IndexPath that additionally expects the given set of + * pushed-down Bloom filters. * * The clone shares all substructure with the original path (parent, - * pathtarget, clauses, etc.); only the rows estimate is reduced to reflect - * the filters' combined selectivity, and expected_filters is set. This is - * safe because create_plan() treats the clone identically to the original - * (it ignores expected_filters), and add_path() may freely pfree the clone. - * - * Only the plain scan path node types that can receive a pushed-down filter - * are supported (matching find_bloom_filter_recipient in createplan.c). - * Returns NULL for unsupported path types. + * pathtarget, indexclauses, pathkeys, etc.); we reuse the source path's + * already-derived indexclauses/pathkeys and its cost rather than rebuilding + * them, and then apply_expected_filters() makes the same cost/rows adjustment + * core makes for every other scan type -- charging the per-tuple probe cost + * and shrinking the row estimate. This is safe because create_plan() treats + * the clone identically to the original (it ignores expected_filters), and + * add_path() may freely pfree the clone. * - * XXX This should probably adjust the CPU cost in some way. It assumes the - * filter checks are free, which does not seem right. + * Other base-relation scan path types are built directly by their real + * constructors with the filters passed in (see create_seqscan_path, + * create_bitmap_heap_path, etc.); IndexPath is the exception because + * create_index_path() would re-derive its indexclauses/pathkeys from scratch + * -- see the comment on the T_IndexScan/T_IndexOnlyScan case in + * reparameterize_path() for the same tradeoff made there. */ Path * create_filtered_scan_path(PlannerInfo *root, Path *subpath, List *filters) { - Path *newpath; - size_t sz; - - switch (nodeTag(subpath)) - { - case T_Path: - /* plain seqscan/samplescan etc. */ - sz = sizeof(Path); - break; - case T_IndexPath: - sz = sizeof(IndexPath); - break; - case T_BitmapHeapPath: - sz = sizeof(BitmapHeapPath); - break; - case T_TidPath: - sz = sizeof(TidPath); - break; - case T_TidRangePath: - sz = sizeof(TidRangePath); - break; - case T_CustomPath: + IndexPath *newpath; - /* - * A base-relation CustomScan provider that advertised - * CUSTOMPATH_SUPPORT_BLOOM_FILTERS can receive a pushed-down - * filter and apply it in its own scan loop - * .generate_expected_filter_paths() only offers such paths here, - * so we need not re-check the flag. - */ - sz = sizeof(CustomPath); - break; - default: - /* unsupported scan path type */ - return NULL; - } + Assert(IsA(subpath, IndexPath)); - newpath = (Path *) palloc(sz); - memcpy(newpath, subpath, sz); + newpath = (IndexPath *) palloc(sizeof(IndexPath)); + memcpy(newpath, subpath, sizeof(IndexPath)); - newpath->expected_filters = filters; - newpath->rows = clamp_row_est(subpath->rows * - expected_filters_selectivity(filters)); + apply_expected_filters(&newpath->path, filters); - return newpath; + return (Path *) newpath; } /* @@ -1254,7 +1245,8 @@ add_partial_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, */ Path * create_seqscan_path(PlannerInfo *root, RelOptInfo *rel, - Relids required_outer, int parallel_workers) + Relids required_outer, int parallel_workers, + List *filters) { Path *pathnode = makeNode(Path); @@ -1269,6 +1261,7 @@ create_seqscan_path(PlannerInfo *root, RelOptInfo *rel, pathnode->pathkeys = NIL; /* seqscan has unordered result */ cost_seqscan(pathnode, root, rel, pathnode->param_info); + apply_expected_filters(pathnode, filters); return pathnode; } @@ -1278,7 +1271,8 @@ create_seqscan_path(PlannerInfo *root, RelOptInfo *rel, * Creates a path node for a sampled table scan. */ Path * -create_samplescan_path(PlannerInfo *root, RelOptInfo *rel, Relids required_outer) +create_samplescan_path(PlannerInfo *root, RelOptInfo *rel, + Relids required_outer, List *filters) { Path *pathnode = makeNode(Path); @@ -1293,6 +1287,7 @@ create_samplescan_path(PlannerInfo *root, RelOptInfo *rel, Relids required_outer pathnode->pathkeys = NIL; /* samplescan has unordered result */ cost_samplescan(pathnode, root, rel, pathnode->param_info); + apply_expected_filters(pathnode, filters); return pathnode; } @@ -1381,7 +1376,8 @@ create_bitmap_heap_path(PlannerInfo *root, Path *bitmapqual, Relids required_outer, double loop_count, - int parallel_degree) + int parallel_degree, + List *filters) { BitmapHeapPath *pathnode = makeNode(BitmapHeapPath); @@ -1400,6 +1396,7 @@ create_bitmap_heap_path(PlannerInfo *root, cost_bitmap_heap_scan(&pathnode->path, root, rel, pathnode->path.param_info, bitmapqual, loop_count); + apply_expected_filters(&pathnode->path, filters); return pathnode; } @@ -1514,7 +1511,7 @@ create_bitmap_or_path(PlannerInfo *root, */ TidPath * create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals, - Relids required_outer) + Relids required_outer, List *filters) { TidPath *pathnode = makeNode(TidPath); @@ -1532,6 +1529,7 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals, cost_tidscan(&pathnode->path, root, rel, tidquals, pathnode->path.param_info); + apply_expected_filters(&pathnode->path, filters); return pathnode; } @@ -1544,7 +1542,7 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals, TidRangePath * create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, List *tidrangequals, Relids required_outer, - int parallel_workers) + int parallel_workers, List *filters) { TidRangePath *pathnode = makeNode(TidRangePath); @@ -1562,6 +1560,7 @@ create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, cost_tidrangescan(&pathnode->path, root, rel, tidrangequals, pathnode->path.param_info); + apply_expected_filters(&pathnode->path, filters); return pathnode; } @@ -4176,9 +4175,9 @@ reparameterize_path(PlannerInfo *root, Path *path, switch (path->pathtype) { case T_SeqScan: - return create_seqscan_path(root, rel, required_outer, 0); + return create_seqscan_path(root, rel, required_outer, 0, NIL); case T_SampleScan: - return create_samplescan_path(root, rel, required_outer); + return create_samplescan_path(root, rel, required_outer, NIL); case T_IndexScan: case T_IndexOnlyScan: { @@ -4206,7 +4205,7 @@ reparameterize_path(PlannerInfo *root, Path *path, rel, bpath->bitmapqual, required_outer, - loop_count, 0); + loop_count, 0, NIL); } case T_SubqueryScan: { diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 01b9ee5b1cf..2fbe7f39b44 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -381,8 +381,8 @@ }, { name => 'bloom_filter_pushdown_max', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', - short_desc => 'Maximum number of pushed-down hash join bloom filters considered per scan.', - long_desc => 'Bounds how many interesting bloom filters the planner enumerates subsets of when building filter-aware scan paths.', + short_desc => 'Maximum number of hash join bloom filters combined into a single pushed-down filter set.', + long_desc => 'Bounds how many interesting bloom filters the planner will combine together when building filter-aware scan path variants for a base relation.', flags => 'GUC_EXPLAIN', variable => 'bloom_filter_pushdown_max', boot_val => '3', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 9080e9e0784..fe89e5a275c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -486,8 +486,8 @@ # - Other Planner Options - -#bloom_filter_pushdown_max = 3 # range 0-10 -#bloom_filter_pushdown_threshold = 0.3 # range 0.0-1.0 +#bloom_filter_pushdown_max = 3 # range 0-10 +#bloom_filter_pushdown_threshold = 0.3 # range 0.0-1.0 #bloom_filter_pushdown_max_build_relids = 3 # range 1-100 #bloom_filter_pushdown_max_build_sets = 100 # range 1-INT_MAX #default_statistics_target = 100 # range 1-10000 diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index cd3e0128f4f..1ae4d96dc1a 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1070,6 +1070,16 @@ typedef struct RelOptInfo struct Path *cheapest_total_path; List *cheapest_parameterized_paths; + /* + * Cache of the interesting Bloom filters this rel could receive from a + * hash join above it. Computed lazily and reused, since both core path + * generation and a CustomScan provider may ask for it, and recomputing it + * is not free. A separate "valid" flag distinguishes "not yet computed" + * from "computed, no filters". + */ + List *bloom_filters pg_node_attr(read_write_ignore); + bool bloom_filters_valid pg_node_attr(read_write_ignore); + /* * parameterization information needed for both base rels and join rels * (see also lateral_vars and lateral_referencers) diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index 0dcae2ae3b3..7d7e080c0db 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -66,13 +66,15 @@ extern bool add_partial_path_precheck(RelOptInfo *parent_rel, extern bool expected_filters_equal(List *a, List *b); extern double expected_filters_selectivity(List *filters); +extern void apply_expected_filters(Path *path, List *filters); extern Path *create_filtered_scan_path(PlannerInfo *root, Path *subpath, List *filters); extern Path *create_seqscan_path(PlannerInfo *root, RelOptInfo *rel, - Relids required_outer, int parallel_workers); + Relids required_outer, int parallel_workers, + List *filters); extern Path *create_samplescan_path(PlannerInfo *root, RelOptInfo *rel, - Relids required_outer); + Relids required_outer, List *filters); extern IndexPath *create_index_path(PlannerInfo *root, IndexOptInfo *index, List *indexclauses, @@ -89,7 +91,8 @@ extern BitmapHeapPath *create_bitmap_heap_path(PlannerInfo *root, Path *bitmapqual, Relids required_outer, double loop_count, - int parallel_degree); + int parallel_degree, + List *filters); extern BitmapAndPath *create_bitmap_and_path(PlannerInfo *root, RelOptInfo *rel, List *bitmapquals); @@ -97,12 +100,14 @@ extern BitmapOrPath *create_bitmap_or_path(PlannerInfo *root, RelOptInfo *rel, List *bitmapquals); extern TidPath *create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, - List *tidquals, Relids required_outer); + List *tidquals, Relids required_outer, + List *filters); extern TidRangePath *create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, List *tidrangequals, Relids required_outer, - int parallel_workers); + int parallel_workers, + List *filters); extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel, AppendPathInput input, diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 59801f9b0df..d94bddafed3 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -61,6 +61,7 @@ extern PGDLLIMPORT join_search_hook_type join_search_hook; extern RelOptInfo *make_one_rel(PlannerInfo *root, List *joinlist); extern RelOptInfo *standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels); +extern List *find_bloom_filter_combinations(PlannerInfo *root, RelOptInfo *rel); extern void generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows); diff --git a/src/test/modules/test_bloom_customscan/expected/test_bloom_customscan.out b/src/test/modules/test_bloom_customscan/expected/test_bloom_customscan.out index 8e784209901..11eea4b134e 100644 --- a/src/test/modules/test_bloom_customscan/expected/test_bloom_customscan.out +++ b/src/test/modules/test_bloom_customscan/expected/test_bloom_customscan.out @@ -107,8 +107,98 @@ SELECT test_bloom_cs_rejected_rows() > 0 AS filter_rejected_rows; t (1 row) +-- Two independent joins: the provider should offer, and the planner should +-- choose, a CustomPath expecting a combined filter from both joins. +-- cpu_operator_cost is lowered so combining is robustly cheaper rather than a +-- near-tie. +CREATE TABLE cs_wide_a (id int, label text); +CREATE TABLE cs_wide_b (id int, label text); +INSERT INTO cs_wide_a SELECT g, 'a' || g FROM generate_series(1, 400) g; +INSERT INTO cs_wide_b SELECT g, 'b' || g FROM generate_series(1, 400) g; +ANALYZE cs_wide_a; +ANALYZE cs_wide_b; +SET test_bloom_customscan.enable = on; +SET enable_seqscan = off; +SET cpu_operator_cost = 0.0001; +EXPLAIN (COSTS OFF, VERBOSE) +SELECT count(*) FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id; + QUERY PLAN +---------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Hash Join + Hash Cond: (f.b = wb.id) + -> Hash Join + Output: f.b + Hash Cond: (f.a = wa.id) + -> Custom Scan (TestBloomCustomScan) on public.cs_fact f + Output: f.a, f.b + -> Hash + Output: wa.id + Bloom Filter 1 + -> Custom Scan (TestBloomCustomScan) on public.cs_wide_a wa + Output: wa.id + -> Hash + Output: wb.id + Bloom Filter 2 + -> Custom Scan (TestBloomCustomScan) on public.cs_wide_b wb + Output: wb.id +(19 rows) + +SELECT test_bloom_cs_reset(); + test_bloom_cs_reset +--------------------- + +(1 row) + +SELECT count(*) FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id; + count +------- + 8000 +(1 row) + +SELECT test_bloom_cs_rejected_rows() > 0 AS filter_rejected_rows; + filter_rejected_rows +---------------------- + t +(1 row) + +-- Correctness: the result must be identical with and without the filters. +SET test_bloom_customscan.enable = on; +SET enable_seqscan = off; +CREATE TEMP TABLE r_cs2 AS + SELECT f.a, count(*) AS n FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id + GROUP BY f.a; +SET test_bloom_customscan.enable = off; +SET enable_seqscan = on; +CREATE TEMP TABLE r_plain2 AS + SELECT f.a, count(*) AS n FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id + GROUP BY f.a; +SELECT count(*) AS cs_minus_plain + FROM (SELECT * FROM r_cs2 EXCEPT SELECT * FROM r_plain2) x; + cs_minus_plain +---------------- + 0 +(1 row) + +SELECT count(*) AS plain_minus_cs + FROM (SELECT * FROM r_plain2 EXCEPT SELECT * FROM r_cs2) x; + plain_minus_cs +---------------- + 0 +(1 row) + -- cleanup +RESET cpu_operator_cost; SET test_bloom_customscan.enable = off; SET enable_seqscan = on; -DROP TABLE cs_fact, cs_dim; +DROP TABLE cs_fact, cs_dim, cs_wide_a, cs_wide_b; DROP EXTENSION test_bloom_customscan; diff --git a/src/test/modules/test_bloom_customscan/sql/test_bloom_customscan.sql b/src/test/modules/test_bloom_customscan/sql/test_bloom_customscan.sql index daae84a531a..8da2e0789e6 100644 --- a/src/test/modules/test_bloom_customscan/sql/test_bloom_customscan.sql +++ b/src/test/modules/test_bloom_customscan/sql/test_bloom_customscan.sql @@ -59,8 +59,57 @@ SELECT count(*) FROM cs_fact f JOIN cs_dim d ON f.a = d.id AND f.b = d.id2; SELECT test_bloom_cs_perkey_built() AS perkey_filters_built; SELECT test_bloom_cs_rejected_rows() > 0 AS filter_rejected_rows; +-- Two independent joins: the provider should offer, and the planner should +-- choose, a CustomPath expecting a combined filter from both joins. +-- cpu_operator_cost is lowered so combining is robustly cheaper rather than a +-- near-tie. +CREATE TABLE cs_wide_a (id int, label text); +CREATE TABLE cs_wide_b (id int, label text); +INSERT INTO cs_wide_a SELECT g, 'a' || g FROM generate_series(1, 400) g; +INSERT INTO cs_wide_b SELECT g, 'b' || g FROM generate_series(1, 400) g; +ANALYZE cs_wide_a; +ANALYZE cs_wide_b; + +SET test_bloom_customscan.enable = on; +SET enable_seqscan = off; +SET cpu_operator_cost = 0.0001; + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT count(*) FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id; + +SELECT test_bloom_cs_reset(); +SELECT count(*) FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id; +SELECT test_bloom_cs_rejected_rows() > 0 AS filter_rejected_rows; + +-- Correctness: the result must be identical with and without the filters. +SET test_bloom_customscan.enable = on; +SET enable_seqscan = off; +CREATE TEMP TABLE r_cs2 AS + SELECT f.a, count(*) AS n FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id + GROUP BY f.a; + +SET test_bloom_customscan.enable = off; +SET enable_seqscan = on; +CREATE TEMP TABLE r_plain2 AS + SELECT f.a, count(*) AS n FROM cs_fact f + JOIN cs_wide_a wa ON f.a = wa.id + JOIN cs_wide_b wb ON f.b = wb.id + GROUP BY f.a; + +SELECT count(*) AS cs_minus_plain + FROM (SELECT * FROM r_cs2 EXCEPT SELECT * FROM r_plain2) x; +SELECT count(*) AS plain_minus_cs + FROM (SELECT * FROM r_plain2 EXCEPT SELECT * FROM r_cs2) x; + -- cleanup +RESET cpu_operator_cost; SET test_bloom_customscan.enable = off; SET enable_seqscan = on; -DROP TABLE cs_fact, cs_dim; +DROP TABLE cs_fact, cs_dim, cs_wide_a, cs_wide_b; DROP EXTENSION test_bloom_customscan; diff --git a/src/test/modules/test_bloom_customscan/test_bloom_customscan.c b/src/test/modules/test_bloom_customscan/test_bloom_customscan.c index 99338c7d361..30f53ff45c7 100644 --- a/src/test/modules/test_bloom_customscan/test_bloom_customscan.c +++ b/src/test/modules/test_bloom_customscan/test_bloom_customscan.c @@ -64,6 +64,8 @@ typedef struct BloomCSScanState } BloomCSScanState; /* forward declarations */ +static void bloom_cs_add_path(RelOptInfo *rel, Cost startup_cost, + Cost total_cost, List *filters); static Plan *bloom_cs_plan_custom_path(PlannerInfo *root, RelOptInfo *rel, CustomPath *best_path, List *tlist, List *clauses, List *custom_plans); @@ -93,18 +95,57 @@ static const CustomExecMethods bloom_cs_exec_methods = { }; /* - * set_rel_pathlist_hook: offer a bloom-filter-capable CustomPath for plain + * bloom_cs_add_path + * Build and add one bloom-filter-capable CustomPath, optionally expecting + * the given set of pushed-down filters. We don't do anything smart with + * the filters ourselves, so apply_expected_filters() supplies the same + * generic per-tuple probe cost and row-count adjustment core uses for + * stock scan types. + */ +static void +bloom_cs_add_path(RelOptInfo *rel, Cost startup_cost, Cost total_cost, + List *filters) +{ + CustomPath *cpath = makeNode(CustomPath); + + cpath->path.pathtype = T_CustomScan; + cpath->path.parent = rel; + cpath->path.pathtarget = rel->reltarget; + cpath->path.param_info = NULL; + cpath->path.parallel_aware = false; + cpath->path.parallel_safe = false; + cpath->path.parallel_workers = 0; + cpath->path.rows = rel->rows; + cpath->path.startup_cost = startup_cost; + cpath->path.total_cost = total_cost; + cpath->path.pathkeys = NIL; + + cpath->flags = CUSTOMPATH_SUPPORT_BLOOM_FILTERS; + cpath->custom_paths = NIL; + cpath->custom_private = NIL; + cpath->methods = &bloom_cs_path_methods; + + apply_expected_filters(&cpath->path, filters); + + add_path(rel, (Path *) cpath); +} + +/* + * set_rel_pathlist_hook: offer bloom-filter-capable CustomPaths for plain * heap base relations. We copy the cost of the existing sequential scan path - * so join costing stays sane; the test forces the custom scan to be chosen - * with "SET enable_seqscan = off" (the CustomPath keeps disabled_nodes = 0). + * so join costing stays sane. + * + * Besides the plain, filter-oblivious path, we also offer one CustomPath per + * combination of interesting Bloom filters this rel could receive from a + * hash join above it. */ static void bloom_cs_set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte) { - CustomPath *cpath; Cost startup_cost = 0; Cost total_cost = 0; + List *combinations; ListCell *lc; if (prev_set_rel_pathlist_hook) @@ -132,25 +173,16 @@ bloom_cs_set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, } } - cpath = makeNode(CustomPath); - cpath->path.pathtype = T_CustomScan; - cpath->path.parent = rel; - cpath->path.pathtarget = rel->reltarget; - cpath->path.param_info = NULL; - cpath->path.parallel_aware = false; - cpath->path.parallel_safe = false; - cpath->path.parallel_workers = 0; - cpath->path.rows = rel->rows; - cpath->path.startup_cost = startup_cost; - cpath->path.total_cost = total_cost; - cpath->path.pathkeys = NIL; + bloom_cs_add_path(rel, startup_cost, total_cost, NIL); - cpath->flags = CUSTOMPATH_SUPPORT_BLOOM_FILTERS; - cpath->custom_paths = NIL; - cpath->custom_private = NIL; - cpath->methods = &bloom_cs_path_methods; + combinations = find_bloom_filter_combinations(root, rel); - add_path(rel, (Path *) cpath); + foreach(lc, combinations) + { + List *combo = (List *) lfirst(lc); + + bloom_cs_add_path(rel, startup_cost, total_cost, combo); + } } static Plan * diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out index 553fe8e5c0c..46566b2e32f 100644 --- a/src/test/regress/expected/graph_table.out +++ b/src/test/regress/expected/graph_table.out @@ -259,8 +259,8 @@ SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'U SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; a | customer_name | cid | order_id ---+---------------+-----+---------- - 2 | customer1 | 1 | 1 1 | customer1 | 1 | 1 + 2 | customer1 | 1 | 1 (2 rows) -- bare lateral reference in WHERE clause diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 00fb9efc69d..52373af054a 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -3292,14 +3292,14 @@ select 1 from tenk1 where (hundred, thousand) in (select twothousand, twothousand from onek); QUERY PLAN ------------------------------------------------- - Hash Right Semi Join - Hash Cond: (onek.twothousand = tenk1.hundred) - -> Seq Scan on onek - Bloom Filter 1: keys=(twothousand) + Hash Join + Hash Cond: (tenk1.hundred = onek.twothousand) + -> Seq Scan on tenk1 + Filter: (hundred = thousand) -> Hash - Bloom Filter 1 - -> Seq Scan on tenk1 - Filter: (hundred = thousand) + -> HashAggregate + Group Key: onek.twothousand + -> Seq Scan on onek (8 rows) reset enable_memoize; @@ -4110,38 +4110,44 @@ select ss1.d1 from on i8.q1 = i4.f1 on t1.tenthous = ss1.d1 where t1.unique1 < i4.f1; - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Nested Loop - Output: (64)::information_schema.cardinal_number - -> Seq Scan on public.tenk1 t3 - Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4 - Filter: (t3.fivethous < 0) - -> Nested Loop - Output: t1.tenthous, t2.ten + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Hash Join + Output: ((64)::information_schema.cardinal_number) + Hash Cond: (t2.ten = t1.tenthous) + -> Seq Scan on public.tenk1 t2 + Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4 + Bloom Filter 1: keys=(t2.ten) + -> Hash + Output: t1.tenthous, ((64)::information_schema.cardinal_number) + Bloom Filter 1 -> Nested Loop - Output: t1.tenthous, t2.ten, i4.f1 - Join Filter: (t1.unique1 < i4.f1) + Output: t1.tenthous, (64)::information_schema.cardinal_number + Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer) + -> Seq Scan on public.tenk1 t3 + Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4 + Filter: (t3.fivethous < 0) -> Nested Loop - Output: t1.tenthous, t1.unique1, t2.ten - Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer) + Output: t1.tenthous -> Nested Loop - Output: t2.ten - Join Filter: (((64)::information_schema.cardinal_number)::integer = t2.ten) - -> Result - Output: ((abs(t3.unique1))::double precision + random()) - -> Seq Scan on public.tenk1 t2 - Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4 - -> Index Scan using tenk1_thous_tenthous on public.tenk1 t1 - Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4 - Index Cond: (t1.tenthous = t2.ten) - -> Seq Scan on public.int4_tbl i4 - Output: i4.f1 - Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer) - -> Seq Scan on public.int8_tbl i8 - Output: i8.q1, i8.q2 - Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer) -(29 rows) + Output: t1.tenthous, i4.f1 + Join Filter: (t1.unique1 < i4.f1) + -> Nested Loop + Output: t1.tenthous, t1.unique1 + -> Subquery Scan on ss0 + Output: ss0.x, (64)::information_schema.cardinal_number + -> Result + Output: ((abs(t3.unique1))::double precision + random()) + -> Index Scan using tenk1_thous_tenthous on public.tenk1 t1 + Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4 + Index Cond: (t1.tenthous = (((64)::information_schema.cardinal_number))::integer) + -> Seq Scan on public.int4_tbl i4 + Output: i4.f1 + Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer) + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer) +(35 rows) select ss1.d1 from tenk1 as t1 @@ -5959,20 +5965,18 @@ explain (costs off) select a.unique1, b.unique2 from onek a left join onek b on a.unique1 = b.unique2 where (b.unique2, random() > 0) = any (select q1, random() > 0 from int8_tbl c where c.q1 < b.unique1); - QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------- Hash Join - Hash Cond: (a.unique1 = b.unique2) - -> Index Only Scan using onek_unique1 on onek a - Bloom Filter 1: keys=(unique1) + Hash Cond: (b.unique2 = a.unique1) + -> Seq Scan on onek b + Filter: (ANY ((unique2 = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan any_1).col2))) + SubPlan any_1 + -> Seq Scan on int8_tbl c + Filter: (q1 < b.unique1) -> Hash - Bloom Filter 1 - -> Seq Scan on onek b - Filter: (ANY ((unique2 = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan any_1).col2))) - SubPlan any_1 - -> Seq Scan on int8_tbl c - Filter: (q1 < b.unique1) -(11 rows) + -> Index Only Scan using onek_unique1 on onek a +(9 rows) select a.unique1, b.unique2 from onek a left join onek b on a.unique1 = b.unique2 @@ -6928,27 +6932,25 @@ where exists (select 1 from t t4 Hash Cond: (t6.b = t4.b) -> Seq Scan on pg_temp.t t6 Output: t6.a, t6.b - Bloom Filter 2: keys=(t6.b) + Bloom Filter 1: keys=(t6.b) -> Hash Output: t4.b, t5.b, t5.a - Bloom Filter 2 + Bloom Filter 1 -> Hash Join Output: t4.b, t5.b, t5.a Inner Unique: true Hash Cond: (t5.b = t4.b) -> Seq Scan on pg_temp.t t5 Output: t5.a, t5.b - Bloom Filter 1: keys=(t5.b) -> Hash Output: t4.b, t4.a - Bloom Filter 1 -> Index Scan using t_a_key on pg_temp.t t4 Output: t4.b, t4.a Index Cond: (t4.a = 1) -> Index Only Scan using t_a_key on pg_temp.t t3 Output: t3.a Index Cond: (t3.a = t5.a) -(36 rows) +(34 rows) select t1.a from t t1 left join t t2 on t1.a = t2.a @@ -9566,14 +9568,12 @@ select * from fkest f1 Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10)) -> Seq Scan on fkest f2 Filter: (x100 = 2) - Bloom Filter 1: keys=(x, x10b) -> Hash - Bloom Filter 1 -> Seq Scan on fkest f1 Filter: (x100 = 2) -> Index Scan using fkest_x_x10_x100_idx on fkest f3 Index Cond: (x = f1.x) -(12 rows) +(10 rows) alter table fkest add constraint fk foreign key (x, x10b, x100) references fkest (x, x10, x100); @@ -9582,21 +9582,22 @@ 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 ---------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------- Hash Join - Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10)) - -> Nested Loop - -> Seq Scan on fkest f2 - Filter: (x100 = 2) - Bloom Filter 1: keys=(x, x10b) - -> Index Scan using fkest_x_x10_x100_idx on fkest f3 - Index Cond: (x = f2.x) + Hash Cond: (f3.x = f1.x) + -> Seq Scan on fkest f3 + Bloom Filter 1: keys=(x) -> Hash Bloom Filter 1 - -> Seq Scan on fkest f1 - Filter: (x100 = 2) -(12 rows) + -> 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) +(13 rows) rollback; -- @@ -9784,52 +9785,44 @@ select * from j1 natural join j2; explain (verbose, costs off) select * from j1 inner join (select distinct id from j3) j3 on j1.id = j3.id; - QUERY PLAN ------------------------------------------------ - Hash Join + QUERY PLAN +----------------------------------------- + Nested Loop Output: j1.id, j3.id Inner Unique: true - Hash Cond: (j1.id = j3.id) - -> Seq Scan on public.j1 - Output: j1.id - Bloom Filter 1: keys=(j1.id) - -> Hash + Join Filter: (j1.id = j3.id) + -> Unique Output: j3.id - Bloom Filter 1 - -> Unique + -> Sort Output: j3.id - -> Sort + Sort Key: j3.id + -> Seq Scan on public.j3 Output: j3.id - Sort Key: j3.id - -> Seq Scan on public.j3 - Output: j3.id -(17 rows) + -> Seq Scan on public.j1 + Output: j1.id +(13 rows) -- ensure group by clause allows the inner to become unique explain (verbose, costs off) select * from j1 inner join (select id from j3 group by id) j3 on j1.id = j3.id; - QUERY PLAN ------------------------------------------------ - Hash Join + QUERY PLAN +----------------------------------------- + Nested Loop Output: j1.id, j3.id Inner Unique: true - Hash Cond: (j1.id = j3.id) - -> Seq Scan on public.j1 - Output: j1.id - Bloom Filter 1: keys=(j1.id) - -> Hash + Join Filter: (j1.id = j3.id) + -> Group Output: j3.id - Bloom Filter 1 - -> Group + Group Key: j3.id + -> Sort Output: j3.id - Group Key: j3.id - -> Sort + Sort Key: j3.id + -> Seq Scan on public.j3 Output: j3.id - Sort Key: j3.id - -> Seq Scan on public.j3 - Output: j3.id -(18 rows) + -> Seq Scan on public.j1 + Output: j1.id +(14 rows) drop table j1; drop table j2; diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out index 40461acf17c..16cfa23c523 100644 --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -322,17 +322,15 @@ USING source AS s ON t.tid = s.sid WHEN MATCHED THEN UPDATE SET balance = 0; - QUERY PLAN ------------------------------------------- + QUERY PLAN +---------------------------------------- Merge on target t -> Hash Join Hash Cond: (s.sid = t.tid) -> Seq Scan on source s - Bloom Filter 1: keys=(sid) -> Hash - Bloom Filter 1 -> Seq Scan on target t -(8 rows) +(6 rows) EXPLAIN (COSTS OFF) MERGE INTO target t @@ -340,17 +338,15 @@ USING source AS s ON t.tid = s.sid WHEN MATCHED THEN DELETE; - QUERY PLAN ------------------------------------------- + QUERY PLAN +---------------------------------------- Merge on target t -> Hash Join Hash Cond: (s.sid = t.tid) -> Seq Scan on source s - Bloom Filter 1: keys=(sid) -> Hash - Bloom Filter 1 -> Seq Scan on target t -(8 rows) +(6 rows) EXPLAIN (COSTS OFF) MERGE INTO target t diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out index 7fff6a720aa..b52528870ef 100644 --- a/src/test/regress/expected/misc_functions.out +++ b/src/test/regress/expected/misc_functions.out @@ -614,16 +614,14 @@ CREATE FUNCTION my_gen_series(int, int) RETURNS SETOF integer SUPPORT test_support_func; EXPLAIN (COSTS OFF) SELECT * FROM tenk1 a JOIN my_gen_series(1,1000) g ON a.unique1 = g; - QUERY PLAN ----------------------------------------------- + QUERY PLAN +---------------------------------------- Hash Join - Hash Cond: (a.unique1 = g.g) - -> Seq Scan on tenk1 a - Bloom Filter 1: keys=(unique1) + Hash Cond: (g.g = a.unique1) + -> Function Scan on my_gen_series g -> Hash - Bloom Filter 1 - -> Function Scan on my_gen_series g -(7 rows) + -> Seq Scan on tenk1 a +(5 rows) EXPLAIN (COSTS OFF) SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g; diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out index 50cd3be8030..3fc1379df9a 100644 --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -716,19 +716,17 @@ UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57 Hash Cond: (joinme_1.f2j = foo_1.f2) -> Seq Scan on pg_temp.joinme joinme_1 Output: joinme_1.ctid, joinme_1.f2j - Bloom Filter 2: keys=(joinme_1.f2j) + Bloom Filter 1: keys=(joinme_1.f2j) -> Hash Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, joinme.ctid, joinme.other, joinme.f2j, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid - Bloom Filter 2 + Bloom Filter 1 -> Hash Join Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, joinme.ctid, joinme.other, joinme.f2j, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid Hash Cond: (joinme.f2j = foo_1.f2) -> Seq Scan on pg_temp.joinme Output: joinme.ctid, joinme.other, joinme.f2j - Bloom Filter 1: keys=(joinme.f2j) -> Hash Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid - Bloom Filter 1 -> Nested Loop Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid Join Filter: (foo_1.f2 = foo_2.f2) @@ -737,7 +735,7 @@ UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57 Filter: (foo_2.f3 = 57) -> Seq Scan on pg_temp.foo foo_1 Output: foo_1.f2, foo_1.tableoid, foo_1.ctid -(29 rows) +(27 rows) UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57 RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index f1a23f38a03..ad362f73d94 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -2779,14 +2779,12 @@ EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5; -> Hash Join Hash Cond: (b.a = r.a) -> Seq Scan on base_tbl b - Bloom Filter 1: keys=(a) -> Hash - Bloom Filter 1 -> Seq Scan on ref_tbl r SubPlan exists_1 -> Index Only Scan using ref_tbl_pkey on ref_tbl r_1 Index Cond: (a = b.a) -(11 rows) +(9 rows) DROP TABLE base_tbl, ref_tbl CASCADE; NOTICE: drop cascades to view rw_view1 -- 2.50.1 (Apple Git-155)