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.
1158 lines
36 KiB
C++
1158 lines
36 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
|
|
|
|
Result-row transport for parallel query: the row shape the two ends agree on,
|
|
and the batch implementation of the two ends themselves.
|
|
|
|
sql_parallel_transport.h says what a transport is and what the interface
|
|
promises; this is the one that exists today. What a worker computes and what
|
|
the manager does with a row once it has arrived are not here -- they are in
|
|
sql_parallel_execution.cc, which reaches the transport only through
|
|
pwt_row_layout, pwt_row_sink and pwt_row_source.
|
|
*/
|
|
|
|
#include "mariadb.h"
|
|
#include "mysqld_error.h"
|
|
#include "sql_priv.h"
|
|
#include "unireg.h"
|
|
#include "sql_class.h"
|
|
#include "sql_select.h"
|
|
#include "sql_parallel_workers.h"
|
|
|
|
#ifdef HAVE_PSI_INTERFACE
|
|
static PSI_cond_key key_COND_pwt_data_space;
|
|
static PSI_cond_info all_pwt_transport_conds[]=
|
|
{
|
|
{ &key_COND_pwt_data_space, "pwt_batch_source::COND_data_space", 0},
|
|
};
|
|
|
|
void pwt_transport_init_psi_keys(void)
|
|
{
|
|
mysql_cond_register("sql", all_pwt_transport_conds,
|
|
array_elements(all_pwt_transport_conds));
|
|
}
|
|
#endif /* HAVE_PSI_INTERFACE */
|
|
|
|
|
|
/*****************************************************************************
|
|
pwt_row_layout -- the shape both ends agree on
|
|
*****************************************************************************/
|
|
|
|
|
|
/**
|
|
@brief
|
|
result_defn := one clone per shipped column.
|
|
|
|
@description
|
|
Clones, so that the query's own items are never bound to a container field,
|
|
and so that the definition can be rebuilt from scratch -- which is what the
|
|
fall-back out of pre-aggregation does, having appended partial columns to a
|
|
definition it then has to take them back off.
|
|
|
|
@return true on error (my_error() called).
|
|
*/
|
|
|
|
bool pwt_row_layout::clone_base_defn(THD *thd)
|
|
{
|
|
result_defn.empty();
|
|
List_iterator_fast<Item> li(ship_list);
|
|
Item *sel_item;
|
|
while ((sel_item= li++))
|
|
{
|
|
Item *c= sel_item->deep_copy_with_checks(thd);
|
|
if (!c || result_defn.push_back(c, thd->mem_root))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item));
|
|
return true;
|
|
}
|
|
}
|
|
n_ship_base= result_defn.elements;
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@description
|
|
Forget that the workers were going to pre-aggregate. Everything downstream
|
|
reads 'grouped' -- the worker's terminal, its setup, and the manager's drain
|
|
-- so clearing it here is the whole of the decision. The allocations stay on
|
|
the statement's mem_root, which outlives all of this.
|
|
*/
|
|
|
|
void pwt_row_layout::forget_aggregates()
|
|
{
|
|
/* plan_aggregates stays: the query still groups, the manager just does it. */
|
|
grouped= false;
|
|
whole_table= false;
|
|
n_sums= 0;
|
|
mgr_sums= nullptr;
|
|
partial_items= nullptr;
|
|
group_defn= nullptr;
|
|
group_pos= nullptr;
|
|
n_group= 0;
|
|
group_parts= 0;
|
|
group_length= 0;
|
|
group_null_parts= 0;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Which shipped column holds the field 'want'.
|
|
|
|
@return its position in ship_list, or -1 if it is not shipped.
|
|
*/
|
|
|
|
int pwt_row_layout::ship_pos_of(Field *want) const
|
|
{
|
|
uint pos= 0;
|
|
Item *it;
|
|
List_iterator_fast<Item> si(const_cast<List<Item>&>(ship_list));
|
|
while ((it= si++))
|
|
{
|
|
if (it->type() == Item::FIELD_ITEM && ((Item_field *) it)->field == want)
|
|
return (int) pos;
|
|
pos++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Remember which shipped column each ORDER element sorts on.
|
|
|
|
@description
|
|
Every element names a base-table field -- the gate has already refused an
|
|
order it could not say that of -- and every field the query reads is
|
|
shipped, so each one is found. The positions are what build_sort_order()
|
|
turns into an order over a container.
|
|
|
|
@return true on error (my_error() called).
|
|
*/
|
|
|
|
bool pwt_row_layout::build_sort_positions(THD *thd, ORDER *plan_sort)
|
|
{
|
|
n_sort= 0;
|
|
for (ORDER *o= plan_sort; o; o= o->next)
|
|
n_sort++;
|
|
DBUG_ASSERT(n_sort);
|
|
|
|
if (!(sort_pos= thd->alloc<uint>(n_sort)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) (n_sort * sizeof(uint)));
|
|
return true;
|
|
}
|
|
|
|
uint k= 0;
|
|
for (ORDER *o= plan_sort; o; o= o->next, k++)
|
|
{
|
|
Item *real= (*o->item)->real_item();
|
|
DBUG_ASSERT(real->type() == Item::FIELD_ITEM);
|
|
int pos= ship_pos_of(((Item_field *) real)->field);
|
|
if (pos < 0)
|
|
{
|
|
DBUG_ASSERT(0); // every column the query reads is shipped
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"parallel query: ORDER BY column is not shipped");
|
|
return true;
|
|
}
|
|
sort_pos[k]= (uint) pos;
|
|
}
|
|
plan_order= plan_sort;
|
|
return false;
|
|
}
|
|
|
|
|
|
ORDER *pwt_row_layout::build_sort_order(THD *thd, TABLE *container)
|
|
{
|
|
DBUG_ASSERT(plan_sorts && plan_order && n_sort);
|
|
ORDER *so= thd->alloc<ORDER>(n_sort);
|
|
if (!so)
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) (n_sort * sizeof(ORDER)));
|
|
return nullptr;
|
|
}
|
|
uint k= 0;
|
|
for (ORDER *o= plan_order; o; o= o->next, k++)
|
|
{
|
|
/*
|
|
A field of the container, not of the base table the plan named: this
|
|
order sorts the rows the workers produced, which is where they are.
|
|
Same column, because sort_pos[k] is the position the plan's field was
|
|
shipped in and the container was built from the same definition.
|
|
*/
|
|
Item *f= new (thd->mem_root) Item_field(thd,
|
|
container->field[sort_pos[k]]);
|
|
if (!f)
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item_field));
|
|
return nullptr;
|
|
}
|
|
bzero((char *) &so[k], sizeof(ORDER));
|
|
so[k].item_ptr= f;
|
|
so[k].item= &so[k].item_ptr;
|
|
so[k].direction= o->direction;
|
|
so[k].next= k + 1 < n_sort ? &so[k + 1] : nullptr;
|
|
}
|
|
return so;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Work out what travels, define the columns, and build the manager's
|
|
receiving container.
|
|
|
|
@description
|
|
What travels is every base-table column the query reads, in table order.
|
|
See the class comment for why that rather than the projected select list.
|
|
|
|
The definition is a list of clones, so the query's own items are never
|
|
bound to a container field, and the manager and every worker build the
|
|
identical layout from it.
|
|
|
|
@return true on error (my_error() has been called).
|
|
*/
|
|
|
|
bool pwt_row_layout::build(THD *thd, JOIN *join_arg, TABLE **tables,
|
|
uint n_tables, pwt_preagg_kind preagg,
|
|
ORDER *plan_group, ORDER *plan_sort)
|
|
{
|
|
DBUG_ENTER("pwt_row_layout::build");
|
|
join= join_arg;
|
|
plan_aggregates= grouped= (preagg != PWT_PREAGG_NONE);
|
|
whole_table= (preagg == PWT_PREAGG_WHOLE);
|
|
DBUG_ASSERT((preagg == PWT_PREAGG_GROUPED) == (plan_group != nullptr));
|
|
plan_sorts= (plan_sort != nullptr);
|
|
|
|
for (uint t= 0; t < n_tables; t++)
|
|
{
|
|
TABLE *tbl= tables[t];
|
|
for (Field **f= tbl->field; *f; f++)
|
|
{
|
|
if (!bitmap_is_set(tbl->read_set, (*f)->field_index))
|
|
continue;
|
|
Item *itf= new (thd->mem_root) Item_field(thd, *f);
|
|
if (!itf || ship_list.push_back(itf, thd->mem_root))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item_field));
|
|
DBUG_RETURN(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
A query reading no column of any table still needs a row shape, because the
|
|
transport measures its rows in record images. "SELECT COUNT(*)" is that
|
|
query: it reads no column, and the count is the number of images that
|
|
arrive.
|
|
*/
|
|
if (ship_list.is_empty())
|
|
{
|
|
Item *one= new (thd->mem_root) Item_int(thd, (longlong) 1, 1);
|
|
if (!one || ship_list.push_back(one, thd->mem_root))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item_int));
|
|
DBUG_RETURN(true);
|
|
}
|
|
}
|
|
|
|
if (!(copy_back= new (thd->mem_root) Copy_field[ship_list.elements]))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0),
|
|
(int) (ship_list.elements * sizeof(Copy_field)));
|
|
DBUG_RETURN(true);
|
|
}
|
|
|
|
if (clone_base_defn(thd))
|
|
DBUG_RETURN(true);
|
|
|
|
if (plan_sorts && build_sort_positions(thd, plan_sort))
|
|
DBUG_RETURN(true);
|
|
|
|
/*
|
|
Define grouping information in our per worker aggregate container.
|
|
Create and instantiate our temporary tables
|
|
*/
|
|
|
|
if (grouped && build_aggregates(thd, plan_group))
|
|
DBUG_RETURN(true);
|
|
|
|
/*
|
|
Twice at most: once wanting to pre-aggregate, and once more having found
|
|
that this plan cannot. See the foot of the loop.
|
|
*/
|
|
for (;;)
|
|
{
|
|
ORDER *group= nullptr;
|
|
if (grouped && !(group= clone_group_defn(thd)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) (n_group * sizeof(ORDER)));
|
|
DBUG_RETURN(true);
|
|
}
|
|
if (make_container(thd, &recv, group))
|
|
{
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"parallel query: failed to build the result row container");
|
|
DBUG_RETURN(true);
|
|
}
|
|
|
|
/*
|
|
A group key create_tmp_table() could not build as an index is not one a
|
|
worker can look a group up in: it falls back to a unique constraint, which
|
|
is a hash of the row in a hidden column, enforced on write, with nothing
|
|
to read a group back out of. accumulate_group() needs the index.
|
|
|
|
Whether it managed one can only be asked here, of the finished table.
|
|
can_run_query_in_workers() asks what it can of the SQL -- the key parts
|
|
are plain columns, none wider than a varchar, the whole no wider than a
|
|
blob -- but whether that becomes an index depends on the engine
|
|
create_tmp_table() picked and that engine's own key limits, and it picks
|
|
the engine from the row it is building.
|
|
|
|
So give up pre-aggregating and build the layout the query would have had
|
|
without it. The query still runs in the workers; they ship their rows and
|
|
the manager groups them, which is what every query did before there were
|
|
partials to ship.
|
|
*/
|
|
bool no_group_index= !(recv.table->group && recv.table->s->keys == 1 &&
|
|
!recv.table->s->have_unique_constraint());
|
|
/*
|
|
Nothing this tree can express reaches the real thing: the gate already
|
|
refuses the two shapes that make create_tmp_table() fall back for a heap
|
|
container, and its engine-limit test is only in the on-disk branch. So
|
|
the fall-back is reached on demand instead, or it would never be run.
|
|
*/
|
|
DBUG_EXECUTE_IF("pwt_no_group_index", no_group_index= true;);
|
|
|
|
if (!grouped || !no_group_index)
|
|
break;
|
|
|
|
DBUG_PRINT("info", ("no group index on the result container, "
|
|
"shipping rows instead"));
|
|
free_container(thd, &recv);
|
|
forget_aggregates();
|
|
if (clone_base_defn(thd))
|
|
DBUG_RETURN(true);
|
|
}
|
|
reclength= recv.table->s->reclength;
|
|
|
|
/*
|
|
Pair each container column with the base-table field it was projected from,
|
|
so that receiving a row is a copy per column back into the manager's own
|
|
records. Position i of ship_list is column i of the container, which is how
|
|
make_container() built it. The one item that is not an Item_field is the
|
|
filler shipped for a query that reads no column, and it has nowhere to go
|
|
back to.
|
|
*/
|
|
{
|
|
List_iterator_fast<Item> si(ship_list);
|
|
Item *it;
|
|
n_copy_back= 0;
|
|
for (uint i= 0; (it= si++); i++)
|
|
if (it->type() == Item::FIELD_ITEM)
|
|
copy_back[n_copy_back++].set(((Item_field*) it)->field,
|
|
recv.table->field[i], false);
|
|
}
|
|
|
|
/* add our Item_field partial_items, for MIN/MAX's direct_add(). */
|
|
for (uint i= 0; i < n_sums; i++)
|
|
if (!(partial_items[i]= new (thd->mem_root)
|
|
Item_field(thd, recv.table->field[n_ship_base + i])))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item_field));
|
|
DBUG_RETURN(true);
|
|
}
|
|
DBUG_RETURN(false);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Define the partial columns and the group accumulation key
|
|
|
|
@return true on error (my_error() called).
|
|
*/
|
|
|
|
bool pwt_row_layout::build_aggregates(THD *thd, ORDER *plan_group)
|
|
{
|
|
Item *one= nullptr;
|
|
n_sums= 0;
|
|
for (Item_sum **s= join->sum_funcs; *s; s++)
|
|
n_sums++;
|
|
if (!(mgr_sums= thd->alloc<Item_sum*>(n_sums)) ||
|
|
!(partial_items= thd->alloc<Item*>(n_sums)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) (n_sums * sizeof(void*)));
|
|
return true;
|
|
}
|
|
|
|
uint i= 0;
|
|
for (Item_sum **s= join->sum_funcs; *s; s++, i++)
|
|
{
|
|
mgr_sums[i]= *s;
|
|
Item *c= (*s)->deep_copy_with_checks(thd);
|
|
if (!c || result_defn.push_back(c, thd->mem_root))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
The key. Each entry has to name a result_defn item while create_tmp_table()
|
|
runs, because that is how it finds which column of the table it is building
|
|
the key part covers; group_pos remembers which column that was, so a copy
|
|
of the key can later be re-pointed at a container's own fields.
|
|
*/
|
|
n_group= 0;
|
|
for (ORDER *g= plan_group; g; g= g->next)
|
|
n_group++;
|
|
/*
|
|
Aggregating over the whole table is one group, and the column it is keyed
|
|
on is a constant this layout adds: nothing in the query distinguishes one
|
|
row from another, so anything constant does. The definition carries it like
|
|
any other column, after the partials, and from here on this shape is the
|
|
grouped one with a single group in it -- which is what lets the worker
|
|
accumulate through end_update() into a keyed container, and keeps the
|
|
container keyed, which is the form Item_sum_avg::create_tmp_field() packs
|
|
its count into.
|
|
*/
|
|
if (whole_table)
|
|
{
|
|
DBUG_ASSERT(!n_group);
|
|
one= new (thd->mem_root) Item_int(thd, (longlong) 1, 1);
|
|
if (!one || result_defn.push_back(one, thd->mem_root))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(Item_int));
|
|
return true;
|
|
}
|
|
n_group= 1;
|
|
}
|
|
if (!(group_defn= thd->alloc<ORDER>(n_group)) ||
|
|
!(group_pos= thd->alloc<uint>(n_group)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) (n_group * sizeof(ORDER)));
|
|
return true;
|
|
}
|
|
if (whole_table)
|
|
{
|
|
bzero((char *) &group_defn[0], sizeof(ORDER));
|
|
group_defn[0].item_ptr= one;
|
|
group_defn[0].item= &group_defn[0].item_ptr;
|
|
group_defn[0].direction= ORDER::ORDER_ASC;
|
|
group_defn[0].next= nullptr;
|
|
group_pos[0]= result_defn.elements - 1;
|
|
}
|
|
|
|
uint k= 0;
|
|
for (ORDER *g= plan_group; g; g= g->next, k++)
|
|
{
|
|
Field *want= ((Item_field *) (*g->item)->real_item())->field;
|
|
|
|
uint pos= 0;
|
|
Item *it;
|
|
List_iterator_fast<Item> si(ship_list);
|
|
while ((it= si++))
|
|
{
|
|
if (it->type() == Item::FIELD_ITEM && ((Item_field *) it)->field == want)
|
|
break;
|
|
pos++;
|
|
}
|
|
if (!it)
|
|
{
|
|
DBUG_ASSERT(0); // every column the query reads is shipped
|
|
return true;
|
|
}
|
|
|
|
List_iterator_fast<Item> di(result_defn);
|
|
Item *defn_item= nullptr;
|
|
for (uint j= 0; j <= pos; j++)
|
|
defn_item= di++;
|
|
|
|
bzero((char *) &group_defn[k], sizeof(ORDER));
|
|
group_defn[k].item_ptr= defn_item;
|
|
group_defn[k].item= &group_defn[k].item_ptr;
|
|
group_defn[k].direction= ORDER::ORDER_ASC;
|
|
group_defn[k].next= k + 1 < n_group ? &group_defn[k + 1] : nullptr;
|
|
group_pos[k]= pos;
|
|
}
|
|
|
|
/*
|
|
Measure the key once, here, and hand the sizes to every container.
|
|
create_tmp_table() would otherwise re-derive them per container from items
|
|
that have since been re-pointed at that container's own fields.
|
|
*/
|
|
TMP_TABLE_PARAM measure;
|
|
measure.init();
|
|
calc_group_buffer(&measure, group_defn);
|
|
group_parts= measure.group_parts;
|
|
group_length= measure.group_length;
|
|
group_null_parts= measure.group_null_parts;
|
|
return false;
|
|
}
|
|
|
|
|
|
ORDER *pwt_row_layout::clone_group_defn(THD *thd)
|
|
{
|
|
DBUG_ASSERT(group_defn && n_group);
|
|
ORDER *copy= thd->alloc<ORDER>(n_group);
|
|
if (!copy)
|
|
return nullptr;
|
|
for (uint k= 0; k < n_group; k++)
|
|
{
|
|
copy[k]= group_defn[k];
|
|
copy[k].item= ©[k].item_ptr; // at this copy's own storage
|
|
copy[k].next= k + 1 < n_group ? ©[k + 1] : nullptr;
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
|
|
/*
|
|
@brief
|
|
Build one container of this layout: the record image both ends copy.
|
|
|
|
@description
|
|
A tmp table used for its record buffer and its fields. Whether rows are
|
|
ever written into it through the storage engine is the transport's
|
|
business: the batch transport writes none, and ships images of record[0].
|
|
|
|
@return true on error, false on success (*out set).
|
|
*/
|
|
|
|
bool pwt_row_layout::make_container(THD *thd, pwt_row_container *out,
|
|
ORDER *group)
|
|
{
|
|
return make_container_from(thd, result_defn, out, group);
|
|
}
|
|
|
|
|
|
bool pwt_row_layout::make_container_from(THD *thd, List<Item> &defn,
|
|
pwt_row_container *out,
|
|
ORDER *group)
|
|
{
|
|
/*
|
|
out needs its own TMP_TABLE_PARAM because
|
|
1) create_tmp_table() overwrites param->func_count with the number
|
|
of items it actually has to copy, so a second table built from the same
|
|
param allocates fewer fields than the layout needs.
|
|
2) create_tmp_table() allocates param->start_recinfo out of the table's
|
|
own mem_root, so the column descriptions belong to one container and
|
|
die with it; sharing a param would leave every container but the last
|
|
describing a table that has been freed.
|
|
*/
|
|
TMP_TABLE_PARAM *param= new (thd->mem_root) TMP_TABLE_PARAM;
|
|
if (!param)
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(TMP_TABLE_PARAM));
|
|
return true;
|
|
}
|
|
param->init();
|
|
count_field_types(join->select_lex, param, defn, false);
|
|
param->skip_create_table= true;
|
|
/*
|
|
This container is written by a worker and read by the manager, so it is
|
|
instantiated cross-thread below. A rebuild on disk has to be opened the
|
|
same way, and for a pre-aggregating worker that rebuild happens inside
|
|
end_update(), which reads the decision from here.
|
|
*/
|
|
param->cross_thread= true;
|
|
/* The key sizes measured once in build_aggregates(), not re-derived here. */
|
|
param->group_parts= group_parts;
|
|
param->group_length= group_length;
|
|
param->group_null_parts= group_null_parts;
|
|
|
|
/*
|
|
TMP_TABLE_ALL_COLUMNS makes create_tmp_table() give every item of the list a
|
|
field, including the constant ones. By default it skips constants, which is
|
|
right for a query that materialises its result and can evaluate them once
|
|
outside the table, but wrong here: the layout has to mirror the definition
|
|
one for one, because the worker projects item i into field i and ships the
|
|
record image, and the manager copies field i back to the column it came
|
|
from. Without this a list holding a constant built a table with fewer fields
|
|
than the projection walks over.
|
|
*/
|
|
const ulonglong opts= join->select_options | TMP_TABLE_ALL_COLUMNS;
|
|
TABLE *t= create_tmp_table(thd, param, defn,
|
|
group, false, grouped,
|
|
opts, HA_POS_ERROR,
|
|
&empty_clex_str, true, false);
|
|
if (!t)
|
|
return true;
|
|
if (instantiate_tmp_table(t, param->keyinfo, param->start_recinfo,
|
|
¶m->recinfo, opts, true /*cross_thread*/))
|
|
{
|
|
free_tmp_table(thd, t);
|
|
return true;
|
|
}
|
|
/*
|
|
The whole transport is positional, so a layout that does not match the
|
|
definition item for item would have the two ends reading different columns,
|
|
or walking past the end of the field array. Refuse instead, whatever the
|
|
reason turns out to be.
|
|
*/
|
|
DBUG_ASSERT(t->s->fields == defn.elements);
|
|
if (t->s->fields != defn.elements)
|
|
{
|
|
free_tmp_table(thd, t);
|
|
return true;
|
|
}
|
|
out->table= t;
|
|
out->param= param;
|
|
return false;
|
|
}
|
|
|
|
|
|
void pwt_row_layout::free_container(THD *thd, pwt_row_container *c)
|
|
{
|
|
if (c->table)
|
|
{
|
|
free_tmp_table(thd, c->table);
|
|
c->table= nullptr;
|
|
}
|
|
c->param= nullptr; // points into c->table
|
|
}
|
|
|
|
|
|
void pwt_row_layout::cleanup(THD *thd)
|
|
{
|
|
free_container(thd, &recv);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Hand each of the query's aggregates the partial now in recv.
|
|
|
|
@description
|
|
One direct_add() per aggregate, choosing the overload by what the aggregate
|
|
is. A NULL partial comes from a worker that saw no non-NULL value in this
|
|
group, and has to be skipped rather than read as a zero or taken as the
|
|
extreme -- which is what each overload does with it, given that it is told.
|
|
The two SUM overloads are told differently: the real one by a flag, the
|
|
decimal one by a null pointer, where a pointer to decimal zero would mean
|
|
the value 0.
|
|
|
|
The value stays pending on the aggregate until something consumes it, which
|
|
here is the plan's own terminal: the update_field() or reset_field() it
|
|
calls once it knows which group the row belongs to takes the direct value
|
|
in place of the row.
|
|
*/
|
|
|
|
void pwt_row_layout::direct_add_partials()
|
|
{
|
|
for (uint i= 0; i < n_sums; i++)
|
|
{
|
|
Item_sum *a= mgr_sums[i];
|
|
Field *f= recv.table->field[n_ship_base + i];
|
|
|
|
switch (a->sum_func()) {
|
|
case Item_sum::COUNT_FUNC:
|
|
((Item_sum_count *) a)->direct_add(f->is_null() ? 0 : f->val_int());
|
|
break;
|
|
case Item_sum::SUM_FUNC:
|
|
if (a->result_type() == DECIMAL_RESULT)
|
|
{
|
|
my_decimal buf;
|
|
((Item_sum_sum *) a)->direct_add(f->is_null() ? (my_decimal *) NULL
|
|
: f->val_decimal(&buf));
|
|
}
|
|
else
|
|
((Item_sum_sum *) a)->direct_add(f->is_null() ? 0.0 : f->val_real(),
|
|
f->is_null());
|
|
break;
|
|
case Item_sum::MIN_FUNC:
|
|
case Item_sum::MAX_FUNC:
|
|
((Item_sum_min_max *) a)->direct_add(partial_items[i]);
|
|
break;
|
|
case Item_sum::AVG_FUNC:
|
|
{
|
|
/*
|
|
An average does not merge, so what a worker computed for a group is
|
|
both halves of one: the sum of its rows and how many there were.
|
|
Item_sum_avg::create_tmp_field() packs exactly that into one field --
|
|
the value followed by an 8-byte count -- and update_field() has been
|
|
maintaining it there all along, so the partial column already holds
|
|
both and this only has to take them apart again the same way.
|
|
*/
|
|
Item_sum_avg *avg= (Item_sum_avg *) a;
|
|
const uchar *res= f->ptr;
|
|
if (a->result_type() == DECIMAL_RESULT)
|
|
{
|
|
my_decimal partial;
|
|
binary2my_decimal(E_DEC_FATAL_ERROR, res, &partial,
|
|
avg->f_precision, avg->f_scale);
|
|
const longlong cnt= sint8korr(res + avg->dec_bin_size);
|
|
/* No rows behind it: a null sum, so that it adds nothing. */
|
|
avg->direct_add(cnt ? &partial : (my_decimal *) NULL,
|
|
(ulonglong) cnt);
|
|
}
|
|
else
|
|
{
|
|
double val;
|
|
float8get(val, res);
|
|
const longlong cnt= sint8korr(res + sizeof(double));
|
|
avg->direct_add(val, cnt == 0, (ulonglong) cnt);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
DBUG_ASSERT(0); // the gate accepts no others
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Put the row now in recv_record() back where the query expects to read it.
|
|
|
|
One copy per shipped column, container field to the base-table field it was
|
|
projected from. After this the manager's records hold what a serial scan
|
|
would have left there.
|
|
*/
|
|
|
|
void pwt_row_layout::copy_back_row()
|
|
{
|
|
for (uint i= 0; i < n_copy_back; i++)
|
|
(*copy_back[i].do_copy)(©_back[i]);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Make the manager's own records fit to receive rows.
|
|
|
|
@description
|
|
The manager's tables were opened but never read, so clear the flags a
|
|
reader would have left. Copy_field captures &table->null_row, and a stale
|
|
null_row would make every copied field read as NULL.
|
|
|
|
Writing into these records is not something a SELECT's write_set allows,
|
|
and Field::store() asserts on that, so mark the fields writable for the
|
|
duration. A record is being filled here in place of the reader that would
|
|
normally have filled it, which is what the helper is for.
|
|
|
|
@return true on error.
|
|
*/
|
|
|
|
bool pwt_row_layout::begin_receive(THD *thd, TABLE **tables, uint n_tables)
|
|
{
|
|
if (!(saved_write_set= (MY_BITMAP**)
|
|
thd->alloc(n_tables * sizeof(MY_BITMAP*))))
|
|
return true;
|
|
for (uint t= 0; t < n_tables; t++)
|
|
{
|
|
tables[t]->status= 0;
|
|
tables[t]->null_row= false;
|
|
saved_write_set[t]= dbug_tmp_use_all_columns(tables[t],
|
|
&tables[t]->write_set);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void pwt_row_layout::end_receive(TABLE **tables, uint n_tables)
|
|
{
|
|
if (!saved_write_set)
|
|
return;
|
|
for (uint t= 0; t < n_tables; t++)
|
|
dbug_tmp_restore_column_map(&tables[t]->write_set, saved_write_set[t]);
|
|
saved_write_set= nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
@description
|
|
Runs in the worker's thread, before its first row. Take the container: the
|
|
engine accounts a write to TABLE::in_use's status counters, and a projection
|
|
into a field asks in_use for the session's time zone and sql_mode.
|
|
*/
|
|
|
|
bool pwt_tmp_table_sink::begin()
|
|
{
|
|
container->table->in_use= current_thd;
|
|
container->table->file->rebind_to_thread();
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
@brief Has the consumer asked us to stop?
|
|
|
|
@description
|
|
Read under the lock rather than peeked at, and only once every so many rows
|
|
-- the answer only ever changes once, and reading it costs the same mutex
|
|
the batch transport takes to hand a buffer over. Stopping is not required
|
|
for correctness here: the manager cannot ask before it has drained
|
|
everything it wants, so this only saves a worker from finishing work
|
|
nobody will read.
|
|
*/
|
|
|
|
bool pwt_tmp_table_sink::stop_requested()
|
|
{
|
|
mysql_mutex_lock(&manager->LOCK_data);
|
|
bool stop= manager->workers_must_stop;
|
|
mysql_mutex_unlock(&manager->LOCK_data);
|
|
return stop;
|
|
}
|
|
|
|
|
|
/*
|
|
@brief Keep one finished row.
|
|
|
|
@description
|
|
The row is already in the container's record buffer -- the projection put
|
|
it there -- so storing it is one engine write and no copy.
|
|
*/
|
|
|
|
int pwt_tmp_table_sink::emit_row(const uchar *rec)
|
|
{
|
|
TABLE *table= container->table;
|
|
DBUG_ASSERT(rec == table->record[0]);
|
|
(void) rec;
|
|
|
|
int err= table->file->ha_write_tmp_row(table->record[0]);
|
|
if (unlikely(err))
|
|
{
|
|
if (err == HA_ERR_RECORD_FILE_FULL)
|
|
{
|
|
/*
|
|
The heap container is full: rebuild it on disk and carry on there. The
|
|
column descriptions it is driven from are this container's own, which
|
|
is what pairing each container with its own TMP_TABLE_PARAM was for --
|
|
a shared one would describe some other container, in a mem_root that
|
|
may already be gone.
|
|
|
|
The row that did not fit is written by the conversion itself, from
|
|
record[0], so there is nothing to re-emit here.
|
|
|
|
Run on this worker's thread and with this worker's THD, which is what
|
|
makes the thread questions live: the conversion re-opens the table, and
|
|
Aria binds an open handle to the opening thread's my_thread_var, freed
|
|
when this worker's THD is destroyed. The manager reads this container
|
|
afterwards. Being worked through.
|
|
*/
|
|
THD *worker_thd= current_thd;
|
|
const int64 before= worker_thd->status_var.local_memory_used;
|
|
|
|
if (create_internal_tmp_table_from_heap(worker_thd, table,
|
|
container->param->start_recinfo,
|
|
&container->param->recinfo,
|
|
err, 0, NULL,
|
|
/*cross_thread=*/ true))
|
|
return PWT_EMIT_ERROR; // already reported
|
|
|
|
/*
|
|
What the rebuild allocated into the container is thread-specific memory
|
|
charged to this thread, and the manager is the thread that will free it.
|
|
Move the charge with the thing it accounts for: take it off our books,
|
|
so ~THD finds them square, and hand it to the manager in cleanup(),
|
|
which runs with every worker joined and before free_containers().
|
|
Adding to the manager's counter from here would race with the manager,
|
|
which is draining at the same time.
|
|
*/
|
|
spilled_memory+= worker_thd->status_var.local_memory_used - before;
|
|
worker_thd->status_var.local_memory_used= before;
|
|
}
|
|
else
|
|
{
|
|
table->file->print_error(err, MYF(0));
|
|
return PWT_EMIT_ERROR;
|
|
}
|
|
}
|
|
|
|
if (++since_check == PWT_ROW_GANULARITY)
|
|
{
|
|
since_check= 0;
|
|
if (stop_requested())
|
|
return PWT_EMIT_STOP;
|
|
}
|
|
return PWT_EMIT_OK;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
For the tmp table transport, this only signals the manager that we are done
|
|
*/
|
|
|
|
bool pwt_tmp_table_sink::flush()
|
|
{
|
|
/*
|
|
Past this the manager may scan the container, on its own thread, whenever
|
|
it gets to it. Anything this producer had open on the table has to be
|
|
closed by now: handler::ha_rnd_init() asserts that it is, and a worker that
|
|
aggregated into this container left its index open until something closed
|
|
it. Asserted here rather than left to the assert in the manager, which
|
|
fires only when the manager happens to win the race.
|
|
*/
|
|
DBUG_ASSERT(!container || !container->table ||
|
|
container->table->file->inited == handler::NONE);
|
|
mysql_mutex_lock(&manager->LOCK_data);
|
|
done= true;
|
|
mysql_cond_signal(&manager->COND_data_avail);
|
|
mysql_mutex_unlock(&manager->LOCK_data);
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@description
|
|
Give the container back to the manager before it frees it. Called on the
|
|
manager's thread with every worker joined, so a container this worker took
|
|
and never returned -- it failed, or was killed -- would otherwise be freed
|
|
naming a THD that no longer exists.
|
|
*/
|
|
|
|
void pwt_tmp_table_sink::cleanup()
|
|
{
|
|
if (container)
|
|
{
|
|
if (container->table)
|
|
{
|
|
container->table->in_use= manager->thd;
|
|
container->table->file->rebind_to_thread();
|
|
}
|
|
container= nullptr;
|
|
}
|
|
/*
|
|
The memory the worker allocated into the container and the manager is about
|
|
to free. Safe to touch the manager's counter here: every worker has been
|
|
joined, so this runs on the manager's own thread.
|
|
*/
|
|
if (spilled_memory && manager->thd)
|
|
{
|
|
manager->thd->status_var.local_memory_used+= spilled_memory;
|
|
spilled_memory= 0;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@description
|
|
pwt_tmp_table_source -- the consuming end of the temporary-table transport
|
|
allocate our per worker sink pointers
|
|
*/
|
|
|
|
bool pwt_tmp_table_source::init(THD *thd, pwt_manager *mgr, uint n_workers,
|
|
uint reclength_arg)
|
|
{
|
|
manager= mgr;
|
|
n_sinks= n_workers;
|
|
reclength= reclength_arg;
|
|
if (!(sinks= thd->alloc<pwt_tmp_table_sink*>(n_workers)))
|
|
return true;
|
|
for (uint i= 0; i < n_workers; i++)
|
|
sinks[i]= nullptr;
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
allocate our per worker sinks
|
|
*/
|
|
|
|
pwt_row_sink *pwt_tmp_table_source::make_sink(THD *thd, uint worker_nr,
|
|
pwt_row_container *container)
|
|
{
|
|
DBUG_ASSERT(worker_nr < n_sinks);
|
|
#ifndef DBUG_OFF
|
|
/*
|
|
No two containers may share their column descriptions. Rebuilding a full
|
|
container on disk is driven from them, and they are allocated out of the
|
|
table's own mem_root, so one param between containers describes only the
|
|
last one built and points into memory freed with it. Every container is the
|
|
same shape, so nothing about the values would give that away -- this is the
|
|
check that fails on the code this replaced.
|
|
*/
|
|
for (uint j= 0; j < n_sinks; j++)
|
|
DBUG_ASSERT(!sinks[j] || sinks[j]->container->param != container->param);
|
|
#endif
|
|
pwt_tmp_table_sink *s= new (thd->mem_root) pwt_tmp_table_sink;
|
|
if (!s || s->init(manager, this, container))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(pwt_tmp_table_sink));
|
|
return nullptr;
|
|
}
|
|
sinks[worker_nr]= s;
|
|
return s;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Claim the next complete result set, waiting for one if need be.
|
|
|
|
@description
|
|
Claimed rather than merely found: the sink is marked taken under the same
|
|
lock that made it visible, so it cannot be scanned twice.
|
|
|
|
This is the manager's only wait, so it is also where the team's own state
|
|
is noticed -- a worker killed, a worker failed, and "nothing left to read
|
|
and nobody still running" as end of data. Waiting on active_workers rather
|
|
than on every sink being done is what covers a worker that exits without
|
|
flushing at all.
|
|
|
|
@return 0 = *out claimed, -1 = end of data, 1 = error (reported).
|
|
*/
|
|
|
|
int pwt_tmp_table_source::claim_next_result(pwt_tmp_table_sink **out)
|
|
{
|
|
THD *thd= manager->thd;
|
|
PSI_stage_info old_stage;
|
|
struct timespec wait;
|
|
wait.tv_nsec= 0;
|
|
|
|
mysql_mutex_lock(&manager->LOCK_data);
|
|
for (;;)
|
|
{
|
|
for (uint i= 0; i < n_sinks; i++)
|
|
{
|
|
if (sinks[i] && sinks[i]->done && !sinks[i]->taken)
|
|
{
|
|
sinks[i]->taken= true;
|
|
mysql_mutex_unlock(&manager->LOCK_data);
|
|
*out= sinks[i];
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int res;
|
|
if ((res= manager->locked__process_manager_wakeup()) ||
|
|
(res=(thd->killed != NOT_KILLED)))
|
|
{
|
|
mysql_mutex_unlock(&manager->LOCK_data);
|
|
return res;
|
|
}
|
|
/*
|
|
wait for a result set, a finishing worker, or a 1s tick to re-check
|
|
killed. ENTER_COND/EXIT_COND publish the "Reading data from parallel
|
|
workers" stage and register the cond so a KILL of the manager wakes it.
|
|
*/
|
|
wait.tv_sec= time(0) + 1;
|
|
thd->ENTER_COND(&manager->COND_data_avail, &manager->LOCK_data,
|
|
&stage_reading_data_from_parallel_worker, &old_stage);
|
|
mysql_cond_timedwait(&manager->COND_data_avail, &manager->LOCK_data, &wait);
|
|
thd->EXIT_COND(&old_stage); // unlocks LOCK_data
|
|
mysql_mutex_lock(&manager->LOCK_data); // re-lock for the next pass
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
Copy the next result row's record image into dst.
|
|
|
|
@description
|
|
Scans one finished worker's container to its end, then claims the next.
|
|
Rows therefore arrive in whole-worker runs; the interface promises no
|
|
order and the manager's drain loop assumes none.
|
|
|
|
@return
|
|
0 = row copied into dst, -1 = end of data, 1 = error.
|
|
*/
|
|
|
|
int pwt_tmp_table_source::next_row(uchar *dst)
|
|
{
|
|
DBUG_ENTER("pwt_tmp_table_source::next_row");
|
|
|
|
for (;;)
|
|
{
|
|
if (cur)
|
|
{
|
|
TABLE *t= cur->container->table;
|
|
int err= t->file->ha_rnd_next(t->record[0]);
|
|
if (likely(!err))
|
|
{
|
|
memcpy(dst, t->record[0], reclength);
|
|
DBUG_RETURN(0);
|
|
}
|
|
if (err != HA_ERR_END_OF_FILE)
|
|
{
|
|
t->file->print_error(err, MYF(0));
|
|
DBUG_RETURN(1);
|
|
}
|
|
release_position(); // this result set is exhausted
|
|
}
|
|
|
|
pwt_tmp_table_sink *next;
|
|
int rc= claim_next_result(&next);
|
|
if (rc)
|
|
DBUG_RETURN(rc); // -1 end of data, 1 error
|
|
|
|
/*
|
|
Take the container back from the worker that filled it before reading:
|
|
the engine accounts what we read to TABLE::in_use. Safe without further
|
|
locking -- claim_next_result() saw 'done', which that worker published
|
|
under LOCK_data as the last thing it did to this container.
|
|
*/
|
|
TABLE *t= next->container->table;
|
|
t->in_use= manager->thd;
|
|
t->file->rebind_to_thread();
|
|
if (int err= t->file->ha_rnd_init(true))
|
|
{
|
|
t->file->print_error(err, MYF(0));
|
|
DBUG_RETURN(1);
|
|
}
|
|
cur= next;
|
|
scan_open= true;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@brief
|
|
End the open scan. The containers are freed just after the workers are
|
|
reaped, and a scan left open would be one the manager still holds on a table
|
|
about to go.
|
|
*/
|
|
|
|
void pwt_tmp_table_source::release_position()
|
|
{
|
|
if (cur && scan_open)
|
|
cur->container->table->file->ha_rnd_end();
|
|
cur= nullptr;
|
|
scan_open= false;
|
|
}
|
|
|
|
|
|
/*
|
|
@brief
|
|
Create our transport. Only temporary table transport supported at this time
|
|
pwt_batch_source left commented out for now.
|
|
*/
|
|
|
|
pwt_row_source *pwt_create_transport(THD *thd, pwt_manager *mgr,
|
|
uint n_workers, uint reclength)
|
|
{
|
|
pwt_tmp_table_source *src= new (thd->mem_root) pwt_tmp_table_source;
|
|
if (!src || src->init(thd, mgr, n_workers, reclength))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(pwt_tmp_table_source));
|
|
return nullptr;
|
|
}
|
|
return src;
|
|
}
|