MDEV-40802 COUNT(DISTINCT <blob>) fails when its tmp table converts - #5561
Open
arcivanov wants to merge 1 commit into
Open
MDEV-40802 COUNT(DISTINCT <blob>) fails when its tmp table converts#5561arcivanov wants to merge 1 commit into
arcivanov wants to merge 1 commit into
Conversation
`COUNT(DISTINCT)` collects the distinct values in a temporary table
with a unique constraint over the aggregate's arguments, and treats a
duplicate key error from the write as "value already seen":
```c
if (!table->file->is_fatal_error(error, HA_CHECK_DUP))
return FALSE; // duplicate, not an error
```
For a blob argument the record holds only a pointer to the value, so
`Aggregator_distinct::setup()` cannot use the `Unique` tree, which
compares raw record bytes, and every value goes through that write
instead.
When such a write overflows the in-memory table,
`create_internal_tmp_table_from_heap()` copies the stored rows to an
on-disk table and then writes the row that overflowed, which until
then was held in `record[0]` alone. Whether a duplicate key error on
that last write is fatal is decided by the caller's
`ignore_last_dupp_key_error` argument, and `Aggregator_distinct::add()`
passed **0** three lines below the code that ignores the very same
condition. The statement failed with
ERROR 1169 (23000): Can't write, because of unique constraint,
to table '(temporary)'
Pass **1** instead, so that a duplicate arriving through the conversion
is discarded exactly like one arriving through the ordinary write. The
result is `table->file->stats.records` of that table, so not storing
the duplicate is what makes the count right.
The argument is the same upstream, where it is unreachable: a
temporary table with a blob column was created on the on-disk engine
to begin with, so the conversion was never entered for the only
tables whose pending row can be a duplicate. Supporting blob columns
in the in-memory engine made the table start in memory and convert.
New tests `heap.count_distinct_blob_convert` and
`heap.count_distinct_blob_convert_debug`.
A write rejected as a duplicate returns its record to the free list and
never reaches the allocation of the blob value, so only the first copy
of a value makes the in-memory table grow, and the write that finds it
full is the second copy of the value stored last. That holds only while
a record slot is what the table runs out of first. Blob values come out
of the same space, and only a write that is not a duplicate ever
allocates one, so when a blob allocation is the one that hits the limit,
the pending row is not a duplicate at all.
Which of the two runs out first follows from how records and blob values
pack together, not from any threshold on the value width. Of 24 measured
combinations of width and `max_heap_table_size`, 20 convert but only 8
reach a duplicate pending row, so asserting that the table was converted
does not establish that the ignored duplicate was reached.
The first test uses widths measured to overflow on a record slot. The
second removes the dependency on that measurement, injecting the
duplicate through a new debug point in
`Tmp_table_default_copier::copy_rows()`, beside the one the row copy
loop already carries. Every value is present twice, so whichever copy
the injected duplicate discards, the other one is still written and the
count does not depend on which write overflowed.
The status counter is read with the in-memory limit restored. The status
table is materialized into a temporary table of its own, and its
VARIABLE_VALUE column is wide enough to be stored as a blob, so under
the shrunken limit that table can overflow and be converted as well, and
would then report its own conversion.
gkodinov
approved these changes
Aug 18, 2026
gkodinov
left a comment
Member
There was a problem hiding this comment.
Thank you for your contribution! This is a preliminary review.
LGTM. Please stand by for the final review.
Member
|
FYI: According to our development cycle we work on bugs In the following periods 15 Mar-30 Apr, 15 Jun-30 Jul, 15 Sep-30 Oct and 15 Dec-31 Jan. So, please, expect to get a review somewhere between these two dates and the goal is to have your PR merged before the second date |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MDEV-40802
COUNT(DISTINCT <blob>)aborts the statement withwhen the temporary table that collects the distinct values outgrows the
in-memory limit and is converted to an on-disk table.
Cause
Aggregator_distinctdeduplicates in one of two ways. For scalar arguments itbuilds a
Uniquetree over raw record bytes. For a blob argument it cannot:the record holds only a pointer to the value, so a raw comparison would compare
addresses.
setup()therefore skips the tree and every value goes through awrite into a temporary table carrying a unique constraint, where a duplicate
key error is the ordinary outcome and is treated as "value already seen":
When such a write overflows the in-memory table instead,
create_internal_tmp_table_from_heap()copies the stored rows to an on-disktable and then writes the row that overflowed, which until then was held in
record[0]alone. Whether a duplicate key error on that last write is fatal isdecided by the caller's
ignore_last_dupp_key_errorargument, andAggregator_distinct::add()passed0three lines below the code that ignoresthe very same condition.
The argument is the same upstream, where it is unreachable: a temporary table
with a blob column was created on the on-disk engine to begin with, so the
conversion was never entered for the only tables whose pending row can be a
duplicate. Supporting blob columns in the in-memory engine made the table start
in memory and convert, which turned a dead argument live.
Fix
Pass
1, so a duplicate arriving through the conversion is discarded exactlylike one arriving through the ordinary write.
endup()reads the result astable->file->stats.records, so not storing the duplicate is what makes thecount right.
The three other call sites that pass
0were checked and are correct: theinformation schema accumulator in
sql_show.cchas no unique constraint atall,
end_update()probes the key before writing, andsql_window.ccusesWindow_rowid_remapper, which replaces a row rather than appending one.Test
heap.count_distinct_blob_convertcovers one blob argument, a blob argument ofa different width, two blob arguments, a blob combined with a non-blob, a
non-blob control that stays on the
Uniquetree, and a grouped aggregate. Eachcase asserts
CREATED_TMP_DISK_TABLES > 0, so a future sizing change fails thetest rather than silently voiding its coverage. A second run at 512M is the
control.
The reproduction window is narrow, for a mechanical reason worth recording. A
write rejected as a duplicate allocates a record, fails the key check and
returns the record to the free list;
hp_write_blobs()runs after the keyloop, so a duplicate never allocates blob space. With short values the record
space is what runs out, duplicates hit it too, and the overflowing row is a
duplicate. With values of a kilobyte or more the blob space runs out first, and
only a write that is not a duplicate ever reaches it, so the overflowing row is
always distinct and the failure cannot be observed at all. The test uses short
values for that reason.
Verification
ER_DUP_UNIQUE. Fixed build: it passes.The fix was backed out and re-applied to confirm both directions.
--suite=main,heap: 1432/1432 pass.--ps-protocol,--cursor-protocoland--view-protocol.