Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions src/Interpreters/ExpressionAnalyzer.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <memory>
#include <stack>

#include <Core/Block.h>
#include <Core/Settings.h>
Expand Down Expand Up @@ -151,6 +152,37 @@ bool allowEarlyConstantFolding(const ActionsDAG & actions, const Settings & sett
return true;
}

/// True when `node` folds a server constant such as `hostName` or `tcpPort`.
/// `__getScalar` is excluded: the initiator evaluates it and sends one value to every shard.
bool containsServerConstant(const ActionsDAG::Node * node)
{
std::stack<const ActionsDAG::Node *> stack;
if (node)
stack.push(node);

while (!stack.empty())
{
const auto * current = stack.top();
stack.pop();

while (current && current->type == ActionsDAG::ActionType::ALIAS)
current = current->children.empty() ? nullptr : current->children.front();

if (!current)
continue;

/// `__getScalar` is marked as a server constant, but the initiator evaluates it
/// and sends the same value to every shard, so it does not have to stay a key.
if (current->function_base && current->function_base->isServerConstant() && current->function_base->getName() != "__getScalar")
return true;

for (const auto * child : current->children)
stack.push(child);
Comment on lines +179 to +180

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Deduplicate DAG nodes during traversal

Because an ActionsDAG can share child nodes, this traversal can revisit the same subtree exponentially many times. For example, a linear sequence of aliases such as plus(x0, x0) AS x1, plus(x1, x1) AS x2, and so on produces a constant DAG where each level references the same child twice; checking the final alias explores roughly 2^N paths when no server constant exists, allowing a compact old-analyzer query to consume an analysis thread indefinitely. Track visited node pointers before pushing children.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The alias chain in this comment does not keep an analysis thread busy. containsServerConstant does not record visited nodes, and an ActionsDAG can store the same child twice, but this example never reaches that walk as a compact graph.

The old analyzer expands aliases in QueryNormalizer before aggregation analysis. The expanded tree is limited by max_expanded_ast_elements (default 500000). A chain 1 AS x0, x0 + x0 AS x1, … throws TOO_BIG_AST at depth 16. Depth 15 finishes in about 0.25s (enable_analyzer = 0). The same SELECT list with GROUP BY 1 takes about 0.15s, so the extra time is the expanded tree, not an unbounded walk of a shared DAG.

Measured on a local server:

Depth Result
8 0.009s
12 0.028s
14 0.120s
15 0.244s
16 TOO_BIG_AST

}

return false;
}

LoggerPtr getLogger() { return ::getLogger("ExpressionAnalyzer"); }

}
Expand Down Expand Up @@ -325,6 +357,9 @@ void ExpressionAnalyzer::analyzeAggregation(ActionsDAG & temp_actions)
else
group_by_kind = GroupByKind::ORDINARY;
bool use_nulls = group_by_kind != GroupByKind::ORDINARY && getContext()->getSettingsRef()[Setting::group_by_use_nulls];
/// Cluster table function workers leave `distributed_depth` at 0, same as a local query.
/// `SECONDARY_QUERY` is what distinguishes them: a server constant differs per shard only there.
const bool is_secondary_query = getContext()->getClientInfo().query_kind == ClientInfo::QueryKind::SECONDARY_QUERY;

/// For GROUPING SETS with multiple groups we always add virtual __grouping_set column
/// With set number, which is used as an additional key at the stage of merging aggregating data.
Expand Down Expand Up @@ -361,7 +396,9 @@ void ExpressionAnalyzer::analyzeAggregation(ActionsDAG & temp_actions)
if (getContext()->getClientInfo().distributed_depth == 0 || settings.distributed_group_by_no_merge > 0)
{
/// Constant expressions have non-null column pointer at this stage.
if (node->column)
/// A server constant stays only on a secondary query, where each shard has its own value.
/// A local query has one value, so the key is removed with other constants.
if (node->column && !(is_secondary_query && containsServerConstant(node)))
{
select_query->group_by_with_constant_keys = true;

Expand Down Expand Up @@ -415,7 +452,9 @@ void ExpressionAnalyzer::analyzeAggregation(ActionsDAG & temp_actions)
if (getContext()->getClientInfo().distributed_depth == 0 || settings.distributed_group_by_no_merge > 0)
{
/// Constant expressions have non-null column pointer at this stage.
if (node->column)
/// A server constant stays only on a secondary query, where each shard has its own value.
/// A local query has one value, so the key is removed with other constants.
if (node->column && !(is_secondary_query && containsServerConstant(node)))
{
select_query->group_by_with_constant_keys = true;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1 10
1 10
1 10
32 changes: 32 additions & 0 deletions tests/queries/0_stateless/05296_s3_cluster_hostname_group_by.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- Tags: no-fasttest
-- Tag no-fasttest: Depends on Minio
-- https://github.com/Altinity/ClickHouse/issues/2139

INSERT INTO FUNCTION s3('http://localhost:11111/test/05296_s3_cluster_hostname_group_by.tsv', 'TSV', 'x UInt32')
SELECT number FROM numbers(10)
SETTINGS s3_truncate_on_insert = 1;

SELECT host != '', c FROM
(
SELECT hostName() AS host, count() AS c
FROM s3Cluster('test_cluster_two_shards_localhost', 'http://localhost:11111/test/05296_s3_cluster_hostname_group_by.tsv', 'TSV', 'x UInt32')
GROUP BY host
)
SETTINGS enable_analyzer = 0;

SELECT host != '', c FROM
(
SELECT hostName() AS host, count() AS c
FROM s3('http://localhost:11111/test/05296_s3_cluster_hostname_group_by.tsv', 'TSV', 'x UInt32')
GROUP BY host
SETTINGS object_storage_cluster = 'test_cluster_two_shards_localhost'
)
SETTINGS enable_analyzer = 0;

SELECT host != '', c FROM
(
SELECT hostName() AS host, count() AS c
FROM s3Cluster('test_cluster_two_shards_localhost', 'http://localhost:11111/test/05296_s3_cluster_hostname_group_by.tsv', 'TSV', 'x UInt32')
GROUP BY host
)
SETTINGS enable_analyzer = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
3
3
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- A scalar subquery is evaluated once and sent to every shard as `__getScalar`.
-- It must not stay a GROUP BY key just because that function is marked as a server constant.
-- https://github.com/Altinity/ClickHouse/pull/2490

SELECT count() FROM numbers(3) GROUP BY (SELECT [1, 2]) SETTINGS enable_analyzer = 0;

SELECT count() FROM numbers(3) GROUP BY (SELECT [CAST(1, 'Dynamic')]) SETTINGS enable_analyzer = 0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
3
3
3
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- A local query has one server, so a folded server constant is an ordinary GROUP BY constant.
-- Keeping it makes `Dynamic` fail group-key validation.
-- https://github.com/Altinity/ClickHouse/pull/2490

SELECT count() FROM numbers(3) GROUP BY hostName() SETTINGS enable_analyzer = 0;

SELECT count() FROM numbers(3) GROUP BY CAST(hostName(), 'Dynamic') SETTINGS enable_analyzer = 0;

SELECT count() FROM numbers(3) GROUP BY [CAST(hostName(), 'Dynamic')] SETTINGS enable_analyzer = 0;
Loading