From 060bd2445ea9cba9adadd73505689d6f06583ee8 Mon Sep 17 00:00:00 2001 From: amit Date: Fri, 24 Aug 2018 12:39:36 +0900 Subject: [PATCH v2 1/3] Overhaul partitioned table update/delete planning Current method, inheritance_planner, applies grouping_planner and hence query_planner to the query repeatedly with each leaf partition replacing the root parent as the query's result relation. One big drawback of this approach is that it cannot use partprune.c to perform partition pruning on the partitioned result relation, because it can only be invoked if query_planner sees the partitioned relation itself in the query. That is not true with the existing method, because as mentioned above, query_planner is invoked with the partitioned relation replaced with individual leaf partitions. While most of the work in each repitition of grouping_planner (and query_planner) is same, a couple of things may differ from partition to partition -- 1. Join planning may produce different Paths for joining against different result partitions, 2. grouping_planner may produce different top-level target lists for different partitions, based on their TupleDescs. This commit rearranges things so that, only the planning steps that affect 1 and 2 above are repeated for partitions that are selected by query_planner by applying partprune.c based pruning to the original partitioned result rel. That makes things faster because 1. partprune.c based pruning is used instead of using constraint exclusion for each partition, 2. grouping_planner (and query_planner) is invoked only once instead of for every partition thus saving cycles and memory. This still doesn't help much if no partitions are pruned, because we still repeat join planning and makes copies of the query for each partition, but for common cases where only handful partitions remain after pruning, this makes things significanly faster. --- doc/src/sgml/ddl.sgml | 15 +- src/backend/optimizer/path/allpaths.c | 97 ++++++- src/backend/optimizer/plan/planmain.c | 4 +- src/backend/optimizer/plan/planner.c | 378 ++++++++++++++++++++------- src/backend/optimizer/prep/prepunion.c | 28 +- src/backend/optimizer/util/plancat.c | 30 --- src/test/regress/expected/partition_join.out | 4 +- 7 files changed, 416 insertions(+), 140 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index b5ed1b7939..53c479fbb8 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -3933,16 +3933,6 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; setting. - - - Currently, pruning of partitions during the planning of an - UPDATE or DELETE command is - implemented using the constraint exclusion method (however, it is - controlled by the enable_partition_pruning rather than - constraint_exclusion) — see the following section - for details and caveats that apply. - - Execution-time partition pruning currently occurs for the Append and MergeAppend node types. @@ -3964,9 +3954,8 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; Constraint exclusion is a query optimization - technique similar to partition pruning. While it is primarily used - for partitioning implemented using the legacy inheritance method, it can be - used for other purposes, including with declarative partitioning. + technique similar to partition pruning. It is primarily used + for partitioning implemented using the legacy inheritance method. diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 0e80aeb65c..5937c0436a 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -36,6 +36,7 @@ #include "optimizer/pathnode.h" #include "optimizer/paths.h" #include "optimizer/plancat.h" +#include "optimizer/planmain.h" #include "optimizer/planner.h" #include "optimizer/prep.h" #include "optimizer/restrictinfo.h" @@ -119,6 +120,9 @@ static void set_namedtuplestore_pathlist(PlannerInfo *root, RelOptInfo *rel, static void set_worktable_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte); static RelOptInfo *make_rel_from_joinlist(PlannerInfo *root, List *joinlist); +static RelOptInfo *partitionwise_make_rel_from_joinlist(PlannerInfo *root, + RelOptInfo *parent, + List *joinlist); static bool subquery_is_pushdown_safe(Query *subquery, Query *topquery, pushdown_safety_info *safetyInfo); static bool recurse_pushdown_safe(Node *setOp, Query *topquery, @@ -181,13 +185,30 @@ make_one_rel(PlannerInfo *root, List *joinlist) /* * Generate access paths for the entire join tree. + * + * If we're doing this for an UPDATE or DELETE query whose target is a + * partitioned table, we must do the join planning against each of its + * leaf partitions instead. */ - rel = make_rel_from_joinlist(root, joinlist); + if (root->parse->resultRelation && + root->parse->commandType != CMD_INSERT && + root->simple_rel_array[root->parse->resultRelation] && + root->simple_rel_array[root->parse->resultRelation]->part_scheme) + { + RelOptInfo *rootrel = root->simple_rel_array[root->parse->resultRelation]; - /* - * The result should join all and only the query's base rels. - */ - Assert(bms_equal(rel->relids, root->all_baserels)); + rel = partitionwise_make_rel_from_joinlist(root, rootrel, joinlist); + } + else + { + rel = make_rel_from_joinlist(root, joinlist); + + /* + * The result should join all and only the query's base rels. + */ + Assert(bms_equal(rel->relids, root->all_baserels)); + + } return rel; } @@ -2591,6 +2612,72 @@ generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows) } /* + * partitionwise_make_rel_from_joinlist + * performs join planning against each of the leaf partitions contained + * in the partition tree whose root relation is 'parent' + * + * Recursively called for each partitioned table contained in a given + *partition tree. + */ +static RelOptInfo * +partitionwise_make_rel_from_joinlist(PlannerInfo *root, + RelOptInfo *parent, + List *joinlist) +{ + int i; + + Assert(root->parse->resultRelation != 0); + Assert(parent->part_scheme != NULL); + + for (i = 0; i < parent->nparts; i++) + { + RelOptInfo *partrel = parent->part_rels[i]; + AppendRelInfo *appinfo; + List *translated_joinlist; + List *saved_join_info_list = list_copy(root->join_info_list); + + /* Ignore pruned partitions. */ + if (IS_DUMMY_REL(partrel)) + continue; + + /* + * Hack to make the join planning code believe that 'partrel' can + * be joined against. + */ + partrel->reloptkind = RELOPT_BASEREL; + + /* + * Replace references to the parent rel in expressions relevant to join + * planning. + */ + appinfo = root->append_rel_array[partrel->relid]; + translated_joinlist = (List *) + adjust_appendrel_attrs(root, (Node *) joinlist, + 1, &appinfo); + root->join_info_list = (List *) + adjust_appendrel_attrs(root, + (Node *) root->join_info_list, + 1, &appinfo); + /* Reset join planning data structures for a new partition. */ + root->join_rel_list = NIL; + root->join_rel_hash = NULL; + + /* Recurse if the partition is itself a partitioned table. */ + if (partrel->part_scheme != NULL) + partrel = partitionwise_make_rel_from_joinlist(root, partrel, + translated_joinlist); + else + /* Perform the join planning and save the resulting relation. */ + parent->part_rels[i] = + make_rel_from_joinlist(root, translated_joinlist); + + root->join_info_list = saved_join_info_list; + } + + return parent; +} + +/* * make_rel_from_joinlist * Build access paths using a "joinlist" to guide the join path search. * diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c index b05adc70c4..3f0d80eaa6 100644 --- a/src/backend/optimizer/plan/planmain.c +++ b/src/backend/optimizer/plan/planmain.c @@ -266,7 +266,9 @@ query_planner(PlannerInfo *root, List *tlist, /* Check that we got at least one usable path */ if (!final_rel || !final_rel->cheapest_total_path || - final_rel->cheapest_total_path->param_info != NULL) + final_rel->cheapest_total_path->param_info != NULL || + (final_rel->relid == root->parse->resultRelation && + root->parse->commandType == CMD_INSERT)) elog(ERROR, "failed to construct the join relation"); return final_rel; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 96bf0601a8..076dbd3d62 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -238,6 +238,16 @@ static bool group_by_has_partkey(RelOptInfo *input_rel, List *targetList, List *groupClause); +static void partitionwise_adjust_scanjoin_target(PlannerInfo *root, + RelOptInfo *parent, + List **partition_subroots, + List **partitioned_rels, + List **resultRelations, + List **subpaths, + List **WCOLists, + List **returningLists, + List **rowMarks); + /***************************************************************************** * @@ -959,7 +969,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, * needs special processing, else go straight to grouping_planner. */ if (parse->resultRelation && - rt_fetch(parse->resultRelation, parse->rtable)->inh) + rt_fetch(parse->resultRelation, parse->rtable)->inh && + rt_fetch(parse->resultRelation, parse->rtable)->relkind != + RELKIND_PARTITIONED_TABLE) inheritance_planner(root); else grouping_planner(root, false, tuple_fraction); @@ -1688,6 +1700,14 @@ grouping_planner(PlannerInfo *root, bool inheritance_update, RelOptInfo *current_rel; RelOptInfo *final_rel; ListCell *lc; + List *orig_parse_tlist = list_copy(parse->targetList); + List *partition_subroots = NIL; + List *partitioned_rels = NIL; + List *partition_resultRelations = NIL; + List *partition_subpaths = NIL; + List *partition_WCOLists = NIL; + List *partition_returningLists = NIL; + List *partition_rowMarks = NIL; /* Tweak caller-supplied tuple_fraction if have LIMIT/OFFSET */ if (parse->limitCount || parse->limitOffset) @@ -2018,13 +2038,44 @@ grouping_planner(PlannerInfo *root, bool inheritance_update, scanjoin_targets_contain_srfs = NIL; } - /* Apply scan/join target. */ - scanjoin_target_same_exprs = list_length(scanjoin_targets) == 1 - && equal(scanjoin_target->exprs, current_rel->reltarget->exprs); - apply_scanjoin_target_to_paths(root, current_rel, scanjoin_targets, - scanjoin_targets_contain_srfs, - scanjoin_target_parallel_safe, - scanjoin_target_same_exprs); + /* + * For an UPDATE/DELETE query whose target is partitioned table, we + * must generate the targetlist for each of its leaf partitions and + * apply that. + */ + if (current_rel->reloptkind == RELOPT_BASEREL && + current_rel->part_scheme && + current_rel->relid == root->parse->resultRelation && + parse->commandType != CMD_INSERT) + { + /* + * scanjoin_target shouldn't have changed from final_target, + * because UPDATE/DELETE doesn't support various features that + * would've required modifications that are performed above. + * That's important because we'll generate final_target freshly + * for each partition in partitionwise_adjust_scanjoin_target. + */ + Assert(scanjoin_target == final_target); + root->parse->targetList = orig_parse_tlist; + partitionwise_adjust_scanjoin_target(root, current_rel, + &partition_subroots, + &partitioned_rels, + &partition_resultRelations, + &partition_subpaths, + &partition_WCOLists, + &partition_returningLists, + &partition_rowMarks); + } + else + { + /* Apply scan/join target. */ + scanjoin_target_same_exprs = list_length(scanjoin_targets) == 1 + && equal(scanjoin_target->exprs, current_rel->reltarget->exprs); + apply_scanjoin_target_to_paths(root, current_rel, scanjoin_targets, + scanjoin_targets_contain_srfs, + scanjoin_target_parallel_safe, + scanjoin_target_same_exprs); + } /* * Save the various upper-rel PathTargets we just computed into @@ -2136,93 +2187,119 @@ grouping_planner(PlannerInfo *root, bool inheritance_update, final_rel->useridiscurrent = current_rel->useridiscurrent; final_rel->fdwroutine = current_rel->fdwroutine; - /* - * Generate paths for the final_rel. Insert all surviving paths, with - * LockRows, Limit, and/or ModifyTable steps added if needed. - */ - foreach(lc, current_rel->pathlist) + if (current_rel->reloptkind == RELOPT_BASEREL && + current_rel->relid == root->parse->resultRelation && + current_rel->part_scheme && + parse->commandType != CMD_INSERT) { - Path *path = (Path *) lfirst(lc); - - /* - * If there is a FOR [KEY] UPDATE/SHARE clause, add the LockRows node. - * (Note: we intentionally test parse->rowMarks not root->rowMarks - * here. If there are only non-locking rowmarks, they should be - * handled by the ModifyTable node instead. However, root->rowMarks - * is what goes into the LockRows node.) - */ - if (parse->rowMarks) - { - path = (Path *) create_lockrows_path(root, final_rel, path, - root->rowMarks, - SS_assign_special_param(root)); - } - - /* - * If there is a LIMIT/OFFSET clause, add the LIMIT node. - */ - if (limit_needed(parse)) - { - path = (Path *) create_limit_path(root, final_rel, path, - parse->limitOffset, - parse->limitCount, - offset_est, count_est); - } - - /* - * If this is an INSERT/UPDATE/DELETE, and we're not being called from - * inheritance_planner, add the ModifyTable node. - */ - if (parse->commandType != CMD_SELECT && !inheritance_update) - { - List *withCheckOptionLists; - List *returningLists; - List *rowMarks; - - /* - * Set up the WITH CHECK OPTION and RETURNING lists-of-lists, if - * needed. - */ - if (parse->withCheckOptions) - withCheckOptionLists = list_make1(parse->withCheckOptions); - else - withCheckOptionLists = NIL; - - if (parse->returningList) - returningLists = list_make1(parse->returningList); - else - returningLists = NIL; - - /* - * If there was a FOR [KEY] UPDATE/SHARE clause, the LockRows node - * will have dealt with fetching non-locked marked rows, else we - * need to have ModifyTable do that. - */ - if (parse->rowMarks) - rowMarks = NIL; - else - rowMarks = root->rowMarks; - - path = (Path *) + Path *path = (Path *) create_modifytable_path(root, final_rel, parse->commandType, parse->canSetTag, parse->resultRelation, - NIL, - false, - list_make1_int(parse->resultRelation), - list_make1(path), - list_make1(root), - withCheckOptionLists, - returningLists, - rowMarks, - parse->onConflict, + partitioned_rels, + root->partColsUpdated, + partition_resultRelations, + partition_subpaths, + partition_subroots, + partition_WCOLists, + partition_returningLists, + partition_rowMarks, + NULL, SS_assign_special_param(root)); - } - - /* And shove it into final_rel */ add_path(final_rel, path); } + else + { + /* + * Generate paths for the final_rel. Insert all surviving paths, with + * LockRows, Limit, and/or ModifyTable steps added if needed. + */ + foreach(lc, current_rel->pathlist) + { + Path *path = (Path *) lfirst(lc); + + /* + * If there is a FOR [KEY] UPDATE/SHARE clause, add the LockRows + * node. (Note: we intentionally test parse->rowMarks not + * root->rowMarks here. If there are only non-locking rowmarks, + * they should be handled by the ModifyTable node instead. + * However, root->rowMarks is what goes into the LockRows node.) + */ + if (parse->rowMarks) + { + path = (Path *) + create_lockrows_path(root, final_rel, path, + root->rowMarks, + SS_assign_special_param(root)); + } + + /* + * If there is a LIMIT/OFFSET clause, add the LIMIT node. + */ + if (limit_needed(parse)) + { + path = (Path *) create_limit_path(root, final_rel, path, + parse->limitOffset, + parse->limitCount, + offset_est, count_est); + } + + /* + * If this is an INSERT/UPDATE/DELETE, and we're not being called + * from inheritance_planner, add the ModifyTable node. + */ + if (parse->commandType != CMD_SELECT && !inheritance_update) + { + List *withCheckOptionLists; + List *returningLists; + List *rowMarks; + + /* + * Set up the WITH CHECK OPTION and RETURNING lists-of-lists, + * if needed. + */ + if (parse->withCheckOptions) + withCheckOptionLists = list_make1(parse->withCheckOptions); + else + withCheckOptionLists = NIL; + + if (parse->returningList) + returningLists = list_make1(parse->returningList); + else + returningLists = NIL; + + /* + * If there was a FOR [KEY] UPDATE/SHARE clause, the LockRows + * node will have dealt with fetching non-locked marked rows, + * else we need to have ModifyTable do that. + */ + if (parse->rowMarks) + rowMarks = NIL; + else + rowMarks = root->rowMarks; + + path = (Path *) + create_modifytable_path(root, final_rel, + parse->commandType, + parse->canSetTag, + parse->resultRelation, + NIL, + false, + list_make1_int(parse->resultRelation), + list_make1(path), + list_make1(root), + withCheckOptionLists, + returningLists, + rowMarks, + parse->onConflict, + SS_assign_special_param(root)); + } + + /* And shove it into final_rel */ + add_path(final_rel, path); + } + } /* * Generate partial paths for final_rel, too, if outer query levels might @@ -2259,6 +2336,129 @@ grouping_planner(PlannerInfo *root, bool inheritance_update, } /* + * partitionwise_adjust_scanjoin_target + * adjusts query's targetlist for each partition in the partition tree + * whose root is 'parent' and apply it to their paths via + * apply_scanjoin_target_to_paths + * + * Its output also consists of various pieces of information that will go + * into the ModifyTable node that will be created for this query. + */ +static void +partitionwise_adjust_scanjoin_target(PlannerInfo *root, + RelOptInfo *parent, + List **subroots, + List **partitioned_rels, + List **resultRelations, + List **subpaths, + List **WCOLists, + List **returningLists, + List **rowMarks) +{ + Query *parse = root->parse; + int i; + + *partitioned_rels = lappend(*partitioned_rels, + list_make1_int(parent->relid)); + + for (i = 0; i < parent->nparts; i++) + { + RelOptInfo *child_rel = parent->part_rels[i]; + AppendRelInfo *appinfo; + int relid; + List *tlist; + PathTarget *scanjoin_target; + bool scanjoin_target_parallel_safe; + bool scanjoin_target_same_exprs; + PlannerInfo *partition_subroot; + Query *partition_parse; + + /* Ignore pruned partitions. */ + if (IS_DUMMY_REL(child_rel)) + continue; + + /* + * Extract the original relid of partition to fetch its AppendRelInfo. + * We must find it like this, because + * partitionwise_make_rel_from_joinlist replaces the original rel + * with one generated by join planning which may be different. + */ + relid = -1; + while ((relid = bms_next_member(child_rel->relids, relid)) > 0) + if (root->append_rel_array[relid] && + root->append_rel_array[relid]->parent_relid == + parent->relid) + break; + + appinfo = root->append_rel_array[relid]; + + /* Translate Query structure for this partition. */ + partition_parse = (Query *) + adjust_appendrel_attrs(root, + (Node *) parse, + 1, &appinfo); + + /* Recurse if partition is itself a partitioned table. */ + if (child_rel->part_scheme) + { + root->parse = partition_parse; + partitionwise_adjust_scanjoin_target(root, child_rel, + subroots, + partitioned_rels, + resultRelations, + subpaths, + WCOLists, + returningLists, + rowMarks); + /* Restore the Query for processing the next partition. */ + root->parse = parse; + } + else + { + /* + * Generate a separate PlannerInfo for this partition. We'll need + * it when generating the ModifyTable subplan for this partition. + */ + partition_subroot = makeNode(PlannerInfo); + *subroots = lappend(*subroots, partition_subroot); + memcpy(partition_subroot, root, sizeof(PlannerInfo)); + partition_subroot->parse = partition_parse; + + /* + * Preprocess the translated targetlist and save it in the + * partition's PlannerInfo for the perusal of later planning + * steps. + */ + tlist = preprocess_targetlist(partition_subroot); + partition_subroot->processed_tlist = tlist; + + /* Apply scan/join target. */ + scanjoin_target = create_pathtarget(root, tlist); + scanjoin_target_same_exprs = equal(scanjoin_target->exprs, + child_rel->reltarget->exprs); + scanjoin_target_parallel_safe = + is_parallel_safe(root, (Node *) scanjoin_target->exprs); + apply_scanjoin_target_to_paths(root, child_rel, + list_make1(scanjoin_target), + NIL, + scanjoin_target_parallel_safe, + scanjoin_target_same_exprs); + + /* Collect information that will go into the ModifyTable */ + *resultRelations = lappend_int(*resultRelations, relid); + *subpaths = lappend(*subpaths, child_rel->cheapest_total_path); + if (partition_parse->withCheckOptions) + *WCOLists = lappend(*WCOLists, partition_parse->withCheckOptions); + if (partition_parse->returningList) + *returningLists = lappend(*returningLists, + partition_parse->returningList); + if (partition_parse->rowMarks) + *rowMarks = lappend(*rowMarks, partition_parse->rowMarks); + } + } +} + +/* * Do preprocessing for groupingSets clause and related data. This handles the * preliminary steps of expanding the grouping sets, organizing them into lists * of rollups, and preparing annotations which will later be filled in with @@ -6964,7 +7164,9 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, } /* Build new paths for this relation by appending child paths. */ - if (live_children != NIL) + if (live_children != NIL && + !(rel->reloptkind == RELOPT_BASEREL && + rel->relid == root->parse->resultRelation)) add_paths_to_append_rel(root, rel, live_children); } diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index 690b6bbab7..f4c485cdc9 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -2265,8 +2265,34 @@ adjust_appendrel_attrs_mutator(Node *node, context->appinfos); return (Node *) phv; } + + if (IsA(node, SpecialJoinInfo)) + { + SpecialJoinInfo *oldinfo = (SpecialJoinInfo *) node; + SpecialJoinInfo *newinfo = makeNode(SpecialJoinInfo); + + memcpy(newinfo, oldinfo, sizeof(SpecialJoinInfo)); + newinfo->min_lefthand = adjust_child_relids(oldinfo->min_lefthand, + context->nappinfos, + context->appinfos); + newinfo->min_righthand = adjust_child_relids(oldinfo->min_righthand, + context->nappinfos, + context->appinfos); + newinfo->syn_lefthand = adjust_child_relids(oldinfo->syn_lefthand, + context->nappinfos, + context->appinfos); + newinfo->syn_righthand = adjust_child_relids(oldinfo->syn_righthand, + context->nappinfos, + context->appinfos); + newinfo->semi_rhs_exprs = + (List *) expression_tree_mutator((Node *) + oldinfo->semi_rhs_exprs, + adjust_appendrel_attrs_mutator, + (void *) context); + return (Node *) newinfo; + } + /* Shouldn't need to handle planner auxiliary nodes here */ - Assert(!IsA(node, SpecialJoinInfo)); Assert(!IsA(node, AppendRelInfo)); Assert(!IsA(node, PlaceHolderInfo)); Assert(!IsA(node, MinMaxAggInfo)); diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 8369e3ad62..8d67f21f42 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1265,36 +1265,6 @@ get_relation_constraints(PlannerInfo *root, } } - /* - * Append partition predicates, if any. - * - * For selects, partition pruning uses the parent table's partition bound - * descriptor, instead of constraint exclusion which is driven by the - * individual partition's partition constraint. - */ - if (enable_partition_pruning && root->parse->commandType != CMD_SELECT) - { - List *pcqual = RelationGetPartitionQual(relation); - - if (pcqual) - { - /* - * Run the partition quals through const-simplification similar to - * check constraints. We skip canonicalize_qual, though, because - * partition quals should be in canonical form already; also, - * since the qual is in implicit-AND format, we'd have to - * explicitly convert it to explicit-AND format and back again. - */ - pcqual = (List *) eval_const_expressions(root, (Node *) pcqual); - - /* Fix Vars to have the desired varno */ - if (varno != 1) - ChangeVarNodes((Node *) pcqual, 1, varno, 0); - - result = list_concat(result, pcqual); - } - } - heap_close(relation, NoLock); return result; diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out index 7d04d12c6e..9074182512 100644 --- a/src/test/regress/expected/partition_join.out +++ b/src/test/regress/expected/partition_join.out @@ -1752,7 +1752,7 @@ WHERE EXISTS ( Filter: (c IS NULL) -> Nested Loop -> Seq Scan on int4_tbl - -> Subquery Scan on ss_1 + -> Subquery Scan on ss -> Limit -> Seq Scan on int8_tbl int8_tbl_1 -> Nested Loop Semi Join @@ -1760,7 +1760,7 @@ WHERE EXISTS ( Filter: (c IS NULL) -> Nested Loop -> Seq Scan on int4_tbl - -> Subquery Scan on ss_2 + -> Subquery Scan on ss -> Limit -> Seq Scan on int8_tbl int8_tbl_2 (28 rows) -- 2.11.0