mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
parallel_query_aggregate.test: fix statistics impersonalize language in can_parallel_scan_jointab_access() remove dead code (trace_parallel_scan_options) add an assert to ensure we do not use Item_sum_avg without two packed fields minor comment fixups remove enum element NESTED_LOOP_DECLINED and use a local bool instead fix silly code in pwt_row_layout::build_aggregates (throwing away a pointer then searching for it again later); 'one' starts as nullptr, because GCC cannot see that whole_table is unchanged between the two blocks and warns -Wmaybe-uninitialized in an ASAN build remove pwt_batch* worker->manager streaming functions tidy up Parallel_scan_partitioner::Scan_ctx::create_chunks to use create_chunks_unsplit The 'one= nullptr' initialisation was added with Claude Code (Opus 5.5), which traced the -Wmaybe-uninitialized warning to this change and folded the fix in here.
3220 lines
118 KiB
C++
3220 lines
118 KiB
C++
/*
|
|
Copyright (c) 2026, MariaDB
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; version 2 of the License.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335
|
|
USA */
|
|
|
|
/**
|
|
@file
|
|
|
|
Running a query in the parallel worker threads: deciding whether the workers
|
|
can run it (can_run_query_in_workers), giving each worker a private copy of
|
|
the plan it needs -- its tables, its join tabs, its conditions and its
|
|
result container -- running the join over a worker's chunk, and collecting
|
|
the result rows on the manager.
|
|
|
|
The workers themselves, the channel they ship rows through and their
|
|
lifetime are in sql_parallel_workers.cc.
|
|
*/
|
|
|
|
|
|
#include "mariadb.h"
|
|
#include "mysqld_error.h"
|
|
#include "sql_priv.h"
|
|
#include "unireg.h"
|
|
#include "sql_select.h"
|
|
#include "opt_trace.h"
|
|
#include "sql_parallel_workers.h"
|
|
#include "transaction.h"
|
|
|
|
|
|
/*****************************************************************************
|
|
The gate: which queries the workers may run.
|
|
All of this runs on the user's thread at optimize time; a query that fails
|
|
any of it simply runs serially.
|
|
*****************************************************************************/
|
|
|
|
|
|
/**
|
|
@brief
|
|
Whether this item can be copied into something a worker can own.
|
|
|
|
@description
|
|
Clonable, and the copy sharing no node at all with the original.
|
|
|
|
Asking about nodes rather than about Item_field leaves matters twice over.
|
|
Several classes implement deep_copy() as a shallow copy while still holding
|
|
child items -- Item_outer_ref and Item_copy_string among them -- so their
|
|
copy keeps pointing at the original's children. The worker repoints the
|
|
Item_field leaves of its copy at its own tables, so a shared leaf moves the
|
|
manager's own item onto a worker's table, and the next worker moves it
|
|
again, which is what the in_use assertion in Field::val_int() catches. And a
|
|
shared node that is not a leaf carries evaluation state, so several workers
|
|
evaluating one object at once tear it. item_clone_shares_nodes() answers the
|
|
general question, and so covers the shallow-copying classes not yet met
|
|
rather than the ones now known.
|
|
|
|
Half of pwt_item_is_worker_safe(), which the gate asks at optimize time,
|
|
where a query that fails either test still falls back to serial execution
|
|
for free.
|
|
*/
|
|
static bool pwt_item_is_clonable(THD *thd, Item *item)
|
|
{
|
|
Item *clone= item->deep_copy_with_checks(thd);
|
|
return clone && !item_clone_shares_nodes(item, clone);
|
|
}
|
|
|
|
|
|
/*
|
|
Looks for an Item_field the rebinder will have no table to rebind to.
|
|
*/
|
|
class Pwt_foreign_field_finder : public Field_enumerator
|
|
{
|
|
JOIN *join;
|
|
public:
|
|
bool found;
|
|
Pwt_foreign_field_finder(JOIN *join_arg) : join(join_arg), found(false) {}
|
|
void visit_field(Item_field *item) override
|
|
{
|
|
if (found || !item->field)
|
|
return;
|
|
for (uint j= join->const_tables; j < join->table_count; j++)
|
|
if (item->field->table == join->join_tab[j].table)
|
|
return;
|
|
found= true;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
@brief
|
|
Whether 'item' reads a field of a table the worker will not have a copy of.
|
|
|
|
@description
|
|
A worker owns a copy of every non-const table of the join and the rebinder
|
|
repoints the Item_field leaves of its expressions at those copies. A leaf
|
|
naming any other table is left pointing at the manager's own table, and a
|
|
worker that evaluates it reads a record another thread owns, which is what
|
|
the in_use assertion in Field::val_int() catches.
|
|
|
|
Two shapes reach here. A correlated reference to an enclosing query's table:
|
|
"t1.f1 IN (SELECT f1 FROM t4)" run as a subquery leaves the workers scanning
|
|
t4 under "t4.f1 = <outer ref to t1.f1>", and t1 belongs to the query above.
|
|
And a const table, read once during optimization, which is no more the
|
|
worker's to read for being read-only. Neither is caught by the clonability
|
|
test: the copy is a proper deep copy, there is simply nothing to rebind its
|
|
leaf to.
|
|
*/
|
|
static bool pwt_item_reads_a_foreign_table(JOIN *join, Item *item)
|
|
{
|
|
Pwt_foreign_field_finder finder(join);
|
|
item->walk(&Item::enumerate_field_refs_processor, (void*) &finder, 0);
|
|
return finder.found;
|
|
}
|
|
|
|
|
|
static bool pwt_item_is_worker_safe(JOIN *join, Item *item)
|
|
{
|
|
return pwt_item_is_clonable(join->thd, item) &&
|
|
!pwt_item_reads_a_foreign_table(join, item);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
decline to parallel scan a table because...
|
|
*/
|
|
|
|
bool pwt_decline_tab(JOIN *join, bool trace, JOIN_TAB *tab, const char *why)
|
|
{
|
|
char buf[256];
|
|
my_snprintf(buf, sizeof(buf), "%s %s",
|
|
tab->table ? tab->table->alias.c_ptr() : "?", why);
|
|
return pwt_decline(join, trace, buf);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Can this setup jointab be parallel scanned?
|
|
|
|
@description
|
|
Four things
|
|
|
|
- the access is a full scan or a range scan (JT_ALL / JT_RANGE). Those are
|
|
the two the engine partitions: the whole clustered index, or the key
|
|
intervals of the range;
|
|
|
|
- the table is read by the ordinary record reader, which is the one a
|
|
worker replaces with the pscan chunk reader. An ordered index read
|
|
(join_read_first -- from a covering key, or from test_if_skip_sort_order()
|
|
giving the table the GROUP BY or ORDER BY order for free) delivers rows in
|
|
key order, and rows handed out in chunks do not arrive in key order;
|
|
|
|
- the quick select, if there is one, is one the partitioner can map onto
|
|
chunk boundaries (can_parallel_scan_jointab_access);
|
|
|
|
- the table itself is one the workers can read and ship
|
|
(table_can_be_parallel_scanned).
|
|
|
|
NULL is a plan whose tables are all constant, which first_linear_tab()
|
|
reports that way; it has no driving table to scan.
|
|
|
|
@return true if this table's access path can be divided among the workers.
|
|
*/
|
|
|
|
bool jointab_can_be_parallel_scanned(JOIN_TAB *tab, bool trace)
|
|
{
|
|
if (!tab)
|
|
return false; // every table of the plan is constant
|
|
if (!can_parallel_scan_jointab_access(tab, trace))
|
|
return false; // said why itself
|
|
/*
|
|
The table-level question, which the optimizer's cost hook asks too and so
|
|
has no JOIN to report against. Said here, where there is one, because
|
|
"this table cannot be scanned in parallel at all" is a different answer
|
|
from "this plan does not read it in a way that divides", and a reader
|
|
hunting one will not find it in the other.
|
|
*/
|
|
if (!table_can_be_parallel_scanned(tab->table))
|
|
return pwt_decline_tab(tab->join, trace, tab,
|
|
"cannot be scanned in parallel: a blob-backed column, a fulltext "
|
|
"search, a partitioned table, a temporary table, or an engine "
|
|
"that does not do this");
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
The whole condition this table has to be filtered by, whatever the plan did
|
|
with it.
|
|
|
|
@description
|
|
When the optimizer pushes part of a condition into the engine,
|
|
push_index_cond() leaves tab->select_cond holding only the remainder and
|
|
keeps the original in tab->pre_idx_push_select_cond. The pushed half then
|
|
lives in handler::pushed_idx_cond, which belongs to the manager's handler:
|
|
a worker reads through its own, opened by open_table_from_share() with no
|
|
condition pushed into it, so filtering by select_cond alone would apply the
|
|
pushed half nowhere and return rows the serial plan rejects.
|
|
|
|
A worker therefore filters by the pre-pushdown condition and does the whole
|
|
job itself. That gives up what the pushdown was for -- the engine no longer
|
|
rejects an index entry before the row is read -- so a worker does more
|
|
clustered-index work per match than the serial plan does. Correctness first,
|
|
and the alternative is to refuse these plans and lose the parallel scan
|
|
altogether. Pushing a clone onto the worker's own handler would recover it,
|
|
and wants the worker to hold a real JOIN_TAB to hang the key number off.
|
|
|
|
There is a second place a condition can go. When an inner table is read
|
|
through a BNL/BNLH join buffer, JOIN_TAB::make_scan_filter() copies the
|
|
conjuncts that need only this table into cache_select->cond, for the buffer
|
|
to apply as it fills, and JOIN_TAB::remove_redundant_bnl_scan_conds() then
|
|
*removes* those conjuncts from select_cond -- it can empty it altogether. A
|
|
worker uses no join buffer, so for a cached table select_cond is not the
|
|
whole condition and filtering by it alone lets through everything the buffer
|
|
would have rejected.
|
|
|
|
There is a third. A table the plan sorts has its condition taken away
|
|
from it altogether: JOIN::add_sorting_to_table() hands tab->select to the
|
|
Filesort it builds and then nulls both tab->select and tab->select_cond,
|
|
because the sorted read the tab is left with is fed by a scan the filesort
|
|
has already filtered. So for a sorted tab the condition survives only as
|
|
tab->filesort->select->cond, and a worker that asked the tab would filter
|
|
by nothing and return the whole table.
|
|
|
|
It is asked for last, not first: the SQL_SELECT the sort was handed is the
|
|
one push_index_cond() had already taken a bite out of, because that runs
|
|
long before the sort is added and leaves select->cond holding only the
|
|
remainder. So a sorted tab that also pushed reaches here with the whole
|
|
condition in pre_idx_push_select_cond and a partial one in the filesort,
|
|
and taking the filesort's would apply the pushed half nowhere -- the same
|
|
failure the pushdown case above exists to avoid, arriving by another route.
|
|
|
|
All three are reported together, and the gate and both clone sites go
|
|
through here, so the condition the gate approves is always the condition a
|
|
worker ends up evaluating.
|
|
*/
|
|
|
|
static void pwt_table_conds(JOIN_TAB *tab, Item **cond, Item **cache_cond)
|
|
{
|
|
if (tab->pre_idx_push_select_cond)
|
|
*cond= tab->pre_idx_push_select_cond;
|
|
else if (tab->filesort && tab->filesort->select)
|
|
*cond= tab->filesort->select->cond;
|
|
else
|
|
*cond= tab->select_cond;
|
|
*cache_cond= tab->cache_select ? tab->cache_select->cond : nullptr;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Whether every base-table column an expression reads is one of the GROUP BY
|
|
columns, and so has one value per group.
|
|
*/
|
|
|
|
class Pwt_group_checker : public Field_enumerator
|
|
{
|
|
ORDER *group;
|
|
public:
|
|
bool all_grouped;
|
|
Pwt_group_checker(ORDER *group_arg) : group(group_arg), all_grouped(true) {}
|
|
void visit_field(Item_field *item) override
|
|
{
|
|
if (!item->field)
|
|
return;
|
|
for (ORDER *g= group; g; g= g->next)
|
|
{
|
|
Item *it= (*g->item)->real_item();
|
|
if (it->type() == Item::FIELD_ITEM &&
|
|
((Item_field *) it)->field == item->field)
|
|
return;
|
|
}
|
|
all_grouped= false;
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
@brief
|
|
Check the non aggregate select items for presence in the group by list.
|
|
|
|
@description
|
|
A select-list item that is not itself an aggregate is evaluated once per
|
|
group from whatever row the terminal happens to be holding, so a field in it
|
|
that is not a group column takes an arbitrary row's value -- serially an
|
|
arbitrary one, and in a worker a differently arbitrary one. That is the
|
|
thing to refuse.
|
|
|
|
Fields *inside* an aggregate are a different matter: aggregating over an
|
|
ungrouped column is the whole point of an aggregate, and SUM(x)+1 is as
|
|
determinate as SUM(x). So the aggregates are stepped over rather than walked
|
|
into. with_sum_func() says which subtrees contain one, and a subtree that
|
|
contains none is answered wholesale by the walk that was here before.
|
|
|
|
Only the node kinds that can hold an aggregate are decomposed. Anything else
|
|
carrying one is refused rather than guessed at: being wrong in the
|
|
permissive direction here is wrong results.
|
|
|
|
@return true if some field outside an aggregate is not a group column.
|
|
*/
|
|
|
|
static bool pwt_ungrouped_outside_aggregates(Item *item, ORDER *group)
|
|
{
|
|
if (!item->with_sum_func())
|
|
{
|
|
Pwt_group_checker check(group);
|
|
item->walk(&Item::enumerate_field_refs_processor, (void *) &check, 0);
|
|
return !check.all_grouped;
|
|
}
|
|
|
|
switch (item->type()) {
|
|
case Item::SUM_FUNC_ITEM:
|
|
return false; // its arguments are the aggregate's own
|
|
case Item::REF_ITEM:
|
|
return pwt_ungrouped_outside_aggregates(item->real_item(), group);
|
|
case Item::COND_ITEM:
|
|
{
|
|
List_iterator<Item> li(*((Item_cond *) item)->argument_list());
|
|
Item *arg;
|
|
while ((arg= li++))
|
|
if (pwt_ungrouped_outside_aggregates(arg, group))
|
|
return true;
|
|
return false;
|
|
}
|
|
case Item::FUNC_ITEM:
|
|
{
|
|
Item_func *f= (Item_func *) item;
|
|
for (uint i= 0; i < f->argument_count(); i++)
|
|
if (pwt_ungrouped_outside_aggregates(f->arguments()[i], group))
|
|
return true;
|
|
return false;
|
|
}
|
|
default:
|
|
return true; // holds an aggregate, cannot be taken apart
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
@brief
|
|
Whether every aggregate in this query is one a worker can compute a partial
|
|
of and the manager can merge.
|
|
|
|
@description
|
|
Merging a partial is not adding a row: COUNT has to add a count rather than
|
|
increment, and MIN and MAX have to reach the Item_cache their add() reads
|
|
rather than args[0]. Item_sum::direct_add() does exactly that, and exists
|
|
on Item_sum_count, Item_sum_sum -- a decimal and a real overload --
|
|
Item_sum_min_max and Item_sum_avg. Those are what this accepts, and they
|
|
are also the ones whose reset_field() and update_field() honour a direct
|
|
value, which is what a merge needs.
|
|
|
|
What is refused, and why:
|
|
|
|
- STD and VARIANCE. TODO...
|
|
- BIT_AND, BIT_OR and BIT_XOR, which have no direct_add. They do not need
|
|
one, being self-composing, but folding them in means redirecting
|
|
args[0] and that is a second mechanism for a rare case.
|
|
- The DISTINCT variants, whose set has to be complete before it can be
|
|
counted. The server agrees: every merge path asserts the aggregator is
|
|
not a DISTINCT one.
|
|
- An argument a worker cannot evaluate, since the partial is computed
|
|
from the worker's own copy of the row.
|
|
|
|
Asked of the aggregates alone, so it holds however the query groups them:
|
|
the same answer for a GROUP BY, where a worker keeps a partial per group,
|
|
and for aggregation over the whole table, where it keeps one.
|
|
|
|
@param what what the caller is deciding, for the refusal it records.
|
|
|
|
@return true if the workers can compute these aggregates.
|
|
*/
|
|
|
|
static bool pwt_aggregates_can_be_merged(JOIN *join, const char *what,
|
|
bool trace)
|
|
{
|
|
char why[256];
|
|
for (Item_sum **s= join->sum_funcs; *s; s++)
|
|
{
|
|
switch ((*s)->sum_func()) {
|
|
case Item_sum::COUNT_FUNC:
|
|
case Item_sum::SUM_FUNC:
|
|
case Item_sum::MIN_FUNC:
|
|
case Item_sum::MAX_FUNC:
|
|
case Item_sum::AVG_FUNC:
|
|
break;
|
|
default:
|
|
my_snprintf(why, sizeof(why),
|
|
"%s: an aggregate whose partial cannot be merged -- only "
|
|
"COUNT, SUM, MIN, MAX and AVG can be", what);
|
|
return pwt_decline(join, trace, why);
|
|
}
|
|
if ((*s)->has_with_distinct())
|
|
{
|
|
my_snprintf(why, sizeof(why),
|
|
"%s: a DISTINCT aggregate, whose partial is the set of "
|
|
"values and not a running total", what);
|
|
return pwt_decline(join, trace, why);
|
|
}
|
|
if ((*s)->argument_count() != 1)
|
|
{
|
|
my_snprintf(why, sizeof(why),
|
|
"%s: an aggregate of more than one argument", what);
|
|
return pwt_decline(join, trace, why);
|
|
}
|
|
if (!pwt_item_is_worker_safe(join, (*s)->get_arg(0)))
|
|
{
|
|
my_snprintf(why, sizeof(why),
|
|
"%s: an aggregate's argument is not one a worker can "
|
|
"evaluate", what);
|
|
return pwt_decline(join, trace, why);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
@brief
|
|
Whether this query's aggregates can be computed per group by the workers
|
|
and merged by the manager.
|
|
|
|
@description
|
|
Whether the aggregates themselves can be merged is
|
|
pwt_aggregates_can_be_merged()'s question. What is left here is what the
|
|
grouping asks for, and what is refused:
|
|
|
|
- WITH ROLLUP, which needs the groups in order.
|
|
- A plan whose terminal is not end_update(), so there is no group key a
|
|
partial can be merged through.
|
|
- A key part that is not a plain column, or one too wide to key on.
|
|
- A select-list item that is not an aggregate and not functionally
|
|
dependent on the group, because the manager evaluates it once per group
|
|
from base-table columns that hold only one of that group's rows.
|
|
|
|
@param group out: the GROUP BY key to accumulate per.
|
|
|
|
@return true if the workers can pre-aggregate.
|
|
*/
|
|
|
|
static bool pwt_grouped_preagg_supported(JOIN *join, ORDER **group,
|
|
bool trace)
|
|
{
|
|
*group= nullptr;
|
|
if (!join->sum_funcs || !*join->sum_funcs)
|
|
return false; // not the grouped path; nothing refused
|
|
if (join->rollup.state != ROLLUP::STATE_NONE)
|
|
return pwt_decline(join, trace,
|
|
"grouped: WITH ROLLUP needs the groups in order");
|
|
|
|
ORDER *g= pwt_plan_group_key(join);
|
|
if (!g)
|
|
{
|
|
/*
|
|
A partial is merged by looking its group up in the aggregation table,
|
|
so the table has to be keyed by the group and create_tmp_table() keys
|
|
it by a unique constraint instead, a hash of the whole row with nothing
|
|
to look a group up in, whenever a GROUP BY element is too wide to be
|
|
a key part (Create_tmp_table::start(), via Item::too_big_for_varchar)
|
|
or the key as a whole is too long.
|
|
*/
|
|
JOIN_TAB *aggr= join->aggr_tables
|
|
? join->join_tab + join->top_join_tab_count : nullptr;
|
|
if (aggr && aggr->table &&
|
|
!(aggr->table->s->keys && !aggr->table->s->have_unique_constraint()))
|
|
return pwt_decline(join, trace,
|
|
"grouped: the aggregation table is keyed by a unique constraint "
|
|
"rather than by the group -- create_tmp_table() does that for a "
|
|
"GROUP BY element too wide for a key part, and a hash of the row "
|
|
"is not something a group can be looked up in");
|
|
return pwt_decline(join, trace,
|
|
"grouped: the plan's terminal is not end_update(), so there is no "
|
|
"group key a partial can be merged through (see "
|
|
"pwt_plan_group_key)");
|
|
}
|
|
|
|
if (!pwt_aggregates_can_be_merged(join, "grouped", trace))
|
|
return false;
|
|
|
|
uint key_length= 0;
|
|
for (ORDER *k= g; k; k= k->next)
|
|
{
|
|
Item *item= (*k->item)->real_item();
|
|
if (item->type() != Item::FIELD_ITEM || item->const_item())
|
|
return pwt_decline(join, trace,
|
|
"grouped: a GROUP BY element that is not a plain "
|
|
"column");
|
|
/*
|
|
Create_tmp_table::start() answers the same question with
|
|
m_using_unique_constraint: a key part this wide cannot be a group key, so
|
|
the table it builds would be keyed by a hash of the whole row, which
|
|
there is no way to look a group up in. Asked here so the plan is refused
|
|
rather than built and then found unusable.
|
|
*/
|
|
if ((*k->item)->too_big_for_varchar())
|
|
return pwt_decline(join, trace, "grouped: a GROUP BY column too wide for a key "
|
|
"part, so create_tmp_table() would key the "
|
|
"container by a unique constraint rather than "
|
|
"by the group");
|
|
Field *f= ((Item_field *) item)->field;
|
|
key_length+= f->type() == MYSQL_TYPE_VARCHAR ||
|
|
f->type() == MYSQL_TYPE_VAR_STRING
|
|
? f->field_length + HA_KEY_BLOB_LENGTH : f->pack_length();
|
|
if ((*k->item)->maybe_null())
|
|
key_length++;
|
|
}
|
|
if (key_length >= MAX_BLOB_WIDTH)
|
|
return pwt_decline(join, trace,
|
|
"grouped: the GROUP BY key is too long for a key "
|
|
"part, so create_tmp_table() would key the "
|
|
"container by a unique constraint rather than by "
|
|
"the group");
|
|
|
|
List_iterator_fast<Item> li(join->fields_list);
|
|
Item *it;
|
|
while ((it= li++))
|
|
{
|
|
if (it->const_item())
|
|
continue;
|
|
if (pwt_ungrouped_outside_aggregates(it, g))
|
|
return pwt_decline(join, trace,
|
|
"grouped: a select list item reads a column "
|
|
"outside an aggregate that GROUP BY does not "
|
|
"name, so its value is whichever row the "
|
|
"terminal happened to hold");
|
|
}
|
|
|
|
*group= g;
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
The ORDER BY this thread has to apply itself, if there is one.
|
|
|
|
@description
|
|
make_aggr_tables_info() can sort the driving table's own read --
|
|
add_sorting_to_table() on join_tab[const_tables] -- for a plan that needs
|
|
no temporary table. The workers take that read over, so the sort loses the
|
|
scan it was attached to, and a plan of this shape has no aggregation table
|
|
to move it to: the terminal after the driving tab sends straight to the
|
|
client. The manager does it instead, over the rows it drains, which costs
|
|
nothing in ordering terms because a sort is indifferent to the order its
|
|
input arrives in.
|
|
|
|
What it can take is limited by where it does it. It sorts its own result
|
|
container, so every ORDER BY element has to name a column that is in that
|
|
container -- a field of a scanned table that the query actually reads, and
|
|
so ships. An expression, or a field of some other table, has no column
|
|
there to sort by, and the plan is left serial.
|
|
|
|
A plan that has an aggregation table (join->aggr_tables) is not this shape:
|
|
its sort belongs to that table and AGGR_OP::end_send() performs it, which
|
|
is the manager's existing terminal path rather than this one.
|
|
|
|
@return the order to sort by, or nullptr if this plan's sort is not ours.
|
|
*/
|
|
|
|
ORDER *pwt_manager_sort_order(JOIN *join)
|
|
{
|
|
JOIN_TAB *first= first_linear_tab(join, WITH_BUSH_ROOTS,
|
|
WITHOUT_CONST_TABLES);
|
|
if (!first || !first->filesort || join->aggr_tables)
|
|
return nullptr;
|
|
|
|
Filesort *fs= first->filesort;
|
|
/*
|
|
A sort that returns row ids, unpacks into other fields, or stops early is
|
|
doing something for the plan beyond ordering, and the manager's is a plain
|
|
sort of a container. LIMIT is refused by the gate already; the limit here
|
|
is the filesort's own, which a plan can set separately.
|
|
*/
|
|
if (!fs->order || fs->limit != HA_ROWS_MAX || fs->sort_positions ||
|
|
fs->set_all_read_bits || fs->unpack)
|
|
return nullptr;
|
|
|
|
for (ORDER *o= fs->order; o; o= o->next)
|
|
{
|
|
Item *real= (*o->item)->real_item();
|
|
if (real->type() != Item::FIELD_ITEM)
|
|
return nullptr; // an expression: nothing to sort by
|
|
Field *f= ((Item_field *) real)->field;
|
|
|
|
bool scanned= false;
|
|
for (uint t= join->const_tables; t < join->table_count; t++)
|
|
if (join->join_tab[t].table == f->table)
|
|
{
|
|
scanned= true;
|
|
break;
|
|
}
|
|
if (!scanned)
|
|
return nullptr;
|
|
/*
|
|
Shipped is the same question as read: pwt_row_layout::build() ships every
|
|
column whose read_set bit is set. Asked rather than assumed, because this
|
|
runs before the layout exists.
|
|
*/
|
|
if (!bitmap_is_set(f->table->read_set, f->field_index))
|
|
return nullptr;
|
|
}
|
|
return fs->order;
|
|
}
|
|
|
|
|
|
/**
|
|
@description
|
|
The GROUP BY key this plan will pre-aggregate on, or nullptr if it will not.
|
|
Asked by the gate and again when the workers are built, rather than carried
|
|
between them: it is derived from the finished plan, which does not change in
|
|
between.
|
|
*/
|
|
|
|
/*
|
|
@brief
|
|
Whether the workers can pre-aggregate a query that aggregates over the
|
|
whole table -- SELECT COUNT(*), SUM(b) FROM t, with no GROUP BY.
|
|
|
|
@description
|
|
The aggregates are the same question as for a GROUP BY, and
|
|
pwt_aggregates_can_be_merged() asks it. What differs is the shape around
|
|
them. There is no group, so there is no plan aggregation table either: the
|
|
server aggregates such a query in end_send_group(), straight off the last
|
|
join tab, with nothing materialised in between.
|
|
|
|
That is enough, because end_send_group() reaches Item_sum::add(), and add()
|
|
honours a value placed by direct_add() for every aggregate this accepts --
|
|
COUNT adds a count rather than incrementing, AVG takes a partial sum with
|
|
the number of rows behind it. So the manager merges partials through the
|
|
plan's own terminal here exactly as it does through end_update() there, and
|
|
the drain needs no case of its own.
|
|
|
|
What the workers accumulate into does need one, and it is borrowed rather
|
|
than built: the layout gives this shape a group of one constant, so a
|
|
worker keeps one partial in a keyed container and ships one row, which is
|
|
the grouped path with a single group in it. Beyond costing one column, that
|
|
is what keeps Item_sum_avg::create_tmp_field() packing the count beside the
|
|
sum -- it does that only for a container with a group -- so an average
|
|
survives the trip.
|
|
|
|
Refused: a select-list item that reads a column outside an aggregate. Its
|
|
value is whichever row the terminal happened to hold, and a worker would
|
|
hold a different one. Standard SQL does not allow it and ONLY_FULL_GROUP_BY
|
|
rejects it; without that mode the server returns an arbitrary row's value.
|
|
|
|
@return true if the workers can pre-aggregate.
|
|
*/
|
|
|
|
static bool pwt_whole_table_preagg_supported(JOIN *join, bool trace)
|
|
{
|
|
if (!join->sum_funcs || !*join->sum_funcs)
|
|
return false; // nothing to pre-aggregate
|
|
if (!pwt_aggregates_can_be_merged(join, "whole-table", trace))
|
|
return false;
|
|
|
|
List_iterator_fast<Item> li(join->fields_list);
|
|
Item *it;
|
|
while ((it= li++))
|
|
{
|
|
if (it->const_item())
|
|
continue;
|
|
if (pwt_ungrouped_outside_aggregates(it, nullptr))
|
|
return pwt_decline(join, trace,
|
|
"whole-table: a select list item reads a column "
|
|
"outside an aggregate, so its value is whichever "
|
|
"row the terminal happened to hold");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
pwt_preagg_kind pwt_preagg_shape(JOIN *join, ORDER **group, bool trace)
|
|
{
|
|
*group= nullptr;
|
|
if (!join->group_list && !join->group &&
|
|
!join->select_lex->agg_func_used() && !join->select_lex->with_sum_func)
|
|
return PWT_PREAGG_NONE; // not an aggregating query at all
|
|
|
|
/*
|
|
Which of the two shapes to ask about is the query's own grouping, not the
|
|
plan's. A query that names a GROUP BY is the grouped shape whatever the
|
|
optimizer then did with it -- if the plan has no group key to merge a
|
|
partial through, that is a refusal of this query and not an invitation to
|
|
treat it as aggregating over everything it reads, which would answer a
|
|
different question. Only a query that never grouped reaches the second.
|
|
*/
|
|
if (join->group_list || join->group)
|
|
return pwt_grouped_preagg_supported(join, group, trace) ? PWT_PREAGG_GROUPED
|
|
: PWT_PREAGG_NONE;
|
|
|
|
return pwt_whole_table_preagg_supported(join, trace) ? PWT_PREAGG_WHOLE
|
|
: PWT_PREAGG_NONE;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
decline to parallel scan join because...
|
|
*/
|
|
|
|
bool pwt_decline(JOIN *join, bool trace, const char *why)
|
|
{
|
|
DBUG_PRINT("info", ("parallel scan declined: %s", why));
|
|
if (trace && unlikely(join->thd->trace_started()) &&
|
|
join->thd->variables.parallel_worker_threads > 0)
|
|
{
|
|
Json_writer_object trace_pscan(join->thd);
|
|
trace_pscan.add("parallel_scan_declined_because", why);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
PROTOTYPE. Whether to run the workers as a bare parallel scan.
|
|
|
|
@description
|
|
Not for the shipping tree: a second execution path, behind
|
|
debug_dbug='+d,pwt_scan_only', in which the workers do nothing but read
|
|
their chunk of the driving table and ship the rows. There are no worker
|
|
JOIN_TABs, no cloned conditions or select list, and no join in a worker.
|
|
This thread runs the plan it would have run serially, and the only
|
|
difference is that the driving table's JOIN_TAB reads its rows from the
|
|
transport instead of from the handler.
|
|
|
|
What it is for. The gate admits a few per cent of the queries in the test
|
|
suite, because almost every refusal it makes is about evaluating cloned
|
|
Items in another thread rather than about dividing a scan. Take the
|
|
evaluation away and nearly all of them go: outer joins, subqueries,
|
|
aggregates, DISTINCT, LIMIT, window functions all become eligible, because
|
|
this thread does them. So the chunked scan and the transport -- the two
|
|
layers underneath -- can be run over the whole suite instead of over the
|
|
sliver the real gate allows.
|
|
|
|
What it cannot tell you, which is the more important half. Everything a
|
|
worker does with an Item is gone, and that is where the defects have
|
|
actually been: a worker THD that did not carry the session's time zone, a
|
|
condition left only in a Filesort, a reader that wanted a SQL_SELECT the
|
|
worker copy did not have, a materialized subquery re-opened per worker.
|
|
None of those are reachable here -- most cannot even exist in this shape.
|
|
Passing in this mode says nothing about the path that ships.
|
|
|
|
Its value is therefore as a lower layer's test and as a bisection tool: a
|
|
query that answers wrongly in both modes is wrong in the scan or the
|
|
transport, and one that answers wrongly only in the full mode is wrong in
|
|
the worker join.
|
|
|
|
@return true if this statement should run scan-only.
|
|
*/
|
|
|
|
bool pwt_scan_only_enabled()
|
|
{
|
|
bool on= false;
|
|
DBUG_EXECUTE_IF("pwt_scan_only", on= true;);
|
|
return on;
|
|
}
|
|
|
|
|
|
bool can_run_query_in_workers(JOIN *join, JOIN_TAB *scan_tab, bool trace)
|
|
{
|
|
DBUG_ENTER("can_run_query_in_workers");
|
|
SELECT_LEX *sl= join->select_lex;
|
|
|
|
if (!jointab_can_be_parallel_scanned(scan_tab, trace))
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"no table this plan reads can be divided"));
|
|
/* The loop below reads the driving table as join_tab[const_tables]. */
|
|
DBUG_ASSERT(scan_tab == join->join_tab + join->const_tables);
|
|
|
|
/*
|
|
Scan-only: everything below this point asks whether a worker can evaluate
|
|
part of this query, and in that mode no worker evaluates anything. The one
|
|
question left is the one already answered above -- can the driving table's
|
|
access path be divided -- with the sort excluded, because a filesort on the
|
|
driving tab is performed by create_sort_index() reading the handler
|
|
directly, which would walk straight past the transport.
|
|
*/
|
|
if (pwt_scan_only_enabled())
|
|
{
|
|
if (scan_tab->filesort || scan_tab->filesort_result)
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"scan-only: the driving table is sorted, and a filesort "
|
|
"reads the handler rather than the transport"));
|
|
DBUG_RETURN(true);
|
|
}
|
|
/*
|
|
A GROUP BY the workers can pre-aggregate is the one query shape that is
|
|
allowed to want a temporary table, hold aggregates, and group. Everything
|
|
below that would refuse those three is asked to let this one through.
|
|
*/
|
|
ORDER *preagg_group= nullptr;
|
|
const bool preagg= pwt_preagg_shape(join, &preagg_group, trace) !=
|
|
PWT_PREAGG_NONE;
|
|
|
|
if (join->need_tmp && !preagg) // group/distinct/order/...
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"the plan needs a temporary table and the workers cannot "
|
|
"pre-aggregate into it"));
|
|
/*
|
|
Any cap on the rows this select may send: an explicit LIMIT/OFFSET, or the
|
|
session's sql_select_limit, which arrives with explicit_limit still unset
|
|
-- mysql_execute_command() assigns it as the default limit of a top-level
|
|
SELECT. unit->lim is what end_send() would have read, so that is the fact
|
|
tested rather than the syntax -- which also keeps a derived table's select
|
|
eligible, its unit carrying no session cap.
|
|
|
|
Who enforces it decides whether the plan can run in the workers, and that
|
|
differs by shape.
|
|
|
|
A plan that pre-aggregates is fine as it stands. This thread does not send
|
|
those rows itself: it hands each drained row to the plan's own terminal and
|
|
then signals end of records, and everything from there is the serial chain
|
|
-- sub_select_postjoin_aggr(), AGGR_OP::end_send(), the sorted read of the
|
|
aggregation table, and end_send(), which counts the rows out against
|
|
unit->lim exactly as it would without any of this. Nothing is capped in a
|
|
worker, so there is no question of one deciding which rows survive: a
|
|
worker ships partials for every group it saw, and the limit is applied
|
|
after the groups are complete and in order.
|
|
|
|
A plan that does not is refused. The drain sends those rows itself, through
|
|
send_data_with_check(), which skips the offset but counts nothing -- so a
|
|
capped query would send every row it drained.
|
|
*/
|
|
if (!join->unit->lim.is_unlimited() && !preagg)
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"the select has a row limit -- a LIMIT clause, or the "
|
|
"session's sql_select_limit -- which the drain does not "
|
|
"enforce for a plan that does not pre-aggregate"));
|
|
/*
|
|
WITH TIES keeps sending past the limit while the ORDER BY values are
|
|
unchanged, which end_send() decides from join->order_fields. Left refused
|
|
until that is known to hold for a result this thread assembled rather than
|
|
read.
|
|
*/
|
|
if (join->unit->lim.is_with_ties())
|
|
DBUG_RETURN(pwt_decline(join, trace, "the select has FETCH ... WITH TIES"));
|
|
if (join->select_options & OPTION_FOUND_ROWS) // SQL_CALC_FOUND_ROWS
|
|
DBUG_RETURN(pwt_decline(join, trace, "SQL_CALC_FOUND_ROWS"));
|
|
if (join->procedure)
|
|
DBUG_RETURN(pwt_decline(join, trace, "the select has a PROCEDURE"));
|
|
if (sl->have_window_funcs())
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"the select has window functions, which read across rows"));
|
|
if ((sl->agg_func_used() || sl->with_sum_func) && !preagg)
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"the select aggregates and the workers cannot pre-aggregate "
|
|
"this plan"));
|
|
|
|
if ((join->group_list || join->group) && !preagg)
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"the select groups and the workers cannot "
|
|
"pre-aggregate this plan"));
|
|
if (join->select_distinct)
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"SELECT DISTINCT, which no worker can decide alone"));
|
|
if (join->having || join->tmp_having)
|
|
DBUG_RETURN(pwt_decline(join, trace, "the select has a HAVING clause"));
|
|
if (join->outer_join) // no outer joins
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"the join has an outer join, whose null-complemented rows a "
|
|
"worker cannot decide on its own"));
|
|
|
|
// every non-const join table must be one the worker can scan / look up itself
|
|
for (uint j= join->const_tables; j < join->table_count; j++)
|
|
{
|
|
JOIN_TAB *tab= &join->join_tab[j];
|
|
if (tab->bush_children) // semijoin materialization
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is a semijoin materialization"));
|
|
}
|
|
/*
|
|
Every table the worker joins is re-opened from its share --
|
|
open_worker_tables() gives each worker its own TABLE so the handlers do
|
|
not contend -- and only a base table can be opened that way. A temporary
|
|
table's share describes something built in memory for this statement:
|
|
open_table_from_share() walks share->field cloning each column and
|
|
crashes on it.
|
|
|
|
The plan reaches here holding one whenever a subquery was materialized:
|
|
SJ-Materialization-Lookup leaves the materialized result in the top-level
|
|
table list as <subqueryN>, read by eq_ref on its distinct key, and that
|
|
tab has no bush_children to catch it -- only the Scan variant does.
|
|
|
|
The driving table is checked by table_can_be_parallel_scanned(), which
|
|
asks the same question among others; this is the rest of the plan.
|
|
*/
|
|
if (j > join->const_tables && tab->table->s->tmp_table != NO_TMP_TABLE)
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is a temporary table, which a worker cannot "
|
|
"re-open from its share"));
|
|
}
|
|
/*
|
|
The worker runs a plain nested loop, so it implements none of the semijoin
|
|
duplicate-elimination strategies. Nothing in the flat table list shows that
|
|
FirstMatch or DuplicateWeedout is in force, and a worker that ignores them
|
|
emits exactly the duplicates they exist to remove, so refuse a plan that
|
|
uses any of them. bush_children above covers the materialized ones.
|
|
*/
|
|
if (tab->sj_strategy != SJ_OPT_NONE || tab->loosescan_match_tab ||
|
|
tab->do_firstmatch || tab->check_weed_out_table ||
|
|
tab->flush_weedout_table || tab->first_weedout_table)
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is read under a semijoin duplicate-elimination strategy, "
|
|
"which a worker's plain nested loop does not implement"));
|
|
}
|
|
if (tab->rowid_filter) // rowid filter
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is read through a rowid filter"));
|
|
}
|
|
/*
|
|
The driving table may be read through a quick select -- checked above,
|
|
where the whole access path is -- but the tables joined after it may not:
|
|
a worker's tab keeps the record source make_join_readinfo() chose and
|
|
reads through no quick select of its own. setup_worker_jointabs() sets
|
|
the copy's 'select' to NULL, because the SQL_SELECT and the quick hanging
|
|
off it name the manager's table and its Items.
|
|
|
|
use_quick == 2 is "Range checked for each record", where there is no
|
|
quick select yet to find: make_join_readinfo() gave the tab
|
|
join_init_quick_read_record(), which builds one per outer row by calling
|
|
test_if_quick_select(), and that begins "delete tab->select->quick".
|
|
Testing for a quick therefore misses exactly the plan that needs the
|
|
SQL_SELECT most, and the worker dereferences the NULL.
|
|
|
|
So the test is on the reader the plan chose, not on what it has built
|
|
yet.
|
|
*/
|
|
if (j > join->const_tables &&
|
|
tab->select && (tab->select->quick || tab->use_quick == 2))
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is joined through a quick select, which a worker's copy of "
|
|
"the tab does not have"));
|
|
}
|
|
if (j > join->const_tables) // non-driving table access
|
|
{
|
|
if (tab->type != JT_EQ_REF && tab->type != JT_REF && tab->type != JT_ALL)
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is joined by an access method a worker does not run "
|
|
"(only eq_ref, ref and a full scan)"));
|
|
}
|
|
if (tab->type == JT_EQ_REF || tab->type == JT_REF)
|
|
for (uint p= 0; p < tab->ref.key_parts; p++)
|
|
{
|
|
if (tab->ref.cond_guards && tab->ref.cond_guards[p]) // subquery trigger
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"has a subquery-triggered ref condition"));
|
|
}
|
|
if (!pwt_item_is_worker_safe(join, tab->ref.items[p]))
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is looked up through a ref a worker "
|
|
"cannot evaluate"));
|
|
}
|
|
}
|
|
}
|
|
Item *tab_cond, *tab_cache_cond;
|
|
pwt_table_conds(tab, &tab_cond, &tab_cache_cond);
|
|
if ((tab_cond && !pwt_item_is_worker_safe(join, tab_cond)) ||
|
|
(tab_cache_cond && !pwt_item_is_worker_safe(join, tab_cache_cond)))
|
|
{
|
|
DBUG_RETURN(pwt_decline_tab(join, trace, tab,
|
|
"is filtered by a condition a worker cannot evaluate"));
|
|
}
|
|
}
|
|
|
|
// select list safe for a worker to evaluate
|
|
List_iterator_fast<Item> li(join->fields_list);
|
|
Item *item;
|
|
while ((item= li++))
|
|
if (!pwt_item_is_worker_safe(join, item))
|
|
{
|
|
DBUG_RETURN(pwt_decline(join, trace,
|
|
"a select list item is not one a worker can evaluate"));
|
|
}
|
|
DBUG_RETURN(true);
|
|
}
|
|
|
|
|
|
// _scan_ functions are for the PROTOTYPE test, no worker side join
|
|
|
|
bool pwt_manager::begin_scan_receive(THD *thd)
|
|
{
|
|
return layout.begin_receive(thd, exec.tables, exec.n_tables);
|
|
}
|
|
|
|
|
|
void pwt_manager::end_scan_receive()
|
|
{
|
|
layout.end_receive(exec.tables, exec.n_tables);
|
|
}
|
|
|
|
|
|
int pwt_manager::next_scanned_row()
|
|
{
|
|
int rc= source->next_row(layout.recv_record());
|
|
if (rc < 0)
|
|
return -1; // every worker is done
|
|
if (rc > 0)
|
|
return 1; // killed, or a worker failed
|
|
/*
|
|
Put the shipped columns where the plan expects to read them: the manager's
|
|
own record buffer for this table. From here the executor cannot tell the
|
|
row came from a worker rather than from the handler.
|
|
*/
|
|
layout.copy_back_row();
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* The manager for the join this table belongs to. */
|
|
static pwt_manager *pwt_mgr_of(TABLE *table)
|
|
{
|
|
JOIN_TAB *tab= table->reginfo.join_tab;
|
|
DBUG_ASSERT(tab && tab->join && tab->join->parallel_work_manager);
|
|
return (pwt_manager*) tab->join->parallel_work_manager;
|
|
}
|
|
|
|
|
|
static int pwt_mgr_scan_read_record(READ_RECORD *info)
|
|
{
|
|
int rc= pwt_mgr_of(info->table)->next_scanned_row();
|
|
if (rc > 0 && info->print_error && !info->table->in_use->is_error())
|
|
my_error(ER_INTERNAL_ERROR, MYF(0), "parallel scan: worker failed");
|
|
return rc;
|
|
}
|
|
|
|
|
|
static int pwt_mgr_scan_init_read_record(JOIN_TAB *tab)
|
|
{
|
|
pwt_manager *mgr= pwt_mgr_of(tab->table);
|
|
if (mgr->begin_scan_receive(tab->join->thd))
|
|
return 1;
|
|
tab->read_record.table= tab->table;
|
|
tab->read_record.read_record_func= pwt_mgr_scan_read_record;
|
|
tab->read_record.read_record_func_and_unpack_calls= pwt_mgr_scan_read_record;
|
|
/* read_first_record means "and read the first row" -- see
|
|
join_init_read_record(). */
|
|
return tab->read_record.read_record();
|
|
}
|
|
|
|
|
|
int pwt_manager::start_scan_only(THD *thd, JOIN *join, JOIN_TAB *scan_tab)
|
|
{
|
|
DBUG_ENTER("pwt_manager::start_scan_only");
|
|
scan_only= true;
|
|
|
|
int err= init_parallel_workers(thd, join, scan_tab);
|
|
if (err == HA_ERR_UNSUPPORTED)
|
|
DBUG_RETURN(-1); // engine declined
|
|
if (err)
|
|
DBUG_RETURN(1);
|
|
|
|
status_var_increment(thd->status_var.parallel_queries_executed);
|
|
|
|
/*
|
|
And divert the plan's read of this table. Everything after it -- the join,
|
|
the conditions this thread still owns, the terminals -- is the executor's
|
|
own, untouched.
|
|
*/
|
|
scan_only_tab= scan_tab;
|
|
saved_read_first_record= scan_tab->read_first_record;
|
|
scan_tab->read_first_record= pwt_mgr_scan_init_read_record;
|
|
|
|
/*
|
|
push_index_cond() left this tab filtering by the remainder, with the rest
|
|
in the manager handler's pushed_idx_cond. That handler no longer produces
|
|
the rows, so the pushed half would be applied nowhere and this thread would
|
|
see rows the serial plan rejects. Give the tab the whole condition back for
|
|
the length of the scan -- the same reason pwt_table_conds() prefers
|
|
pre_idx_push_select_cond in the full path, reached from the other side.
|
|
*/
|
|
if (scan_tab->pre_idx_push_select_cond)
|
|
{
|
|
saved_select_cond= scan_tab->select_cond;
|
|
scan_tab->set_select_cond(scan_tab->pre_idx_push_select_cond, __LINE__);
|
|
}
|
|
DBUG_RETURN(0);
|
|
}
|
|
|
|
|
|
/*
|
|
@brief
|
|
Scan-only PROTOTYPE: set the workers scanning, and leave do_select() to run
|
|
the plan normally on top of them.
|
|
|
|
@return 0 if the workers are running, -1 to run serially, 1 on error.
|
|
*/
|
|
|
|
int run_scan_only_workers(JOIN *join, JOIN_TAB *scan_tab)
|
|
{
|
|
DBUG_ENTER("run_scan_only_workers");
|
|
THD *thd= join->thd;
|
|
pwt_manager *mgr= new (thd->mem_root) pwt_manager;
|
|
if (!mgr)
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(pwt_manager));
|
|
DBUG_RETURN(1);
|
|
}
|
|
join->parallel_work_manager= mgr;
|
|
|
|
int rc= mgr->start_scan_only(thd, join, scan_tab);
|
|
if (rc < 0)
|
|
join->parallel_work_manager= nullptr;
|
|
DBUG_RETURN(rc);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Drive worker-side execution from do_select_parallel()
|
|
spin up the workers, collect and send their result rows.
|
|
|
|
@return
|
|
0 = handled (result sent)
|
|
1 = error
|
|
-1 = the engine declined the parallel scan
|
|
(caller should run the query serially instead).
|
|
*/
|
|
|
|
int run_worker_side_join(JOIN *join, JOIN_TAB *scan_tab)
|
|
{
|
|
DBUG_ENTER("run_worker_side_join");
|
|
DBUG_PRINT("info", ("parallel scan on %s", scan_tab->table->alias.ptr()));
|
|
THD *thd= join->thd;
|
|
pwt_manager *mgr= new (thd->mem_root) pwt_manager;
|
|
if (!mgr)
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(pwt_manager));
|
|
DBUG_RETURN(1);
|
|
}
|
|
join->parallel_work_manager= mgr;
|
|
|
|
int err= mgr->init_parallel_workers(thd, join, scan_tab);
|
|
if (err == HA_ERR_UNSUPPORTED)
|
|
{
|
|
/*
|
|
The engine declined the parallel scan -- e.g. this is a locking read, so
|
|
not a consistent read (see parallel_init_coordinator). Nothing was set up
|
|
and no error was raised: return the "declined" code so do_select() runs
|
|
the query serially. Returning an error here would fail the statement with
|
|
an empty diagnostics area.
|
|
*/
|
|
DBUG_PRINT("info", ("engine declined the parallel scan, running serially"));
|
|
join->parallel_work_manager= nullptr;
|
|
DBUG_RETURN(-1);
|
|
}
|
|
if (err)
|
|
DBUG_RETURN(1); // init_parallel_workers has raised the error
|
|
|
|
status_var_increment(thd->status_var.parallel_queries_executed);
|
|
|
|
DBUG_RETURN(mgr->drain_and_send(join));
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Visitor that rebinds an Item_field from a manager join table to the
|
|
worker's private copy of that table.
|
|
|
|
@description
|
|
A deep_copy of a WHERE/ref/select-list Item tree carries Item_field nodes
|
|
whose Field* still points into a manager join table. We walk the clone with
|
|
enumerate_field_refs_processor and, for every field that belongs to one of
|
|
the parallel-scanned tables (from_tables[i]), repoint it at the same column
|
|
of the worker's copy (to_tables[i]).
|
|
|
|
A field of any other table is left untouched, and there must not be one:
|
|
the worker would read a record the manager owns, which is what the in_use
|
|
assertion in Field::val_int() catches. The gate has already refused any
|
|
expression that reaches one, so the case cannot arise here -- see
|
|
pwt_item_reads_a_foreign_table().
|
|
*/
|
|
|
|
class Pwt_field_rebinder : public Field_enumerator
|
|
{
|
|
TABLE **from_tables, **to_tables;
|
|
uint n;
|
|
public:
|
|
Pwt_field_rebinder(TABLE **from, TABLE **to, uint n_arg)
|
|
: from_tables(from), to_tables(to), n(n_arg) {}
|
|
void visit_field(Item_field *item) override
|
|
{
|
|
if (!item->field)
|
|
return;
|
|
for (uint i= 0; i < n; i++)
|
|
{
|
|
if (item->field->table == from_tables[i])
|
|
{
|
|
item->field= to_tables[i]->field[item->field->field_index];
|
|
/*
|
|
Item_field::save_in_field() (used when this field is a top-level
|
|
projection item) copies from result_field, not field. The worker's
|
|
value for this column lives in the rebound source field, so point
|
|
result_field there too. Harmless for fields used only in val_*()
|
|
(conditions, ref values and sub-expressions read field directly).
|
|
*/
|
|
item->result_field= item->field;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
@brief
|
|
Deep-clone an Item tree and rebind its join-table field references to the
|
|
worker's private table copies.
|
|
|
|
@return
|
|
our cloned Item
|
|
nullptr if the item is not clonable (deep_copy declined)
|
|
*/
|
|
|
|
static Item *pwt_clone_rebind(THD *thd, Item *src,
|
|
TABLE **from, TABLE **to, uint n)
|
|
{
|
|
if (!src)
|
|
return nullptr;
|
|
|
|
Item *clone= src->deep_copy_with_checks(thd);
|
|
if (!clone)
|
|
return nullptr;
|
|
|
|
/*
|
|
1. Rebind every field reference to the worker's table copy. The fields stay
|
|
fixed and keep their column position, so no name re-resolution happens.
|
|
The gate rejected anything whose copy is not independent, so the walk
|
|
below can only reach fields this clone owns.
|
|
*/
|
|
DBUG_ASSERT(!item_clone_shares_nodes(src, clone));
|
|
Pwt_field_rebinder rebinder(from, to, n);
|
|
clone->walk(&Item::enumerate_field_refs_processor, (void*) &rebinder, 0);
|
|
|
|
/*
|
|
2. deep_copy duplicates the tree structurally but does not rebuild the
|
|
fix-time caches some predicates hold (e.g. Item_func_in's in_vector,
|
|
a comparator's collation/cmp_item) -- those would still compare against
|
|
the original args and read the wrong table. Reset those caches on the
|
|
non-field items (cleanup_excluding_fields_processor leaves the rebound,
|
|
still-fixed fields alone) and re-fix the tree so they rebuild against the
|
|
rebound args. A bare field clone is already fixed and needs no re-fix.
|
|
(Same protocol the optimizer uses for re-evaluable clones, e.g.
|
|
opt_rewrite_remove_casefold.cc.)
|
|
*/
|
|
clone->walk(&Item::cleanup_excluding_fields_processor, (void*) nullptr, 0);
|
|
if (!clone->fixed() && clone->fix_fields(thd, &clone))
|
|
return nullptr;
|
|
|
|
return clone;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Clone both halves of one table's condition and rebind them to the worker's
|
|
tables, ANDed together when there are two.
|
|
*out is the clone, or NULL if the table has no condition at all.
|
|
|
|
@return
|
|
false success
|
|
true error
|
|
*/
|
|
|
|
static bool pwt_clone_table_conds(THD *thd, JOIN_TAB *tab,
|
|
TABLE **from, TABLE **to, uint n,
|
|
Item **out)
|
|
{
|
|
Item *cond, *cache_cond, *c= nullptr, *cc= nullptr;
|
|
pwt_table_conds(tab, &cond, &cache_cond);
|
|
*out= nullptr;
|
|
|
|
if (cond && !(c= pwt_clone_rebind(thd, cond, from, to, n)))
|
|
return true;
|
|
if (cache_cond && !(cc= pwt_clone_rebind(thd, cache_cond, from, to, n)))
|
|
return true;
|
|
if (!c || !cc)
|
|
{
|
|
*out= c ? c : cc;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Both clones are already fixed, so the conjunction needs no more than
|
|
quick_fix_field() -- which is what remove_redundant_bnl_scan_conds() itself
|
|
does when it rebuilds a condition out of fixed conjuncts. The worker only
|
|
ever evaluates this item, so the fix-time caches Item_cond::fix_fields()
|
|
would rebuild (used_tables, not_null_tables) are not read.
|
|
*/
|
|
Item_cond_and *both= new (thd->mem_root) Item_cond_and(thd, c, cc);
|
|
if (!both)
|
|
return true;
|
|
both->quick_fix_field();
|
|
*out= both;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Build a worker-bound clone of a ref-access descriptor (mirrors the store_key
|
|
setup in create_ref_for_key). The cloned ref has its own key buffer and a
|
|
store_key per key part that evaluates the worker-bound key-value items into
|
|
that buffer; cp_buffer_from_ref() then builds the lookup key from it.
|
|
|
|
All key parts (even constant ones) get a runtime store_key -- we set
|
|
const_ref_part_map to 0 so cp_buffer_from_ref recomputes every part each
|
|
lookup. Re-evaluating a constant per lookup is cheap and avoids the
|
|
"filled once at setup" bookkeeping.
|
|
|
|
@return true on error.
|
|
*/
|
|
|
|
static bool clone_table_ref(THD *thd, TABLE_REF *src, TABLE *wtable,
|
|
TABLE **from, TABLE **to, uint n, TABLE_REF *dst)
|
|
{
|
|
const uint kp= src->key_parts;
|
|
const uint len= src->key_length;
|
|
KEY *keyinfo= wtable->key_info + src->key;
|
|
|
|
dst->key_parts= kp;
|
|
dst->key_length= len;
|
|
dst->key= src->key;
|
|
dst->key_err= 1;
|
|
dst->has_record= FALSE;
|
|
dst->null_rejecting= src->null_rejecting;
|
|
dst->disable_cache= FALSE;
|
|
dst->null_ref_part= NO_REF_PART;
|
|
dst->null_ref_key= nullptr;
|
|
dst->const_ref_part_map= 0; // recompute every part each lookup
|
|
dst->uses_splitting= FALSE;
|
|
dst->use_count= 0;
|
|
dst->cond_guards= nullptr; // gated: no subquery trigger guards
|
|
dst->depend_map= src->depend_map;
|
|
|
|
if (!(dst->key_buff= thd->calloc<uchar>(ALIGN_SIZE(len) * 2)) ||
|
|
!(dst->key_copy= thd->alloc<store_key*>(kp + 1)) ||
|
|
!(dst->items= thd->alloc<Item*>(kp)))
|
|
return true;
|
|
dst->key_buff2= dst->key_buff + ALIGN_SIZE(len);
|
|
|
|
store_key **ref_key= dst->key_copy;
|
|
uchar *key_buff= dst->key_buff;
|
|
for (uint i= 0; i < kp; i++)
|
|
{
|
|
Item *it= pwt_clone_rebind(thd, src->items[i], from, to, n);
|
|
if (!it)
|
|
return true;
|
|
dst->items[i]= it;
|
|
|
|
KEY_PART_INFO *kpi= keyinfo->key_part + i;
|
|
const uint maybe_null= MY_TEST(kpi->null_bit);
|
|
Item *real= it->real_item();
|
|
store_key *sk;
|
|
if (real->type() == Item::FIELD_ITEM)
|
|
sk= new store_key_field(thd, kpi->field, key_buff + maybe_null,
|
|
maybe_null ? key_buff : 0, kpi->length,
|
|
((Item_field*) real)->field, real->full_name());
|
|
else
|
|
sk= new store_key_item(thd, kpi->field, key_buff + maybe_null,
|
|
maybe_null ? key_buff : 0, kpi->length, it, FALSE);
|
|
if (!sk)
|
|
return true;
|
|
*ref_key++= sk;
|
|
key_buff+= kpi->store_length;
|
|
}
|
|
*ref_key= nullptr; // end marker
|
|
return false;
|
|
}
|
|
|
|
|
|
static void close_one_worker_table(TABLE **t)
|
|
{
|
|
if (*t)
|
|
{
|
|
(*t)->file->update_global_table_stats();
|
|
closefrm(*t);
|
|
my_free(*t);
|
|
*t= nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Open this worker's private copy of every non-const join table from the
|
|
shared TABLE_SHARE.
|
|
|
|
@description
|
|
open_table_from_share() runs here on the manager thread, so the open must
|
|
happen with in_use == current_thd (handler::ha_thd() asserts that, and
|
|
ha_innobase::open() calls it); we repoint in_use at the worker afterwards.
|
|
InnoDB caches the THD lazily (update_thd() on first use), not at open, so
|
|
each worker gets a private handler and they scan concurrently without a
|
|
shared-scan lock. exec.tables[0] is the parallel-scanned driving table
|
|
(also kept as exec.scan_table). Self-cleans on failure.
|
|
|
|
@return true on error.
|
|
*/
|
|
bool pwt_manager::open_worker_tables(THD *thd, pwt_worker *worker)
|
|
{
|
|
worker->exec.n_tables= exec.n_tables;
|
|
/* the table array, plus the ANALYZE counters this worker will fill in */
|
|
if (!(worker->exec.tables= thd->alloc<TABLE*>(exec.n_tables)) ||
|
|
!(worker->exec.tab_stats= thd->calloc<Table_access_tracker>(exec.n_tables)) ||
|
|
!(worker->exec.tab_hstats= thd->calloc<ha_handler_stats>(exec.n_tables)) ||
|
|
!(worker->exec.tab_trackers=
|
|
thd->calloc<Exec_time_tracker>(exec.n_tables)))
|
|
return true;
|
|
|
|
for (uint t= 0; t < exec.n_tables; t++)
|
|
worker->exec.tables[t]= nullptr;
|
|
|
|
for (uint t= 0; t < exec.n_tables; t++)
|
|
{
|
|
TABLE *src= exec.tables[t];
|
|
TABLE *st= (TABLE*) my_malloc(key_memory_TABLE, sizeof(TABLE),
|
|
MYF(MY_WME | MY_ZEROFILL));
|
|
if (!st)
|
|
goto err;
|
|
|
|
if (open_table_from_share(thd, src->s, &src->s->table_name,
|
|
HA_OPEN_KEYFILE | HA_TRY_READ_ONLY,
|
|
EXTRA_RECORD, thd->open_options, st,
|
|
false, nullptr))
|
|
{
|
|
my_free(st);
|
|
goto err;
|
|
}
|
|
st->in_use= worker->thd;
|
|
st->file->ha_handler_stats_reset();
|
|
/*
|
|
The manager's tables have the ANALYZE time trackers the optimizer hung
|
|
off the plan; these copies are opened after the plan was made and have
|
|
none, so the handler would time nothing. Give each one a tracker of its
|
|
own, which snapshot_table_stats() folds into this worker's engine_time.
|
|
*/
|
|
st->file->set_time_tracker(&worker->exec.tab_trackers[t]);
|
|
/*
|
|
Give the copy the manager table's place in the join: its bit in the table
|
|
map and its join position. Items cloned onto this table take used_tables()
|
|
from TABLE::map, so with a zero map a rebound predicate looks like a
|
|
constant -- Item_cond::fix_fields() then evaluates it while we are still
|
|
cloning it (can_eval_in_optimize()), reading a record buffer that no row
|
|
has been read into yet.
|
|
*/
|
|
st->map= src->map;
|
|
st->tablenr= src->tablenr;
|
|
/*
|
|
Mirror the manager table's column bitmaps into the copy's own, rather than
|
|
marking every column: the optimizer has already marked exactly the columns
|
|
this query reads, and the engine builds its fetch template from read_set,
|
|
so a blanket all-columns read_set would convert every column of every row
|
|
to MySQL format instead of just the ones the worker evaluates. Copying
|
|
also keeps write_set empty for a table we only read, so a store into a
|
|
source field still trips marked_for_write().
|
|
|
|
The copy owns these bitmaps, so nothing is shared with the manager. Done
|
|
here, before the conditions and the select list are cloned onto these
|
|
tables, so the read_set is already in place if anything evaluates a field
|
|
during cloning. open_worker_tables() runs after the optimizer has
|
|
finished, so src's bitmaps are final.
|
|
*/
|
|
bitmap_copy(&st->def_read_set, src->read_set);
|
|
bitmap_copy(&st->def_write_set, src->write_set);
|
|
st->column_bitmaps_set(&st->def_read_set, &st->def_write_set);
|
|
worker->exec.tables[t]= st;
|
|
}
|
|
worker->exec.scan_table= worker->exec.tables[0]; // the driving table
|
|
return false;
|
|
|
|
err:
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"init_parallel_workers: failed to open worker table from share");
|
|
worker->close_tables();
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Take the engine's per-table counters out of the tables while they are still
|
|
open, so the manager can add them to what ANALYZE reads.
|
|
*/
|
|
|
|
void pwt_worker::snapshot_table_stats()
|
|
{
|
|
for (uint i= 0; i < exec.n_tables; i++)
|
|
{
|
|
handler *file= exec.tables[i]->file;
|
|
ha_handler_stats *hs= file->handler_stats;
|
|
if (!hs)
|
|
continue;
|
|
/*
|
|
Time in the engine is normally folded in from the handler's tracker when
|
|
the table is closed, by close_thread_table(), and a worker's tables are
|
|
closed after this snapshot and without going through that path. So fold
|
|
it in here, or the worker's engine_time would always be zero.
|
|
*/
|
|
if (hs->active)
|
|
if (Exec_time_tracker *tracker= file->get_time_tracker())
|
|
hs->engine_time+= tracker->get_cycles();
|
|
exec.tab_hstats[i].add(hs);
|
|
}
|
|
}
|
|
|
|
|
|
void pwt_worker::close_tables()
|
|
{
|
|
if (exec.tables)
|
|
for (uint i= 0; i < exec.n_tables; i++)
|
|
close_one_worker_table(&exec.tables[i]);
|
|
exec.scan_table= nullptr; // == exec.tables[0], closed above
|
|
}
|
|
|
|
|
|
void pwt_manager::free_containers(THD *thd)
|
|
{
|
|
for (uint i= 0; i < nworkers(); i++)
|
|
{
|
|
layout.free_container(thd, &workers[i]->exec.result);
|
|
}
|
|
/*
|
|
The sort result reads through the container, so it goes first. Both are
|
|
ours alone: nothing outside this thread ever saw either.
|
|
*/
|
|
delete sort_result;
|
|
sort_result= nullptr;
|
|
layout.free_container(thd, &sort_container);
|
|
layout.cleanup(thd);
|
|
}
|
|
|
|
|
|
#ifndef DBUG_OFF
|
|
/*
|
|
Every JOIN_TAB field the worker's executor could reach that the gate is
|
|
supposed to have made inert. Checked on the manager's tab before it is copied,
|
|
so relaxing a gate without teaching the copy about the field it lets through
|
|
fails here rather than silently executing with the manager's state -- a copy
|
|
keeps the manager's pointer in anything the copy does not overwrite, which is
|
|
the same trap TABLE::map and TABLE::in_use were.
|
|
|
|
This is not only a guard. Writing it is what found the BNL scan filter: the
|
|
question "is JOIN_TAB::cache_select inert?" turned out to have the answer no,
|
|
and a wrong result behind it. Anything added here should be *checked* against
|
|
the suite, not assumed.
|
|
*/
|
|
static void pwt_assert_tab_inert(JOIN_TAB *tab, bool is_driving)
|
|
{
|
|
/* Outer joins: the gate refuses join->outer_join outright. */
|
|
DBUG_ASSERT(!tab->first_inner);
|
|
DBUG_ASSERT(!tab->last_inner);
|
|
DBUG_ASSERT(!tab->first_upper);
|
|
DBUG_ASSERT(!tab->on_expr_ref || !*tab->on_expr_ref);
|
|
DBUG_ASSERT(!tab->on_precond);
|
|
|
|
/* Semijoin strategies and semijoin materialization. */
|
|
DBUG_ASSERT(!tab->bush_children);
|
|
DBUG_ASSERT(tab->sj_strategy == SJ_OPT_NONE);
|
|
DBUG_ASSERT(!tab->loosescan_match_tab);
|
|
DBUG_ASSERT(!tab->do_firstmatch);
|
|
DBUG_ASSERT(!tab->flush_weedout_table);
|
|
DBUG_ASSERT(!tab->check_weed_out_table);
|
|
DBUG_ASSERT(!tab->first_weedout_table);
|
|
DBUG_ASSERT(!tab->emb_sj_nest);
|
|
|
|
/*
|
|
Rowid filters and range/index_merge access. The driving table may carry a
|
|
quick select -- its intervals are what the engine partitions -- but the
|
|
worker never reads through it: setup_worker_jointabs() clears the copy's
|
|
'select', and the worker filters by the cloned condition instead.
|
|
*/
|
|
DBUG_ASSERT(!tab->rowid_filter);
|
|
DBUG_ASSERT(is_driving || !tab->select || !tab->select->quick);
|
|
/*
|
|
"Range checked for each record": the reader builds a quick select per outer
|
|
row out of tab->select, which a worker does not have. Asserted next to the
|
|
quick above because it is the same rule -- an inner tab reads through no
|
|
quick select of its own -- and the one the quick test cannot see, there
|
|
being nothing built yet at this point.
|
|
*/
|
|
DBUG_ASSERT(is_driving || tab->use_quick != 2);
|
|
|
|
/*
|
|
A sort is only ever the driving table's -- pwt_manager_sort_order() takes
|
|
that plan shape, and the manager runs the sort -- and no sort has run yet.
|
|
*/
|
|
DBUG_ASSERT(is_driving || !tab->filesort);
|
|
DBUG_ASSERT(!tab->filesort_result);
|
|
|
|
/* Materialized derived tables, CTEs and split materialization. */
|
|
DBUG_ASSERT(!tab->split_derived_to_update);
|
|
|
|
/* DISTINCT and HAVING are refused, so neither shortcut applies. */
|
|
DBUG_ASSERT(!tab->shortcut_for_distinct);
|
|
DBUG_ASSERT(!tab->having);
|
|
|
|
/* The driving table is scanned; only the tables after it are looked up. */
|
|
DBUG_ASSERT(is_driving || tab->type == JT_ALL || tab->type == JT_REF ||
|
|
tab->type == JT_EQ_REF);
|
|
}
|
|
#endif
|
|
|
|
|
|
#ifndef DBUG_OFF
|
|
/*
|
|
Every JOIN field the worker's executor could reach that the gate is supposed to
|
|
have made inert. Same purpose as pwt_assert_tab_inert(), one level up: relaxing
|
|
a gate without teaching setup_worker_join() about the field it lets through
|
|
fails here rather than running with a field this worker's JOIN does not carry.
|
|
*/
|
|
static void pwt_assert_join_inert(JOIN *join, bool grouped)
|
|
{
|
|
DBUG_ASSERT(!join->outer_join);
|
|
DBUG_ASSERT(!join->select_distinct);
|
|
// DBUG_ASSERT(!join->order);
|
|
/*
|
|
A pre-aggregating plan does have a temp table, a group and a sort-and-group
|
|
terminal: they belong to the aggregation stage, which the manager runs and
|
|
a worker does not. What matters is that the worker's own JOIN does not
|
|
carry them, and it cannot -- setup_worker_join() builds a fresh one and
|
|
names every field it copies.
|
|
*/
|
|
if (!grouped)
|
|
{
|
|
DBUG_ASSERT(!join->need_tmp);
|
|
DBUG_ASSERT(!join->group && !join->group_list);
|
|
DBUG_ASSERT(!join->sort_and_group);
|
|
}
|
|
DBUG_ASSERT(!join->having && !join->tmp_having);
|
|
DBUG_ASSERT(!join->procedure);
|
|
DBUG_ASSERT(!join->group_optimized_away);
|
|
DBUG_ASSERT(!(join->select_options & OPTION_FOUND_ROWS));
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
@brief
|
|
Build the JOIN this worker's JOIN_TABs belong to.
|
|
|
|
@description
|
|
Only the fields the executor reads are carried over, by name. The rest are
|
|
whatever JOIN's own constructor makes them, which is why the constructor is
|
|
used rather than a copy of the manager's object: JOIN declares its copy
|
|
constructor and assignment private and unimplemented, so copying it means
|
|
copying bytes past a deliberate prohibition, and every field this code did
|
|
not think about would then hold a value that looks chosen. Built by the
|
|
constructor, an unconsidered field holds JOIN::init()'s default, and the
|
|
static_assert below makes a field added upstream a compile-time prompt to
|
|
decide which of the two it should be.
|
|
|
|
What the executor reads, established by walking sub_select() and
|
|
evaluate_join_record() for the shapes the gate allows: thd, join_tab,
|
|
return_tab and found_records. map2table is read only for
|
|
split_derived_to_update, join_tab_execution_startup() only inside its two
|
|
semijoin-materialization branches, and JOIN_TAB::preread_init() returns
|
|
before touching join->thd unless the table is a materialized derived -- all
|
|
of which pwt_assert_tab_inert() asserts away.
|
|
|
|
result is deliberately left null. It is the manager's connection to the
|
|
client, and drain_and_send() is the only thing that may send a
|
|
row; a worker that reached for it should crash rather than write to a socket
|
|
two threads share.
|
|
|
|
@return true on error.
|
|
*/
|
|
bool pwt_manager::setup_worker_join(THD *thd, pwt_worker *worker)
|
|
{
|
|
#ifndef DBUG_OFF
|
|
pwt_assert_join_inert(exec.join, layout.plan_aggregates);
|
|
#endif
|
|
|
|
if (!(worker->exec.join= new (thd->mem_root)
|
|
JOIN(worker->thd, exec.join->fields_list, exec.join->select_options,
|
|
nullptr)))
|
|
return true;
|
|
|
|
/*
|
|
The worker's JOIN describes the worker's array, which holds the join's
|
|
non-const tables and nothing else. Not the manager's counts: the manager
|
|
counts the const tables it resolved before the join started, and
|
|
JOIN_TAB::pfs_batch_update() finds the innermost table by
|
|
join_tab + table_count - 1, which with the manager's count lands past the
|
|
end of a worker's array. join_tab itself is set by setup_worker_jointabs(),
|
|
once there is an array to point at.
|
|
*/
|
|
worker->exec.join->table_count= exec.n_tables;
|
|
worker->exec.join->top_join_tab_count= exec.n_tables;
|
|
worker->exec.join->const_tables= 0;
|
|
worker->exec.join->select_lex= exec.join->select_lex;
|
|
/*
|
|
And the unit, which the JOIN constructor leaves alone. The executor reads
|
|
the statement's row limit through it -- join_init_read_record() asks
|
|
join->unit->lim for one before it opens a scan -- and a JOIN built by hand
|
|
here would otherwise reach that with whatever the mem_root held.
|
|
|
|
The manager's own unit, not a copy: it is read, never written, and the
|
|
manager holds it until every worker has been joined. What it says is also
|
|
what the workers need to hear, since the gate refuses a select that carries
|
|
a limit at all.
|
|
*/
|
|
worker->exec.join->unit= exec.join->unit;
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
The executor plumbing this function installs on each tab. Defined below, with
|
|
the rest of what runs on a worker thread.
|
|
*/
|
|
static int pwt_pscan_init_read_record(JOIN_TAB *tab);
|
|
static enum_nested_loop_state pwt_end_send(JOIN *join, JOIN_TAB *join_tab,
|
|
bool end_of_records);
|
|
|
|
/**
|
|
@brief
|
|
Give this worker its own copy of each of the join's non-const JOIN_TABs,
|
|
rebound to the worker: its private TABLE copies, its cloned conditions and
|
|
refs, its own trackers. Tables are taken in join order, which is the order a
|
|
worker joins them in, because a ref value reads the tables before it.
|
|
|
|
The tabs are copied rather than built from nothing so that a field this code
|
|
does not know about holds the value the optimizer chose instead of a zero
|
|
that would look deliberate. What must not survive the copy is anything that
|
|
points into the manager's execution state, so every such field is either
|
|
overwritten below or asserted inert by pwt_assert_tab_inert().
|
|
|
|
@return true on error.
|
|
*/
|
|
bool pwt_manager::setup_worker_jointabs(THD *thd, pwt_worker *worker)
|
|
{
|
|
if (!(worker->exec.jointabs= thd->alloc<JOIN_TAB>(exec.n_tables)))
|
|
return true;
|
|
worker->exec.join->join_tab= worker->exec.jointabs;
|
|
|
|
for (uint k= 0; k < exec.n_tables; k++)
|
|
{
|
|
JOIN_TAB *mtab= exec.jointabs[k];
|
|
JOIN_TAB *wtab= &worker->exec.jointabs[k];
|
|
|
|
#ifndef DBUG_OFF
|
|
pwt_assert_tab_inert(mtab, k == 0);
|
|
#endif
|
|
*wtab= *mtab;
|
|
|
|
wtab->table= worker->exec.tables[k];
|
|
|
|
/*
|
|
Both halves of the condition, ANDed, so select_cond alone is the whole of
|
|
what this table is filtered by. The places the optimizer had moved parts
|
|
of it to are then cleared: they name items belonging to the manager, and a
|
|
worker that read them would be evaluating another thread's Items.
|
|
*/
|
|
if (pwt_clone_table_conds(thd, mtab, exec.tables, worker->exec.tables,
|
|
exec.n_tables, &wtab->select_cond))
|
|
return true;
|
|
wtab->pre_idx_push_select_cond= nullptr;
|
|
wtab->cache_select= nullptr;
|
|
wtab->select= nullptr;
|
|
/*
|
|
The plan's sort is the manager's to run (sort_and_send()) and the
|
|
Filesort is the manager's to free: JOIN_TAB::cleanup() deletes both the
|
|
filesort and, through it, the SQL_SELECT holding the plan's condition, so
|
|
a worker's copy must not carry pointers a second cleanup would delete
|
|
again. Nothing in a worker reads them either -- the driving tab reads
|
|
through the chunk reader, not join_init_read_record().
|
|
*/
|
|
wtab->filesort= nullptr;
|
|
wtab->filesort_result= nullptr;
|
|
|
|
/* No join buffer in a worker: it joins a row at a time. */
|
|
wtab->cache= nullptr;
|
|
wtab->use_join_cache= FALSE;
|
|
wtab->jbuf_tracker= nullptr;
|
|
|
|
/* ANALYZE reads these back off the manager, see quiesce_workers(). */
|
|
wtab->tracker= &worker->exec.tab_stats[k];
|
|
|
|
wtab->join= worker->exec.join;
|
|
|
|
/*
|
|
And the way back. join_read_next_same() and its relatives find the
|
|
JOIN_TAB they are reading for through TABLE::reginfo.join_tab, at
|
|
execution time, so a worker's table has to point at the worker's tab: the
|
|
optimizer set this on the manager's tables and open_table_from_share()
|
|
leaves a copy without it. Third field of TABLE to need this after map and
|
|
in_use -- assume anything the optimizer sets on a TABLE is missing on the
|
|
copy until checked.
|
|
*/
|
|
wtab->table->reginfo.join_tab= wtab;
|
|
|
|
/*
|
|
Where a row goes once every table has matched: on to the next table, and
|
|
off the end of the last one into pwt_end_send() rather than end_send().
|
|
That is the whole of the difference between what a worker runs and what a
|
|
serial plan runs.
|
|
*/
|
|
wtab->next_select= (k + 1 < exec.n_tables) ? sub_select : pwt_end_send;
|
|
|
|
/*
|
|
The driving table comes from the engine's chunk reader. The others keep
|
|
the record source make_join_readinfo() chose, which the struct copy
|
|
brought with it: those functions take the JOIN_TAB they are called with,
|
|
so they read this worker's table through this worker's ref and its own
|
|
read_record.
|
|
*/
|
|
if (!k)
|
|
wtab->read_first_record= pwt_pscan_init_read_record;
|
|
|
|
/*
|
|
READ_RECORD mixes plan and per-scan state. make_join_readinfo() sets the
|
|
row-fetching function and the unlock function at optimize time -- for a
|
|
ref table join_read_next_same, for eq_ref join_no_more_records -- while
|
|
the buffers and cursors are filled by read_first_record() when the scan
|
|
starts. So clear it, which drops any state a previous execution of this
|
|
statement left, and put the plan half back by hand. Zeroing all of it left
|
|
null function pointers for sub_select() to call.
|
|
*/
|
|
bzero((char*) &wtab->read_record, sizeof(wtab->read_record));
|
|
wtab->read_record.read_record_func= mtab->read_record.read_record_func;
|
|
wtab->read_record.read_record_func_and_unpack_calls=
|
|
mtab->read_record.read_record_func_and_unpack_calls;
|
|
wtab->read_record.unlock_row= mtab->read_record.unlock_row;
|
|
wtab->read_record.table= wtab->table;
|
|
wtab->read_record.thd= worker->thd;
|
|
wtab->read_record.print_error= TRUE;
|
|
|
|
if (k && (wtab->type == JT_EQ_REF || wtab->type == JT_REF))
|
|
{
|
|
if (clone_table_ref(thd, &mtab->ref, wtab->table,
|
|
exec.tables, worker->exec.tables, exec.n_tables,
|
|
&wtab->ref))
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Recomputed, not copied: it answers "is this the innermost table", which
|
|
sub_select() asserts against the live answer, and the worker's array is
|
|
not the manager's. Last, because it reads the condition and the type set
|
|
above.
|
|
*/
|
|
wtab->cached_pfs_batch_update= wtab->pfs_batch_update();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Build this worker's private clones of the driving table's WHERE condition
|
|
and the select list, rebinding their field references to the worker's table
|
|
copies.
|
|
|
|
@return true error
|
|
false clone built
|
|
*/
|
|
bool pwt_manager::clone_worker_exprs(THD *thd, pwt_worker *worker)
|
|
{
|
|
TABLE **from= exec.tables;
|
|
TABLE **to= worker->exec.tables;
|
|
|
|
// shipped columns -> per-item projection into the container's field[i]
|
|
worker->exec.proj_count= layout.ship_list.elements;
|
|
worker->exec.proj=
|
|
(Item**) thd->alloc(worker->exec.proj_count * sizeof(Item*));
|
|
if (!worker->exec.proj)
|
|
return true;
|
|
|
|
List_iterator_fast<Item> li(layout.ship_list);
|
|
Item *src;
|
|
uint i= 0;
|
|
while ((src= li++))
|
|
{
|
|
if (!(worker->exec.proj[i++]= pwt_clone_rebind(thd, src, from, to,
|
|
exec.n_tables)))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
The worker whose thread we are on. sub_select() drives the join through
|
|
function pointers whose signatures are fixed -- READ_RECORD::Read_func takes a
|
|
READ_RECORD and Next_select_func takes a JOIN and a JOIN_TAB -- and neither
|
|
reaches the pwt_worker, so the callbacks below find it here. A worker is
|
|
exactly one thread, which is what makes this sound; the pattern is mysqld.cc's
|
|
THR_THD. Set for the length of execute_and_handoff() and cleared after, so a
|
|
callback reached from anywhere else finds nothing rather than a stale worker.
|
|
*/
|
|
static thread_local pwt_worker *pwt_self;
|
|
|
|
|
|
/* Next row of this worker's chunk of the driving table. */
|
|
int pwt_worker::pscan_next_row()
|
|
{
|
|
return exec.scan_table->file->ha_parallel_get_next_row(exec.handler_ctx);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Give this worker its own grouping table, aggregates and group key.
|
|
|
|
@description
|
|
Runs on the manager's thread, with the rest of this worker's setup, because
|
|
every container is built there -- see pwt_row_container.
|
|
|
|
The aggregate is cloned and its argument is cloned and rebound separately,
|
|
by the same route the row transport rebinds any expression: the aggregate
|
|
must not go through fix_fields() again, because Item_sum::fix_fields()
|
|
registers it with the select_lex the manager is also using. So the shell is
|
|
copied, the rebound argument is grafted in, and setup_caches() rebuilds
|
|
whatever the shell derived from the old argument -- for MIN and MAX the
|
|
Item_cache pair and the comparator bound to them, for the others nothing.
|
|
|
|
The clone then accumulates into a column of this worker's grouping table
|
|
rather than into itself, which is the binding create_tmp_table() makes for
|
|
the query's own aggregates and what reset_field()/update_field() use.
|
|
|
|
The key entries had to name the layout's definition items while
|
|
create_tmp_table() ran, because that is how it finds which column each key
|
|
part covers. What they have to name from here on is something that reads
|
|
the row the key is being built for, and a definition item does not: it is a
|
|
clone of an Item_field over the *manager's* copy of the base table, which
|
|
no worker ever fills. Left that way, every row keys on NULL and the whole
|
|
chunk becomes one group.
|
|
|
|
@return true on error.
|
|
*/
|
|
|
|
bool pwt_manager::setup_worker_preagg(THD *thd, pwt_worker *worker)
|
|
{
|
|
if (!layout.grouped)
|
|
return false;
|
|
|
|
/*
|
|
The columns of this worker's grouping table, defined by items that read
|
|
this worker's tables. That is what makes end_update() usable unmodified:
|
|
it fills the table with copy_fields(), through the Copy_field pairs
|
|
create_tmp_table() derives from these very items, and it builds the group
|
|
key with save_org_in_field() from the same items. Define the columns from
|
|
the manager's items instead and both would read records no worker fills.
|
|
*/
|
|
List<Item> defn;
|
|
{
|
|
List_iterator_fast<Item> li(layout.ship_list);
|
|
Item *src;
|
|
while ((src= li++))
|
|
{
|
|
Item *c= pwt_clone_rebind(thd, src, exec.tables, worker->exec.tables,
|
|
exec.n_tables);
|
|
if (!c || defn.push_back(c, thd->mem_root))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Then this worker's own aggregate Items. Cloned, and their arguments
|
|
cloned and rebound separately, by the same route the row transport
|
|
rebinds any expression.
|
|
The aggregate must not go through fix_fields() again, because
|
|
Item_sum::fix_fields() registers it with the select_lex the manager is also
|
|
using. So the shell is copied, the rebound argument is grafted in, and
|
|
setup_caches() rebuilds whatever the shell derived from the old argument
|
|
for MIN and MAX the Item_cache pair and the comparator bound to them.
|
|
*/
|
|
if (!(worker->exec.sums= thd->alloc<Item_sum*>(layout.n_sums + 1)))
|
|
return true;
|
|
worker->exec.sums[layout.n_sums]= nullptr; // sum_funcs is NULL-terminated
|
|
|
|
for (uint i= 0; i < layout.n_sums; i++)
|
|
{
|
|
Item *item_clone= layout.mgr_sums[i]->deep_copy_with_checks(thd);
|
|
if (!item_clone)
|
|
return true;
|
|
|
|
Item_sum *item_sum= (Item_sum *) item_clone;
|
|
Item *arg= pwt_clone_rebind(thd, layout.mgr_sums[i]->get_arg(0),
|
|
exec.tables, worker->exec.tables,
|
|
exec.n_tables);
|
|
if (!arg)
|
|
return true;
|
|
|
|
item_sum->arguments()[0]= arg;
|
|
item_sum->get_orig_args()[0]= arg;
|
|
item_sum->setup_caches(thd);
|
|
|
|
/*
|
|
Its own aggregator, so that reset_field() has the kind it asserts on and
|
|
it is not the one the query's own aggregate is using.
|
|
*/
|
|
if (item_sum->set_aggregator(thd, Aggregator::SIMPLE_AGGREGATOR))
|
|
return true;
|
|
|
|
if (defn.push_back(item_sum, thd->mem_root))
|
|
return true;
|
|
|
|
worker->exec.sums[i]= item_sum;
|
|
}
|
|
|
|
/*
|
|
The group key, over this worker's definition items. create_tmp_table()
|
|
reads them to find which column each key part covers, and end_update()
|
|
reads them again per row to build the key -- from the worker's tables,
|
|
which is where the row it is keying actually is.
|
|
*/
|
|
ORDER *group= thd->calloc<ORDER>(layout.n_group);
|
|
if (!group)
|
|
return true;
|
|
|
|
for (uint k= 0; k < layout.n_group; k++)
|
|
{
|
|
List_iterator_fast<Item> di(defn);
|
|
Item *it= nullptr;
|
|
for (uint j= 0; j <= layout.group_pos[k]; j++)
|
|
it= di++;
|
|
group[k].item_ptr= it;
|
|
group[k].item= &group[k].item_ptr;
|
|
group[k].direction= ORDER::ORDER_ASC;
|
|
group[k].next= k + 1 < layout.n_group ? &group[k + 1] : nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
Aggregates accumulate into this table which the manager reads.
|
|
A pre-aggregating worker produces one row per group.
|
|
*/
|
|
if (layout.make_container_from(thd, defn, &worker->exec.result, group))
|
|
return true;
|
|
|
|
/*
|
|
The manager reads it as its own recv container, so the two layouts have to
|
|
agree. They are built from different item lists -- this one from the
|
|
worker's, that one from the manager's -- so they agree by both deriving
|
|
from the same columns, not by construction.
|
|
*/
|
|
DBUG_ASSERT(worker->exec.result.table->s->reclength == layout.reclength);
|
|
DBUG_ASSERT(worker->exec.result.table->s->fields ==
|
|
layout.recv.table->s->fields);
|
|
|
|
#ifndef DBUG_OFF
|
|
/*
|
|
An AVG partial is only mergeable if the container holds its count beside
|
|
its sum. Item_sum_avg::create_tmp_field() packs the two into one binary
|
|
string field -- but only when the table has a group, which is the whole
|
|
reason the whole-table shape is given a constant group. Nothing else
|
|
enforces that, so check it here: the unpacked form is a plain double or
|
|
decimal field, and the manager would read it as a sum with no count.
|
|
*/
|
|
for (uint i= 0; i < layout.n_sums; i++)
|
|
{
|
|
Item_sum *s= worker->exec.sums[i];
|
|
if (s->sum_func() != Item_sum::AVG_FUNC)
|
|
continue;
|
|
Item_sum_avg *avg= (Item_sum_avg *) s;
|
|
Field *f= worker->exec.result.table->field[layout.n_ship_base + i];
|
|
DBUG_ASSERT(f->type() == MYSQL_TYPE_STRING &&
|
|
f->charset() == &my_charset_bin &&
|
|
f->pack_length() ==
|
|
sizeof(longlong) +
|
|
(avg->result_type() == DECIMAL_RESULT ? avg->dec_bin_size
|
|
: sizeof(double)));
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
Aggregation JOIN_TAB send to end_update().
|
|
It reads the table, the param (for the key buffer, the Copy_field pairs and
|
|
items_to_copy), the aggregate list off the JOIN, and its own AGGR_OP for
|
|
the one branch that switches write function. Everything else it touches is
|
|
a counter.
|
|
*/
|
|
JOIN_TAB *aggr_tab= thd->calloc<JOIN_TAB>(1);
|
|
if (!aggr_tab)
|
|
return true;
|
|
aggr_tab->join= worker->exec.join;
|
|
aggr_tab->table= worker->exec.result.table;
|
|
aggr_tab->tmp_table_param= worker->exec.result.param;
|
|
if (!(aggr_tab->aggr= new (thd->mem_root) AGGR_OP(aggr_tab)))
|
|
return true;
|
|
aggr_tab->aggr->set_write_func(end_update);
|
|
worker->exec.aggr_tab= aggr_tab;
|
|
|
|
/* The aggregates end_update() folds into are the JOIN's, so they are ours. */
|
|
worker->exec.join->sum_funcs= worker->exec.sums;
|
|
return false;
|
|
}
|
|
|
|
|
|
int pwt_worker::emit_joined_row()
|
|
{
|
|
DBUG_ENTER("pwt_worker::emit_joined_row");
|
|
/*
|
|
Evaluate each cloned select-list item (which reads from the worker's table
|
|
copies, now holding the current matching row of every table) and store it
|
|
into the matching exec.result field. Evaluation errors are
|
|
intercepted by PWT_error_handler, which trips fatal_error; the caller
|
|
checks that.
|
|
*/
|
|
for (uint i= 0; i < exec.proj_count; i++)
|
|
exec.proj[i]->save_in_field(exec.result.table->field[i], false);
|
|
|
|
if (manager->is_fatal_error()) // projection raised an error
|
|
DBUG_RETURN(PWT_EMIT_ERROR);
|
|
|
|
DBUG_RETURN(sink->emit_row(exec.result.record()));
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Read the next row of this worker's chunk of the driving table.
|
|
|
|
@description
|
|
The driving table's record source. Every other table in the join keeps the
|
|
one make_join_readinfo() chose, because those read through the worker's own
|
|
handler and its own ref; only this one has to come from the engine's chunk
|
|
reader instead of a plain scan.
|
|
|
|
@return 0 a row, -1 end of this worker's chunks, > 0 error. The contract
|
|
READ_RECORD::Read_func has, see rr_handle_error().
|
|
*/
|
|
static int pwt_pscan_read_record(READ_RECORD *info)
|
|
{
|
|
int err= pwt_self->pscan_next_row();
|
|
if (!err)
|
|
return 0;
|
|
if (err == HA_ERR_END_OF_FILE)
|
|
return -1;
|
|
if (info->print_error)
|
|
info->table->file->print_error(err, MYF(0));
|
|
return 1;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Point the driving table's JOIN_TAB at the chunk reader.
|
|
|
|
@return 0 always: parallel_init_worker() is called once by
|
|
execute_and_handoff() before the join starts, not here, because a
|
|
failure to get a chunk has to be told apart from a failure to read
|
|
one.
|
|
*/
|
|
static int pwt_pscan_init_read_record(JOIN_TAB *tab)
|
|
{
|
|
/*
|
|
Only the row-fetching half. setup_worker_jointabs() has already put the rest
|
|
in place, unlock_row included, and evaluate_join_record() calls that.
|
|
*/
|
|
tab->read_record.read_record_func= pwt_pscan_read_record;
|
|
tab->read_record.read_record_func_and_unpack_calls= pwt_pscan_read_record;
|
|
/*
|
|
And read the first row, which is what read_first_record() means -- see the
|
|
tail of join_init_read_record(). Returning without reading leaves
|
|
evaluate_join_record() to run once on a record buffer nothing has filled,
|
|
which shows up as one extra row per worker.
|
|
*/
|
|
return tab->read_record.read_record();
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
What the worker does with a fully joined row: the end of its nested loop.
|
|
|
|
@description
|
|
Stands where end_send() stands in a serial plan. The manager, not this, is
|
|
what talks to the client.
|
|
|
|
@return NESTED_LOOP_OK to keep going, NESTED_LOOP_QUERY_LIMIT when the
|
|
manager has asked this worker to stop, NESTED_LOOP_ERROR on a failed
|
|
projection.
|
|
*/
|
|
static enum_nested_loop_state pwt_end_send(JOIN *join, JOIN_TAB *join_tab,
|
|
bool end_of_records)
|
|
{
|
|
DBUG_ENTER("pwt_end_send");
|
|
if (end_of_records)
|
|
DBUG_RETURN(NESTED_LOOP_OK);
|
|
|
|
/*
|
|
A pre-aggregating worker hands the row to the server's own grouped
|
|
aggregation, over its own tab: AGGR_OP::put_record() prepares the table on
|
|
first use and then calls end_update(), unmodified. Its result is already
|
|
the enum this function returns.
|
|
*/
|
|
if (pwt_self->manager->row_layout().grouped)
|
|
{
|
|
THD *thd= pwt_self->thd;
|
|
/*
|
|
record local memory used so that if a we allocate another block, or
|
|
HEAP->Aria conversion happens, then owed gets transferred to the manager
|
|
*/
|
|
const int64 before= thd->status_var.local_memory_used;
|
|
const enum_nested_loop_state rc= pwt_self->exec.aggr_tab->aggr->put_record();
|
|
const int64 owed= thd->status_var.local_memory_used - before;
|
|
if (owed)
|
|
{
|
|
thd->status_var.local_memory_used= before;
|
|
pwt_self->sink->account_spilled_memory(owed);
|
|
}
|
|
DBUG_RETURN(rc);
|
|
}
|
|
|
|
switch (pwt_self->emit_joined_row()) {
|
|
case PWT_EMIT_OK: DBUG_RETURN(NESTED_LOOP_OK);
|
|
case PWT_EMIT_STOP: DBUG_RETURN(NESTED_LOOP_QUERY_LIMIT);
|
|
default: DBUG_RETURN(NESTED_LOOP_ERROR);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Scan-only PROTOTYPE: project the row now in this worker's copy of the
|
|
driving table into the result container and ship it.
|
|
|
|
@description
|
|
The full path evaluates a clone of the query's select list here. Scanning
|
|
needs no such thing: the shipped columns are the columns themselves, so the
|
|
projection is one Item_field per column, built over this worker's own table
|
|
in setup_scan_only_worker().
|
|
|
|
@return pwt_emit_result.
|
|
*/
|
|
|
|
int pwt_worker::emit_scanned_row()
|
|
{
|
|
for (uint i= 0; i < exec.scan_proj_count; i++)
|
|
exec.scan_proj[i]->save_in_field(exec.result.table->field[i], false);
|
|
|
|
if (manager->is_fatal_error())
|
|
return PWT_EMIT_ERROR;
|
|
return sink->emit_row(exec.result.record());
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Scan-only PROTOTYPE: read this worker's chunks and ship every row.
|
|
|
|
@description
|
|
execute_and_handoff() without the join. No inner tables to lock or index,
|
|
no grouping table, no nested loop -- the chunk reader, and the transport.
|
|
The manager applies the query's conditions, because in this mode it runs
|
|
the whole plan.
|
|
|
|
@return 0, or a handler error code.
|
|
*/
|
|
|
|
int pwt_worker::execute_scan_only()
|
|
{
|
|
DBUG_ENTER("pwt_worker::execute_scan_only");
|
|
TABLE *src= exec.scan_table;
|
|
int err= 0;
|
|
bool killed= false;
|
|
|
|
exec.result.table->use_all_columns();
|
|
|
|
if (sink->begin())
|
|
DBUG_RETURN(HA_ERR_GENERIC);
|
|
|
|
/* The manager's snapshot, for the same reason as the full path. */
|
|
if (ha_clone_consistent_snapshot(thd, manager->thd))
|
|
{
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"parallel worker: cannot read the manager's snapshot");
|
|
DBUG_RETURN(HA_ERR_UNSUPPORTED);
|
|
}
|
|
|
|
if ((err= src->file->ha_external_lock(thd, F_RDLCK)))
|
|
DBUG_RETURN(err);
|
|
|
|
if ((err= src->file->parallel_init_worker(exec.handler_ctx)))
|
|
goto scan_exit;
|
|
|
|
for (;;)
|
|
{
|
|
if (thd->killed || manager->is_fatal_error())
|
|
{
|
|
killed= true;
|
|
break;
|
|
}
|
|
if ((err= pscan_next_row()))
|
|
{
|
|
/*
|
|
Normalised here rather than at the exit label: running out of chunks is
|
|
how this loop ends, and everything after it -- the flush that publishes
|
|
the container to the manager above all -- tests for success.
|
|
*/
|
|
if (err == HA_ERR_END_OF_FILE)
|
|
err= 0;
|
|
break;
|
|
}
|
|
|
|
int rc= emit_scanned_row();
|
|
if (rc == PWT_EMIT_STOP) // the manager has had enough
|
|
break;
|
|
if (rc == PWT_EMIT_ERROR)
|
|
{
|
|
err= thd->is_error() ? thd->get_stmt_da()->sql_errno() : HA_ERR_GENERIC;
|
|
break;
|
|
}
|
|
}
|
|
src->file->parallel_end_worker();
|
|
|
|
if (!err && !killed)
|
|
sink->flush();
|
|
|
|
scan_exit:
|
|
/*
|
|
The other end of file: parallel_init_worker() reports it when there is no
|
|
chunk left for this worker at all, which is success -- it has nothing to
|
|
scan and nothing to publish. The full path normalises it in the same place
|
|
and for the same reason.
|
|
*/
|
|
if (err == HA_ERR_END_OF_FILE)
|
|
err= 0;
|
|
src->file->ha_index_or_rnd_end();
|
|
src->file->ha_external_lock(thd, F_UNLCK);
|
|
DBUG_RETURN(err);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Run the query over the worker's private chunk of the driving table, joining
|
|
the other tables, and hand the *result* rows to the manager through this
|
|
worker's end of the transport.
|
|
|
|
The worker scans its own copy of the driving table (exec.scan_table, opened
|
|
with in_use == this worker's thd) so the workers scan concurrently with no
|
|
shared-scan lock. The join itself is the server's own nested loop:
|
|
sub_select() is driven over this worker's JOIN_TABs, reading the driving
|
|
table through the engine's chunk reader and every other table through this
|
|
worker's handler, and each fully joined row leaves the last table through
|
|
pwt_end_send(), which projects the shipped columns into exec.result and
|
|
hands that record to exec's sink. The manager (drain_and_send) only
|
|
concatenates these final rows.
|
|
|
|
@return
|
|
0 on success, or a handler error code. A clean stop requested by the manager
|
|
(PWT_EMIT_STOP) also returns success: the manager is done, not in error.
|
|
*/
|
|
|
|
int pwt_worker::execute_and_handoff()
|
|
{
|
|
DBUG_ENTER("pwt_worker::execute_and_handoff");
|
|
if (manager->is_scan_only())
|
|
DBUG_RETURN(execute_scan_only());
|
|
TABLE *src= exec.scan_table;
|
|
//pwt_manager *mgr= manager;
|
|
const uint nt= exec.n_tables;
|
|
int err= 0;
|
|
uint i;
|
|
bool killed= false;
|
|
|
|
// the source tables were marked in open_worker_tables; this one is ours
|
|
exec.result.table->use_all_columns(); // we write every result column
|
|
|
|
/*
|
|
First thing on this thread: let the transport claim whatever it holds per
|
|
producer. Before this, everything it owns was built by the manager.
|
|
*/
|
|
if (sink->begin())
|
|
DBUG_RETURN(HA_ERR_GENERIC);
|
|
|
|
/*
|
|
Adopt the manager's snapshot before touching any table. We run in our own
|
|
THD, hence in our own transaction, so without this every worker would open
|
|
its own read view at its first read: the workers, and the manager they work
|
|
for, could each see a different version of the tables, and the chunk
|
|
boundaries the manager's engine computed would not even belong to the
|
|
snapshot we scan. The manager pinned its snapshot in
|
|
parallel_init_coordinator() before any worker was created, and holds it
|
|
until the workers have been reaped (quiesce_workers), which is also what
|
|
keeps purge from removing the versions we still need.
|
|
|
|
This covers every table we read, not just the parallel-scanned one: the
|
|
snapshot belongs to the transaction, so the inner tables of the join are
|
|
read at the same point in time as the driving table.
|
|
*/
|
|
if (ha_clone_consistent_snapshot(thd, manager->thd))
|
|
{
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"parallel worker: cannot read the manager's snapshot");
|
|
DBUG_RETURN(HA_ERR_UNSUPPORTED);
|
|
}
|
|
|
|
/*
|
|
Our handles bypassed lock_tables(), so take the engine-level read lock on
|
|
every table ourselves; InnoDB needs this to register a table with its trx
|
|
before reading it. All-or-nothing: unlock the locked prefix on failure.
|
|
*/
|
|
uint locked= 0;
|
|
while (locked < nt)
|
|
{
|
|
if ((err= exec.tables[locked]->file->ha_external_lock(thd, F_RDLCK)))
|
|
{
|
|
while (locked--)
|
|
exec.tables[locked]->file->ha_external_lock(thd, F_UNLCK);
|
|
DBUG_RETURN(err);
|
|
}
|
|
locked++;
|
|
}
|
|
|
|
// Initialise the index on each ref/eq_ref inner table; reused across lookups.
|
|
for (i= 1; i < nt; i++)
|
|
{
|
|
JOIN_TAB *it= &exec.jointabs[i];
|
|
if ((it->type == JT_EQ_REF || it->type == JT_REF) &&
|
|
(err= it->table->file->ha_index_init(it->ref.key, it->sorted)))
|
|
break;
|
|
}
|
|
|
|
if (err)
|
|
goto exec_exit;
|
|
|
|
/*
|
|
src is this worker's own copy of the driving table, opened from the share by
|
|
open_worker_tables(), so its handler is not the one the coordinator was
|
|
initialised on. The scan parameters live on the master's handler, and reach
|
|
this one through exec.handler_ctx, which the master made.
|
|
*/
|
|
if ((err= src->file->parallel_init_worker(exec.handler_ctx)))
|
|
goto exec_exit;
|
|
|
|
{
|
|
/*
|
|
Run the join the way do_select() runs it: once to produce the rows, then
|
|
once more to signal end of records, which is what lets an operator that
|
|
buffers flush. The chunk reader, the conditions, the refs and the trackers
|
|
all hang off this worker's own JOIN_TABs, so the executor never touches
|
|
the manager's. pwt_end_send() takes each finished row.
|
|
*/
|
|
pwt_self= this;
|
|
enum_nested_loop_state rc= sub_select(exec.join, exec.jointabs, FALSE);
|
|
if (rc >= NESTED_LOOP_OK && !thd->killed)
|
|
rc= sub_select(exec.join, exec.jointabs, TRUE);
|
|
pwt_self= nullptr;
|
|
|
|
if (rc == NESTED_LOOP_ERROR)
|
|
err= thd->is_error() ? thd->get_stmt_da()->sql_errno() : HA_ERR_GENERIC;
|
|
else if (thd->killed && !thd->is_error())
|
|
my_error(ER_QUERY_INTERRUPTED, MYF(0));
|
|
killed= thd->killed;
|
|
}
|
|
src->file->parallel_end_worker();
|
|
|
|
/*
|
|
Close the container's index before the transport is told this producer is
|
|
finished. A pre-aggregating worker leaves it open -- AGGR_OP::put_record()
|
|
opened it and nothing since has closed it -- and the manager scans the
|
|
container as soon as it is marked done, which it does on another thread and
|
|
with a handler that asserts nothing else has the table open. So this has to
|
|
happen on this side of the hand-off, not in the tidy-up below.
|
|
*/
|
|
if (manager->row_layout().grouped && exec.result.table)
|
|
exec.result.table->file->ha_index_or_rnd_end();
|
|
|
|
/*
|
|
Nothing to flush for a pre-aggregating worker: the rows it aggregated are
|
|
already in the container the manager reads.
|
|
|
|
hand over whatever the transport is still holding for us
|
|
*/
|
|
if (!err && !killed)
|
|
sink->flush();
|
|
|
|
exec_exit:
|
|
if (err == HA_ERR_END_OF_FILE)
|
|
err= 0;
|
|
|
|
// end any open index/rnd scans (no-op for tables left in NONE state), unlock
|
|
for (i= 1; i < nt; i++)
|
|
exec.jointabs[i].table->file->ha_index_or_rnd_end();
|
|
/* Again for the paths that reach here without having handed anything over. */
|
|
if (manager->row_layout().grouped && exec.result.table)
|
|
exec.result.table->file->ha_index_or_rnd_end();
|
|
for (i= 0; i < nt; i++)
|
|
exec.tables[i]->file->ha_external_lock(thd, F_UNLCK);
|
|
|
|
DBUG_RETURN(err);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief Run the query for this worker, ship the result rows to the manager,
|
|
then tidy up.
|
|
*/
|
|
|
|
void pwt_worker::execute_and_signal_manager()
|
|
{
|
|
DBUG_ENTER("pwt_worker::execute_and_signal_manager");
|
|
|
|
err= execute_and_handoff();
|
|
|
|
/*
|
|
End the worker's read transaction now, while we are still on the worker
|
|
thread. destroy_background_thd() -> THD::cleanup() rolls the transaction
|
|
back and asserts (trans_check) that the statement transaction is already
|
|
empty, so we must close it out here. Any commit failure is captured by the
|
|
installed PWT_error_handler.
|
|
*/
|
|
trans_commit_stmt(thd);
|
|
trans_commit(thd);
|
|
|
|
|
|
if (err)
|
|
exec.scan_table->file->print_error(err, MYF(0));
|
|
|
|
DBUG_VOID_RETURN;
|
|
}
|
|
|
|
|
|
/*
|
|
@brief
|
|
Scan-only PROTOTYPE: the worker's projection.
|
|
|
|
@description
|
|
pwt_row_layout::build() ships every column of the driving table whose
|
|
read_set bit is set, in table order, and open_worker_tables() copies that
|
|
read_set onto the worker's table. So walking the worker's own fields the
|
|
same way visits the same columns in the same order, and column i of the
|
|
container is the i-th of them.
|
|
|
|
@return true on error (my_error() called).
|
|
*/
|
|
|
|
bool pwt_manager::setup_scan_only_proj(THD *thd, pwt_worker *worker)
|
|
{
|
|
TABLE *wt= worker->exec.scan_table;
|
|
const uint n= layout.ship_list.elements;
|
|
|
|
if (!(worker->exec.scan_proj= thd->alloc<Item*>(n)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) (n * sizeof(Item*)));
|
|
return true;
|
|
}
|
|
|
|
uint k= 0;
|
|
for (Field **f= wt->field; *f && k < n; f++)
|
|
{
|
|
if (!bitmap_is_set(wt->read_set, (*f)->field_index))
|
|
continue;
|
|
if (!(worker->exec.scan_proj[k]= new (thd->mem_root) Item_field(thd, *f)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item_field));
|
|
return true;
|
|
}
|
|
k++;
|
|
}
|
|
/*
|
|
A query that reads no column at all still has a row shape -- build() ships
|
|
a constant so that the transport has something to measure -- and there is
|
|
no field to project into it. Nothing downstream reads that column's value,
|
|
only the arrival of the record, so leave it as the container's default.
|
|
*/
|
|
worker->exec.scan_proj_count= k;
|
|
DBUG_ASSERT(k == n || k == 0);
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
The container the drain collects into when the plan's sort is ours.
|
|
|
|
@description
|
|
One more container of the transport's own layout, so a drained record can
|
|
be written into it as it stands. It is this thread's alone -- written,
|
|
sorted and read here -- unlike the workers', which cross a thread boundary.
|
|
|
|
@return true on error (my_error() called).
|
|
*/
|
|
|
|
bool pwt_manager::setup_sort_stage(THD *thd)
|
|
{
|
|
if (layout.make_container(thd, &sort_container, nullptr))
|
|
{
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"parallel query: could not build the sort container");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Collect the record just drained.
|
|
|
|
@description
|
|
The container has the layout's own shape, so the record is written as it
|
|
arrived, with no projection: the columns are already the ones the sort and
|
|
the copy-back name. A heap table that fills is rebuilt on disk, the same
|
|
way a worker's container is, except that this one never leaves this thread
|
|
and so needs none of the cross-thread accounting.
|
|
|
|
@return 0 on success, 1 on error.
|
|
*/
|
|
|
|
int pwt_manager::sort_collect(THD *thd)
|
|
{
|
|
TABLE *t= sort_container.table;
|
|
memcpy(t->record[0], layout.recv_record(), layout.reclength);
|
|
|
|
int err= t->file->ha_write_tmp_row(t->record[0]);
|
|
if (likely(!err))
|
|
return 0;
|
|
if (err != HA_ERR_RECORD_FILE_FULL)
|
|
{
|
|
t->file->print_error(err, MYF(0));
|
|
return 1;
|
|
}
|
|
/* Rebuilds on disk and writes the row that did not fit. */
|
|
if (create_internal_tmp_table_from_heap(thd, t,
|
|
sort_container.param->start_recinfo,
|
|
&sort_container.param->recinfo,
|
|
err, 0, NULL))
|
|
return 1; // already reported
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Sort what was collected and send it to the client.
|
|
|
|
@description
|
|
The plan's own Filesort cannot be reused: its order names the manager's
|
|
base-table fields, and the rows are in the container. build_sort_order()
|
|
gives the equivalent order over the container's own fields, and the sort
|
|
runs there. Nothing filters -- the workers applied the condition -- so the
|
|
Filesort is built without a select.
|
|
|
|
Reading back is the ordinary way a sorted result is read, and each row then
|
|
takes the path a drained row would have taken: back into the manager's
|
|
base-table records, where this query's items read it, and out.
|
|
|
|
@return 0 on success, 1 on error.
|
|
*/
|
|
|
|
int pwt_manager::sort_and_send(JOIN *join)
|
|
{
|
|
DBUG_ENTER("pwt_manager::sort_and_send");
|
|
THD *thd= join->thd;
|
|
TABLE *t= sort_container.table;
|
|
int ret= 0;
|
|
|
|
ORDER *so= layout.build_sort_order(thd, t);
|
|
if (!so)
|
|
DBUG_RETURN(1);
|
|
|
|
Filesort *fs= new (thd->mem_root) Filesort(so, HA_ROWS_MAX, false, nullptr);
|
|
if (!fs)
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Filesort));
|
|
DBUG_RETURN(1);
|
|
}
|
|
fs->accepted_rows= &join->accepted_rows; // for ROWNUM
|
|
|
|
/*
|
|
The plan's tracker, so ANALYZE reports this sort as the plan's sort, which
|
|
is what it is. A plan that was never asked to explain itself has none.
|
|
*/
|
|
Filesort_tracker *tracker= exec.scan_tab->filesort
|
|
? exec.scan_tab->filesort->tracker : nullptr;
|
|
if (!tracker &&
|
|
!(tracker= new (thd->mem_root) Filesort_tracker(thd->lex->analyze_stmt)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Filesort_tracker));
|
|
DBUG_RETURN(1);
|
|
}
|
|
|
|
t->file->info(HA_STATUS_VARIABLE); // filesort wants the count
|
|
if (!(sort_result= filesort(thd, t, fs, tracker, join, 0)))
|
|
DBUG_RETURN(1);
|
|
t->file->ha_index_or_rnd_end();
|
|
|
|
READ_RECORD info;
|
|
if (init_read_record(&info, thd, t, nullptr, sort_result, 0, true, false))
|
|
DBUG_RETURN(1);
|
|
|
|
for (;;)
|
|
{
|
|
int err= info.read_record();
|
|
if (err)
|
|
{
|
|
ret= err > 0; // < 0 is end of records
|
|
break;
|
|
}
|
|
if (unlikely(thd->killed))
|
|
{
|
|
thd->send_kill_message();
|
|
ret= 1;
|
|
break;
|
|
}
|
|
|
|
memcpy(layout.recv_record(), t->record[0], layout.reclength);
|
|
layout.copy_back_row();
|
|
|
|
int serr= join->result->send_data_with_check(join->fields_list, join->unit,
|
|
join->send_records);
|
|
if (unlikely(serr))
|
|
{
|
|
if (serr > 0)
|
|
{
|
|
ret= 1;
|
|
break;
|
|
}
|
|
join->duplicate_rows++; // serr < 0: duplicate row
|
|
}
|
|
join->send_records++;
|
|
join->accepted_rows++;
|
|
}
|
|
end_read_record(&info);
|
|
DBUG_RETURN(ret);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Drain the workers' result rows and send them to the client.
|
|
|
|
@description
|
|
The workers ran the whole select-project query over their disjoint chunks
|
|
and produced the final result rows; the manager just takes them from the
|
|
transport, in whatever order it delivers them, and sends each to the client. The select-list metadata was
|
|
already sent (from the query's own field list) before do_select() ran, so we
|
|
only supply the row values here -- the select list is evaluated against the
|
|
manager's own records, which the drain has filled from the shipped columns.
|
|
send_records is advanced so do_select() can set limit_found_rows.
|
|
|
|
@return
|
|
0 success (all rows sent)
|
|
1 error.
|
|
*/
|
|
|
|
int pwt_manager::drain_and_send(JOIN *join)
|
|
{
|
|
DBUG_ENTER("pwt_manager::drain_and_send");
|
|
uchar *dst= layout.recv_record();
|
|
int ret= 0;
|
|
|
|
if (layout.begin_receive(thd, exec.tables, exec.n_tables))
|
|
DBUG_RETURN(1);
|
|
|
|
if (layout.plan_aggregates)
|
|
{
|
|
// the plan aggregates, so last_tab is the worker row destination
|
|
JOIN_TAB *last_tab= join->join_tab + join->top_join_tab_count - 1;
|
|
|
|
for (;;)
|
|
{
|
|
int rc= source->next_row(dst);
|
|
if (rc < 0)
|
|
break; // -1: all rows drained
|
|
if (rc > 0)
|
|
{
|
|
ret= 1; // killed / worker error
|
|
break;
|
|
}
|
|
|
|
layout.copy_back_row();
|
|
/*
|
|
Hand the row to the plan's own terminal, which is end_update(): it
|
|
knows which group the row belongs to.
|
|
|
|
When the workers pre-aggregated, the row is one worker's partials for
|
|
that group, so prime the query's aggregates with them first -- folding
|
|
a partial in is exactly what end_update() then does, because the
|
|
update_field() (or, for a group it has not seen, reset_field()) it
|
|
calls per aggregate takes the direct value in place of the row.
|
|
|
|
When they did not -- the fall-back out of pre-aggregation -- the row is
|
|
a plain joined row, and the terminal counts it. That is the same
|
|
grouping, done a row at a time instead of a group at a time.
|
|
*/
|
|
if (layout.grouped)
|
|
layout.direct_add_partials();
|
|
enum_nested_loop_state nls=
|
|
(*last_tab->next_select) (join, last_tab + 1, FALSE);
|
|
if (nls < NESTED_LOOP_OK) // ERROR or KILLED
|
|
{
|
|
ret= 1;
|
|
break;
|
|
}
|
|
}
|
|
/*
|
|
End of records. For a grouped query this is the call that makes the
|
|
aggregation stage read back what it accumulated and send it; there is
|
|
nothing to do for a query whose rows went straight to the client.
|
|
*/
|
|
if (!ret)
|
|
{
|
|
enum_nested_loop_state nls= (*last_tab->next_select)(join, last_tab + 1,
|
|
TRUE);
|
|
if (nls < NESTED_LOOP_OK)
|
|
ret= 1;
|
|
}
|
|
layout.end_receive(exec.tables, exec.n_tables);
|
|
|
|
DBUG_PRINT("info", ("join records:%llu", join->send_records));
|
|
DBUG_RETURN(ret);
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
int rc= source->next_row(dst);
|
|
if (rc < 0)
|
|
break; // -1: all rows drained
|
|
if (rc > 0)
|
|
{
|
|
ret= 1; // killed / worker error
|
|
break;
|
|
}
|
|
|
|
/*
|
|
A plan whose sort is ours collects the row instead of sending it: the
|
|
order it goes out in is not the order it arrived in, so nothing can be
|
|
sent until every worker has finished. sort_and_send() does the rest.
|
|
*/
|
|
if (layout.plan_sorts)
|
|
{
|
|
if (sort_collect(thd))
|
|
{
|
|
ret= 1;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Put the shipped columns back where the query expects to find them. */
|
|
layout.copy_back_row();
|
|
|
|
int err= join->result->send_data_with_check(join->fields_list, join->unit,
|
|
join->send_records);
|
|
if (unlikely(err))
|
|
{
|
|
if (err > 0)
|
|
{
|
|
ret= 1;
|
|
break;
|
|
}
|
|
join->duplicate_rows++; // err < 0: duplicate row
|
|
}
|
|
join->send_records++;
|
|
join->accepted_rows++;
|
|
}
|
|
|
|
if (!ret && layout.plan_sorts)
|
|
ret= sort_and_send(join);
|
|
|
|
layout.end_receive(exec.tables, exec.n_tables);
|
|
|
|
DBUG_PRINT("info", ("join records:%llu", join->send_records));
|
|
DBUG_RETURN(ret);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Can we scan this table in parallel, extensive check.
|
|
|
|
@description
|
|
Asked of the driving table only, and asked about the *access method*: the
|
|
table-level question -- a real base table, no blob-backed columns, not
|
|
fulltext-searched, not partitioned, an engine that does this at all -- is
|
|
table_can_be_parallel_scanned()'s, and the caller asks it first. Keeping
|
|
the two apart matters because the optimizer's cost hook
|
|
(scale_cost_for_parallel_scan) asks only the table-level one, and a copy
|
|
here would be free to drift from it.
|
|
*/
|
|
|
|
bool can_parallel_scan_jointab_access(JOIN_TAB *join_tab, bool trace)
|
|
{
|
|
/*
|
|
Two access methods are eligible: a table scan (EXPLAIN type=ALL) and a
|
|
range scan over one index. A range scan reaches here as either JT_ALL or
|
|
JT_RANGE, depending on how the plan arrived at it -- make_join_select()
|
|
promotes a ref to JT_RANGE when the range uses a longer key -- and both
|
|
read through join_init_read_record. read_first_record is checked next to
|
|
->type because this runs after make_join_readinfo(), which is what actually
|
|
picked the reader.
|
|
|
|
A complete scan of one index (type=index, JT_NEXT) is NOT eligible, even
|
|
though InnoDB advertises PSCAN_INDEX_FULL. init_parallel_workers() already
|
|
knows how to name the index for one, but nothing in the SQL layer has
|
|
driven it yet: the plans that reach here as JT_NEXT are mostly covering
|
|
reads, and a worker's handler is not given the plan's keyread, so taking
|
|
one would throw away the reason the index was chosen. Until that is
|
|
settled, this is the check that keeps the two apart.
|
|
*/
|
|
if (join_tab->join->ordered_index_usage != JOIN::ordered_index_void)
|
|
return pwt_decline(join_tab->join, trace,
|
|
join_tab->join->ordered_index_usage == JOIN::ordered_index_group_by
|
|
? "an index supplies the GROUP BY order, rows aren't splittable"
|
|
: "an index supplies the ORDER BY order, rows aren't splittable");
|
|
if (!((join_tab->type == JT_ALL || join_tab->type == JT_RANGE) &&
|
|
join_tab->read_first_record == join_init_read_record))
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the driving table's access method is not one the "
|
|
"engine divides (not a full or range scan read "
|
|
"through join_init_read_record)");
|
|
|
|
/*
|
|
The plan may be relying on this table's rows arriving in index order to
|
|
satisfy ORDER BY or GROUP BY without a filesort, but a parallel scan
|
|
does not preserve that order. Reject parallelization in that case.
|
|
*/
|
|
|
|
if (join_tab->filesort_result)
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the driving table's rows have already been sorted");
|
|
if (join_tab->need_to_build_rowid_filter || join_tab->rowid_filter)
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the driving table is read through a rowid filter");
|
|
if (join_tab->distinct)
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the driving table's read eliminates duplicates");
|
|
|
|
/*
|
|
A sort of this table's own read, added by make_aggr_tables_info() after the
|
|
plan was otherwise settled. This one is not about the order rows arrive in
|
|
-- a sort does not care -- it is about where the sort can happen. The
|
|
filesort is bound to the read the workers take over, and there is no
|
|
post-join stage to move it to: for these plans make_aggr_tables_info()
|
|
builds no aggregation table, so the terminal after the driving tab is
|
|
end_send() straight to the client. Until the manager can sort what it
|
|
drains, the sort has nowhere to go and the plan has to run serially.
|
|
|
|
Kept apart from the checks above because it is the one of them that a
|
|
manager-side sort stage would lift, and because the trace should say which
|
|
of the two reasons applied. pwt_table_conds() already knows where a sorted
|
|
tab keeps its condition, so the worker half is in place.
|
|
*/
|
|
if (join_tab->filesort && !pwt_manager_sort_order(join_tab->join))
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the manager cannot sort what the workers produce "
|
|
"(see pwt_manager_sort_order)");
|
|
|
|
const uint32 pscan_support= join_tab->table->file->parallel_scan_support();
|
|
SQL_SELECT *sql_select= join_tab->select;
|
|
|
|
if (sql_select && sql_select->quick)
|
|
{
|
|
/*
|
|
The case of a range scan. Any single index will do, primary or
|
|
secondary: the coordinator converts each endpoint against the index it
|
|
was asked to divide, and the worker opens that same index. What is asked
|
|
of the plan is that it be one plain range with few enough intervals to be
|
|
worth splitting.
|
|
|
|
QS_TYPE_RANGE and nothing else. An index merge or a ROR intersect
|
|
combines several indexes and has no single interval list to hand over; a
|
|
GROUP BY min/max scan visits one row per group rather than a range; and a
|
|
descending read (QS_TYPE_RANGE_DESC) walks the intervals backwards, which
|
|
the partitioner does not produce.
|
|
|
|
Which of the two bits is asked for is decided by whether the index is the
|
|
clustered one, because that is what the engine is really being asked to
|
|
divide. The engine then decides whether it can partition this particular
|
|
index -- see Parallel_scan_coordinator::resolve_index(), which declines spatial,
|
|
FTS, virtual-column and descending indexes -- and we fall back to the
|
|
serial reader on HA_ERR_UNSUPPORTED.
|
|
*/
|
|
const uint MAX_PARALLEL_SCAN_RANGES= 128;
|
|
if (sql_select->quick->get_type() == QUICK_SELECT_I::QS_TYPE_RANGE &&
|
|
sql_select->quick->index < join_tab->table->s->keys &&
|
|
join_tab->use_quick != 2 /*exclude dynamic range*/ &&
|
|
((QUICK_RANGE_SELECT*) sql_select->quick)->num_ranges() <=
|
|
MAX_PARALLEL_SCAN_RANGES)
|
|
{
|
|
/*
|
|
A rowid-ordered scan collects the row ids, sorts them and then sweeps
|
|
the clustered index in that order. Taking such a plan would quietly
|
|
throw the sorted sweep away. Leave it serial.
|
|
*/
|
|
if (((QUICK_RANGE_SELECT*) sql_select->quick)->mrr_flags &
|
|
DSMRR_IMPL_SORT_ROWIDS)
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the range is a rowid-ordered scan, whose sorted "
|
|
"sweep chunking would throw away");
|
|
|
|
const bool clustered=
|
|
sql_select->quick->index == join_tab->table->s->primary_key;
|
|
if (!(pscan_support & (clustered ? handler::PSCAN_TABLE_RANGE
|
|
: handler::PSCAN_INDEX_RANGE)))
|
|
return pwt_decline(join_tab->join, trace, clustered
|
|
? "the engine does not divide a range of the clustered index"
|
|
: "the engine does not divide a range of a secondary index");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the quick select is not one plain range the "
|
|
"partitioner can map onto chunk boundaries "
|
|
"(index merge, ROR intersect, GROUP BY min/max, a "
|
|
"descending read, a dynamic range, or too many "
|
|
"intervals)");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!(pscan_support & handler::PSCAN_TABLE_FULL))
|
|
return pwt_decline(join_tab->join, trace,
|
|
"the engine does not divide a full table scan");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
The GROUP BY key the plan's own aggregation table is built on, if the
|
|
terminal that reads it is the one that looks a group up by that key.
|
|
|
|
@description
|
|
end_update() reads a row, builds the group key from table->group and folds
|
|
the row into whichever group it finds -- the one terminal whose answer does
|
|
not depend on the order rows arrive in, which is what lets the driving scan
|
|
be handed out in chunks at all. It is also the one a partial can be merged
|
|
through, because the update_field() it calls per aggregate consumes a
|
|
direct value in place of the row.
|
|
|
|
The key is taken from the aggregation table rather than from
|
|
join->group_list, and not only because that is the one end_update() will
|
|
actually key on: make_aggr_tables_info() sets join->group_list to NULL once
|
|
the temp table has taken the grouping over, so by this point it is gone.
|
|
|
|
Lives here because end_update() is static to this file.
|
|
|
|
@return the key, or nullptr if this plan's terminal is not that one.
|
|
*/
|
|
|
|
ORDER *pwt_plan_group_key(JOIN *join)
|
|
{
|
|
if (!join->aggr_tables)
|
|
return nullptr;
|
|
JOIN_TAB *last= join->join_tab + join->top_join_tab_count - 1;
|
|
if (last->next_select != sub_select_postjoin_aggr)
|
|
return nullptr;
|
|
JOIN_TAB *aggr_tab= last + 1;
|
|
if (!aggr_tab->aggr || aggr_tab->aggr->get_write_func() != end_update ||
|
|
!aggr_tab->table)
|
|
return nullptr;
|
|
return aggr_tab->table->group;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Run this query's driving-table scan in parallel workers if possible.
|
|
|
|
@description
|
|
Test if
|
|
1) worker threads are available;
|
|
2) the table itself is one a worker can read and ship
|
|
(jointab_can_be_parallel_scanned);
|
|
3) the access method the plan settled on is one the engine will divide
|
|
(can_parallel_scan_jointab_access), which is also where the engine's
|
|
parallel_scan_support() bitmap is matched against it;
|
|
4) the query itself is one the workers can run end to end
|
|
(can_run_query_in_workers) -- a streaming select-project[-join] whose
|
|
every expression can be cloned onto a worker's own table copies.
|
|
|
|
Called late in optimize_stage2() when we know the table's access method.
|
|
A table given an ordered index scan after the fact is not one that can
|
|
be handed out in chunks.
|
|
|
|
Engine-intrinsic constraints (consistent-read only, record format,
|
|
discarded tablespace, ...) are not known here. They are enforced later
|
|
inside parallel_init_coordinator(), which declines with HA_ERR_UNSUPPORTED
|
|
so run_worker_side_join() falls back to serial execution. The table keeps
|
|
the serial reader either way: the manager never scans it, it only collects
|
|
the workers' result rows, and the serial reader is what the fall-back path
|
|
needs. do_select() dispatches on worker_side_parallel.
|
|
|
|
(Un)Sets join->worker_side_parallel
|
|
*/
|
|
|
|
void parallel_join_check(JOIN *join)
|
|
{
|
|
JOIN_TAB *first= first_linear_tab(join, WITH_BUSH_ROOTS,
|
|
WITHOUT_CONST_TABLES);
|
|
/*
|
|
Only two questions, because can_run_query_in_workers() opens by asking
|
|
jointab_can_be_parallel_scanned() of the driving tab, which in turn asks
|
|
can_parallel_scan_jointab_access(). Asking those here as well would answer them
|
|
twice and report each refusal twice.
|
|
*/
|
|
if (join->thd->variables.parallel_worker_threads > 0 && //1
|
|
can_run_query_in_workers(join, first, /*trace=*/ true)) //2
|
|
{
|
|
first->use_parallel_scan= join->worker_side_parallel= true;
|
|
if (unlikely(join->thd->trace_started()))
|
|
{
|
|
Json_writer_object trace_pscan(join->thd);
|
|
trace_pscan.add("chosen_for_parallel_scan",
|
|
first->table->alias.c_ptr());
|
|
/*
|
|
What the workers will divide: the whole clustered index, or the key
|
|
intervals of the range scan. The two are partitioned the same way,
|
|
but which one it is decides whether the chunk boundaries are bounded
|
|
by the range, so say so rather than leaving it to be inferred from
|
|
the access method shown elsewhere in the trace.
|
|
*/
|
|
trace_pscan.add("range_scan",
|
|
first->select && first->select->quick != NULL);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
join->worker_side_parallel= false;
|
|
if (first)
|
|
first->use_parallel_scan= false;
|
|
if (unlikely(join->thd->trace_started()) &&
|
|
join->thd->variables.parallel_worker_threads > 0)
|
|
{
|
|
Json_writer_object trace_pscan(join->thd);
|
|
trace_pscan.add("parallel_scan_declined",
|
|
first ? first->table->alias.c_ptr() : "");
|
|
/*
|
|
Which check objected is not decided here: each one says so itself,
|
|
through pwt_decline(), at the point it fires. This only names the table
|
|
the question was asked about, and covers the one case that reaches here
|
|
having asked nothing -- a plan whose tables are all constant.
|
|
*/
|
|
if (!first)
|
|
trace_pscan.add("parallel_scan_declined_because",
|
|
"there is no table to divide");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
|
|
Called from do_select() in sql_select.cc, run either the worker side
|
|
join, the scan only in the workers, or set declined
|
|
*/
|
|
|
|
enum_nested_loop_state do_select_parallel(JOIN *join, bool *declined)
|
|
{
|
|
*declined= true;
|
|
|
|
if (!join->worker_side_parallel)
|
|
return NESTED_LOOP_ERROR;
|
|
|
|
/*
|
|
PROTOTYPE, behind debug_dbug='+d,pwt_scan_only'. The workers are a
|
|
parallel reader for the driving table and nothing more: no join runs in
|
|
them, so nothing here is diverted. The plan below is the one a serial
|
|
execution would run, and the only difference is where join_tab's first
|
|
table gets its rows. See pwt_scan_only_enabled().
|
|
*/
|
|
if (pwt_scan_only_enabled())
|
|
{
|
|
int rc;
|
|
JOIN_TAB *scan_tab= first_linear_tab(join, WITH_BUSH_ROOTS,
|
|
WITHOUT_CONST_TABLES);
|
|
join->worker_side_parallel= false;
|
|
rc= run_scan_only_workers(join, scan_tab);
|
|
if (rc == 0)
|
|
return NESTED_LOOP_OK;
|
|
*declined= false;
|
|
return NESTED_LOOP_ERROR;
|
|
}
|
|
else
|
|
{
|
|
JOIN_TAB *scan_tab= first_linear_tab(join, WITH_BUSH_ROOTS,
|
|
WITHOUT_CONST_TABLES);
|
|
/*
|
|
Instantiate the parallel worker thread manager and
|
|
Initialize our workers (test for engine support)
|
|
Run the join on the workers, -1 if init_parallel_workers failed
|
|
*/
|
|
int wr= run_worker_side_join(join, scan_tab);
|
|
if (wr >= 0)
|
|
{
|
|
*declined= false;
|
|
return wr ? NESTED_LOOP_ERROR : NESTED_LOOP_OK;
|
|
}
|
|
else
|
|
join->worker_side_parallel= false; // declined, run serially
|
|
}
|
|
|
|
return NESTED_LOOP_ERROR;
|
|
}
|
|
|