Commit 3373c715 authored by David Rowley's avatar David Rowley

Speed up finding EquivalenceClasses for a given set of rels

Previously in order to determine which ECs a relation had members in, we
had to loop over all ECs stored in PlannerInfo's eq_classes and check if
ec_relids mentioned the relation.  For the most part, this was fine, as
generally, unless queries were fairly complex, the overhead of performing
the lookup would have not been that significant.  However, when queries
contained large numbers of joins and ECs, the overhead to find the set of
classes matching a given set of relations could become a significant
portion of the overall planning effort.

Here we allow a much more efficient method to access the ECs which match a
given relation or set of relations.  A new Bitmapset field in RelOptInfo
now exists to store the indexes into PlannerInfo's eq_classes list which
each relation is mentioned in.  This allows very fast lookups to find all
ECs belonging to a single relation.  When we need to lookup ECs belonging
to a given pair of relations, we can simply bitwise-AND the Bitmapsets from
each relation and use the result to perform the lookup.

We also take the opportunity to write a new implementation of
generate_join_implied_equalities which makes use of the new indexes.
generate_join_implied_equalities_for_ecs must remain as is as it can be
given a custom list of ECs, which we can't easily determine the indexes of.

This was originally intended to fix the performance penalty of looking up
foreign keys matching a join condition which was introduced by 100340e2.
However, we're speeding up much more than just that here.

Author: David Rowley, Tom Lane
Reviewed-by: Tom Lane, Tomas Vondra
Discussion: https://postgr.es/m/6970.1545327857@sss.pgh.pa.us
parent 894af78f
......@@ -2195,6 +2195,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
WRITE_NODE_FIELD(cte_plan_ids);
WRITE_NODE_FIELD(multiexpr_params);
WRITE_NODE_FIELD(eq_classes);
WRITE_BOOL_FIELD(ec_merging_done);
WRITE_NODE_FIELD(canon_pathkeys);
WRITE_NODE_FIELD(left_join_clauses);
WRITE_NODE_FIELD(right_join_clauses);
......@@ -2261,6 +2262,7 @@ _outRelOptInfo(StringInfo str, const RelOptInfo *node)
WRITE_UINT_FIELD(pages);
WRITE_FLOAT_FIELD(tuples, "%.0f");
WRITE_FLOAT_FIELD(allvisfrac, "%.6f");
WRITE_BITMAPSET_FIELD(eclass_indexes);
WRITE_NODE_FIELD(subroot);
WRITE_NODE_FIELD(subplan_params);
WRITE_INT_FIELD(rel_parallel_workers);
......
......@@ -64,6 +64,10 @@ static bool reconsider_outer_join_clause(PlannerInfo *root,
bool outer_on_left);
static bool reconsider_full_join_clause(PlannerInfo *root,
RestrictInfo *rinfo);
static Bitmapset *get_eclass_indexes_for_relids(PlannerInfo *root,
Relids relids);
static Bitmapset *get_common_eclass_indexes(PlannerInfo *root, Relids relids1,
Relids relids2);
/*
......@@ -341,10 +345,11 @@ process_equivalence(PlannerInfo *root,
/*
* Case 2: need to merge ec1 and ec2. This should never happen after
* we've built any canonical pathkeys; if it did, those pathkeys might
* be rendered non-canonical by the merge.
* the ECs have reached canonical state; otherwise, pathkeys could be
* rendered non-canonical by the merge, and relation eclass indexes
* would get broken by removal of an eq_classes list entry.
*/
if (root->canon_pathkeys != NIL)
if (root->ec_merging_done)
elog(ERROR, "too late to merge equivalence classes");
/*
......@@ -743,6 +748,26 @@ get_eclass_for_sort_expr(PlannerInfo *root,
root->eq_classes = lappend(root->eq_classes, newec);
/*
* If EC merging is already complete, we have to mop up by adding the new
* EC to the eclass_indexes of the relation(s) mentioned in it.
*/
if (root->ec_merging_done)
{
int ec_index = list_length(root->eq_classes) - 1;
int i = -1;
while ((i = bms_next_member(newec->ec_relids, i)) > 0)
{
RelOptInfo *rel = root->simple_rel_array[i];
Assert(rel->reloptkind == RELOPT_BASEREL);
rel->eclass_indexes = bms_add_member(rel->eclass_indexes,
ec_index);
}
}
MemoryContextSwitchTo(oldcontext);
return newec;
......@@ -800,42 +825,71 @@ get_eclass_for_sort_expr(PlannerInfo *root,
void
generate_base_implied_equalities(PlannerInfo *root)
{
int ec_index;
ListCell *lc;
Index rti;
/*
* At this point, we're done absorbing knowledge of equivalences in the
* query, so no further EC merging should happen, and ECs remaining in the
* eq_classes list can be considered canonical. (But note that it's still
* possible for new single-member ECs to be added through
* get_eclass_for_sort_expr().)
*/
root->ec_merging_done = true;
ec_index = 0;
foreach(lc, root->eq_classes)
{
EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc);
bool can_generate_joinclause = false;
int i;
Assert(ec->ec_merged == NULL); /* else shouldn't be in list */
Assert(!ec->ec_broken); /* not yet anyway... */
/* Single-member ECs won't generate any deductions */
if (list_length(ec->ec_members) <= 1)
continue;
/*
* Generate implied equalities that are restriction clauses.
* Single-member ECs won't generate any deductions, either here or at
* the join level.
*/
if (list_length(ec->ec_members) > 1)
{
if (ec->ec_has_const)
generate_base_implied_equalities_const(root, ec);
else
generate_base_implied_equalities_no_const(root, ec);
/* Recover if we failed to generate required derived clauses */
if (ec->ec_broken)
generate_base_implied_equalities_broken(root, ec);
/* Detect whether this EC might generate join clauses */
can_generate_joinclause =
(bms_membership(ec->ec_relids) == BMS_MULTIPLE);
}
if (ec->ec_has_const)
generate_base_implied_equalities_const(root, ec);
else
generate_base_implied_equalities_no_const(root, ec);
/*
* Mark the base rels cited in each eclass (which should all exist by
* now) with the eq_classes indexes of all eclasses mentioning them.
* This will let us avoid searching in subsequent lookups. While
* we're at it, we can mark base rels that have pending eclass joins;
* this is a cheap version of has_relevant_eclass_joinclause().
*/
i = -1;
while ((i = bms_next_member(ec->ec_relids, i)) > 0)
{
RelOptInfo *rel = root->simple_rel_array[i];
/* Recover if we failed to generate required derived clauses */
if (ec->ec_broken)
generate_base_implied_equalities_broken(root, ec);
}
Assert(rel->reloptkind == RELOPT_BASEREL);
/*
* This is also a handy place to mark base rels (which should all exist by
* now) with flags showing whether they have pending eclass joins.
*/
for (rti = 1; rti < root->simple_rel_array_size; rti++)
{
RelOptInfo *brel = root->simple_rel_array[rti];
rel->eclass_indexes = bms_add_member(rel->eclass_indexes,
ec_index);
if (brel == NULL)
continue;
if (can_generate_joinclause)
rel->has_eclass_joins = true;
}
brel->has_eclass_joins = has_relevant_eclass_joinclause(root, brel);
ec_index++;
}
}
......@@ -1073,11 +1127,72 @@ generate_join_implied_equalities(PlannerInfo *root,
Relids outer_relids,
RelOptInfo *inner_rel)
{
return generate_join_implied_equalities_for_ecs(root,
root->eq_classes,
join_relids,
outer_relids,
inner_rel);
List *result = NIL;
Relids inner_relids = inner_rel->relids;
Relids nominal_inner_relids;
Relids nominal_join_relids;
Bitmapset * matching_ecs;
int i;
/* If inner rel is a child, extra setup work is needed */
if (IS_OTHER_REL(inner_rel))
{
Assert(!bms_is_empty(inner_rel->top_parent_relids));
/* Fetch relid set for the topmost parent rel */
nominal_inner_relids = inner_rel->top_parent_relids;
/* ECs will be marked with the parent's relid, not the child's */
nominal_join_relids = bms_union(outer_relids, nominal_inner_relids);
}
else
{
nominal_inner_relids = inner_relids;
nominal_join_relids = join_relids;
}
/*
* Get all eclasses in common between inner_rel's relids and outer_relids
*/
matching_ecs = get_common_eclass_indexes(root, inner_rel->relids,
outer_relids);
i = -1;
while ((i = bms_next_member(matching_ecs, i)) >= 0)
{
EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
List *sublist = NIL;
/* ECs containing consts do not need any further enforcement */
if (ec->ec_has_const)
continue;
/* Single-member ECs won't generate any deductions */
if (list_length(ec->ec_members) <= 1)
continue;
/* Sanity check that this eclass overlaps the join */
Assert(bms_overlap(ec->ec_relids, nominal_join_relids));
if (!ec->ec_broken)
sublist = generate_join_implied_equalities_normal(root,
ec,
join_relids,
outer_relids,
inner_relids);
/* Recover if we failed to generate required derived clauses */
if (ec->ec_broken)
sublist = generate_join_implied_equalities_broken(root,
ec,
nominal_join_relids,
outer_relids,
nominal_inner_relids,
inner_rel);
result = list_concat(result, sublist);
}
return result;
}
/*
......@@ -2022,12 +2137,24 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
Index var2varno = fkinfo->ref_relid;
AttrNumber var2attno = fkinfo->confkey[colno];
Oid eqop = fkinfo->conpfeqop[colno];
RelOptInfo *rel1 = root->simple_rel_array[var1varno];
RelOptInfo *rel2 = root->simple_rel_array[var2varno];
List *opfamilies = NIL; /* compute only if needed */
ListCell *lc1;
foreach(lc1, root->eq_classes)
Bitmapset *matching_ecs;
int i;
/* Consider only eclasses mentioning both relations */
Assert(root->ec_merging_done);
Assert(IS_SIMPLE_REL(rel1));
Assert(IS_SIMPLE_REL(rel2));
matching_ecs = bms_intersect(rel1->eclass_indexes,
rel2->eclass_indexes);
i = -1;
while ((i = bms_next_member(matching_ecs, i)) >= 0)
{
EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc1);
EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes,
i);
bool item1member = false;
bool item2member = false;
ListCell *lc2;
......@@ -2037,14 +2164,6 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
continue;
/* Note: it seems okay to match to "broken" eclasses here */
/*
* If eclass visibly doesn't have members for both rels, there's no
* need to grovel through the members.
*/
if (!bms_is_member(var1varno, ec->ec_relids) ||
!bms_is_member(var2varno, ec->ec_relids))
continue;
foreach(lc2, ec->ec_members)
{
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2);
......@@ -2104,11 +2223,19 @@ add_child_rel_equivalences(PlannerInfo *root,
RelOptInfo *parent_rel,
RelOptInfo *child_rel)
{
ListCell *lc1;
int i;
foreach(lc1, root->eq_classes)
/*
* EC merging should be complete already, so we can use the parent rel's
* eclass_indexes to avoid searching all of root->eq_classes.
*/
Assert(root->ec_merging_done);
Assert(IS_SIMPLE_REL(parent_rel));
i = -1;
while ((i = bms_next_member(parent_rel->eclass_indexes, i)) >= 0)
{
EquivalenceClass *cur_ec = (EquivalenceClass *) lfirst(lc1);
EquivalenceClass *cur_ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
int num_members;
/*
......@@ -2119,12 +2246,8 @@ add_child_rel_equivalences(PlannerInfo *root,
if (cur_ec->ec_has_volatile)
continue;
/*
* No point in searching if child's topmost parent rel is not
* mentioned in eclass.
*/
if (!bms_is_subset(child_rel->top_parent_relids, cur_ec->ec_relids))
continue;
/* Sanity check eclass_indexes only contain ECs for parent_rel */
Assert(bms_is_subset(child_rel->top_parent_relids, cur_ec->ec_relids));
/*
* We don't use foreach() here because there's no point in scanning
......@@ -2202,6 +2325,9 @@ add_child_rel_equivalences(PlannerInfo *root,
(void) add_eq_member(cur_ec, child_expr,
new_relids, new_nullable_relids,
true, cur_em->em_datatype);
/* Record this EC index for the child rel */
child_rel->eclass_indexes = bms_add_member(child_rel->eclass_indexes, i);
}
}
}
......@@ -2241,7 +2367,10 @@ generate_implied_equalities_for_column(PlannerInfo *root,
List *result = NIL;
bool is_child_rel = (rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
Relids parent_relids;
ListCell *lc1;
int i;
/* Should be OK to rely on eclass_indexes */
Assert(root->ec_merging_done);
/* Indexes are available only on base or "other" member relations. */
Assert(IS_SIMPLE_REL(rel));
......@@ -2252,12 +2381,16 @@ generate_implied_equalities_for_column(PlannerInfo *root,
else
parent_relids = NULL; /* not used, but keep compiler quiet */
foreach(lc1, root->eq_classes)
i = -1;
while ((i = bms_next_member(rel->eclass_indexes, i)) >= 0)
{
EquivalenceClass *cur_ec = (EquivalenceClass *) lfirst(lc1);
EquivalenceClass *cur_ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
EquivalenceMember *cur_em;
ListCell *lc2;
/* Sanity check eclass_indexes only contain ECs for rel */
Assert(is_child_rel || bms_is_subset(rel->relids, cur_ec->ec_relids));
/*
* Won't generate joinclauses if const or single-member (the latter
* test covers the volatile case too)
......@@ -2265,14 +2398,6 @@ generate_implied_equalities_for_column(PlannerInfo *root,
if (cur_ec->ec_has_const || list_length(cur_ec->ec_members) <= 1)
continue;
/*
* No point in searching if rel not mentioned in eclass (but we can't
* tell that for a child rel).
*/
if (!is_child_rel &&
!bms_is_subset(rel->relids, cur_ec->ec_relids))
continue;
/*
* Scan members, looking for a match to the target column. Note that
* child EC members are considered, but only when they belong to the
......@@ -2366,11 +2491,25 @@ bool
have_relevant_eclass_joinclause(PlannerInfo *root,
RelOptInfo *rel1, RelOptInfo *rel2)
{
ListCell *lc1;
Bitmapset *matching_ecs;
int i;
foreach(lc1, root->eq_classes)
/* Examine only eclasses mentioning both rel1 and rel2 */
matching_ecs = get_common_eclass_indexes(root, rel1->relids,
rel2->relids);
i = -1;
while ((i = bms_next_member(matching_ecs, i)) >= 0)
{
EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc1);
EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes,
i);
/*
* Sanity check that get_common_eclass_indexes gave only ECs
* containing both rels.
*/
Assert(bms_overlap(rel1->relids, ec->ec_relids));
Assert(bms_overlap(rel2->relids, ec->ec_relids));
/*
* Won't generate joinclauses if single-member (this test covers the
......@@ -2382,12 +2521,12 @@ have_relevant_eclass_joinclause(PlannerInfo *root,
/*
* We do not need to examine the individual members of the EC, because
* all that we care about is whether each rel overlaps the relids of
* at least one member, and a test on ec_relids is sufficient to prove
* that. (As with have_relevant_joinclause(), it is not necessary
* that the EC be able to form a joinclause relating exactly the two
* given rels, only that it be able to form a joinclause mentioning
* both, and this will surely be true if both of them overlap
* ec_relids.)
* at least one member, and get_common_eclass_indexes() and the single
* member check above are sufficient to prove that. (As with
* have_relevant_joinclause(), it is not necessary that the EC be able
* to form a joinclause relating exactly the two given rels, only that
* it be able to form a joinclause mentioning both, and this will
* surely be true if both of them overlap ec_relids.)
*
* Note we don't test ec_broken; if we did, we'd need a separate code
* path to look through ec_sources. Checking the membership anyway is
......@@ -2399,9 +2538,8 @@ have_relevant_eclass_joinclause(PlannerInfo *root,
* since the join result is likely to be small even though it'll end
* up being an unqualified nestloop.
*/
if (bms_overlap(rel1->relids, ec->ec_relids) &&
bms_overlap(rel2->relids, ec->ec_relids))
return true;
return true;
}
return false;
......@@ -2419,11 +2557,17 @@ have_relevant_eclass_joinclause(PlannerInfo *root,
bool
has_relevant_eclass_joinclause(PlannerInfo *root, RelOptInfo *rel1)
{
ListCell *lc1;
Bitmapset *matched_ecs;
int i;
foreach(lc1, root->eq_classes)
/* Examine only eclasses mentioning rel1 */
matched_ecs = get_eclass_indexes_for_relids(root, rel1->relids);
i = -1;
while ((i = bms_next_member(matched_ecs, i)) >= 0)
{
EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc1);
EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes,
i);
/*
* Won't generate joinclauses if single-member (this test covers the
......@@ -2436,8 +2580,7 @@ has_relevant_eclass_joinclause(PlannerInfo *root, RelOptInfo *rel1)
* Per the comment in have_relevant_eclass_joinclause, it's sufficient
* to find an EC that mentions both this rel and some other rel.
*/
if (bms_overlap(rel1->relids, ec->ec_relids) &&
!bms_is_subset(ec->ec_relids, rel1->relids))
if (!bms_is_subset(ec->ec_relids, rel1->relids))
return true;
}
......@@ -2570,3 +2713,54 @@ is_redundant_with_indexclauses(RestrictInfo *rinfo, List *indexclauses)
return false;
}
/*
* get_eclass_indexes_for_relids
* Build and return a Bitmapset containing the indexes into root's
* eq_classes list for all eclasses that mention any of these relids
*/
static Bitmapset *
get_eclass_indexes_for_relids(PlannerInfo *root, Relids relids)
{
Bitmapset *ec_indexes = NULL;
int i = -1;
/* Should be OK to rely on eclass_indexes */
Assert(root->ec_merging_done);
while ((i = bms_next_member(relids, i)) > 0)
{
RelOptInfo *rel = root->simple_rel_array[i];
ec_indexes = bms_add_members(ec_indexes, rel->eclass_indexes);
}
return ec_indexes;
}
/*
* get_common_eclass_indexes
* Build and return a Bitmapset containing the indexes into root's
* eq_classes list for all eclasses that mention rels in both
* relids1 and relids2.
*/
static Bitmapset *
get_common_eclass_indexes(PlannerInfo *root, Relids relids1, Relids relids2)
{
Bitmapset *rel1ecs;
Bitmapset *rel2ecs;
int relid;
rel1ecs = get_eclass_indexes_for_relids(root, relids1);
/*
* We can get away with just using the relation's eclass_indexes directly
* when relids2 is a singleton set.
*/
if (bms_get_singleton_member(relids2, &relid))
rel2ecs = root->simple_rel_array[relid]->eclass_indexes;
else
rel2ecs = get_eclass_indexes_for_relids(root, relids2);
/* Calculate and return the common EC indexes, recycling the left input. */
return bms_int_members(rel1ecs, rel2ecs);
}
......@@ -48,9 +48,7 @@ static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
* entry if there's not one already.
*
* Note that this function must not be used until after we have completed
* merging EquivalenceClasses. (We don't try to enforce that here; instead,
* equivclass.c will complain if a merge occurs after root->canon_pathkeys
* has become nonempty.)
* merging EquivalenceClasses.
*/
PathKey *
make_canonical_pathkey(PlannerInfo *root,
......@@ -61,6 +59,10 @@ make_canonical_pathkey(PlannerInfo *root,
ListCell *lc;
MemoryContext oldcontext;
/* Can't make canonical pathkeys if the set of ECs might still change */
if (!root->ec_merging_done)
elog(ERROR, "too soon to build canonical pathkeys");
/* The passed eclass might be non-canonical, so chase up to the top */
while (eclass->ec_merged)
eclass = eclass->ec_merged;
......
......@@ -139,6 +139,12 @@ query_planner(PlannerInfo *root,
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(final_rel);
/*
* We don't need to run generate_base_implied_equalities, but
* we do need to pretend that EC merging is complete.
*/
root->ec_merging_done = true;
/*
* We still are required to call qp_callback, in case it's
* something like "SELECT 2+2 ORDER BY 1".
......
......@@ -618,6 +618,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
root->cte_plan_ids = NIL;
root->multiexpr_params = NIL;
root->eq_classes = NIL;
root->ec_merging_done = false;
root->append_rel_list = NIL;
root->rowMarks = NIL;
memset(root->upper_rels, 0, sizeof(root->upper_rels));
......
......@@ -886,6 +886,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
subroot->cte_plan_ids = NIL;
subroot->multiexpr_params = NIL;
subroot->eq_classes = NIL;
subroot->ec_merging_done = false;
subroot->append_rel_list = NIL;
subroot->rowMarks = NIL;
memset(subroot->upper_rels, 0, sizeof(subroot->upper_rels));
......
......@@ -120,6 +120,15 @@ plan_set_operations(PlannerInfo *root)
Assert(parse->windowClause == NIL);
Assert(parse->distinctClause == NIL);
/*
* In the outer query level, we won't have any true equivalences to deal
* with; but we do want to be able to make pathkeys, which will require
* single-member EquivalenceClasses. Indicate that EC merging is complete
* so that pathkeys.c won't complain.
*/
Assert(root->eq_classes == NIL);
root->ec_merging_done = true;
/*
* We'll need to build RelOptInfos for each of the leaf subqueries, which
* are RTE_SUBQUERY rangetable entries in this Query. Prepare the index
......
......@@ -218,6 +218,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
rel->pages = 0;
rel->tuples = 0;
rel->allvisfrac = 0;
rel->eclass_indexes = NULL;
rel->subroot = NULL;
rel->subplan_params = NIL;
rel->rel_parallel_workers = -1; /* set up in get_relation_info */
......@@ -629,6 +630,7 @@ build_join_rel(PlannerInfo *root,
joinrel->pages = 0;
joinrel->tuples = 0;
joinrel->allvisfrac = 0;
joinrel->eclass_indexes = NULL;
joinrel->subroot = NULL;
joinrel->subplan_params = NIL;
joinrel->rel_parallel_workers = -1;
......@@ -808,6 +810,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
joinrel->pages = 0;
joinrel->tuples = 0;
joinrel->allvisfrac = 0;
joinrel->eclass_indexes = NULL;
joinrel->subroot = NULL;
joinrel->subplan_params = NIL;
joinrel->serverid = InvalidOid;
......
......@@ -265,6 +265,8 @@ struct PlannerInfo
List *eq_classes; /* list of active EquivalenceClasses */
bool ec_merging_done; /* set true once ECs are canonical */
List *canon_pathkeys; /* list of "canonical" PathKeys */
List *left_join_clauses; /* list of RestrictInfos for mergejoinable
......@@ -505,6 +507,8 @@ typedef struct PartitionSchemeData *PartitionScheme;
* pages - number of disk pages in relation (zero if not a table)
* tuples - number of tuples in relation (not considering restrictions)
* allvisfrac - fraction of disk pages that are marked all-visible
* eclass_indexes - EquivalenceClasses that mention this rel (filled
* only after EC merging is complete)
* subroot - PlannerInfo for subquery (NULL if it's not a subquery)
* subplan_params - list of PlannerParamItems to be passed to subquery
*
......@@ -678,6 +682,8 @@ typedef struct RelOptInfo
BlockNumber pages; /* size estimates derived from pg_class */
double tuples;
double allvisfrac;
Bitmapset *eclass_indexes; /* Indexes in PlannerInfo's eq_classes list of
* ECs that mention this rel */
PlannerInfo *subroot; /* if subquery */
List *subplan_params; /* if subquery */
int rel_parallel_workers; /* wanted number of parallel workers */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment