MDEV-21879 GROUP_CONCAT(DISTINCT ORDER BY) is wrong when Unique spills - #5574
MDEV-21879 GROUP_CONCAT(DISTINCT ORDER BY) is wrong when Unique spills#5574arcivanov wants to merge 2 commits into
Conversation
GROUP_CONCAT() with ORDER BY collects all rows of the group in a TREE and only cuts it down in repack_tree(). The repack was triggered by (tree_len >> GCONCAT_REPACK_FACTOR) > thd->gconcat_max_len() with GCONCAT_REPACK_FACTOR 10, that is when the rows in the tree had produced 1024 * group_concat_max_len bytes, or 1G with the default settings. On top of that tree_len only counted the length of the strings, while the tree costs sizeof(TREE_ELEMENT) + reclength per row. For GROUP_CONCAT(int_col ORDER BY int_col) that is about 40 bytes per row against 6 bytes of result, so the tree had grown to several GB before the first repack. In practice the server ran out of memory first and the repack code was close to never used. The tree is now limited by the memory it has really allocated, tree->allocated, instead of by the length of the strings it holds. The limit is MY_MAX(thd->ram_limitation(), thd->gconcat_max_len()) and is never set so low that the tree can not hold a few rows. repack_tree() builds a new tree while the old one is still in memory, so the peak usage is the size we start the repack at plus the size we copy to. To keep the sum within the limit it is split into GCONCAT_TREE_PARTS parts; the repack starts when GCONCAT_TREE_REPACK_PARTS of them are used and copies to the remaining part. The part we do not copy to is also the room the tree has to grow before the next repack, which keeps the repacks amortized. Other changes: - tree_len is removed. It was only read by the old trigger. - repack_tree() decided that it had run out of memory by testing st.len <= st.maxlen after the walk. That test was only valid because the old trigger guaranteed that a complete copy had to overshoot st.maxlen. A repack triggered by memory can complete the walk with st.len far below st.maxlen, which would have failed the query with a wrong out of memory error. There is now an explicit flag for it. - The length that decides which rows to keep now also counts the separator that is put between two rows, so that it matches what val_str() will produce. - When the memory limit stops the copy, the result becomes shorter than group_concat_max_len. dump_leaf_key() can not detect this, as the result never reaches the maximum length. This is now remembered in result_cut and reported to the user. - All cut value reporting is moved to val_str(); dump_leaf_key() only marks that the result was cut. This removes the need to clear the truncated flag of table->blob_storage to avoid a duplicated warning, and gives one warning per group also when val_str() is called more than once for the same group, which repeated the warning before. - Added a function comment for repack_tree() that describes where the rows are cut away and why building a copy frees memory.
`Item_func_group_concat::add()` decided whether a row was a duplicate
by checking whether `Unique::elements_in_tree()` had grown after
`unique_add()`:
uint count= unique_filter->elements_in_tree();
unique_filter->unique_add(get_record_pointer());
if (count == unique_filter->elements_in_tree())
row_eligible= FALSE;
`Unique` flushes its whole in-memory tree to disk when it runs out of
memory, and `elements_in_tree()` only counts what is still in memory.
After the first flush the test says nothing about the rows that were
already spilled.
**MDEV-11563** made this harmless for `GROUP_CONCAT(DISTINCT x)` by
building the result in `val_str()` from `unique_filter->walk()`, which
merges the spilled parts back in. It left the `ORDER BY` case alone.
There the result comes from the sort tree, which `add()` fills gated by
`row_eligible`, so the defect is still fully live.
Both directions of the failure are reachable, depending on how often
the filter flushes relative to the insert:
1. Duplicates reach the result. 100 rows holding 50 distinct values
give all 100 values back.
2. Rows are lost. 30 distinct rows of 2000 bytes give one value back.
`JSON_ARRAYAGG(DISTINCT x ORDER BY y)` fails in the same way.
Fixed by not filling the sort tree from `add()` when `DISTINCT` is
used. `val_str()` now walks the merged `unique_filter` into the sort
tree and then walks the sort tree, so the rows are sorted after the
duplicate filtering is complete instead of during it.
`Unique::walk()` merges everything it flushed, so the sort tree can be
handed more rows than fit in memory. `insert_to_order_tree()` repacks
it on the same memory budget `add()` used, and a walk that runs out of
memory sets `result_cut`, so the user gets a cut value warning rather
than a silently short result.
**Behaviour change.** `ORDER BY` does not order rows that tie on the
ordering expression, and which of them comes first changes here. It
used to follow the order the rows were read in; it now follows the
order the duplicate filter keeps them in. Unlike the old order, the new
one depends on neither the memory available nor the physical row order.
`main.gconcat_distinct_spill` checks that, and `main.func_gconcat`
records one such tie.
gkodinov
left a comment
There was a problem hiding this comment.
Thank you for your contribution! This is a preliminary review.
LGTM. One small cleanup proposed.
Please stand by for the final review.
| 00,01,10,11,31 | ||
| select group_concat(distinct a, c order by a) from t1; | ||
| group_concat(distinct a, c order by a) | ||
| 00,01,11,10,31 |
There was a problem hiding this comment.
I'd try to stabilize this test instead of re-recording the new undeterministic order.
There was a problem hiding this comment.
it is deterministic, it's just that determinism has changed.
There was a problem hiding this comment.
See Behavior Change section please
|
BTW, any specific reason this is not based on 12.3 (the lowest affected version according to Jira)? |
Yes, because it depends on other commits that are specifically in bb-blob-main-monty. I'll discuss this with @montywi if/when he is available. There may be further work around GROUP_CONCAT as well (e.g. mem -> HEAP -> Aria spillover mechanisms to eliminate the avoidable truncation of the results). |
37077cc to
5847c1a
Compare
|
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 |
GROUP_CONCAT(DISTINCT x ORDER BY y)andJSON_ARRAYAGG(DISTINCT x ORDER BY y)return a wrong answer once the duplicate filter runs out of memory.
The defect
Item_func_group_concat::add()decides whether a row is a duplicate by checkingwhether
Unique::elements_in_tree()grew afterunique_add().Uniqueflushesits whole in-memory tree to disk when it runs out of memory, and
elements_in_tree()only counts what is still in memory, so after the firstflush that test says nothing about the rows already spilled.
MDEV-11563 made this harmless for
GROUP_CONCAT(DISTINCT x)by building theresult in
val_str()fromunique_filter->walk(), which merges the spilledparts back in. It left the
ORDER BYcase alone, where the result comes from thesort tree that
add()fills gated by the broken test.Both directions of the failure are reachable, depending on how often the filter
flushes relative to the insert:
100 values back.
The fix
add()no longer fills the sort tree whenDISTINCTis used.val_str()walksthe merged
unique_filterinto the sort tree and then walks the sort tree, so therows are sorted after duplicate filtering is complete instead of during it.
Unique::walk()merges everything it flushed, so the sort tree can be handed morerows than fit in memory.
insert_to_order_tree()repacks it on the same memorybudget
add()used, and a walk that runs out of memory setsresult_cutso theuser gets a cut value warning rather than a silently short result.
This targets
bb-blob-main-montyrather thanmainbecause it relies on37077ccef15"Limit the memory used by GROUP_CONCAT() with ORDER BY". Withoutthat commit's
tree->allocatedbound and itsst.oomcorrection torepack_tree(), pouring a spilledUniqueinto the sort tree trades a wronganswer for an out-of-memory error.
Behaviour change
ORDER BYdoes not order rows that tie on the ordering expression, and which ofthem comes first changes here. It used to follow the order the rows were read in;
it now follows the order the duplicate filter keeps them in. Unlike the old order,
the new one depends on neither the memory available nor the physical row order.
main.func_gconcatrecords one such tie and is re-recorded accordingly.Testing
New
main.gconcat_distinct_spillcovers three things, and each assertion wasconfirmed to fail before the fix and pass after, with the test unchanged between
the two runs:
for
GROUP_CONCATandJSON_ARRAYAGG, with and withoutORDER BYrepack_tree()cuts the group, whatevercomes back must still be deduplicated, still ordered, and still valid JSON
order
Full
main+heapsuite: 1436/1436 pass.