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 6105b6524455b1..26da0c0dbba758 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 @@ -19,6 +19,7 @@ import org.apache.doris.catalog.AggregateType; import org.apache.doris.catalog.Column; +import org.apache.doris.catalog.OlapTable; import org.apache.doris.catalog.Type; import org.apache.doris.nereids.CascadesContext; import org.apache.doris.nereids.StatementContext; @@ -125,6 +126,17 @@ public Plan visitPhysicalTopN(PhysicalTopN topN, CascadesContext ctx) { // conflict expr id StatementContext threadStatementContext = StatementScopeIdGenerator.getStatementContext(); for (Relation relation : relationToLazySlotMap.keySet()) { + // TopN lazy materialization relies on BE adding a GLOBAL_ROWID_COL to the + // tablet schema. When light_schema_change=false, the table columns have + // col_unique_id=-1, which causes BE to skip the schema rebuild from + // columns_desc, so the GLOBAL_ROWID_COL is never added and the scan fails. + if (relation instanceof CatalogRelation + && ((CatalogRelation) relation).getTable() instanceof OlapTable + && !((OlapTable) ((CatalogRelation) relation).getTable()).getEnableLightSchemaChange()) { + LOG.debug("Skip TopN lazy materialization for table {} with light_schema_change=false", + ((CatalogRelation) relation).getTable().getName()); + return topN; + } if (relation instanceof CatalogRelation) { CatalogRelation catalogRelation = (CatalogRelation) relation; Column rowIdCol = new Column(Column.GLOBAL_ROWID_COL + catalogRelation.getTable().getName(), 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 ea384d35bd2ec3..159cd611676086 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 @@ -24,6 +24,7 @@ import org.apache.doris.nereids.processor.post.PlanPostProcessors; import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan; import org.apache.doris.nereids.util.PlanChecker; +import org.apache.doris.planner.MaterializationNode; import org.apache.doris.planner.OlapScanNode; import org.apache.doris.planner.PlanFragment; @@ -78,4 +79,36 @@ public void test2() throws Exception { Assertions.assertEquals(1, slots.size()); Assertions.assertEquals("k2", slots.get(0).getColumn().getName()); } + + @Test + public void testLightSchemaChangeFalse() throws Exception { + this.createTable("create table tm_lsc_false (k int, v int) duplicate key(k) " + + "distributed by hash(k) buckets 1 " + + "properties('replication_num' = '1', 'light_schema_change' = 'false')"); + String sql = "select * from tm_lsc_false order by k limit 1"; + PlanChecker checker = PlanChecker.from(connectContext) + .analyze(sql) + .rewrite() + .implement(); + PhysicalPlan plan = checker.getPhysicalPlan(); + plan = new PlanPostProcessors(checker.getCascadesContext()).process(plan); + PlanTranslatorContext translatorContext = new PlanTranslatorContext(checker.getCascadesContext()); + PlanFragment fragment = new PhysicalPlanTranslator(translatorContext).translatePlan(plan); + + // TopN lazy materialization should be skipped for light_schema_change=false, + // so no MaterializationNode should be created. + List materializationNodes = Lists.newArrayList(); + fragment.getPlanRoot().collect(MaterializationNode.class, materializationNodes); + Assertions.assertTrue(materializationNodes.isEmpty(), + "TopN lazy materialization should be skipped for light_schema_change=false"); + + // All columns should be in the scan output (no lazy pruned columns). + List scanNodes = Lists.newArrayList(); + fragment.getPlanRoot().collect(OlapScanNode.class, scanNodes); + Assertions.assertEquals(1, scanNodes.size()); + List slots = scanNodes.get(0).getTupleDesc().getSlots(); + Assertions.assertEquals(2, slots.size()); + Assertions.assertTrue(slots.stream().anyMatch(s -> s.getColumn().getName().equals("k"))); + Assertions.assertTrue(slots.stream().anyMatch(s -> s.getColumn().getName().equals("v"))); + } }