Read-in-order over query plan#42829
Conversation
| LimitBy | ||
| Expression (Before LIMIT BY) | ||
| Union | ||
| Union |
There was a problem hiding this comment.
Changed after liftUpUnion
| SELECT a FROM t_max_rows_to_read WHERE a > 10 ORDER BY a LIMIT 5 SETTINGS max_rows_to_read = 12; | ||
| SELECT a FROM t_max_rows_to_read WHERE a = 10 OR a = 20 SETTINGS max_rows_to_read = 12; |
There was a problem hiding this comment.
Now, both of this queries works.
| Sorting (Stream): a ASC | ||
| Sorting (Stream): a ASC |
There was a problem hiding this comment.
Now, read-in-order optimization happens after plan is built.
Current implementation does not propagate sorting property upwards, so it was changed only for reading step.
| -- enable optimization -> sorting order is propagated from subquery -> merge sort | ||
| -- QUERY: set optimize_sorting_by_input_stream_properties=1;set max_threads=1;EXPLAIN PIPELINE SELECT a FROM (SELECT a FROM optimize_sorting) ORDER BY a | ||
| MergeSortingTransform | ||
| MergingSortedTransform 3 → 1 |
There was a problem hiding this comment.
From the comment, there should have been MergingSortedTransform initially. MergeSortingTransform is a full sort.
It was magically fixed.
| Sorting (Sorting for ORDER BY) | ||
| Sorting (Global): a ASC | ||
| Sorting (Stream): a ASC | ||
| Sorting (Chunk): a ASC, b ASC |
There was a problem hiding this comment.
This is an incorrect property, but it won't be used cause plan is already built.
We should probably fix it later.
| ReadFromStorage | ||
| Header: dummy UInt8 | ||
| Expression | ||
| Union |
There was a problem hiding this comment.
Changed after liftUpUnion
|
Tsan should be fixed in #43009 |
|
Something is wrong with performance tests, probably it does not work for such queries https://s3.amazonaws.com/clickhouse-test-reports/42829/7ac258c2a77fa813052ac67cc67977b05368fa1d/performance_comparison_[4/4]/report.html. |
CurtizJ
left a comment
There was a problem hiding this comment.
In general ok, but let's wait for review from someone who is more familiar with code near plan optimizations.
| return reading; | ||
| } | ||
|
|
||
| if (auto * merge = typeid_cast<ReadFromMerge *>(step)) |
There was a problem hiding this comment.
Will it be supported for other storages like Buffer and MaterializedView, because they have a ReadFromMergeTree inside plan, which is built to read from those storages, right?
There was a problem hiding this comment.
Yes. For MV it is naturally supported. For Buffer I had to support a simple optimization with union.
Anyway, we had a test for such engines.
| using FixedColumns = std::unordered_set<const ActionsDAG::Node *>; | ||
|
|
||
| /// Right now we find only simple cases like 'and(..., and(..., and(column = value, ...), ...' | ||
| void appendFixedColumnsFromFilterExpression(const ActionsDAG::Node & filter_expression, FixedColumns & fiexd_columns) |
| else if (name == "equals") | ||
| { | ||
| const ActionsDAG::Node * maybe_fixed_column = nullptr; | ||
| bool is_singe = true; |
| if (!sort_column_node) | ||
| break; | ||
|
|
||
| if (!dag) |
There was a problem hiding this comment.
Is it possible? Shouldn't we build dag just for input columns even without any functions?
There was a problem hiding this comment.
It is possible. This DAG is from SELECT. For example, SELECT * FROM tab ORDER BY a, b will not have any expression steps, and DAG will be empty.
| auto it = matches.find(child); | ||
| if (it == matches.end()) | ||
| { | ||
| stack.push(Frame{child, {}}); | ||
| break; | ||
| } |
There was a problem hiding this comment.
Need a comment why do we do this if node is not in matches.
|
|
||
| bool found_all_children = true; | ||
| for (const auto * child : frame.mapped_children) | ||
| if (!child) |
There was a problem hiding this comment.
How is it possible to get nullptr in mapped_children?
There was a problem hiding this comment.
Yes. Will write a comment.
| /// This structure stores a node mapping from one DAG to another. | ||
| /// The rule is following: | ||
| /// * Input nodes are mapped by name. | ||
| /// * Function is mapped to function if all children are mapped and function names are same. |
There was a problem hiding this comment.
Does it mean that function is mapped to itself?
There was a problem hiding this comment.
Yes, but from different DAGs.
I mean, this is the idea - to find equal calculations if DAGs
|
Lol, one query from Looks like ordinary read worked faster cause condition on pk is good and we full sort not so many data. parallel execution wins in this case. |
|
And the same for a query from But in this case it's harder to explain why. Probably, computation of |
But in the second case: EXPLAIN PIPELINE
SELECT *
FROM datasets.hits_100m_obfuscated
WHERE UserID = 1988954671305023629
ORDER BY
CounterID ASC,
EventDate ASC
LIMIT 100
SETTINGS query_plan_read_in_order = 0
Query id: 73f61e3e-89ac-488c-97b7-2522428895b7
┌─explain─────────────────────────────┐
│ (Expression) │
│ ExpressionTransform │
│ (Limit) │
│ Limit │
│ (Sorting) │
│ MergingSortedTransform 16 → 1 │
│ (Expression) │
│ ExpressionTransform × 16 │
│ (ReadFromMergeTree) │
│ MergeTreeInOrder × 16 0 → 1 │
└─────────────────────────────────────┘
reading in order also worked, because we have |
|
The case with In that case reading in order was not enable previously, because the chain of monotonic function was not supported. |
kitaisreal
left a comment
There was a problem hiding this comment.
Everything is good. Just added small comments for clarification.
| && !query.final() | ||
| && join_allow_read_in_order; | ||
|
|
||
| if (storage && optimize_read_in_order) |
There was a problem hiding this comment.
This was a fix of a bug from #39157
I was reproduced again with a new implementation, and I had to fix it in a different way.
| if (getContext()->getSettingsRef().allow_experimental_analyzer) | ||
| { | ||
| InterpreterSelectQueryAnalyzer interpreter(ast.getExplainedQuery(), options, getContext()); | ||
| context = interpreter.getContext(); |
There was a problem hiding this comment.
Why we use context from interpreter, not context from EXPLAIN QUERY ?
There was a problem hiding this comment.
There was a stupid bug that some settings were not applied for EXPLAIN.
InterpreterSelectQuery copied context and changed settings only for it, and the following plan.optimize did not see this settings change.
I don't know if this is applied to InterpreterSelectQueryAnalyzer as well, but I think it's better to copy context anyway.
| for (const auto & key_name : key_names) | ||
| order_descr.emplace_back(key_name); | ||
|
|
||
| SortingStep::Settings sort_settings; |
There was a problem hiding this comment.
Consider to add constructor in SortingStep::Settings from Settings. Currently such construction is worse then was before, because you can easy do not initialize some setting, and there will be no error.
src/Planner/Planner.cpp
Outdated
|
|
||
| const Settings & settings = query_context->getSettingsRef(); | ||
|
|
||
| SortingStep::Settings sort_settings; |
There was a problem hiding this comment.
This is copy pases several times in Interpreter and Planner, need to be extracted in separate method, or added constructor.
| size_t tryDistinctReadInOrder(QueryPlan::Node * node, QueryPlan::Nodes & nodes); | ||
| size_t tryDistinctReadInOrder(QueryPlan::Node * node); | ||
|
|
||
| /// Put some steps under union, so that plan optimisation could be applied to union parts separately. |
There was a problem hiding this comment.
Need example in comment. It is impossible to understand what some steps are and what we can expect from this function to do with query plan, without reading its internals.
| using Matches = std::unordered_map<const ActionsDAG::Node *, Match>; | ||
| }; | ||
|
|
||
| MatchedTrees::Matches matchTrees(const ActionsDAG & inner_dag, const ActionsDAG & outer_dag) |
There was a problem hiding this comment.
This function is generally well written, we need to just add some internal comments, for each step that follows documentation above.
There was a problem hiding this comment.
I consider moving it somewhere to common interface, but likely will do it later (after aggregation-in-order)
| /// | ||
| /// So far, 0 means any direction is possible. It is ok for constant prefix. | ||
| int read_direction = 0; | ||
| size_t next_descr_column = 0; |
There was a problem hiding this comment.
Consider to rename next_descr_column into next_description_column.
| while (next_descr_column < description.size() && next_sort_key < sorting_key_columns.size()) | ||
| { | ||
| const auto & sorting_key_column = sorting_key_columns[next_sort_key]; | ||
| const auto & descr = description[next_descr_column]; |
There was a problem hiding this comment.
Consider to rename descr into sort_column_description.
| const auto & sorting_key = reading->getStorageMetadata()->getSortingKey(); | ||
| const auto & sorting_key_columns = sorting_key.column_names; | ||
|
|
||
| return buildInputOrderInfo( |
There was a problem hiding this comment.
Probably if we decide to split this function call in multiple lines, better to have each function argument on the same line.
There was a problem hiding this comment.
Or on the different lines.
|
|
||
| /// This optimisation is obsolete and will be removed. | ||
| /// optimizeReadInOrder covers it. | ||
| size_t tryReuseStorageOrderingForWindowFunctions(QueryPlan::Node * parent_node, QueryPlan::Nodes & /*nodes*/) |
There was a problem hiding this comment.
Need to remove this. It should be supported by default from code above, and even better.
There was a problem hiding this comment.
I like to have it till next stable release just in case.
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
Implement
read-in-orderoptimization on top of query plan. It is enabled by default. Setquery_plan_read_in_order = 0to use previous AST-based version.