Files
datastore/tests/performance/analyzer_nested_if_alias_chain.xml
Dmitry NovikandClaude Opus 4.8 563905578b Reuse already-built action nodes when building ActionsDAG from expressions
With the analyzer, `WITH`-clause expression aliases are shared by pointer in the
query tree (a DAG), but `PlannerActionsVisitorImpl::visitFunction` re-traversed
shared subtrees once per reference. For deeply nested aliases (each referencing
the previous one several times) this is exponential in the chain depth, so
building the `ActionsDAG` could dominate query planning time.

When there is a single actions scope (no lambda is being built),
`visitFunction` now reuses an action node whose name already exists in the scope
instead of re-traversing it. The `ActionsDAG` already de-duplicates added nodes
via `addFunctionIfNecessary`, so the resulting DAG is unchanged - this only
avoids redundant traversal and the associated hashing of large action node
names. Keying on the action node name additionally reuses identical repeated
subexpressions that are not aliased.

This does not touch the analyzer pass manager, which re-traverses the same
shared DAG and remains the larger cost for such queries.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 18:46:18 +02:00

34 lines
2.1 KiB
XML

<test>
<!--
A chain of WITH-clause aliases where each alias is a deeply nested `if` expression that
references the previous alias several times (col2 uses col1 five times, col3 uses col2 five
times, ...). With the analyzer the aliases are shared by pointer in the query tree (a DAG),
so building the ActionsDAG used to re-traverse the shared subtrees once per reference, which
is exponential in the chain depth. `PlannerActionsVisitor::visitFunction` now reuses an
action node whose name already exists in the single actions scope instead of re-traversing
it, removing the exponential planner cost.
The input column is non-constant (so the chain is not constant-folded and the planner path
is actually exercised). Depth is kept moderate so the query stays sub-second.
-->
<settings>
<enable_analyzer>1</enable_analyzer>
<max_expanded_ast_elements>5000000000</max_expanded_ast_elements>
</settings>
<query><![CDATA[
WITH
if(value = 10, 'A', if(value = 20, 'B', if(value = 30, 'C', if(value = 40, 'D', if(value = 50, 'E', 'F'))))) AS col1,
if(col1 = 'A', 'Alpha', if(col1 = 'B', 'Beta', if(col1 = 'C', 'Gamma', if(col1 = 'D', 'Delta', if(col1 = 'E', 'Epsilon', 'Other'))))) AS col2,
if(col2 = 'Alpha', 1, if(col2 = 'Beta', 2, if(col2 = 'Gamma', 3, if(col2 = 'Delta', 4, if(col2 = 'Epsilon', 5, 0))))) AS col3,
if(col3 = 1, 'One', if(col3 = 2, 'Two', if(col3 = 3, 'Three', if(col3 = 4, 'Four', if(col3 = 5, 'Five', 'Zero'))))) AS col4,
if(col4 = 'One', 'Uno', if(col4 = 'Two', 'Dos', if(col4 = 'Three', 'Tres', if(col4 = 'Four', 'Cuatro', if(col4 = 'Five', 'Cinco', 'Cero'))))) AS col5,
if(col5 = 'Uno', 'I', if(col5 = 'Dos', 'II', if(col5 = 'Tres', 'III', if(col5 = 'Cuatro', 'IV', if(col5 = 'Cinco', 'V', 'Other'))))) AS col6,
if(col6 = 'I', 'Primero', if(col6 = 'II', 'Segundo', if(col6 = 'III', 'Tercero', if(col6 = 'IV', 'Cuarto', if(col6 = 'V', 'Quinto', 'Otro'))))) AS col7
SELECT col1, col2, col3, col4, col5, col6, col7
FROM (SELECT number AS value FROM system.numbers LIMIT 100)
FORMAT Null
]]></query>
</test>