This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 908e484f018 [fix](be) Preserve nested paths for lazy rowid fetch
(#67205)
908e484f018 is described below
commit 908e484f01870a18a0e5f0ba0ab87b519860a319
Author: Jerry Hu <[email protected]>
AuthorDate: Fri Aug 28 21:01:42 2026 +0800
[fix](be) Preserve nested paths for lazy rowid fetch (#67205)
### What problem does this PR solve?
Issue Number: None
Related PR: #64242
Problem Summary:
TopN lazy materialization can fetch a nested-pruned column by row ID
after the scan has reduced its child layout. On branch-4.1, the
materialization probe may remap to a slot without the relation's
access-path metadata, and the row-store fetch path cannot honor nested
access paths. That can make the storage iterator layout disagree with
the pruned result column layout.
This backport:
- preserves relation access paths through the lazy-materialization
output;
- routes nested lazy slots away from row-store fetch;
- applies the slot access paths before branch-4.1's per-row
`seek_and_read_by_rowid` calls;
- keeps the parent struct iterator readable while skipped child
iterators are marked individually.
The FE output-slot propagation is included because master already had
that prerequisite when #64242 merged, while branch-4.1 does not.
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [x] Unit Test
- `./run-fe-ut.sh --run
org.apache.doris.nereids.processor.post.materialize.MaterializeProbeVisitorTest`
(5 tests passed)
- [x] Manual test (add detailed scripts or steps below)
- Executed the nested STRUCT/MAP TopN lazy-rowid reproduction on the
branch-4.1 adapted backport with
`topn_lazy_materialization_threshold=1024`.
- The query returned the expected primary keys `25, 21, 17, 13, 9`, and
the BE remained alive.
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
A full current-head BE rebuild was attempted but was blocked by stale
local Lance C++ third-party headers (`LanceScanStatistics` is missing
from the installed headers). It is not counted as validation; CI is
still required.
- Behavior changed:
- [ ] No.
- [x] Yes. Nested-pruned lazy columns use the normal storage rowid fetch
path instead of row-store fetch.
- Does this need documentation?
- [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
be/src/exec/rowid_fetcher.cpp | 24 ++++
be/src/storage/segment/column_reader.cpp | 1 -
.../glue/translator/PhysicalPlanTranslator.java | 27 ++++-
.../post/materialize/MaterializeProbeVisitor.java | 20 +++-
.../plans/physical/PhysicalLazyMaterialize.java | 13 ++-
.../materialize/MaterializeProbeVisitorTest.java | 128 +++++++++++++++++++++
6 files changed, 205 insertions(+), 8 deletions(-)
diff --git a/be/src/exec/rowid_fetcher.cpp b/be/src/exec/rowid_fetcher.cpp
index a8c3689d578..61c5a529d73 100644
--- a/be/src/exec/rowid_fetcher.cpp
+++ b/be/src/exec/rowid_fetcher.cpp
@@ -346,6 +346,28 @@ struct IteratorItem {
StorageReadOptions storage_read_options;
};
+static void set_slot_access_paths(const SlotDescriptor& slot, const
TabletSchema& schema,
+ StorageReadOptions& storage_read_options) {
+ int32_t unique_id = slot.col_unique_id();
+ const int field_index =
+ unique_id >= 0 ? schema.field_index(unique_id) :
schema.field_index(slot.col_name());
+ if (field_index >= 0) {
+ const auto& column = schema.column(field_index);
+ unique_id = column.unique_id() >= 0 ? column.unique_id() :
column.parent_unique_id();
+ }
+ if (unique_id < 0) {
+ return;
+ }
+
+ if (!slot.all_access_paths().empty()) {
+ storage_read_options.all_access_paths[unique_id] =
slot.all_access_paths();
+ }
+
+ if (!slot.predicate_access_paths().empty()) {
+ storage_read_options.predicate_access_paths[unique_id] =
slot.predicate_access_paths();
+ }
+}
+
struct SegItem {
BaseTabletSPtr tablet;
BetaRowsetSharedPtr rowset;
@@ -473,6 +495,7 @@ Status RowIdStorageReader::read_by_rowids(const
PMultiGetRequest& request,
iterator_item.storage_read_options.io_ctx.reader_type =
ReaderType::READER_QUERY;
}
segment = iterator_item.segment;
+ set_slot_access_paths(slots[x], full_read_schema,
iterator_item.storage_read_options);
RETURN_IF_ERROR(segment->seek_and_read_by_rowid(
full_read_schema, &slots[x], row_id, column,
iterator_item.storage_read_options,
iterator_item.iterator));
@@ -1283,6 +1306,7 @@ Status RowIdStorageReader::read_doris_format_row(
iterator_item.storage_read_options.io_ctx.file_cache_miss_policy =
file_cache_miss_policy;
}
+ set_slot_access_paths(slots[x], full_read_schema,
iterator_item.storage_read_options);
for (auto row_id : row_ids) {
RETURN_IF_ERROR(segment->seek_and_read_by_rowid(
full_read_schema, &slots[x], row_id, column,
diff --git a/be/src/storage/segment/column_reader.cpp
b/be/src/storage/segment/column_reader.cpp
index 1a4c42cc7bf..71c0eae2546 100644
--- a/be/src/storage/segment/column_reader.cpp
+++ b/be/src/storage/segment/column_reader.cpp
@@ -1491,7 +1491,6 @@ Status StructFileColumnIterator::set_access_paths(
}
if (!need_to_read) {
- set_reading_flag(ReadingFlag::SKIP_READING);
sub_iterator->set_reading_flag(ReadingFlag::SKIP_READING);
DLOG(INFO) << "Struct column iterator set sub-column " << name <<
" to SKIP_READING";
continue;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index 214f4badf69..4b63cc08636 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -2968,7 +2968,32 @@ public class PhysicalPlanTranslator extends
DefaultPlanVisitor<PlanFragment, Pla
useRowStore = olapTable.storeRowColumn()
&&
CollectionUtils.isEmpty(olapTable.getTableProperty().getCopiedRowStoreColumns());
}
- return useRowStore && canUseRowStoreForLazySlots(lazySlots);
+ return useRowStore && canUseRowStoreForLazySlots(lazySlots)
+ && !hasNestedAccessPaths(rel, lazySlots);
+ }
+
+ private boolean hasNestedAccessPaths(Relation rel, List<Slot> lazySlots) {
+ Set<Integer> lazyColumnUniqueIds = new HashSet<>();
+ for (Slot lazySlot : lazySlots) {
+ SlotReference slotReference = (SlotReference) lazySlot;
+
lazyColumnUniqueIds.add(slotReference.getOriginalColumn().get().getUniqueId());
+ }
+ for (Slot outputSlot : rel.getOutput()) {
+ if (outputSlot instanceof SlotReference) {
+ SlotReference slotReference = (SlotReference) outputSlot;
+ if (slotReference.getOriginalColumn().isPresent()
+ &&
lazyColumnUniqueIds.contains(slotReference.getOriginalColumn().get().getUniqueId())
+ && hasNestedAccessPaths(slotReference)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean hasNestedAccessPaths(SlotReference slotReference) {
+ return slotReference.getAllAccessPaths().map(paths ->
!paths.isEmpty()).orElse(false)
+ || slotReference.getPredicateAccessPaths().map(paths ->
!paths.isEmpty()).orElse(false);
}
@Override
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 8336105a004..feed5e456ac 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
@@ -85,7 +85,9 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
return Optional.empty();
}
if (filter.getInputSlots().contains(context.slot)) {
- return Optional.of(new MaterializeSource((Relation)
filter.child(), context.slot));
+ Relation relation = (Relation) filter.child();
+ return Optional.of(new MaterializeSource(
+ relation, findRelationOutputSlot(relation,
context.slot).orElse(context.slot)));
} else {
return filter.child().accept(this, context);
}
@@ -161,7 +163,8 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
if (scan.getOperativeSlots().contains(context.slot)) {
return Optional.empty();
}
- return Optional.of(new MaterializeSource(scan, context.slot));
+ return Optional.of(
+ new MaterializeSource(scan, findRelationOutputSlot(scan,
context.slot).orElse(context.slot)));
}
@Override
@@ -172,7 +175,8 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
&& !relation.getOperativeSlots().contains(context.slot)) {
// lazy materialize slot must be a passive slot
if (context.slot.getOriginalColumn().isPresent()) {
- return Optional.of(new MaterializeSource(relation,
context.slot));
+ return Optional.of(new MaterializeSource(
+ relation, findRelationOutputSlot(relation,
context.slot).orElse(context.slot)));
} else {
LOG.info("lazy materialize {} failed, because its column is
empty", context.slot);
}
@@ -192,7 +196,8 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
&& !tvfRelation.getOperativeSlots().contains(context.slot)) {
// lazy materialize slot must be a passive slot
if (context.slot.getOriginalColumn().isPresent()) {
- return Optional.of(new MaterializeSource(tvfRelation,
context.slot));
+ return Optional.of(new MaterializeSource(
+ tvfRelation, findRelationOutputSlot(tvfRelation,
context.slot).orElse(context.slot)));
} else {
LOG.info("lazy materialize {} failed, because its column is
empty", context.slot);
}
@@ -239,4 +244,11 @@ public class MaterializeProbeVisitor extends
DefaultPlanVisitor<Optional<Materia
}
}
+ private Optional<SlotReference> findRelationOutputSlot(Relation relation,
SlotReference contextSlot) {
+ return relation.getOutput().stream()
+ .filter(slot -> slot instanceof SlotReference &&
slot.equals(contextSlot))
+ .map(slot -> (SlotReference) slot)
+ .findFirst();
+ }
+
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterialize.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterialize.java
index 51a8bf054f5..bf9cf215960 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterialize.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterialize.java
@@ -167,8 +167,17 @@ public class PhysicalLazyMaterialize<CHILD_TYPE extends
Plan> extends PhysicalUn
// Set originalColumn on the lazy slot so that createSlotDesc
can write
// colUniqueId into the thrift SlotDescriptor — BE needs it to
resolve
// the column during remote fetch.
- Column originalColumn =
materializeMap.get(lazySlot).baseSlot.getOriginalColumn().get();
- outputBuilder.add(((SlotReference)
lazySlot).withColumn(originalColumn));
+ SlotReference baseSlot = materializeMap.get(lazySlot).baseSlot;
+ Column originalColumn = baseSlot.getOriginalColumn().get();
+ SlotReference outputSlot = ((SlotReference)
lazySlot).withColumn(originalColumn);
+ if (baseSlot.getAllAccessPaths().isPresent()) {
+ outputSlot = outputSlot.withAccessPaths(
+ baseSlot.getAllAccessPaths().get(),
+
baseSlot.getPredicateAccessPaths().orElse(ImmutableList.of()),
+
baseSlot.getDisplayAllAccessPaths().orElse(ImmutableList.of()),
+
baseSlot.getDisplayPredicateAccessPaths().orElse(ImmutableList.of()));
+ }
+ outputBuilder.add(outputSlot);
lazyColumnForRel.add(originalColumn);
lazyBaseColumnIdxForRel.add(relationTable.getBaseColumnIdxByName(originalColumn.getName()));
lazySlotLocationForRel.add(loc);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitorTest.java
index de143cea0a3..94d24536693 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/processor/post/materialize/MaterializeProbeVisitorTest.java
@@ -17,15 +17,38 @@
package org.apache.doris.nereids.processor.post.materialize;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.functions.table.VectorSearch;
+import org.apache.doris.nereids.trees.plans.algebra.Relation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterialize;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
import org.apache.doris.nereids.trees.plans.physical.PhysicalTVFRelation;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.qe.ConnectContext;
import org.apache.doris.tablefunction.VectorSearchTableValuedFunction;
+import org.apache.doris.thrift.TAccessPathType;
+import org.apache.doris.thrift.TColumnAccessPath;
+import org.apache.doris.thrift.TDataAccessPath;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
class MaterializeProbeVisitorTest {
@Test
@@ -47,6 +70,111 @@ class MaterializeProbeVisitorTest {
relation, new
MaterializeProbeVisitor.ProbeContext(nestedSlot)).isPresent());
}
+ @Test
+ void testOlapScanUsesRelationSlotWithAccessPaths() {
+ SlotReference contextSlot = new SlotReference("a",
IntegerType.INSTANCE);
+ SlotReference relationSlot = contextSlot.withAccessPaths(
+ ImmutableList.of(dataPath("nested")), ImmutableList.of());
+ contextSlot = (SlotReference) contextSlot.withNullable(false);
+ PhysicalOlapScan scan = mockBaseOlapScan(relationSlot);
+
+ MaterializeProbeVisitor.ProbeContext context = new
MaterializeProbeVisitor.ProbeContext(contextSlot);
+ Optional<MaterializeSource> source = new
MaterializeProbeVisitor().visitPhysicalOlapScan(scan, context);
+
+ Assertions.assertTrue(source.isPresent());
+ Assertions.assertSame(relationSlot, source.get().baseSlot);
+ Assertions.assertEquals(relationSlot.getAllAccessPaths(),
source.get().baseSlot.getAllAccessPaths());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void testFilterUsingIndexUsesRelationSlotWithAccessPaths() {
+ ConnectContext oldContext = ConnectContext.get();
+ ConnectContext context = new ConnectContext();
+ context.getSessionVariable().topNLazyMaterializationUsingIndex = true;
+ context.setThreadLocalInfo();
+ try {
+ SlotReference contextSlot = new SlotReference("a",
IntegerType.INSTANCE);
+ SlotReference relationSlot = contextSlot.withAccessPaths(
+ ImmutableList.of(dataPath("nested")), ImmutableList.of());
+ contextSlot = (SlotReference) contextSlot.withNullable(false);
+ PhysicalOlapScan scan = mockBaseOlapScan(relationSlot);
+
+ PhysicalFilter<PhysicalOlapScan> filter =
Mockito.mock(PhysicalFilter.class);
+ Mockito.when(filter.child()).thenReturn(scan);
+
Mockito.when(filter.getInputSlots()).thenReturn(ImmutableSet.of(contextSlot));
+
+ MaterializeProbeVisitor.ProbeContext probeContext = new
MaterializeProbeVisitor.ProbeContext(contextSlot);
+ Optional<MaterializeSource> source =
+ new MaterializeProbeVisitor().visitPhysicalFilter(filter,
probeContext);
+
+ Assertions.assertTrue(source.isPresent());
+ Assertions.assertSame(relationSlot, source.get().baseSlot);
+ Assertions.assertEquals(relationSlot.getAllAccessPaths(),
source.get().baseSlot.getAllAccessPaths());
+ } finally {
+ if (oldContext == null) {
+ ConnectContext.remove();
+ } else {
+ oldContext.setThreadLocalInfo();
+ }
+ }
+ }
+
+ @Test
+ void testLazyMaterializeOutputKeepsBaseSlotAccessPaths() {
+ Column column = Mockito.mock(Column.class);
+ Mockito.when(column.getName()).thenReturn("a");
+ OlapTable table = Mockito.mock(OlapTable.class);
+ Mockito.when(table.getBaseColumnIdxByName("a")).thenReturn(0);
+ PhysicalOlapScan relation = Mockito.mock(PhysicalOlapScan.class);
+ Mockito.when(relation.getTable()).thenReturn(table);
+ Mockito.when(relation.getAllChildrenTypes()).thenReturn(new BitSet());
+
+ List<TColumnAccessPath> allPaths = ImmutableList.of(dataPath("all"));
+ List<TColumnAccessPath> predicatePaths =
ImmutableList.of(dataPath("predicate"));
+ List<TColumnAccessPath> displayAllPaths =
ImmutableList.of(dataPath("display_all"));
+ List<TColumnAccessPath> displayPredicatePaths =
ImmutableList.of(dataPath("display_predicate"));
+ SlotReference baseSlot = new SlotReference("a", IntegerType.INSTANCE)
+ .withColumn(column)
+ .withAccessPaths(allPaths, predicatePaths, displayAllPaths,
displayPredicatePaths);
+ SlotReference lazySlot = new SlotReference("a", IntegerType.INSTANCE);
+ SlotReference rowId = new SlotReference("__DORIS_ROWID_COL__",
IntegerType.INSTANCE);
+
+ BiMap<Relation, SlotReference> relationToRowId = HashBiMap.create();
+ relationToRowId.put(relation, rowId);
+ Map<Relation, List<Slot>> relationToLazySlotMap = ImmutableMap.of(
+ relation, ImmutableList.<Slot>of(lazySlot));
+ Map<Slot, MaterializeSource> materializeMap = ImmutableMap.of(
+ lazySlot, new MaterializeSource(relation, baseSlot));
+ PhysicalLazyMaterialize<PhysicalOlapScan> materialize = new
PhysicalLazyMaterialize<>(
+ relation, ImmutableList.of(rowId), ImmutableList.of(),
relationToLazySlotMap,
+ relationToRowId, materializeMap);
+
+ SlotReference outputSlot = (SlotReference)
materialize.getOutput().get(0);
+ Assertions.assertEquals(Optional.of(allPaths),
outputSlot.getAllAccessPaths());
+ Assertions.assertEquals(Optional.of(predicatePaths),
outputSlot.getPredicateAccessPaths());
+ Assertions.assertEquals(Optional.of(displayAllPaths),
outputSlot.getDisplayAllAccessPaths());
+ Assertions.assertEquals(Optional.of(displayPredicatePaths),
outputSlot.getDisplayPredicateAccessPaths());
+ }
+
+ private TColumnAccessPath dataPath(String... path) {
+ TColumnAccessPath accessPath = new
TColumnAccessPath(TAccessPathType.DATA);
+ accessPath.data_access_path = new
TDataAccessPath(ImmutableList.copyOf(path));
+ return accessPath;
+ }
+
+ private PhysicalOlapScan mockBaseOlapScan(SlotReference outputSlot) {
+ OlapTable table = Mockito.mock(OlapTable.class);
+ Mockito.when(table.getBaseIndexId()).thenReturn(1L);
+ Mockito.when(table.getKeysType()).thenReturn(KeysType.DUP_KEYS);
+ PhysicalOlapScan scan = Mockito.mock(PhysicalOlapScan.class);
+ Mockito.when(scan.getSelectedIndexId()).thenReturn(1L);
+ Mockito.when(scan.getTable()).thenReturn(table);
+
Mockito.when(scan.getOutput()).thenReturn(ImmutableList.of(outputSlot));
+ Mockito.when(scan.getOperativeSlots()).thenReturn(ImmutableList.of());
+ return scan;
+ }
+
private PhysicalTVFRelation mockVectorSearchRelation() {
PhysicalTVFRelation relation = Mockito.mock(PhysicalTVFRelation.class);
VectorSearch function = Mockito.mock(VectorSearch.class);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]