This is an automated email from the ASF dual-hosted git repository.
englefly pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new d518544d7a6 [fix](fe) Fix TopN lazy materialization for queries
ordered by an alias (#68019)
d518544d7a6 is described below
commit d518544d7a69066f0c6591a67a0ecad47f16eeb1
Author: minghong <[email protected]>
AuthorDate: Tue Sep 22 19:42:27 2026 +0800
[fix](fe) Fix TopN lazy materialization for queries ordered by an alias
(#68019)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
`SELECT lazy_col AS x, lazy_col AS y FROM t ORDER BY x LIMIT 1` failed
planning with
`A expression contains slot not from children`.
The TopN order key is the alias slot, so that alias has to be computed
below the TopN.
`MaterializeProbeVisitor` only protects the order key slot itself (an
order key slot is in
`TopN.getInputSlots()`) and never resolves an identity alias down to the
column the alias reads.
The probe of the other output (`lazy_col AS y`) therefore resolved to
the base column `lazy_col`
and classified it as lazily materialized, so `LazySlotPruning` removed
`lazy_col` from the scan while
`lazy_col AS x` below the TopN still read it. The final `Validator`
rejected the resulting plan and
the query returned an error. With `fe_debug=true` the failure was caught
inside `LazyMaterializeTopN`
instead, which silently skipped lazy materialization (the query
succeeded but lost the optimization).
Reproduction (master, `fe_debug=false`):
```sql
create table t(sort_col int, lazy_col int) duplicate key(sort_col)
distributed by hash(sort_col) buckets 1 properties('replication_num'='1');
select lazy_col as x, lazy_col as y from t order by x limit 1;
-- ERROR 1105: A expression contains slot not from children
-- Slot: lazy_col#1 Children Output:{0, 4}
-- Plan: PhysicalProject[lazy_col#1 AS x#2, __DORIS_GLOBAL_ROWID_COL__t#4]
-- +--PhysicalLazyMaterializeOlapScan[PhysicalOlapScan[t]]
```
Fix: `LazyMaterializeTopN` resolves the TopN order keys through the
identity alias chain of the
Projects under the TopN and adds the resolved slots (plus the
intermediate alias slots) to
`requiredMaterializedSlots`, so the probe rejects every lazy candidate
backed by a column an order
key reads. The resolution stops at set operations, which the probe never
materializes through
(lazy materialization is not supported through set operations today; if
that ever changes, order
keys have to be resolved per branch).
Effect: affected plans now either keep only the ordering column
materialized (other columns are
still fetched lazily) or skip lazy materialization, and the plan stays
valid. Plans that order by a
plain column are unchanged.
### Release note
TopN lazy materialization no longer builds an invalid plan (no more
`A expression contains slot not from children`) when a query orders by
an alias of a column.
The column that feeds the order key is materialized during the scan,
while other columns keep using
lazy materialization.
### Check List (For Author)
- Test
- [x] Regression test
(`regression-test/suites/query_p0/topn_lazy/order_by_alias`)
- [x] Unit Test (`TopnLazyMaterializeTest`, `LazyMaterializeTopNTest`)
- Behavior changed:
- [x] Yes. Queries that order by an alias of a projected column no
longer fail planning; the
ordering column is kept materialized instead of being pruned from the
scan.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../post/materialize/LazyMaterializeTopN.java | 118 ++++++++++++
.../post/materialize/LazySlotPruning.java | 8 +-
.../post/materialize/MaterializeProbeVisitor.java | 30 ++-
.../postprocess/TopnLazyMaterializeTest.java | 156 ++++++++++++++++
.../post/materialize/LazyMaterializeTopNTest.java | 30 +++
.../order_by_alias/topn_lazy_order_by_alias.out | 106 +++++++++++
.../order_by_alias/topn_lazy_order_by_alias.groovy | 202 +++++++++++++++++++++
7 files changed, 640 insertions(+), 10 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopN.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopN.java
index 9acec57125a..11cd45d9115 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopN.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopN.java
@@ -36,16 +36,21 @@ import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.algebra.CatalogRelation;
import org.apache.doris.nereids.trees.plans.algebra.Relation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalCatalogRelation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalGenerate;
import org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterialize;
import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalTVFRelation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
+import org.apache.doris.nereids.util.PlanUtils;
import org.apache.doris.qe.SessionVariable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -107,6 +112,7 @@ public class LazyMaterializeTopN extends PlanPostProcessor {
List<Slot> materializedSlots = new ArrayList<>();
Set<Slot> requiredMaterializedSlots = new HashSet<>();
collectProjectExprInputSlots(topN.child(), requiredMaterializedSlots);
+ collectRequiredAliasSources(topN, requiredMaterializedSlots);
/*
* requiredMaterializedSlots only records slots consumed by
Project/final-projection expressions inside the
@@ -119,6 +125,9 @@ public class LazyMaterializeTopN extends PlanPostProcessor {
* a is in Filter.getInputSlots(). Both return Optional.empty() and
are appended to materializedSlots below.
* Therefore an empty requiredMaterializedSlots set does not mean
every scan column can be delayed; it only
* means no extra Project/final-projection input must be forced
materialized by this local safety check.
+ *
+ * The probe only protects the slots the operators reference directly,
so the columns those slots read
+ * through identity aliases are protected by
collectRequiredAliasSources above.
*/
for (Slot slot : effectiveOutput) {
Optional<MaterializeSource> source =
computeMaterializeSource(topN, (SlotReference) slot,
@@ -292,6 +301,115 @@ public class LazyMaterializeTopN extends
PlanPostProcessor {
}
}
+ /**
+ * Keep the columns consumed below the TopN materialized during the scan,
resolved through identity
+ * aliases.
+ *
+ * <p>{@link MaterializeProbeVisitor} only protects the slot it is
tracing: a slot consumed by an
+ * operator on the way from the TopN down to the relation stops the probe,
but the columns an
+ * identity alias reads are never resolved. For
+ *
+ * <pre>
+ * OuterTopN(order by z)
+ * InnerTopN(order by x)
+ * Project(lazy_col AS x, lazy_col AS y, other_col AS z)
+ * OlapScan
+ * </pre>
+ *
+ * probing the outer output {@code y} resolves to the base column {@code
lazy_col}, so {@code lazy_col}
+ * is classified lazy and {@link LazySlotPruning} removes it from the
scan, while {@code lazy_col AS x}
+ * below the outer TopN is still read by the inner TopN. The plan then
references a slot its child no
+ * longer produces and the final {@link Validator} rejects it. The same
happens when an identity alias
+ * is consumed by a filter, a join condition or any other operator that
stays below the TopN.
+ *
+ * <p>Therefore every slot consumed by this TopN and by the operators
below it (its own order keys, the
+ * expressions of every descendant operator and the slots that are
required materialized already) is
+ * resolved through its identity alias chain. The only consumed slots that
are not required are the ones
+ * {@link MaterializeProbeVisitor#isIndexLazyFilter} keeps lazy: {@link
LazySlotPruning#visitPhysicalFilter}
+ * drops those from the scan's lazy slots for the same reason, so the scan
keeps producing them for the
+ * predicate and nothing below the TopN can be starved. Project
expressions are handled by
+ * {@link #collectProjectExprInputSlots}, which knows that a transparent
{@code Alias(Slot)} output may
+ * still be fetched lazily.
+ *
+ * <p>A set operation is a boundary: {@link MaterializeProbeVisitor} never
reports a lazy source for a
+ * slot produced by a set operation, and {@link #collectIdentityAliasMap}
stops at it, so the aliases
+ * below a set operation are neither resolved nor reachable. If lazy
materialization is ever extended
+ * through set operations, the consumed slots have to be resolved per set
operation branch instead.
+ */
+ private void collectRequiredAliasSources(PhysicalTopN<? extends Plan> topN,
+ Set<Slot> requiredMaterializedSlots) {
+ Map<Slot, Slot> aliasToChild = new HashMap<>();
+ collectIdentityAliasMap(topN.child(), aliasToChild);
+
+ Set<Slot> consumedSlots = new HashSet<>();
+ Set<Slot> indexLazySlots = new HashSet<>();
+ collectConsumedSlots(topN, consumedSlots, indexLazySlots);
+ consumedSlots.addAll(requiredMaterializedSlots);
+ for (Slot slot : consumedSlots) {
+ if (!indexLazySlots.contains(slot)) {
+ collectAliasChain(slot, aliasToChild,
requiredMaterializedSlots);
+ }
+ }
+ }
+
+ /** Collect the slots consumed by {@code plan} itself and by every
operator below it. */
+ private void collectConsumedSlots(Plan plan, Set<Slot> consumedSlots,
Set<Slot> indexLazySlots) {
+ if (plan instanceof PhysicalSetOperation) {
+ // Set operations are not materialized lazily, so nothing below
them can be lazy either.
+ return;
+ }
+ if (!(plan instanceof PhysicalProject)) {
+ // Project expressions are covered by
collectProjectExprInputSlots, which keeps the input of a
+ // transparent Alias(Slot) lazy because that alias output may
still be fetched later.
+ consumedSlots.addAll(plan.getInputSlots());
+ }
+ if (plan instanceof PhysicalFilter
+ &&
MaterializeProbeVisitor.isIndexLazyFilter((PhysicalFilter<?>) plan)) {
+ indexLazySlots.addAll(plan.getInputSlots());
+ }
+ if (plan instanceof PhysicalGenerate) {
+ // PhysicalGenerate.getExpressions() exposes the generators only,
while the lateral conjuncts
+ // are preserved by the implementation rule and executed by the
generate itself. Generator
+ // outputs are produced by the generate, never by the child,
exactly like LogicalGenerate.
+ PhysicalGenerate<?> generate = (PhysicalGenerate<?>) plan;
+ consumedSlots.addAll(Sets.difference(
+ PlanUtils.fastGetInputSlots(generate.getConjuncts()),
+ new HashSet<>(generate.getGeneratorOutput())));
+ }
+ for (Plan child : plan.children()) {
+ collectConsumedSlots(child, consumedSlots, indexLazySlots);
+ }
+ }
+
+ /** Collect {@code alias slot -> child slot} for every identity Alias of
the Projects under the TopN. */
+ private void collectIdentityAliasMap(Plan plan, Map<Slot, Slot>
aliasToChild) {
+ if (plan instanceof PhysicalSetOperation) {
+ // Set operations are not materialized lazily, so aliases below
them are never reached.
+ return;
+ }
+ if (plan instanceof PhysicalProject) {
+ for (NamedExpression project : ((PhysicalProject<?>)
plan).getProjects()) {
+ if (project instanceof Alias && project.child(0) instanceof
Slot) {
+ aliasToChild.putIfAbsent(project.toSlot(), (Slot)
project.child(0));
+ }
+ }
+ }
+ for (Plan child : plan.children()) {
+ collectIdentityAliasMap(child, aliasToChild);
+ }
+ }
+
+ /** Add a slot together with every slot of its alias chain, so the column
the chain ends at is protected. */
+ @VisibleForTesting
+ static void collectAliasChain(Slot slot, Map<Slot, Slot> aliasToChild,
Set<Slot> requiredMaterializedSlots) {
+ Set<Slot> visited = new HashSet<>();
+ Slot current = slot;
+ while (current != null && visited.add(current)) {
+ requiredMaterializedSlots.add(current);
+ current = aliasToChild.get(current);
+ }
+ }
+
private List<SlotReference> moveRowIdsToTail(List<Slot> slots,
Set<SlotReference> rowIds) {
List<SlotReference> reArrangedSlots = new ArrayList<>();
List<SlotReference> reArrangedRowIds = new ArrayList<>();
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazySlotPruning.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazySlotPruning.java
index fce964758f1..edc11ee1261 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazySlotPruning.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/LazySlotPruning.java
@@ -40,7 +40,6 @@ import
org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalTVFRelation;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter;
-import org.apache.doris.qe.SessionVariable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -117,7 +116,7 @@ public class LazySlotPruning extends
DefaultPlanRewriter<LazySlotPruning.Context
@Override
public Plan visitPhysicalFilter(PhysicalFilter<? extends Plan> filter,
Context context) {
- if (SessionVariable.getTopNLazyMaterializationUsingIndex() &&
filter.child() instanceof PhysicalOlapScan) {
+ if (MaterializeProbeVisitor.isIndexLazyFilter(filter)) {
/*
materialization(materializedSlots=[a, b], lazy=[c])
->topn(b)
@@ -128,7 +127,10 @@ public class LazySlotPruning extends
DefaultPlanRewriter<LazySlotPruning.Context
->topn(b)
->project(rowid, b)
->filter(a=1, output=(rowid, a, b))
- ->materializeOlapScan(rowid, lazy=[a,c], T[a,b,c])
+ ->materializeOlapScan(rowid, lazy=[c], T[a,b,c])
+
+ The predicate slot a is dropped from the TopN tuple by the extra
project, but the scan keeps
+ producing it for the predicate: a is removed from the scan's lazy
slots below.
*/
List<Slot> lazySlotsToScan = new ArrayList<>();
boolean lazySlotsChanged = false;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitor.java
index b112595dcef..5d98af97cc6 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitor.java
@@ -77,15 +77,31 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
}
+ /**
+ * Whether {@code filter} keeps its predicate slots lazy.
+ *
+ * <p>With {@code topn_lazy_materialization_using_index} the predicate of
a filter directly above an OLAP
+ * scan is evaluated while scanning, so its slots only have to be
re-materialized above the TopN instead
+ * of being carried through it. This is the single definition of that
shape:
+ * {@link MaterializeProbeVisitor#visitPhysicalFilter} reports those slots
as lazy sources,
+ * {@link LazySlotPruning#visitPhysicalFilter} keeps them out of the
scan's lazy slots (the scan still
+ * produces them for the predicate) and {@link LazyMaterializeTopN} must
not require them materialized.
+ */
+ static boolean isIndexLazyFilter(PhysicalFilter<? extends Plan> filter) {
+ if (!(filter.child() instanceof PhysicalOlapScan)) {
+ return false;
+ }
+ if (!SessionVariable.getTopNLazyMaterializationUsingIndex()) {
+ return false;
+ }
+ // Reject OLAP tables whose storage semantics cannot be reconstructed
from one row-id.
+ return supportOlapTopnLazyMaterialize(((PhysicalOlapScan)
filter.child()).getTable());
+ }
+
@Override
public Optional<MaterializeSource> visitPhysicalFilter(PhysicalFilter<?
extends Plan> filter,
ProbeContext
context) {
- if (SessionVariable.getTopNLazyMaterializationUsingIndex() &&
filter.child() instanceof PhysicalOlapScan) {
- // Reject OLAP tables whose storage semantics cannot be
reconstructed from one row-id.
- OlapTable table = ((PhysicalOlapScan) filter.child()).getTable();
- if (!supportOlapTopnLazyMaterialize(table)) {
- return Optional.empty();
- }
+ if (isIndexLazyFilter(filter)) {
if (filter.getInputSlots().contains(context.slot)) {
Relation relation = (Relation) filter.child();
return Optional.of(new MaterializeSource(
@@ -152,7 +168,7 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
* topn lazy materialization keeps only one row-id for each
relation.</li>
* </ul>
*/
- private boolean supportOlapTopnLazyMaterialize(OlapTable table) {
+ private static boolean supportOlapTopnLazyMaterialize(OlapTable table) {
if (KeysType.AGG_KEYS.equals(table.getKeysType())) {
return false;
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnLazyMaterializeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnLazyMaterializeTest.java
index ee0942c3071..010d3749758 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnLazyMaterializeTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnLazyMaterializeTest.java
@@ -142,6 +142,162 @@ public class TopnLazyMaterializeTest extends SSBTestBase {
materializeNodes.get(0).getLazyBaseColumnIndices());
}
+ @Test
+ public void testOrderByAliasKeepsItsSourceMaterialized() throws Exception {
+ this.createTable("create table lazy_materialize_order_by_alias_tbl("
+ + "sort_col int, lazy_col int, other_col int) "
+ + "duplicate key(sort_col) distributed by hash(sort_col)
buckets 1 "
+ + "properties('replication_num' = '1')");
+ // TestWithFeService enables feDebug, which makes LazyMaterializeTopN
validate its own result and
+ // silently fall back on an invalid plan. Turn it off so an invalid
plan fails the whole test.
+ boolean feDebug = connectContext.getSessionVariable().feDebug;
+ connectContext.getSessionVariable().feDebug = false;
+ try {
+ // The TopN sorts by an alias of lazy_col, so lazy_col must be
materialized for the sort.
+ // Otherwise LazySlotPruning removes it from the scan while
`lazy_col AS x` below the TopN
+ // still reads it, and the resulting plan references a slot its
child no longer produces.
+ PhysicalPlan plan = postProcess("select lazy_col as x, lazy_col as
y "
+ + "from lazy_materialize_order_by_alias_tbl order by x
limit 1");
+ Assertions.assertTrue(
+ plan.collectToList(node -> node instanceof
PhysicalLazyMaterialize).isEmpty(),
+ plan.treeString());
+
+ // Sorting by a bare column that is also aliased must keep that
column materialized.
+ plan = postProcess("select lazy_col, lazy_col as y "
+ + "from lazy_materialize_order_by_alias_tbl order by
lazy_col limit 1");
+ Assertions.assertTrue(
+ plan.collectToList(node -> node instanceof
PhysicalLazyMaterialize).isEmpty(),
+ plan.treeString());
+
+ // Only the column the order key reads is forced materialized,
other_col is still fetched lazily.
+ plan = postProcess("select lazy_col as x, other_col as y "
+ + "from lazy_materialize_order_by_alias_tbl order by x
limit 1");
+ List<PhysicalLazyMaterialize<? extends Plan>> materializeNodes =
plan.collectToList(
+ node -> node instanceof PhysicalLazyMaterialize);
+ Assertions.assertEquals(1, materializeNodes.size(),
plan.treeString());
+ Assertions.assertEquals(ImmutableList.of(ImmutableList.of(2)),
+ materializeNodes.get(0).getLazyBaseColumnIndices());
+
+ // Sorting by a column that is not aliased keeps every projected
column lazily fetched.
+ plan = postProcess("select lazy_col as x, other_col as y "
+ + "from lazy_materialize_order_by_alias_tbl order by
sort_col limit 1");
+ materializeNodes = plan.collectToList(node -> node instanceof
PhysicalLazyMaterialize);
+ Assertions.assertEquals(1, materializeNodes.size(),
plan.treeString());
+ List<Integer> lazyColumnIndexes = Lists.newArrayList();
+
materializeNodes.get(0).getLazyBaseColumnIndices().forEach(lazyColumnIndexes::addAll);
+ lazyColumnIndexes.sort(Integer::compareTo);
+ Assertions.assertEquals(ImmutableList.of(1, 2), lazyColumnIndexes);
+ } finally {
+ connectContext.getSessionVariable().feDebug = feDebug;
+ }
+ }
+
+ @Test
+ public void testNestedTopNKeepsAliasSourceMaterialized() throws Exception {
+ this.createTable("create table lazy_materialize_nested_topn_tbl("
+ + "sort_col int, lazy_col int, other_col int) "
+ + "duplicate key(sort_col) distributed by hash(sort_col)
buckets 1 "
+ + "properties('replication_num' = '1')");
+ boolean feDebug = connectContext.getSessionVariable().feDebug;
+ connectContext.getSessionVariable().feDebug = false;
+ try {
+ // Only the outer TopN is rewritten. Probing its output `y`
resolves to lazy_col, which the
+ // inner TopN still reads through `lazy_col AS x`, so lazy_col
must stay materialized.
+ PhysicalPlan plan = postProcess("select y from (select lazy_col as
x, lazy_col as y, other_col as z "
+ + "from lazy_materialize_nested_topn_tbl order by x limit
2) s order by z limit 1");
+ Assertions.assertTrue(
+ plan.collectToList(node -> node instanceof
PhysicalLazyMaterialize).isEmpty(),
+ plan.treeString());
+
+ // A column that nothing below the TopN reads may still be fetched
lazily through the TopNs.
+ plan = postProcess("select w from (select lazy_col as x, other_col
as w, lazy_col as y, sort_col "
+ + "from lazy_materialize_nested_topn_tbl order by x limit
2) s order by sort_col limit 1");
+ List<PhysicalLazyMaterialize<? extends Plan>> materializeNodes =
plan.collectToList(
+ node -> node instanceof PhysicalLazyMaterialize);
+ Assertions.assertEquals(1, materializeNodes.size(),
plan.treeString());
+ Assertions.assertEquals(ImmutableList.of(ImmutableList.of(2)),
+ materializeNodes.get(0).getLazyBaseColumnIndices());
+ } finally {
+ connectContext.getSessionVariable().feDebug = feDebug;
+ }
+ }
+
+ @Test
+ public void testIndexFilterPredicateSlotStaysLazy() throws Exception {
+ this.createTable("create table lazy_materialize_index_filter_tbl("
+ + "user_id bigint, username varchar(50), age int, addr
varchar(50)) "
+ + "duplicate key(user_id, username) distributed by
hash(user_id) buckets 1 "
+ + "properties('replication_num' = '1')");
+ boolean feDebug = connectContext.getSessionVariable().feDebug;
+ boolean usingIndex =
connectContext.getSessionVariable().topNLazyMaterializationUsingIndex;
+ connectContext.getSessionVariable().feDebug = false;
+ connectContext.getSessionVariable().topNLazyMaterializationUsingIndex
= true;
+ try {
+ // The filter is evaluated through the index, so its predicate
column is kept by the scan for
+ // the predicate and re-materialized lazily above the TopN. It
must stay lazy even though an
+ // operator below the TopN consumes it.
+ PhysicalPlan plan = postProcess("select * from
lazy_materialize_index_filter_tbl "
+ + "where user_id = 1 order by username limit 1");
+ List<PhysicalLazyMaterialize<? extends Plan>> materializeNodes =
plan.collectToList(
+ node -> node instanceof PhysicalLazyMaterialize);
+ Assertions.assertEquals(1, materializeNodes.size(),
plan.treeString());
+ List<Integer> lazyColumnIndexes = Lists.newArrayList();
+
materializeNodes.get(0).getLazyBaseColumnIndices().forEach(lazyColumnIndexes::addAll);
+ lazyColumnIndexes.sort(Integer::compareTo);
+ Assertions.assertEquals(ImmutableList.of(0, 2, 3),
lazyColumnIndexes, plan.treeString());
+ } finally {
+ connectContext.getSessionVariable().feDebug = feDebug;
+
connectContext.getSessionVariable().topNLazyMaterializationUsingIndex =
usingIndex;
+ }
+ }
+
+ @Test
+ public void testLateralGenerateConjunctKeepsItsAliasSourceMaterialized()
throws Exception {
+ this.createTable("create table lazy_materialize_lateral_tbl("
+ + "sort_col int, lazy_col int, other_col int, arr array<int>) "
+ + "duplicate key(sort_col) distributed by hash(sort_col)
buckets 1 "
+ + "properties('replication_num' = '1')");
+ boolean feDebug = connectContext.getSessionVariable().feDebug;
+ connectContext.getSessionVariable().feDebug = false;
+ try {
+ // The generate conjunct reads `x`, which is an alias of lazy_col.
PhysicalGenerate exposes
+ // only its generators through getInputSlots(), so the conjunct
inputs have to be resolved
+ // explicitly: otherwise lazy_col is pruned from the scan while
`lazy_col AS x` below the
+ // generate still reads it. Only other_col (read by nothing below
the TopN) stays lazy.
+ PhysicalPlan plan = postProcess("select s.y, s.w from (select
lazy_col as x, lazy_col as y, "
+ + "other_col as w, arr, sort_col from
lazy_materialize_lateral_tbl) s "
+ + "left join lateral unnest(s.arr) tt(tag) on tt.tag = s.x
"
+ + "order by s.sort_col limit 1");
+ List<PhysicalLazyMaterialize<? extends Plan>> materializeNodes =
plan.collectToList(
+ node -> node instanceof PhysicalLazyMaterialize);
+ Assertions.assertEquals(1, materializeNodes.size(),
plan.treeString());
+ Assertions.assertEquals(ImmutableList.of(ImmutableList.of(2)),
+ materializeNodes.get(0).getLazyBaseColumnIndices(),
plan.treeString());
+
+ // The conjunct reads the bare lazy_col, which the same Project
also aliases as `y`. Probing
+ // `y` resolves through that alias to lazy_col and no operator
below the TopN stops the probe
+ // for the bare slot, so lazy_col has to stay materialized for the
conjunct as well.
+ plan = postProcess("select s.y, s.w from (select lazy_col as y,
other_col as w, arr, sort_col, "
+ + "lazy_col from lazy_materialize_lateral_tbl) s "
+ + "left join lateral unnest(s.arr) tt(tag) on tt.tag =
s.lazy_col "
+ + "order by s.sort_col limit 1");
+ materializeNodes = plan.collectToList(node -> node instanceof
PhysicalLazyMaterialize);
+ Assertions.assertEquals(1, materializeNodes.size(),
plan.treeString());
+ Assertions.assertEquals(ImmutableList.of(ImmutableList.of(2)),
+ materializeNodes.get(0).getLazyBaseColumnIndices(),
plan.treeString());
+ } finally {
+ connectContext.getSessionVariable().feDebug = feDebug;
+ }
+ }
+
+ private PhysicalPlan postProcess(String sql) {
+ PlanChecker checker = PlanChecker.from(connectContext)
+ .analyze(sql)
+ .rewrite()
+ .implement();
+ return new
PlanPostProcessors(checker.getCascadesContext()).process(checker.getPhysicalPlan());
+ }
+
@Test
public void testLightSchemaChangeFalse() throws Exception {
this.createTable("create table tm_lsc_false (k int, v int) duplicate
key(k) "
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopNTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopNTest.java
index d8763f27082..eb7ec84a039 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopNTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/LazyMaterializeTopNTest.java
@@ -30,8 +30,10 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class LazyMaterializeTopNTest {
@@ -51,4 +53,32 @@ public class LazyMaterializeTopNTest {
Assertions.assertEquals(ImmutableList.of(aliasSlot),
requiredOutputSlots);
}
+
+ @Test
+ public void testCollectAliasChainKeepsTheColumnsOrderKeysRead() {
+ SlotReference baseSlot = new SlotReference("base",
IntegerType.INSTANCE);
+ Slot aliasSlot = new Alias(baseSlot, "alias").toSlot();
+ Slot aliasOfAliasSlot = new Alias(aliasSlot,
"alias_of_alias").toSlot();
+ Map<Slot, Slot> aliasToChild = ImmutableMap.of(aliasOfAliasSlot,
aliasSlot, aliasSlot, baseSlot);
+ Set<Slot> requiredMaterializedSlots = new HashSet<>();
+
+ LazyMaterializeTopN.collectAliasChain(aliasOfAliasSlot, aliasToChild,
requiredMaterializedSlots);
+
+ Assertions.assertEquals(
+ ImmutableSet.of(aliasOfAliasSlot, aliasSlot, baseSlot),
requiredMaterializedSlots);
+ }
+
+ @Test
+ public void testCollectAliasChainStopsOnCycle() {
+ // An alias chain must never keep the planner spinning, even if two
slots share one ExprId.
+ SlotReference baseSlot = new SlotReference("base",
IntegerType.INSTANCE);
+ Slot firstSlot = new Alias(baseSlot, "first").toSlot();
+ Slot secondSlot = new Alias(baseSlot, "second").toSlot();
+ Map<Slot, Slot> aliasToChild = ImmutableMap.of(firstSlot, secondSlot,
secondSlot, firstSlot);
+ Set<Slot> requiredMaterializedSlots = new HashSet<>();
+
+ LazyMaterializeTopN.collectAliasChain(firstSlot, aliasToChild,
requiredMaterializedSlots);
+
+ Assertions.assertEquals(ImmutableSet.of(firstSlot, secondSlot),
requiredMaterializedSlots);
+ }
}
diff --git
a/regression-test/data/query_p0/topn_lazy/order_by_alias/topn_lazy_order_by_alias.out
b/regression-test/data/query_p0/topn_lazy/order_by_alias/topn_lazy_order_by_alias.out
new file mode 100644
index 00000000000..8ad3b624152
--- /dev/null
+++
b/regression-test/data/query_p0/topn_lazy/order_by_alias/topn_lazy_order_by_alias.out
@@ -0,0 +1,106 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !repeated_alias_of_sort_key --
+10 10
+
+-- !repeated_alias_reversed --
+10 10
+
+-- !bare_column_and_alias --
+10 10
+
+-- !other_column_still_lazy --
+10 100
+
+-- !order_by_plain_column --
+10 100
+
+-- !using_index_repeated_alias --
+10 10
+
+-- !using_index_bare_column_and_alias --
+10 10
+
+-- !nested_topn_plan --
+PhysicalResultSink
+--PhysicalProject[s.y]
+----PhysicalTopN[GATHER_SORT]
+------PhysicalProject[s.y, s.z]
+--------PhysicalTopN[MERGE_SORT]
+----------PhysicalDistribute[DistributionSpecGather]
+------------PhysicalTopN[LOCAL_SORT]
+--------------PhysicalProject[topn_lazy_order_by_alias_tbl.lazy_col AS `x`,
topn_lazy_order_by_alias_tbl.lazy_col AS `y`,
topn_lazy_order_by_alias_tbl.other_col AS `z`]
+----------------PhysicalOlapScan[topn_lazy_order_by_alias_tbl]
+
+-- !nested_topn_result --
+10
+
+-- !nested_topn_still_lazy_plan --
+PhysicalResultSink
+--PhysicalProject[s.w]
+----PhysicalLazyMaterialize[materializedSlots:(s.sort_col) lazySlots:(s.w)]
+------PhysicalTopN[GATHER_SORT]
+--------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_tbl,
s.sort_col]
+----------PhysicalTopN[MERGE_SORT]
+------------PhysicalDistribute[DistributionSpecGather]
+--------------PhysicalTopN[LOCAL_SORT]
+----------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_tbl,
topn_lazy_order_by_alias_tbl.lazy_col AS `x`,
topn_lazy_order_by_alias_tbl.sort_col]
+------------------PhysicalLazyMaterializeOlapScan[topn_lazy_order_by_alias_tbl
lazySlots:(topn_lazy_order_by_alias_tbl.other_col)]
+
+-- !index_mode_selective_lazy_plan --
+PhysicalResultSink
+--PhysicalProject[topn_lazy_order_by_alias_tbl.lazy_col,
topn_lazy_order_by_alias_tbl.other_col, x]
+----PhysicalLazyMaterialize[materializedSlots:(x,topn_lazy_order_by_alias_tbl.lazy_col)
lazySlots:(topn_lazy_order_by_alias_tbl.other_col)]
+------PhysicalTopN[MERGE_SORT]
+--------PhysicalDistribute[DistributionSpecGather]
+----------PhysicalTopN[LOCAL_SORT]
+------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_tbl,
topn_lazy_order_by_alias_tbl.lazy_col, topn_lazy_order_by_alias_tbl.lazy_col
AS `x`]
+--------------filter((topn_lazy_order_by_alias_tbl.sort_col > 0))
+----------------PhysicalLazyMaterializeOlapScan[topn_lazy_order_by_alias_tbl
lazySlots:(topn_lazy_order_by_alias_tbl.other_col)]
+
+-- !index_mode_selective_lazy_result --
+10 10 100
+
+-- !index_mode_filter_predicate_lazy_plan --
+PhysicalResultSink
+--PhysicalProject[topn_lazy_order_by_alias_tbl.other_col,
topn_lazy_order_by_alias_tbl.sort_col, x]
+----PhysicalLazyMaterialize[materializedSlots:(x)
lazySlots:(topn_lazy_order_by_alias_tbl.other_col,topn_lazy_order_by_alias_tbl.sort_col)]
+------PhysicalTopN[MERGE_SORT]
+--------PhysicalDistribute[DistributionSpecGather]
+----------PhysicalTopN[LOCAL_SORT]
+------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_tbl,
topn_lazy_order_by_alias_tbl.lazy_col AS `x`]
+--------------filter((topn_lazy_order_by_alias_tbl.sort_col > 0))
+----------------PhysicalLazyMaterializeOlapScan[topn_lazy_order_by_alias_tbl
lazySlots:(topn_lazy_order_by_alias_tbl.other_col)]
+
+-- !index_mode_filter_predicate_lazy_result --
+10 1 100
+
+-- !lateral_generate_conjunct_plan --
+PhysicalResultSink
+--PhysicalProject[s.w, s.y]
+----PhysicalLazyMaterialize[materializedSlots:(s.y,s.sort_col) lazySlots:(s.w)]
+------PhysicalTopN[MERGE_SORT]
+--------PhysicalDistribute[DistributionSpecGather]
+----------PhysicalTopN[LOCAL_SORT]
+------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_arr_tbl,
s.sort_col, s.y]
+--------------PhysicalGenerate
+----------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_arr_tbl,
s.arr, s.sort_col, topn_lazy_order_by_alias_arr_tbl.lazy_col AS `x`,
topn_lazy_order_by_alias_arr_tbl.lazy_col AS `y`]
+------------------PhysicalLazyMaterializeOlapScan[topn_lazy_order_by_alias_arr_tbl
lazySlots:(topn_lazy_order_by_alias_arr_tbl.other_col)]
+
+-- !lateral_generate_conjunct_result --
+10 100
+
+-- !lateral_generate_bare_conjunct_plan --
+PhysicalResultSink
+--PhysicalProject[s.w, s.y]
+----PhysicalLazyMaterialize[materializedSlots:(s.y,s.sort_col) lazySlots:(s.w)]
+------PhysicalTopN[MERGE_SORT]
+--------PhysicalDistribute[DistributionSpecGather]
+----------PhysicalTopN[LOCAL_SORT]
+------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_arr_tbl,
s.sort_col, s.y]
+--------------PhysicalGenerate
+----------------PhysicalProject[regression_test_query_p0_topn_lazy_order_by_alias.__DORIS_GLOBAL_ROWID_COL__topn_lazy_order_by_alias_arr_tbl,
s.arr, s.lazy_col, s.sort_col, topn_lazy_order_by_alias_arr_tbl.lazy_col AS
`y`]
+------------------PhysicalLazyMaterializeOlapScan[topn_lazy_order_by_alias_arr_tbl
lazySlots:(topn_lazy_order_by_alias_arr_tbl.other_col)]
+
+-- !lateral_generate_bare_conjunct_result --
+10 100
+
diff --git
a/regression-test/suites/query_p0/topn_lazy/order_by_alias/topn_lazy_order_by_alias.groovy
b/regression-test/suites/query_p0/topn_lazy/order_by_alias/topn_lazy_order_by_alias.groovy
new file mode 100644
index 00000000000..8fefd563e59
--- /dev/null
+++
b/regression-test/suites/query_p0/topn_lazy/order_by_alias/topn_lazy_order_by_alias.groovy
@@ -0,0 +1,202 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("topn_lazy_order_by_alias") {
+ // TopN lazy materialization only skips the rewrite when a plan validation
fails, and that
+ // validation only runs with fe_debug on. Pin it off so an invalid plan
surfaces as an error.
+ sql """ set fe_debug = false; """
+
+ sql """
+ drop table if exists topn_lazy_order_by_alias_tbl;
+ create table topn_lazy_order_by_alias_tbl (
+ sort_col int,
+ lazy_col int,
+ other_col int
+ ) duplicate key(sort_col)
+ distributed by hash(sort_col) buckets 1
+ properties('replication_num' = '1');
+ """
+ sql """
+ insert into topn_lazy_order_by_alias_tbl values
(3,30,300),(1,10,100),(2,20,200);
+ """
+
+ // Sorting by an alias of lazy_col: lazy_col feeds the sort key, so it
must stay materialized
+ // instead of being pruned from the scan while `lazy_col AS x` below the
TopN still reads it.
+ order_qt_repeated_alias_of_sort_key """
+ select lazy_col as x, lazy_col as y
+ from topn_lazy_order_by_alias_tbl order by x limit 1;
+ """
+
+ // Same shape ordered by the other alias.
+ order_qt_repeated_alias_reversed """
+ select lazy_col as x, lazy_col as y
+ from topn_lazy_order_by_alias_tbl order by y limit 1;
+ """
+
+ // A bare column plus an alias of it, sorted by the bare column.
+ order_qt_bare_column_and_alias """
+ select lazy_col, lazy_col as y
+ from topn_lazy_order_by_alias_tbl order by lazy_col limit 1;
+ """
+
+ // Only the column the order key reads is forced materialized; other_col
is still lazily fetched.
+ order_qt_other_column_still_lazy """
+ select lazy_col as x, other_col as y
+ from topn_lazy_order_by_alias_tbl order by x limit 1;
+ """
+
+ // Control: sorting by a column that is not aliased keeps both columns
lazily fetched.
+ order_qt_order_by_plain_column """
+ select lazy_col as x, other_col as y
+ from topn_lazy_order_by_alias_tbl order by sort_col limit 1;
+ """
+
+ // The same shapes through the inverted-index filter path.
+ sql """ set topn_lazy_materialization_using_index = true; """
+
+ order_qt_using_index_repeated_alias """
+ select lazy_col as x, lazy_col as y
+ from topn_lazy_order_by_alias_tbl where sort_col > 0 order by x limit
1;
+ """
+
+ order_qt_using_index_bare_column_and_alias """
+ select lazy_col, lazy_col as y
+ from topn_lazy_order_by_alias_tbl where sort_col > 0 order by lazy_col
limit 1;
+ """
+
+ sql """ set topn_lazy_materialization_using_index = false; """
+
+ // Only the outer TopN is rewritten. Probing its output `y` resolves to
lazy_col, which the inner TopN
+ // still reads through `lazy_col AS x`, so lazy_col has to stay
materialized. Plain execution hides the
+ // invalid plan behind StmtExecutor.queryRetry, so the plan shape is
asserted as well.
+ sql """ set detail_shape_nodes = 'PhysicalProject'; """
+
+ qt_nested_topn_plan """
+ explain shape plan
+ select y from (
+ select lazy_col as x, lazy_col as y, other_col as z
+ from topn_lazy_order_by_alias_tbl
+ order by x limit 2) s
+ order by z limit 1;
+ """
+
+ order_qt_nested_topn_result """
+ select y from (
+ select lazy_col as x, lazy_col as y, other_col as z
+ from topn_lazy_order_by_alias_tbl
+ order by x limit 2) s
+ order by z limit 1;
+ """
+
+ // Column `w` is read by nothing below the TopN, so it can still be
fetched lazily through both TopNs.
+ qt_nested_topn_still_lazy_plan """
+ explain shape plan
+ select w from (
+ select lazy_col as x, other_col as w, lazy_col as y, sort_col
+ from topn_lazy_order_by_alias_tbl
+ order by x limit 2) s
+ order by sort_col limit 1;
+ """
+
+ // Index mode: the bare lazy_col and its alias both feed the sort key, so
both stay materialized and
+ // only other_col is fetched lazily. Before the fix the bare lazy_col
could be pruned from the scan
+ // while `lazy_col AS x` below the TopN still read it.
+ sql """ set topn_lazy_materialization_using_index = true; """
+
+ qt_index_mode_selective_lazy_plan """
+ explain shape plan
+ select lazy_col as x, lazy_col, other_col
+ from topn_lazy_order_by_alias_tbl where sort_col > 0 order by x limit
1;
+ """
+
+ order_qt_index_mode_selective_lazy_result """
+ select lazy_col as x, lazy_col, other_col
+ from topn_lazy_order_by_alias_tbl where sort_col > 0 order by x limit
1;
+ """
+
+ // A direct filter predicate that no alias reads is evaluated through the
index and fetched by the
+ // scan for the predicate, so it stays lazy above the TopN while the alias
feeding the sort key keeps
+ // its base column materialized.
+ qt_index_mode_filter_predicate_lazy_plan """
+ explain shape plan
+ select lazy_col as x, sort_col, other_col
+ from topn_lazy_order_by_alias_tbl where sort_col > 0 order by x limit
1;
+ """
+
+ order_qt_index_mode_filter_predicate_lazy_result """
+ select lazy_col as x, sort_col, other_col
+ from topn_lazy_order_by_alias_tbl where sort_col > 0 order by x limit
1;
+ """
+
+ sql """ set topn_lazy_materialization_using_index = false; """
+
+ sql """
+ drop table if exists topn_lazy_order_by_alias_arr_tbl;
+ create table topn_lazy_order_by_alias_arr_tbl (
+ sort_col int,
+ lazy_col int,
+ other_col int,
+ arr array<int>
+ ) duplicate key(sort_col)
+ distributed by hash(sort_col) buckets 1
+ properties('replication_num' = '1');
+ """
+ sql """
+ insert into topn_lazy_order_by_alias_arr_tbl values
(3,30,300,[3]),(1,10,100,[1,10]),(2,20,200,[2,20]);
+ """
+
+ // The generate conjunct is evaluated by the generate itself, so the alias
it reads must keep its base
+ // column materialized: PhysicalGenerate exposes only its generators
through getInputSlots(), otherwise
+ // lazy_col is pruned from the scan while `lazy_col AS x` below the
generate still reads it. Only
+ // other_col, which nothing below the TopN reads, is fetched lazily.
+ qt_lateral_generate_conjunct_plan """
+ explain shape plan
+ select s.y, s.w from (
+ select lazy_col as x, lazy_col as y, other_col as w, arr, sort_col
+ from topn_lazy_order_by_alias_arr_tbl) s
+ left join lateral unnest(s.arr) tt(tag) on tt.tag = s.x
+ order by s.sort_col limit 1;
+ """
+
+ order_qt_lateral_generate_conjunct_result """
+ select s.y, s.w from (
+ select lazy_col as x, lazy_col as y, other_col as w, arr, sort_col
+ from topn_lazy_order_by_alias_arr_tbl) s
+ left join lateral unnest(s.arr) tt(tag) on tt.tag = s.x
+ order by s.sort_col limit 1;
+ """
+
+ // The conjunct reads the bare lazy_col, which the same Project also
aliases as `y`. Probing `y`
+ // resolves through that alias to lazy_col, and no operator below the TopN
stops the probe for the bare
+ // slot, so lazy_col has to stay materialized for the conjunct as well.
+ qt_lateral_generate_bare_conjunct_plan """
+ explain shape plan
+ select s.y, s.w from (
+ select lazy_col as y, other_col as w, arr, sort_col, lazy_col
+ from topn_lazy_order_by_alias_arr_tbl) s
+ left join lateral unnest(s.arr) tt(tag) on tt.tag = s.lazy_col
+ order by s.sort_col limit 1;
+ """
+
+ order_qt_lateral_generate_bare_conjunct_result """
+ select s.y, s.w from (
+ select lazy_col as y, other_col as w, arr, sort_col, lazy_col
+ from topn_lazy_order_by_alias_arr_tbl) s
+ left join lateral unnest(s.arr) tt(tag) on tt.tag = s.lazy_col
+ order by s.sort_col limit 1;
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]