Skip to content

Reduce memory consumption by mutations with big subqueries used with IN#46835

Merged
davenger merged 23 commits intomasterfrom
reduce_mem_in_mutation_with_subquery
Apr 17, 2023
Merged

Reduce memory consumption by mutations with big subqueries used with IN#46835
davenger merged 23 commits intomasterfrom
reduce_mem_in_mutation_with_subquery

Conversation

@davenger
Copy link
Copy Markdown
Member

@davenger davenger commented Feb 24, 2023

Changelog category (leave one):

  • Improvement

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 t has multiple parts than for each part a set for subquery SELECT id FROM huge_table is 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

  • Documentation is written (mandatory for new features)

Information about CI checks: https://clickhouse.com/docs/en/development/continuous-integration/

@davenger davenger marked this pull request as draft February 24, 2023 16:37
@robot-ch-test-poll2 robot-ch-test-poll2 added the pr-not-for-changelog This PR should not be mentioned in the changelog label Feb 24, 2023
@UnamedRus
Copy link
Copy Markdown
Contributor

UnamedRus commented Feb 24, 2023

May be, we can have it under setting?

@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch 5 times, most recently from 3429ce6 to 99eb9e1 Compare March 2, 2023 21:49
@davenger davenger changed the title [WIP]Don't use large sets in KeyCondition [WIP]Reduce memory consumption by mutations with big subqueries used with IN Mar 3, 2023
@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch 2 times, most recently from 8f75251 to 3bd503e Compare March 9, 2023 22:09
@vdimir vdimir self-assigned this Mar 10, 2023
if (SetPtr set = data.prepared_sets->get(set_key))
return set;

if (data.prepared_sets_cache)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have several ongoing mutations? Here

std::map<UInt64, MergeTreeMutationEntry> current_mutations_by_version;
we have a map for mutations, probably the same here 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed this to a map

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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth to add cases for other types of mutations

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a testcase with different mutations

@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch from 35ec895 to 3bad593 Compare April 4, 2023 10:02
@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch 8 times, most recently from d21547a to 803d544 Compare April 6, 2023 11:23
@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch 4 times, most recently from 9c6f022 to d911b8c Compare April 11, 2023 21:42
@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch 2 times, most recently from 8c8cd26 to 279ac65 Compare April 14, 2023 14:45
@davenger davenger changed the title [WIP]Reduce memory consumption by mutations with big subqueries used with IN Reduce memory consumption by mutations with big subqueries used with IN Apr 14, 2023
@davenger davenger added pr-improvement Pull request with some product improvements and removed pr-not-for-changelog This PR should not be mentioned in the changelog force tests labels Apr 14, 2023
@davenger davenger marked this pull request as ready for review April 14, 2023 15:17
@davenger
Copy link
Copy Markdown
Member Author

May be, we can have it under setting?

Resurrected use_index_for_in_with_subqueries_max_values setting.

@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch from 279ac65 to 034dce5 Compare April 14, 2023 18:08
@davenger
Copy link
Copy Markdown
Member Author

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())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where promise_to_build_set should be assigned?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it may fail?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we supposed to lock here and wait until builing is finished?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the key difference between this code and similar in CreatingSetsTransform? No loop here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

@vdimir vdimir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@davenger davenger force-pushed the reduce_mem_in_mutation_with_subquery branch from b9fc9f1 to fc4fd3e Compare April 17, 2023 16:24
@davenger davenger merged commit ba5ca15 into master Apr 17, 2023
@davenger davenger deleted the reduce_mem_in_mutation_with_subquery branch April 17, 2023 21:55
@tavplubix
Copy link
Copy Markdown
Member

@davenger, the new tests are flaky with analyzer, please take a look: link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-improvement Pull request with some product improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants