This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 9d243cd7e31 branch-4.2: [fix](external) Keep sub-field predicate 
access paths (#68331)
9d243cd7e31 is described below

commit 9d243cd7e310f07d945119104f7844feb7d6caf5
Author: daidai <[email protected]>
AuthorDate: Tue Sep 22 10:26:10 2026 +0800

    branch-4.2: [fix](external) Keep sub-field predicate access paths (#68331)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #68214
    
    Problem Summary:
    
    `retainPredicatePathsInFinalAllAccessPaths`, added by #68214, removes
    every predicate access path that is not literally one of the final all
    access paths. That is what NULL/OFFSET paths need, because they are
    stripped from the all paths on purpose, but it also removes ordinary
    sub-field paths whenever the all paths collapse to the whole-column
    path:
    
    ```sql
    SELECT s FROM tbl WHERE struct_element(s, 'city') = 'x';
    -- all access paths: [s], predicate access paths: [] (was [s.city])
    ```
    
    BE then cannot tell which sub-column the predicate reads, so it loses
    the eager/lazy split and reads the column as one unit. On branch-4.2
    this shows up in `test_iceberg_variant_read`, where the lazy-read check
    sees `FilteredRowsByLazyRead = 0` for
    
    ```sql
    SELECT CAST(v AS STRING) FROM variant_page_pruning WHERE CAST(v['n'] AS 
INT) > 3000
    ```
    
    Only file scans hit this: on OLAP tables a variant sub-path predicate
    gets its own sub-column slot, so its all paths already contain the
    predicate path. That is why the existing unit tests did not catch it,
    and why the new test uses a struct column, which keeps one slot for the
    whole column.
    
    This PR keeps the NULL/OFFSET cleanup, since BE switches the whole
    iterator to `NULL_MAP_ONLY`/`OFFSET_ONLY` once such a path shows up and
    skips the children. Every other predicate path is kept, and added to the
    all paths when no wider path covers it — the behaviour of master's
    `addPredicatePathsToFinalAllAccessPaths`.
    
    ### Release note
    
    Fix nested/variant sub-field predicates losing lazy materialization on
    branch-4.2 when the whole column is read.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] 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
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Sub-field predicates keep their access paths, restoring
    predicate-first reads.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../nereids/rules/rewrite/NestedColumnPruning.java | 55 ++++++++++++++--------
 .../rules/rewrite/PruneNestedColumnTest.java       | 17 +++++++
 2 files changed, 52 insertions(+), 20 deletions(-)

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 8056ebd747c..d60686c2ec6 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 @@ public class NestedColumnPruning implements CustomRewriter {
                     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 @@ public class NestedColumnPruning implements CustomRewriter {
                     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 @@ public class NestedColumnPruning implements 
CustomRewriter {
     }
 
     /**
-     * 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.
      *
-     * <p>Examples:
-     * <ul>
-     *   <li>All paths {@code [s]}, predicate paths {@code [s.city.NULL]} 
becomes no predicate
-     *       paths after parent NULL removal.</li>
-     *   <li>All paths {@code [s.city.NULL, s.zip]}, predicate paths
-     *       {@code [s.NULL, s.city.NULL]} becomes {@code [s.city.NULL]}.</li>
-     * </ul>
+     * <p>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.
+     *
+     * <p>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<TColumnAccessPath> predicatePaths, List<TColumnAccessPath> 
allPaths) {
-        if (predicatePaths.isEmpty()) {
-            return;
-        }
-
         List<TColumnAccessPath> 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<TColumnAccessPath> allPaths) 
{
+        for (TColumnAccessPath allPath : allPaths) {
+            if (allPath.getType() == predicatePath.getType()
+                    && pathCoversPrefix(getAccessPathList(allPath), 
getAccessPathList(predicatePath))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static boolean hasStrictPrefix(List<String> path, List<String> 
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 71accf14247..f3aa4bf42de 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 class PruneNestedColumnTest extends 
TestWithFeService implements MemoPatt
         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<PhysicalPlan, List<SlotDescriptor>> result = collectComplexSlots(
+                "select s from tbl where struct_element(s, 'city') = 'x'");
+        TreeSet<TColumnAccessPath> allAccessPaths = new TreeSet<>();
+        TreeSet<TColumnAccessPath> 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",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to