diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
index 8056ebd747c214..d60686c2ec6030 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
@@ -401,7 +401,7 @@ && containsDataSkippingOnlyAccessPath(collectAccessPathResults)) {
buildColumnAccessPaths(slot, predicateAccessPaths);
AccessPathInfo accessPathInfo = result.get(slot.getExprId().asInt());
if (accessPathInfo != null) {
- retainPredicatePathsInFinalAllAccessPaths(
+ alignPredicatePathsWithFinalAllAccessPaths(
predicatePaths, accessPathInfo.getAllAccessPaths());
accessPathInfo.getPredicateAccessPaths().addAll(predicatePaths);
}
@@ -413,7 +413,7 @@ && containsDataSkippingOnlyAccessPath(collectAccessPathResults)) {
buildColumnAccessPaths(slot, predicateAccessPaths);
AccessPathInfo accessPathInfo = result.get(slot.getExprId().asInt());
if (accessPathInfo != null) {
- retainPredicatePathsInFinalAllAccessPaths(
+ alignPredicatePathsWithFinalAllAccessPaths(
predicatePaths, accessPathInfo.getAllAccessPaths());
accessPathInfo.getPredicateAccessPaths().addAll(predicatePaths);
}
@@ -861,34 +861,49 @@ private static void stripNullSuffixPaths(
}
/**
- * Keep predicate access paths as a subset of final all access paths after NULL/OFFSET cleanup.
- * Predicate paths are built from filter expressions first, but later all-path rewrites may drop
- * metadata-only paths or collapse paths to whole-column access. Any predicate path not present
- * in final all paths must be removed before sending access info to BE.
+ * Reconcile predicate access paths with the final all access paths. Predicate paths are built
+ * from filter expressions first, but later all-path rewrites drop redundant paths or collapse
+ * them to whole-column access, so a predicate path can end up outside the final all paths.
*
- *
Examples:
- *
- * - All paths {@code [s]}, predicate paths {@code [s.city.NULL]} becomes no predicate
- * paths after parent NULL removal.
- * - All paths {@code [s.city.NULL, s.zip]}, predicate paths
- * {@code [s.NULL, s.city.NULL]} becomes {@code [s.city.NULL]}.
- *
+ * A NULL/OFFSET path is dropped when it is no longer one of the all paths: BE switches the
+ * whole iterator to NULL_MAP_ONLY/OFFSET_ONLY when it sees such a path and skips the children,
+ * so it must not come back through the predicate paths either.
+ *
+ *
Any other path is kept, because BE needs it to read the predicate columns first and
+ * lazily materialize the rest. It is added to the all paths unless a wider path already covers
+ * it, e.g. the whole-column path {@code [s]} covers the predicate path {@code [s.city]}.
*/
- private static void retainPredicatePathsInFinalAllAccessPaths(
+ private static void alignPredicatePathsWithFinalAllAccessPaths(
List predicatePaths, List allPaths) {
- if (predicatePaths.isEmpty()) {
- return;
- }
-
List toRemove = new ArrayList<>();
for (TColumnAccessPath predicatePath : predicatePaths) {
- if (!allPaths.contains(predicatePath)) {
- toRemove.add(predicatePath);
+ if (isMetaOnlyAccessPath(predicatePath)) {
+ if (!allPaths.contains(predicatePath)) {
+ toRemove.add(predicatePath);
+ }
+ } else if (!isCoveredByAllPath(predicatePath, allPaths)) {
+ allPaths.add(predicatePath);
}
}
predicatePaths.removeAll(toRemove);
}
+ private static boolean isMetaOnlyAccessPath(TColumnAccessPath accessPath) {
+ return accessPath.getType() == TAccessPathType.META
+ || isDataSkippingOnlyAccessPath(getAccessPathList(accessPath));
+ }
+
+ private static boolean isCoveredByAllPath(
+ TColumnAccessPath predicatePath, List allPaths) {
+ for (TColumnAccessPath allPath : allPaths) {
+ if (allPath.getType() == predicatePath.getType()
+ && pathCoversPrefix(getAccessPathList(allPath), getAccessPathList(predicatePath))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static boolean hasStrictPrefix(List path, List prefix) {
return path.size() > prefix.size() && path.subList(0, prefix.size()).equals(prefix);
}
diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java
index cd0e08327b29cc..dfe5f595d9e3b1 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneNestedColumnTest.java
@@ -270,6 +270,23 @@ public void testMapElementArrayNullPathCoveredByValueFieldAccess() throws Except
Assertions.assertFalse(predicateAccessPaths.contains(path("s", "m", "*", "NULL")));
}
+ @Test
+ public void testWholeColumnOutputKeepsSubFieldPredicatePath() throws Exception {
+ // The whole column is read, so all access paths collapse to the root path. The predicate
+ // path must survive, otherwise BE cannot read the predicate field first and lazily
+ // materialize the rest of the column.
+ Pair> result = collectComplexSlots(
+ "select s from tbl where struct_element(s, 'city') = 'x'");
+ TreeSet allAccessPaths = new TreeSet<>();
+ TreeSet predicateAccessPaths = new TreeSet<>();
+ for (SlotDescriptor slotDescriptor : result.second) {
+ allAccessPaths.addAll(slotDescriptor.getAllAccessPaths());
+ predicateAccessPaths.addAll(slotDescriptor.getPredicateAccessPaths());
+ }
+ Assertions.assertEquals(ImmutableList.of(path("s")), ImmutableList.copyOf(allAccessPaths));
+ Assertions.assertEquals(ImmutableList.of(path("s", "city")), ImmutableList.copyOf(predicateAccessPaths));
+ }
+
@Test
public void testVariantAccessPath() throws Exception {
assertColumn("select v['a']['B'] from variant_tbl",