Reduce memory consumption by mutations with big subqueries used with IN#46835
Reduce memory consumption by mutations with big subqueries used with IN#46835
Conversation
|
May be, we can have it under setting? |
3429ce6 to
99eb9e1
Compare
8f75251 to
3bd503e
Compare
src/Interpreters/ActionsVisitor.cpp
Outdated
| if (SetPtr set = data.prepared_sets->get(set_key)) | ||
| return set; | ||
|
|
||
| if (data.prepared_sets_cache) |
There was a problem hiding this comment.
| if (data.prepared_sets_cache) | |
| /// Used to share sets for a mutation between MutateTasks for different parts | |
| if (data.prepared_sets_cache) |
| /// NOTE: we only store weak_ptr to PreparedSetsCache, so that the cache is shared between mutation tasks that are executed in parallel. | ||
| /// The goal is to avoiding consuming a lot of memory when the same big sets are used by multiple tasks at the same time. | ||
| /// If the tasks are executed without time overlap, we will destroy the cache to free memory, and the next task might rebuild the same sets. | ||
| std::mutex mutation_prepared_sets_cache_mutex; |
There was a problem hiding this comment.
Is it possible to have several ongoing mutations? Here
ClickHouse/src/Storages/StorageMergeTree.h
Line 149 in c2611c3
| SELECT name FROM system.parts WHERE database=currentDatabase() AND table = '02581_trips' AND active ORDER BY name; | ||
|
|
||
| -- Run mutation with a 'IN big subquery' | ||
| ALTER TABLE 02581_trips UPDATE description='' WHERE id IN (SELECT (number+5)::UInt32 FROM numbers(10000000)) SETTINGS mutations_sync=2; |
There was a problem hiding this comment.
It's worth to add cases for other types of mutations
There was a problem hiding this comment.
added a testcase with different mutations
35ec895 to
3bad593
Compare
d21547a to
803d544
Compare
9c6f022 to
d911b8c
Compare
8c8cd26 to
279ac65
Compare
Resurrected |
279ac65 to
034dce5
Compare
|
Stress test (debug) failure - #48777 |
| explicit PlannerSet(SetPtr set_, QueryTreeNodePtr subquery_node_) | ||
| : set(std::move(set_)) | ||
| explicit PlannerSet(QueryTreeNodePtr subquery_node_) | ||
| : set(promise_to_build_set.get_future()) |
There was a problem hiding this comment.
Where promise_to_build_set should be assigned?
There was a problem hiding this comment.
If I understand the question correctly, the answer is: promise_to_build_set is default-constructed and after this it has a valid internal state so we can get a future from it.
There was a problem hiding this comment.
And it's one set in CreatingSet via promise_to_fill_set.set_value, got it
| const SetPtr & ready_set = set_built_by_another_thread.get(); | ||
| if (!ready_set) | ||
| { | ||
| LOG_TRACE(log, "Failed to use set from cache, key: {}", subquery.key); |
There was a problem hiding this comment.
I described below a case when the Set from ExpressionAnalyzer code path can be not built due to size limits.
| { | ||
| LOG_TRACE(log, "Waiting for set to be build by another thread, key: {}", subquery.key); | ||
| SharedSet set_built_by_another_thread = std::move(std::get<1>(from_cache)); | ||
| const SetPtr & ready_set = set_built_by_another_thread.get(); |
There was a problem hiding this comment.
Are we supposed to lock here and wait until builing is finished?
There was a problem hiding this comment.
Yes, we block here waiting the future value to be set.
| else | ||
| { | ||
| LOG_TRACE(getLogger(), "Waiting for set, key: {}", set_key.toString()); | ||
| set = std::get<1>(from_cache).get(); |
There was a problem hiding this comment.
What's the key difference between this code and similar in CreatingSetsTransform? No loop here
There was a problem hiding this comment.
In Expression Analyzer code the Set is used as an optimization that can be skipped e.g. if the set is too big - we set size limits and break if they are exceeded. So if building the set fails we can continue without it. CreatingSetsTransform expects the Set to be successfully built and cannot continue without it. So there is retry loop in CreatingSetsTransform to handle the case when other thread was building the set from ExpressionAnalyzer code and failed due to limits.
| return true; | ||
| } | ||
|
|
||
| String PreparedSetKey::toString() const |
There was a problem hiding this comment.
Btw, in analyzer, we need to hash the same set differently depending on the type of left operand, check out the comment at PlannerContext::createSetKey in #48754
This solution solves the issue (at least fixes test 00700_decimal_compare, but I'm uncertain if it's not too ad-hoc).
vdimir
left a comment
There was a problem hiding this comment.
I believe I understand the changes, although while reading I found just a bit difficult to follow where the set state changes and where we reference the same future or promise, but I don't think it can be simplified.
Co-authored-by: Vladimir C <[email protected]>
b9fc9f1 to
fc4fd3e
Compare
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
If we run a mutation with IN (subquery) like this:
ALTER TABLE t UPDATE col='new value' WHERE id IN (SELECT id FROM huge_table)and the table
thas multiple parts than for each part a set for subquerySELECT id FROM huge_tableis built in memory. And if there are many parts then this might consume a lot of memory (and lead to an OOM) and CPU.The solution is to introduce a short-lived cache of sets that are currently being built by mutation tasks. If another task of the same mutation is executed concurrently it can lookup the set in the cache, wait for it be be built and reuse it.
Documentation entry for user-facing changes