1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

PQ: Code tidy ups and fixups of tidyups.

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.
This commit is contained in:
Rex Johnston
2026-09-15 14:49:30 +12:00
parent 225fc007b3
commit 1b42e282d6
10 changed files with 68 additions and 346 deletions

View File

@@ -16,6 +16,7 @@
--source include/not_embedded.inc
--source include/have_sequence.inc
--source include/have_debug.inc
--source include/innodb_stable_estimates.inc
--disable_service_connection

View File

@@ -73,7 +73,7 @@ Parallel_queries_executed 0
SELECT JSON_EXTRACT(trace, '$**.parallel_scan_declined_because') AS why
FROM information_schema.optimizer_trace;
why
["an index supplies the ORDER BY order, so we can't split rows", "no table this plan reads can be divided"]
["an index supplies the ORDER BY order, rows aren't splittable", "no table this plan reads can be divided"]
SET SESSION optimizer_trace= @save_trace;
SET SESSION parallel_worker_threads=default;
DROP TABLE t1;

View File

@@ -26,7 +26,7 @@ why
["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", "the plan needs a temporary table and the workers cannot pre-aggregate into it"]
# an index supplying the ORDER BY
why
["an index supplies the ORDER BY order, so we can't split rows", "no table this plan reads can be divided"]
["an index supplies the ORDER BY order, rows aren't splittable", "no table this plan reads can be divided"]
# a sort this thread cannot do over its container
why
["the manager cannot sort what the workers produce (see pwt_manager_sort_order)", "no table this plan reads can be divided"]

View File

@@ -196,48 +196,6 @@ bool jointab_can_be_parallel_scanned(JOIN_TAB *tab, bool trace)
}
/**
@brief
Trace our parallel scan table option.
*/
void trace_parallel_scan_options(JOIN *join)
{
if (unlikely(join->thd->trace_started()))
{
JOIN_TAB *scan_tab= first_linear_tab(join, WITH_BUSH_ROOTS,
WITHOUT_CONST_TABLES);
JOIN_TAB *sorted= NULL;
for (uint t= join->const_tables; t < join->table_count; t++)
if (join->join_tab[t].filesort)
{
sorted= join->join_tab + t;
break;
}
const bool divisible= jointab_can_be_parallel_scanned(scan_tab, false) &&
!sorted;
Json_writer_object trace_pscan(join->thd);
if (divisible)
trace_pscan.add("chosen_for_parallel_scan", scan_tab->table->alias.c_ptr());
else
{
trace_pscan.add("parallel_scan_abandoned",
scan_tab ? scan_tab->table->alias.c_ptr() : "");
/* Named so it can be grepped for: "cause" is a common trace key. */
trace_pscan.add("parallel_scan_abandoned_because",
sorted ?
"the rows would have to reach the join sorted" :
join->ordered_index_usage == join->ordered_index_group_by ?
"an index now supplies the GROUP BY order" :
join->ordered_index_usage == join->ordered_index_order_by ?
"an index now supplies the ORDER BY order" :
"the access path can no longer be divided in chunks");
}
}
}
/**
@brief
The whole condition this table has to be filtered by, whatever the plan did
@@ -2105,6 +2063,31 @@ bool pwt_manager::setup_worker_preagg(THD *thd, pwt_worker *worker)
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
@@ -2230,11 +2213,14 @@ static enum_nested_loop_state pwt_end_send(JOIN *join, JOIN_TAB *join_tab,
if (pwt_self->manager->row_layout().grouped)
{
THD *thd= pwt_self->thd;
// record local memory used so that if a HEAP->Aria conversion happens...
/*
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 (unlikely(owed)) // we can account for it here
if (owed)
{
thd->status_var.local_memory_used= before;
pwt_self->sink->account_spilled_memory(owed);
@@ -2935,8 +2921,8 @@ bool can_parallel_scan_jointab_access(JOIN_TAB *join_tab, bool trace)
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, so we can't split rows"
: "an index supplies the ORDER BY order, so we can't split rows");
? "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,
@@ -3180,13 +3166,15 @@ void parallel_join_check(JOIN *join)
@brief
Called from do_select() in sql_select.cc, run either the worker side
join, the scan only in the workers, or return NESTED_LOOP_DECLINED
join, the scan only in the workers, or set declined
*/
enum_nested_loop_state do_select_parallel(JOIN *join)
enum_nested_loop_state do_select_parallel(JOIN *join, bool *declined)
{
*declined= true;
if (!join->worker_side_parallel)
return NESTED_LOOP_DECLINED;
return NESTED_LOOP_ERROR;
/*
PROTOTYPE, behind debug_dbug='+d,pwt_scan_only'. The workers are a
@@ -3197,12 +3185,15 @@ enum_nested_loop_state do_select_parallel(JOIN *join)
*/
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;
if (run_scan_only_workers(join, scan_tab) > 0)
return NESTED_LOOP_ERROR;
return NESTED_LOOP_DECLINED;
rc= run_scan_only_workers(join, scan_tab);
if (rc == 0)
return NESTED_LOOP_OK;
*declined= false;
return NESTED_LOOP_ERROR;
}
else
{
@@ -3215,11 +3206,14 @@ enum_nested_loop_state do_select_parallel(JOIN *join)
*/
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_DECLINED;
return NESTED_LOOP_ERROR;
}

View File

@@ -394,6 +394,7 @@ bool pwt_row_layout::build(THD *thd, JOIN *join_arg, TABLE **tables,
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++;
@@ -438,7 +439,7 @@ bool pwt_row_layout::build_aggregates(THD *thd, ORDER *plan_group)
if (whole_table)
{
DBUG_ASSERT(!n_group);
Item *one= new (thd->mem_root) Item_int(thd, (longlong) 1, 1);
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));
@@ -454,13 +455,8 @@ bool pwt_row_layout::build_aggregates(THD *thd, ORDER *plan_group)
}
if (whole_table)
{
List_iterator_fast<Item> di(result_defn);
Item *defn_item= nullptr;
for (uint j= 0; j < result_defn.elements; j++)
defn_item= di++; // the constant, pushed last
bzero((char *) &group_defn[0], sizeof(ORDER));
group_defn[0].item_ptr= defn_item;
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;
@@ -555,7 +551,7 @@ bool pwt_row_layout::make_container_from(THD *thd, List<Item> &defn,
ORDER *group)
{
/*
out needs it's own TMP_TABLE_PARAM because
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.
@@ -1159,252 +1155,3 @@ pwt_row_source *pwt_create_transport(THD *thd, pwt_manager *mgr,
}
return src;
}
#if 0
/*
pwt_batch* classes implement streaming from workers to the manager
they may be useful in the future, so left here.
*/
bool pwt_batch_sink::init(pwt_manager *mgr, pwt_batch_source *peer_arg,
uint reclength_arg)
{
manager= mgr;
peer= peer_arg;
reclength= reclength_arg;
count= 0;
full= false;
rows= (uchar*) my_malloc(key_memory_pwt_batch_rows,
(size_t) PWT_ROW_GANULARITY * reclength, MYF(MY_WME));
return rows == nullptr;
}
void pwt_batch_sink::cleanup()
{
my_free(rows);
rows= nullptr;
}
/**
@brief
Hand this worker's filled buffer to the manager.
@description
Marks the buffer ready and blocks until the manager has drained it (clears
'full') or asks the producers to stop. On return the buffer is the worker's
again: either to refill, or to abandon.
This is the only place a worker waits for the manager, and it is what the
temporary-table transport exists to remove.
@return
true the consumer asked us to stop
false the buffer was drained; refill it
*/
bool pwt_batch_sink::handoff()
{
DBUG_ENTER("pwt_batch_sink::handoff");
mysql_mutex_lock(&manager->LOCK_data);
if (manager->workers_must_stop)
{
mysql_mutex_unlock(&manager->LOCK_data);
DBUG_RETURN(true);
}
full= true;
mysql_cond_signal(&manager->COND_data_avail); // wake the consumer
while (full && !manager->workers_must_stop)
{
mysql_cond_wait(&peer->COND_data_space, &manager->LOCK_data);
DBUG_PRINT("info", ("worker wakes"));
}
bool stopped= manager->workers_must_stop;
mysql_mutex_unlock(&manager->LOCK_data);
DBUG_RETURN(stopped);
}
/*
@brief
Take one finished row: copy its record image into the buffer, handing the
buffer over when it fills.
*/
int pwt_batch_sink::emit_row(const uchar *rec)
{
memcpy(rows + (size_t) count * reclength, rec, reclength);
if (++count == PWT_ROW_GANULARITY)
{
if (handoff()) // manager asked us to stop
return PWT_EMIT_STOP;
count= 0; // buffer drained; refill
}
return PWT_EMIT_OK;
}
/*
Hand over the final partial buffer. A stop arriving now is not interesting:
this producer has finished anyway.
*/
bool pwt_batch_sink::flush()
{
if (count)
handoff();
return false;
}
/*****************************************************************************
pwt_batch_source -- the consuming end of the batch transport
*****************************************************************************/
bool pwt_batch_source::init(THD *thd, pwt_manager *mgr, uint n_workers,
uint reclength_arg)
{
manager= mgr;
n_sinks= n_workers;
reclength= reclength_arg;
cur= nullptr;
cur_cursor= 0;
if (!(sinks= thd->alloc<pwt_batch_sink*>(n_workers)))
return true;
for (uint i= 0; i < n_workers; i++)
sinks[i]= nullptr;
mysql_cond_init(key_COND_pwt_data_space, &COND_data_space, nullptr);
inited= true;
return false;
}
pwt_row_sink *pwt_batch_source::make_sink(THD *thd, uint worker_nr,
pwt_row_container *container)
{
DBUG_ASSERT(worker_nr < n_sinks);
/*
Not used: this transport copies the record out of the container rather than
keeping it, so it needs only the size, which it has from the layout.
*/
(void) container;
pwt_batch_sink *s= new (thd->mem_root) pwt_batch_sink;
if (!s || s->init(manager, this, reclength))
{
my_error(ER_OUTOFMEMORY, MYF(0),
(int) (PWT_ROW_GANULARITY * reclength));
return nullptr;
}
sinks[worker_nr]= s;
return s;
}
/*
Release every producer blocked waiting for its buffer back, so it sees the
manager's stop request. The caller sets that request; this only wakes them.
*/
void pwt_batch_source::wake_producers()
{
mysql_mutex_assert_owner(&manager->LOCK_data);
mysql_cond_broadcast(&COND_data_space);
}
void pwt_batch_source::cleanup()
{
if (inited)
{
mysql_cond_destroy(&COND_data_space);
inited= false;
}
}
/*
@brief
Copy the next result row's record image into dst.
@description
Drains one worker's buffer at a time (cur), advancing cur_cursor through
its rows; when the buffer is exhausted it releases that worker to refill
and picks the next ready one. Blocks when no buffer is momentarily ready.
This is also the manager's only wait, so it is where the team's own state
is noticed: a worker killed is propagated to the manager's THD, a worker
error aborts, and "no buffer ready and nobody still running" is end of
data.
@return
0 = row copied into dst, -1 = end of data, 1 = error.
*/
int pwt_batch_source::next_row(uchar *dst)
{
DBUG_ENTER("pwt_batch_source::next_row");
THD *thd= manager->thd;
struct timespec wait;
wait.tv_nsec= 0;
for (;;)
{
if (cur) // draining a worker's buffer
{
if (cur_cursor < cur->count)
{
memcpy(dst, cur->rows + (size_t) cur_cursor * reclength, reclength);
cur_cursor++;
DBUG_RETURN(0);
}
// buffer drained; release the worker so it can refill
mysql_mutex_lock(&manager->LOCK_data);
pwt_batch_sink *drained= cur;
cur= nullptr;
drained->full= false; // buffer is the worker's again
mysql_cond_broadcast(&COND_data_space); // wake it to refill
mysql_mutex_unlock(&manager->LOCK_data);
// fall through and look for the next ready worker
}
// find the next worker whose buffer is filled and ready
pwt_batch_sink *next= nullptr;
PSI_stage_info old_stage;
mysql_mutex_lock(&manager->LOCK_data);
for (;;)
{
for (uint i= 0; i < n_sinks; i++)
if (sinks[i] && sinks[i]->full)
{
next= sinks[i];
break;
}
if (next)
break;
int res;
if ((res= manager->locked__process_manager_wakeup()) ||
(res=(thd->killed != NOT_KILLED)))
{
mysql_mutex_unlock(&manager->LOCK_data);
DBUG_RETURN(res);
}
// wait for a batch, 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
}
cur= next;
cur_cursor= 0; // start of next's buffer
mysql_mutex_unlock(&manager->LOCK_data);
// loop back and drain cur
}
}
#endif

View File

@@ -532,8 +532,7 @@ extern bool can_parallel_scan_jointab_access(JOIN_TAB *join_tab,
bool trace= false);
extern bool pwt_decline(JOIN *join, bool trace, const char *why);
extern bool pwt_scan_only_enabled();
extern void trace_parallel_scan_options(JOIN *join);
extern void parallel_join_check(JOIN *join);
extern enum_nested_loop_state do_select_parallel(JOIN *join);
extern enum_nested_loop_state do_select_parallel(JOIN *join, bool *declined);
#endif

View File

@@ -3546,9 +3546,6 @@ int JOIN::optimize_stage2()
}
}
if (worker_side_parallel)
trace_parallel_scan_options(this);
if (having)
having_is_correlated= MY_TEST(having->used_tables() & OUTER_REF_TABLE_BIT);
tmp_having= having;
@@ -24292,6 +24289,7 @@ do_select(JOIN *join, Procedure *procedure)
}
else
{
bool declined= false;
DBUG_EXECUTE_IF("show_explain_probe_do_select",
if (dbug_user_var_equals_int(join->thd,
"show_explain_probe_select_id",
@@ -24313,7 +24311,8 @@ do_select(JOIN *join, Procedure *procedure)
JOIN_TAB *join_tab= join->join_tab +
(join->tables_list ? join->const_tables : 0);
if ((error= do_select_parallel(join)) == NESTED_LOOP_DECLINED)
error= do_select_parallel(join, &declined);
if (declined)
{
if (join->outer_ref_cond && !join->outer_ref_cond->val_bool())
error= NESTED_LOOP_NO_MORE_ROWS;

View File

@@ -444,8 +444,7 @@ enum enum_nested_loop_state
{
NESTED_LOOP_KILLED= -2, NESTED_LOOP_ERROR= -1,
NESTED_LOOP_OK= 0, NESTED_LOOP_NO_MORE_ROWS= 1,
NESTED_LOOP_QUERY_LIMIT= 3, NESTED_LOOP_CURSOR_LIMIT= 4,
NESTED_LOOP_DECLINED= 5
NESTED_LOOP_QUERY_LIMIT= 3, NESTED_LOOP_CURSOR_LIMIT= 4
};

View File

@@ -808,14 +808,15 @@ dberr_t Parallel_scan_partitioner::Scan_ctx::create_chunk(const Bounds &bounds,
return (err);
}
dberr_t Parallel_scan_partitioner::Scan_ctx::create_chunks_unsplit()
dberr_t Parallel_scan_partitioner::Scan_ctx::create_chunks_unsplit(bool resplit,
size_t split_point)
{
size_t i{};
for (auto bounds : m_bounds_list) {
const bool last = (i + 1 == m_bounds_list.size());
if (dberr_t err= create_chunk(bounds, false,
if (dberr_t err= create_chunk(bounds, resplit && (i >= split_point),
last && m_config.m_scan_range.m_end_inclusive))
return err;
@@ -901,27 +902,7 @@ dberr_t Parallel_scan_partitioner::Scan_ctx::create_chunks()
}
}
size_t i{};
for (auto bounds : m_bounds_list) {
/* Only the last chunk ends at the caller's upper bound, so only it can
be inclusive. */
const bool last = (i + 1 == m_bounds_list.size());
auto err = create_chunk(bounds, i >= split_point,
last && m_config.m_scan_range.m_end_inclusive);
if (err != DB_SUCCESS) {
return (err);
}
++i;
}
m_bounds_list.clear();
m_bounds_list.shrink_to_fit();
return DB_SUCCESS;
return create_chunks_unsplit(true, split_point);
}
dberr_t Parallel_scan_partitioner::add_scan(
@@ -1016,7 +997,7 @@ dberr_t Parallel_scan_partitioner::create_chunks()
scan_ctx->coalesce_bounds(std::max(share, size_t{1}));
if (dberr_t err = scan_ctx->create_chunks_unsplit())
if (dberr_t err = scan_ctx->create_chunks_unsplit(false, 0))
return err;
}

View File

@@ -475,7 +475,8 @@ class Parallel_scan_partitioner::Scan_ctx {
the division was decided knowing every range's size, so there is nothing
left for a worker to correct.
@return DB_SUCCESS or error code. */
[[nodiscard]] dberr_t create_chunks_unsplit();
[[nodiscard]] dberr_t create_chunks_unsplit(bool resplit,
size_t split_point);
/** Walk one level further down, to the children of the sub-trees this
range's boundaries name, and keep what that finds in place of them.
@@ -484,11 +485,12 @@ class Parallel_scan_partitioner::Scan_ctx {
wants -- coalesce_bounds() cuts them back down. What the walk is for is the
count: the pages a range covers is how much work it holds, and the only
measure of that the tree offers without descending into it.
NOTE: MAX_PARALLEL_SCAN_RANGES = 128 see sql/sql_parallel_execution.cc
@return DB_SUCCESS or error code. */
[[nodiscard]] dberr_t partition_deeper();
/** Merge this range's boundaries into runs, leaving it with at most
'target' chunks. Adjacent chunks are contiguous intervals sharing an
2x'target'-1 chunks. Adjacent chunks are contiguous intervals sharing an
endpoint, so a run of them merges into the first one's start and the last
one's end.
@param[in] target the most chunks this range should produce. */