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

morrySnow 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 1271f00190d [fix](rbo) Guard union equal-set propagation (#67886)
1271f00190d is described below

commit 1271f00190d8ab7881adde1fdc78519fc5cb9ff3
Author: morrySnow <[email protected]>
AuthorDate: Sun Sep 20 10:22:16 2026 +0800

    [fix](rbo) Guard union equal-set propagation (#67886)
    
    ## Problem
    
    `UNION ALL` could publish output-column equality that does not hold for
    every row. Downstream rules may then remove a required window ordering
    key and change `RANK()` results.
    
    Two forms reproduce the problem:
    
    1. Regular children project columns in a different order from their
    internal output. With child filters proving `a = b`, a union that
    projects `(c, a, b)` can incorrectly map that equality to `(c, a)` and
    remove `a` from `ORDER BY c, a`.
    2. A regular child proves `a = b`, but constant rows such as `(1, 2)`
    and `(2, 1)` do not. The union can still claim the output columns are
    equal and remove `b` from `ORDER BY a, b`.
    
    ## Root cause
    
    Logical and physical unions independently mapped child equality sets
    through `child.getOutput()` positions. The authoritative union ordinal
    mapping is `regularChildrenOutputs`, which can have a different order.
    The derivation also considered only regular children and ignored every
    constant row carried by the union.
    
    ## Reproduction
    
    Create rows `(1, 1, 10)`, `(2, 2, 10)`, and `(1, 1, 20)`, duplicate a
    filtered `(c, a, b)` child with `UNION ALL`, and compute `RANK() OVER
    (ORDER BY c, a)`. The two rows with `(c, a) = (10, 2)` must have rank 3.
    
    Separately, union filtered table rows satisfying `a = b` with constant
    rows `(1, 2)` and `(2, 1)`, then compute `RANK() OVER (ORDER BY a, b)`.
    The four ordered pairs must receive ranks 1, 2, 3, and 4.
    
    ## Fix
    
    - Share one equal-set derivation between logical and physical unions.
    - Validate and use each regular child's explicit union-output mapping.
    - Intersect child equality classes by per-ordinal class signatures,
    avoiding pairwise quadratic candidate generation.
    - Refine candidates against every constant row after SQL comparison
    coercion and constant folding.
    - Accept only a folded `TRUE`; `NULL`, unsupported coercion,
    non-foldable expressions, and malformed mappings conservatively provide
    no equality proof.
    - Build each surviving equivalence class with linear star edges.
---
 .../nereids/properties/UnionDataTraitUtils.java    | 576 +++++++++++++++++++++
 .../nereids/trees/plans/logical/LogicalUnion.java  |  74 +--
 .../trees/plans/physical/PhysicalUnion.java        |  75 +--
 .../doris/nereids/properties/EqualSetTest.java     | 139 +++++
 .../rules/rewrite/EliminateOrderByKeyTest.java     |  36 ++
 .../union_equal_set/union_equal_set.out            |  28 +
 .../union_equal_set/union_equal_set.groovy         | 121 +++++
 7 files changed, 904 insertions(+), 145 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/UnionDataTraitUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/UnionDataTraitUtils.java
new file mode 100644
index 00000000000..7cdd44feb42
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/UnionDataTraitUtils.java
@@ -0,0 +1,576 @@
+// 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.
+
+package org.apache.doris.nereids.properties;
+
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.NullSafeEqual;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NumericLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.Union;
+import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+import org.apache.doris.qe.ConnectContext;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Derives equal sets shared by logical and physical union plans.
+ *
+ * <p>Two union output slots are equal only when the corresponding values are 
equal in every regular
+ * child and every constant row. Regular children contribute equality 
information through their data
+ * traits, while constant rows contribute equality information through 
constant folding and
+ * null-safe equality semantics.
+ */
+public final class UnionDataTraitUtils {
+
+    /** Utility class; it must not be instantiated. */
+    private UnionDataTraitUtils() {
+    }
+
+    /**
+     * Computes union output equalities that hold for every row source and 
adds them to {@code builder}.
+     *
+     * <p>An output ordinal identifies the same union column across {@code 
outputs}, every entry in
+     * {@code regularChildrenOutputs}, and every constant row. For regular 
children, this method keeps
+     * only groups of ordinals whose mapped child slots belong to the same 
equality class in every
+     * child. It then refines those groups with every constant row. A constant 
row retains a pair only
+     * when folding their expressions and evaluating their null-safe equality 
produces {@code TRUE}.
+     *
+     * <p>The union is assumed to satisfy the structural invariants 
established during analysis: each
+     * regular child has one output mapping and every regular or constant 
input has the union output
+     * width. This method does not validate those invariants. It leaves 
existing entries in
+     * {@code builder} unchanged and only adds equal pairs proven by all union 
inputs.
+     *
+     * @param union union metadata that supplies regular-child output mappings 
and constant rows
+     * @param unionPlan concrete logical or physical union plan that supplies 
children and output slots
+     * @param builder destination to which proven equal pairs between union 
output slots are added
+     */
+    public static void computeEqualSet(Union union, Plan unionPlan, 
DataTrait.Builder builder) {
+        List<Slot> outputs = unionPlan.getOutput();
+        List<Plan> children = unionPlan.children();
+        List<List<SlotReference>> childrenOutputs = 
union.getRegularChildrenOutputs();
+        List<List<NamedExpression>> constantRows = 
union.getConstantExprsList();
+        if (outputs.size() < 2 || (children.isEmpty() && 
constantRows.isEmpty())) {
+            return;
+        }
+
+        List<List<Integer>> equalGroups = children.isEmpty()
+                ? oneGroupForAllOutputs(outputs.size())
+                : intersectChildEqualGroups(children, childrenOutputs, 
outputs.size());
+
+        if (!constantRows.isEmpty() && !equalGroups.isEmpty()) {
+            Optional<ExpressionRewriteContext> context = 
createRewriteContext(unionPlan);
+            for (List<NamedExpression> row : constantRows) {
+                equalGroups = refineByConstantRow(equalGroups, row, context, 
outputs.size());
+                if (equalGroups.isEmpty()) {
+                    return;
+                }
+            }
+        }
+
+        for (List<Integer> equalGroup : equalGroups) {
+            int first = equalGroup.get(0);
+            for (int i = 1; i < equalGroup.size(); i++) {
+                builder.addEqualPair(outputs.get(first), 
outputs.get(equalGroup.get(i)));
+            }
+        }
+    }
+
+    /**
+     * Intersects the equality partitions of all regular union children by 
union output ordinal.
+     *
+     * <p>For each output ordinal, this method builds a signature containing 
that ordinal's equality
+     * class ID in every child. Two output ordinals have the same signature 
exactly when their mapped
+     * child slots are equal in every regular child. Singleton signature 
groups are omitted because
+     * they do not describe an equality between different union outputs.
+     *
+     * @param children regular union children; child {@code i} corresponds to 
mapping {@code i}
+     * @param childrenOutputs mapped child slots indexed first by child and 
then by union output ordinal
+     * @param outputSize number of union output ordinals represented by every 
child mapping
+     * @return groups of at least two output ordinals that are equal in every 
regular child
+     */
+    private static List<List<Integer>> intersectChildEqualGroups(List<Plan> 
children,
+            List<List<SlotReference>> childrenOutputs, int outputSize) {
+        List<List<Integer>> classIdsByChild = new ArrayList<>(children.size());
+        for (int childIndex = 0; childIndex < children.size(); childIndex++) {
+            classIdsByChild.add(equalClassIds(children.get(childIndex), 
childrenOutputs.get(childIndex)));
+        }
+
+        Map<List<Integer>, List<Integer>> ordinalsBySignature = new 
LinkedHashMap<>();
+        for (int outputIndex = 0; outputIndex < outputSize; outputIndex++) {
+            List<Integer> signature = new ArrayList<>(children.size());
+            for (List<Integer> childClassIds : classIdsByChild) {
+                signature.add(childClassIds.get(outputIndex));
+            }
+            ordinalsBySignature.computeIfAbsent(signature, key -> new 
ArrayList<>()).add(outputIndex);
+        }
+        return onlyNonTrivialGroups(ordinalsBySignature.values());
+    }
+
+    /**
+     * Encodes one child's mapped output slots as equality-class IDs.
+     *
+     * <p>Slots in the same child data-trait equal set receive the same ID. A 
mapped slot not present in
+     * any equal set receives its own ID, so it cannot accidentally compare 
equal to a different slot.
+     * Repeated occurrences of the same mapped slot reuse the same ID.
+     *
+     * @param child child plan whose logical data trait defines slot equalities
+     * @param childOutputs child slots in union output-ordinal order
+     * @return class IDs in union output-ordinal order; equal IDs denote equal 
mapped child slots
+     */
+    private static List<Integer> equalClassIds(Plan child, List<SlotReference> 
childOutputs) {
+        DataTrait childTrait = child.getLogicalProperties().getTrait();
+        Map<Slot, Integer> classIdBySlot = new HashMap<>();
+        int nextClassId = 0;
+        for (Set<Slot> equalSet : childTrait.calAllEqualSet()) {
+            for (Slot slot : equalSet) {
+                classIdBySlot.put(slot, nextClassId);
+            }
+            nextClassId++;
+        }
+
+        List<Integer> classIds = new ArrayList<>(childOutputs.size());
+        for (Slot childOutput : childOutputs) {
+            Integer classId = classIdBySlot.get(childOutput);
+            if (classId == null) {
+                classId = nextClassId++;
+                classIdBySlot.put(childOutput, classId);
+            }
+            classIds.add(classId);
+        }
+        return classIds;
+    }
+
+    /**
+     * Refines candidate output equality groups using one constant row.
+     *
+     * <p>Each expression is first folded to a literal when possible. 
Candidate ordinals are bucketed by
+     * a normalized {@link ConstantValueKey} to avoid comparing values that 
clearly differ. Every pair
+     * in a multi-ordinal bucket is then checked independently, and proven 
pairs are merged into
+     * equality components. All typed NULL literals use one shared key, so 
compatible NULL expressions
+     * can reach that final proof without an incompatible pair discarding the 
entire bucket. An ordinal
+     * whose expression cannot be folded, cannot be normalized, or cannot be 
connected to another
+     * ordinal by a proven null-safe equality is omitted from the returned 
groups.
+     *
+     * @param equalGroups candidate output-ordinal groups proven equal by 
inputs processed so far
+     * @param row constant expressions in union output-ordinal order
+     * @param context optional rewrite context used while folding constants 
and comparisons
+     * @param outputSize number of union outputs, used to size the per-ordinal 
literal list
+     * @return non-singleton subgroups whose expressions are also proven equal 
in this constant row
+     */
+    private static List<List<Integer>> refineByConstantRow(List<List<Integer>> 
equalGroups,
+            List<NamedExpression> row, Optional<ExpressionRewriteContext> 
context, int outputSize) {
+        List<Optional<Literal>> literals = new ArrayList<>(outputSize);
+        List<Optional<ConstantValueKey>> valueKeys = new 
ArrayList<>(outputSize);
+        for (NamedExpression expression : row) {
+            Optional<Literal> literal = foldConstant(unwrapAlias(expression), 
context);
+            literals.add(literal);
+            
valueKeys.add(literal.flatMap(UnionDataTraitUtils::constantValueKey));
+        }
+
+        List<List<Integer>> refinedGroups = new ArrayList<>();
+        for (List<Integer> equalGroup : equalGroups) {
+            Map<ConstantValueKey, List<Integer>> ordinalsByValue = new 
LinkedHashMap<>();
+            for (int outputIndex : equalGroup) {
+                Optional<ConstantValueKey> key = valueKeys.get(outputIndex);
+                key.ifPresent(valueKey -> ordinalsByValue
+                        .computeIfAbsent(valueKey, ignored -> new 
ArrayList<>()).add(outputIndex));
+            }
+            for (List<Integer> sameValueOrdinals : ordinalsByValue.values()) {
+                if (sameValueOrdinals.size() <= 1) {
+                    continue;
+                }
+                refinedGroups.addAll(splitByProvenEquality(
+                        sameValueOrdinals, row, literals, valueKeys, context));
+            }
+        }
+        return refinedGroups;
+    }
+
+    /**
+     * Splits one normalized-value bucket into independently proven equality 
components.
+     *
+     * <p>Each bucket position starts in its own disjoint-set component. This 
method evaluates every
+     * unordered pair of output ordinals and merges their components only when 
null-safe comparison
+     * folds to {@code TRUE}. Pairs whose roots are already equal are skipped, 
and proof results are
+     * cached by folded literal value and type so repeated compatible or 
incompatible pairs do not
+     * repeat coercion and folding. Evaluating the remaining pairs makes the 
result independent of
+     * ordinal order and preserves a compatible subgroup even when another 
member, such as an
+     * ARRAY-typed NULL, cannot be coerced with it. Connected pairs may share 
a component because
+     * proven value equality is transitive; singleton components are omitted 
because they publish no
+     * output equality.
+     *
+     * @param sameValueOrdinals output ordinals that share one normalized 
constant-value key
+     * @param row constant expressions in union output-ordinal order
+     * @param literals folded literals in union output-ordinal order
+     * @param valueKeys normalized keys for the folded literals in union 
output-ordinal order
+     * @param context optional rewrite context used for coercion and constant 
evaluation
+     * @return non-singleton ordinal components connected by proven null-safe 
equality pairs
+     */
+    private static List<List<Integer>> splitByProvenEquality(List<Integer> 
sameValueOrdinals,
+            List<NamedExpression> row, List<Optional<Literal>> literals,
+            List<Optional<ConstantValueKey>> valueKeys, 
Optional<ExpressionRewriteContext> context) {
+        int[] parents = new int[sameValueOrdinals.size()];
+        for (int i = 0; i < parents.length; i++) {
+            parents[i] = i;
+        }
+        Map<ConstantComparisonKey, Boolean> proofCache = new HashMap<>();
+
+        for (int left = 0; left < sameValueOrdinals.size(); left++) {
+            for (int right = left + 1; right < sameValueOrdinals.size(); 
right++) {
+                int leftRoot = findRoot(parents, left);
+                int rightRoot = findRoot(parents, right);
+                if (leftRoot == rightRoot) {
+                    continue;
+                }
+
+                int leftOrdinal = sameValueOrdinals.get(left);
+                int rightOrdinal = sameValueOrdinals.get(right);
+                ConstantComparisonKey comparisonKey = new 
ConstantComparisonKey(
+                        new LiteralSignature(literals.get(leftOrdinal).get(), 
valueKeys.get(leftOrdinal).get()),
+                        new LiteralSignature(literals.get(rightOrdinal).get(), 
valueKeys.get(rightOrdinal).get()));
+                Boolean equal = proofCache.get(comparisonKey);
+                if (equal == null) {
+                    equal = isNullSafeEqualInConstantRow(row, leftOrdinal, 
rightOrdinal, context);
+                    proofCache.put(comparisonKey, equal);
+                }
+                if (equal) {
+                    parents[rightRoot] = leftRoot;
+                }
+            }
+        }
+
+        Map<Integer, List<Integer>> ordinalsByRoot = new LinkedHashMap<>();
+        for (int i = 0; i < sameValueOrdinals.size(); i++) {
+            int root = findRoot(parents, i);
+            ordinalsByRoot.computeIfAbsent(root, ignored -> new ArrayList<>())
+                    .add(sameValueOrdinals.get(i));
+        }
+        return onlyNonTrivialGroups(ordinalsByRoot.values());
+    }
+
+    /**
+     * Finds the canonical root of one disjoint-set entry and compresses its 
parent path.
+     *
+     * @param parents disjoint-set parent array indexed by positions in a 
normalized-value bucket
+     * @param index bucket position whose component root is requested
+     * @return root position that identifies the equality component containing 
{@code index}
+     */
+    private static int findRoot(int[] parents, int index) {
+        int root = index;
+        while (parents[root] != root) {
+            root = parents[root];
+        }
+        while (parents[index] != index) {
+            int parent = parents[index];
+            parents[index] = root;
+            index = parent;
+        }
+        return root;
+    }
+
+    /**
+     * Tests whether two ordinals in a constant row are provably equal under 
null-safe semantics.
+     *
+     * <p>The expressions are unwrapped, coerced as operands of {@link 
NullSafeEqual}, and
+     * constant-folded. Only the literal result {@code TRUE} proves equality. 
This deliberately treats
+     * two NULL values as equal while treating a one-sided NULL as unequal. 
{@code FALSE}, an
+     * unavailable fold result, and unsupported coercion or folding all return 
{@code false}.
+     *
+     * @param row constant expressions in union output-ordinal order
+     * @param left ordinal of the left expression to compare
+     * @param right ordinal of the right expression to compare
+     * @param context optional rewrite context used for constant evaluation
+     * @return {@code true} only if the coerced null-safe equality folds to
+     *         {@link BooleanLiteral#TRUE}
+     */
+    private static boolean isNullSafeEqualInConstantRow(List<NamedExpression> 
row, int left, int right,
+            Optional<ExpressionRewriteContext> context) {
+        try {
+            Expression leftExpression = unwrapAlias(row.get(left));
+            Expression rightExpression = unwrapAlias(row.get(right));
+            Expression equality = TypeCoercionUtils.processComparisonPredicate(
+                    new NullSafeEqual(leftExpression, rightExpression));
+            Optional<Literal> result = 
ExpressionUtils.checkConstantExpr(equality, context);
+            // The trait is null-safe, so NULL <=> NULL is a valid proof while 
NULL <=> value is not.
+            return result.isPresent() && 
BooleanLiteral.TRUE.equals(result.get());
+        } catch (RuntimeException e) {
+            // Unsupported coercion or an expression that cannot be folded is 
not proof.
+            return false;
+        }
+    }
+
+    /**
+     * Attempts to fold an expression to a literal without allowing a fold 
failure to publish a trait.
+     *
+     * @param expression expression to evaluate as a constant
+     * @param context optional rewrite context used by constant evaluation
+     * @return the folded literal, or an empty optional when the expression 
cannot be folded safely
+     */
+    private static Optional<Literal> foldConstant(Expression expression,
+            Optional<ExpressionRewriteContext> context) {
+        try {
+            return ExpressionUtils.checkConstantExpr(expression, context);
+        } catch (RuntimeException e) {
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Builds a normalized key used to bucket literals that may be equal.
+     *
+     * <p>All numeric literals share a numeric family and use a 
scale-insensitive decimal value.
+     * String-like and date literals are grouped within their respective 
families by string value.
+     * Other literals retain their concrete class and literal object. Every 
typed NULL literal maps to
+     * the same dedicated key because null-safe equality considers two NULL 
values equal. A failure
+     * while extracting any other value yields an empty optional. Key equality 
is only a prefilter;
+     * {@link #isNullSafeEqualInConstantRow(List, int, int, Optional)} 
performs the final proof.
+     *
+     * @param literal folded literal to normalize
+     * @return a normalized value key, including the shared NULL key, or an 
empty optional if no safe
+     *         key can be produced
+     */
+    private static Optional<ConstantValueKey> constantValueKey(Literal 
literal) {
+        if (literal.isNullLiteral()) {
+            return Optional.of(ConstantValueKey.NULL_VALUE_KEY);
+        }
+        try {
+            if (literal instanceof NumericLiteral) {
+                BigDecimal value = ((NumericLiteral) 
literal).getBigDecimalValue().stripTrailingZeros();
+                return Optional.of(new ConstantValueKey(NumericLiteral.class, 
value));
+            } else if (literal instanceof StringLikeLiteral) {
+                return Optional.of(new 
ConstantValueKey(StringLikeLiteral.class, literal.getStringValue()));
+            } else if (literal instanceof DateLiteral) {
+                return Optional.of(new ConstantValueKey(DateLiteral.class, 
literal.getStringValue()));
+            }
+            return Optional.of(new ConstantValueKey(literal.getClass(), 
literal));
+        } catch (RuntimeException e) {
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Removes all outer alias layers from a named expression.
+     *
+     * @param expression named expression whose underlying value expression is 
needed
+     * @return the first expression below all consecutive outer {@link Alias} 
nodes
+     */
+    private static Expression unwrapAlias(NamedExpression expression) {
+        Expression unwrapped = expression;
+        while (unwrapped instanceof Alias) {
+            unwrapped = unwrapped.child(0);
+        }
+        return unwrapped;
+    }
+
+    /**
+     * Creates the rewrite context needed for context-dependent constant 
folding when one is available.
+     *
+     * <p>Trait derivation can run without a thread-local connection or 
statement context. In that case
+     * callers receive an empty optional and constant evaluation decides 
whether it can proceed without
+     * the context.
+     *
+     * @param plan union plan used as the root of the temporary cascades and 
expression rewrite contexts
+     * @return a rewrite context for the current statement, or an empty 
optional when none is available
+     */
+    private static Optional<ExpressionRewriteContext> 
createRewriteContext(Plan plan) {
+        ConnectContext connectContext = ConnectContext.get();
+        if (connectContext == null || connectContext.getStatementContext() == 
null) {
+            return Optional.empty();
+        }
+        return Optional.of(new ExpressionRewriteContext(plan, 
CascadesContext.initContext(
+                connectContext.getStatementContext(), plan, 
PhysicalProperties.ANY)));
+    }
+
+    /**
+     * Creates the initial candidate group used when a union has only constant 
rows.
+     *
+     * <p>With no regular child, no child trait can rule out equality, so all 
output ordinals begin in
+     * one candidate group. Each constant row subsequently splits or removes 
members from this group.
+     *
+     * @param outputSize number of union output slots
+     * @return one group containing every ordinal from zero (inclusive) to 
{@code outputSize} (exclusive)
+     */
+    private static List<List<Integer>> oneGroupForAllOutputs(int outputSize) {
+        List<Integer> allOutputs = new ArrayList<>(outputSize);
+        for (int outputIndex = 0; outputIndex < outputSize; outputIndex++) {
+            allOutputs.add(outputIndex);
+        }
+        List<List<Integer>> groups = new ArrayList<>(1);
+        groups.add(allOutputs);
+        return groups;
+    }
+
+    /**
+     * Removes singleton and empty ordinal groups that cannot express equality 
between distinct outputs.
+     *
+     * @param groups candidate ordinal groups in the order they should be 
considered
+     * @return a new outer list containing the original group objects whose 
size is greater than one
+     */
+    private static List<List<Integer>> 
onlyNonTrivialGroups(Iterable<List<Integer>> groups) {
+        List<List<Integer>> nonTrivialGroups = new ArrayList<>();
+        for (List<Integer> group : groups) {
+            if (group.size() > 1) {
+                nonTrivialGroups.add(group);
+            }
+        }
+        return nonTrivialGroups;
+    }
+
+    /**
+     * Normalized identity used to pre-group folded constants before 
evaluating null-safe equality.
+     *
+     * <p>The key deliberately combines a literal family with a canonical 
value. The family prevents
+     * unrelated literal categories from sharing a bucket, while allowing 
representations within a
+     * supported category, such as different numeric literal classes, to meet 
in the same bucket. A
+     * matching key is only a cheap candidate signal; it is never used by 
itself as proof of equality.
+     */
+    private static final class ConstantValueKey {
+        /** Shared identity for SQL NULL, independent of the target type 
carried by a {@link NullLiteral}. */
+        private static final ConstantValueKey NULL_VALUE_KEY
+                = new ConstantValueKey(NullLiteral.class, NullLiteral.class);
+
+        /** Literal category used to keep values from unrelated SQL type 
families in separate buckets. */
+        private final Class<?> family;
+
+        /** Canonical value compared within {@link #family}, such as a 
scale-normalized decimal. */
+        private final Object value;
+
+        /**
+         * Creates a key from a literal family and its canonical value.
+         *
+         * @param family normalized literal category used as the first part of 
key identity
+         * @param value canonical value within {@code family}, used as the 
second part of key identity
+         */
+        private ConstantValueKey(Class<?> family, Object value) {
+            this.family = family;
+            this.value = value;
+        }
+
+        /**
+         * Compares both normalized components of this key with another object.
+         *
+         * @param object object to compare with this key
+         * @return {@code true} when {@code object} is a key with the same 
family and canonical value
+         */
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) {
+                return true;
+            }
+            if (!(object instanceof ConstantValueKey)) {
+                return false;
+            }
+            ConstantValueKey that = (ConstantValueKey) object;
+            return family.equals(that.family) && value.equals(that.value);
+        }
+
+        /**
+         * Computes a hash from the same family and canonical value used by 
{@link #equals(Object)}.
+         *
+         * @return hash code for this normalized constant key
+         */
+        @Override
+        public int hashCode() {
+            return Objects.hash(family, value);
+        }
+    }
+
+    /**
+     * Folded literal signature used to reuse a null-safe equality proof 
within one constant row.
+     */
+    private static final class LiteralSignature {
+        private final Class<?> literalClass;
+        private final Object dataType;
+        private final ConstantValueKey valueKey;
+
+        private LiteralSignature(Literal literal, ConstantValueKey valueKey) {
+            this.literalClass = literal.getClass();
+            this.dataType = literal.getDataType();
+            this.valueKey = valueKey;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) {
+                return true;
+            }
+            if (!(object instanceof LiteralSignature)) {
+                return false;
+            }
+            LiteralSignature that = (LiteralSignature) object;
+            return literalClass.equals(that.literalClass)
+                    && Objects.equals(dataType, that.dataType)
+                    && valueKey.equals(that.valueKey);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(literalClass, dataType, valueKey);
+        }
+    }
+
+    /**
+     * Ordered pair of folded literal signatures used as a cache key for a 
comparison proof.
+     */
+    private static final class ConstantComparisonKey {
+        private final LiteralSignature left;
+        private final LiteralSignature right;
+
+        private ConstantComparisonKey(LiteralSignature left, LiteralSignature 
right) {
+            this.left = left;
+            this.right = right;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) {
+                return true;
+            }
+            if (!(object instanceof ConstantComparisonKey)) {
+                return false;
+            }
+            ConstantComparisonKey that = (ConstantComparisonKey) object;
+            return left.equals(that.left) && right.equals(that.right);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(left, right);
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
index 9e276035787..1fbecf16746 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.memo.GroupExpression;
 import org.apache.doris.nereids.properties.DataTrait;
 import org.apache.doris.nereids.properties.LogicalProperties;
 import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.properties.UnionDataTraitUtils;
 import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
@@ -45,14 +46,9 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 
 /**
  * Logical Union.
@@ -290,75 +286,9 @@ public class LogicalUnion extends LogicalSetOperation 
implements Union, OutputPr
         return super.hasUnboundExpression();
     }
 
-    private List<BitSet> mapSlotToIndex(Plan plan, List<Set<Slot>> 
equalSlotsList) {
-        Map<Slot, Integer> slotToIndex = new HashMap<>();
-        for (int i = 0; i < plan.getOutput().size(); i++) {
-            slotToIndex.put(plan.getOutput().get(i), i);
-        }
-        List<BitSet> equalSlotIndicesList = new ArrayList<>();
-        for (Set<Slot> equalSlots : equalSlotsList) {
-            BitSet equalSlotIndices = new BitSet();
-            for (Slot slot : equalSlots) {
-                if (slotToIndex.containsKey(slot)) {
-                    equalSlotIndices.set(slotToIndex.get(slot));
-                }
-            }
-            if (equalSlotIndices.cardinality() > 1) {
-                equalSlotIndicesList.add(equalSlotIndices);
-            }
-        }
-        return equalSlotIndicesList;
-    }
-
     @Override
     public void computeEqualSet(DataTrait.Builder builder) {
-        if (children.isEmpty()) {
-            return;
-        }
-
-        // Get the list of equal slot sets and their corresponding index 
mappings for the first child
-        List<Set<Slot>> childEqualSlotsList = child(0).getLogicalProperties()
-                .getTrait().calAllEqualSet();
-        List<BitSet> childEqualSlotsIndicesList = mapSlotToIndex(child(0), 
childEqualSlotsList);
-        List<BitSet> unionEqualSlotIndicesList = new 
ArrayList<>(childEqualSlotsIndicesList);
-
-        // Traverse all children and find the equal sets that exist in all 
children
-        for (int i = 1; i < children.size(); i++) {
-            Plan child = children.get(i);
-
-            // Get the equal slot sets for the current child
-            childEqualSlotsList = 
child.getLogicalProperties().getTrait().calAllEqualSet();
-
-            // Map slots to indices for the current child
-            childEqualSlotsIndicesList = mapSlotToIndex(child, 
childEqualSlotsList);
-
-            // Only keep the equal pairs that exist in all children of the 
union
-            // This is done by calculating the intersection of all children's 
equal slot indices
-            for (BitSet unionEqualSlotIndices : unionEqualSlotIndicesList) {
-                BitSet intersect = new BitSet();
-                for (BitSet childEqualSlotIndices : 
childEqualSlotsIndicesList) {
-                    if 
(unionEqualSlotIndices.intersects(childEqualSlotIndices)) {
-                        intersect = childEqualSlotIndices;
-                        break;
-                    }
-                }
-                unionEqualSlotIndices.and(intersect);
-            }
-        }
-
-        // Build the functional dependencies for the output slots
-        List<Slot> outputList = getOutput();
-        for (BitSet equalSlotIndices : unionEqualSlotIndicesList) {
-            if (equalSlotIndices.cardinality() <= 1) {
-                continue;
-            }
-            int first = equalSlotIndices.nextSetBit(0);
-            int next = equalSlotIndices.nextSetBit(first + 1);
-            while (next > 0) {
-                builder.addEqualPair(outputList.get(first), 
outputList.get(next));
-                next = equalSlotIndices.nextSetBit(next + 1);
-            }
-        }
+        UnionDataTraitUtils.computeEqualSet(this, this, builder);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
index 24cad14f45a..ee6f5a4d024 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
@@ -21,9 +21,9 @@ import org.apache.doris.nereids.memo.GroupExpression;
 import org.apache.doris.nereids.properties.DataTrait;
 import org.apache.doris.nereids.properties.LogicalProperties;
 import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.properties.UnionDataTraitUtils;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
-import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.plans.AbstractPlan;
 import org.apache.doris.nereids.trees.plans.Plan;
@@ -38,14 +38,9 @@ import org.apache.doris.statistics.model.Statistics;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -189,75 +184,9 @@ public class PhysicalUnion extends PhysicalSetOperation 
implements Union {
         // don't propagate uniform slots
     }
 
-    private List<BitSet> mapSlotToIndex(Plan plan, List<Set<Slot>> 
equalSlotsList) {
-        Map<Slot, Integer> slotToIndex = new HashMap<>();
-        for (int i = 0; i < plan.getOutput().size(); i++) {
-            slotToIndex.put(plan.getOutput().get(i), i);
-        }
-        List<BitSet> equalSlotIndicesList = new ArrayList<>();
-        for (Set<Slot> equalSlots : equalSlotsList) {
-            BitSet equalSlotIndices = new BitSet();
-            for (Slot slot : equalSlots) {
-                if (slotToIndex.containsKey(slot)) {
-                    equalSlotIndices.set(slotToIndex.get(slot));
-                }
-            }
-            if (equalSlotIndices.cardinality() > 1) {
-                equalSlotIndicesList.add(equalSlotIndices);
-            }
-        }
-        return equalSlotIndicesList;
-    }
-
     @Override
     public void computeEqualSet(DataTrait.Builder builder) {
-        if (children.isEmpty()) {
-            return;
-        }
-
-        // Get the list of equal slot sets and their corresponding index 
mappings for the first child
-        List<Set<Slot>> childEqualSlotsList = child(0).getLogicalProperties()
-                .getTrait().calAllEqualSet();
-        List<BitSet> childEqualSlotsIndicesList = mapSlotToIndex(child(0), 
childEqualSlotsList);
-        List<BitSet> unionEqualSlotIndicesList = new 
ArrayList<>(childEqualSlotsIndicesList);
-
-        // Traverse all children and find the equal sets that exist in all 
children
-        for (int i = 1; i < children.size(); i++) {
-            Plan child = children.get(i);
-
-            // Get the equal slot sets for the current child
-            childEqualSlotsList = 
child.getLogicalProperties().getTrait().calAllEqualSet();
-
-            // Map slots to indices for the current child
-            childEqualSlotsIndicesList = mapSlotToIndex(child, 
childEqualSlotsList);
-
-            // Only keep the equal pairs that exist in all children of the 
union
-            // This is done by calculating the intersection of all children's 
equal slot indices
-            for (BitSet unionEqualSlotIndices : unionEqualSlotIndicesList) {
-                BitSet intersect = new BitSet();
-                for (BitSet childEqualSlotIndices : 
childEqualSlotsIndicesList) {
-                    if 
(unionEqualSlotIndices.intersects(childEqualSlotIndices)) {
-                        intersect = childEqualSlotIndices;
-                        break;
-                    }
-                }
-                unionEqualSlotIndices.and(intersect);
-            }
-        }
-
-        // Build the functional dependencies for the output slots
-        List<Slot> outputList = getOutput();
-        for (BitSet equalSlotIndices : unionEqualSlotIndicesList) {
-            if (equalSlotIndices.cardinality() <= 1) {
-                continue;
-            }
-            int first = equalSlotIndices.nextSetBit(0);
-            int next = equalSlotIndices.nextSetBit(first + 1);
-            while (next > 0) {
-                builder.addEqualPair(outputList.get(first), 
outputList.get(next));
-                next = equalSlotIndices.nextSetBit(next + 1);
-            }
-        }
+        UnionDataTraitUtils.computeEqualSet(this, this, builder);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
index d33dd556a8c..7a299e2854d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
@@ -18,6 +18,8 @@
 package org.apache.doris.nereids.properties;
 
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalUnion;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.utframe.TestWithFeService;
 
@@ -98,6 +100,143 @@ class EqualSetTest extends TestWithFeService {
                 .isEmpty());
     }
 
+    @Test
+    void testUnionEqualSetUsesRegularChildOutputMapping() {
+        String sql = "select name, id, id2 from agg where id = id2 "
+                + "union all select name, id, id2 from agg where id = id2";
+        LogicalUnion union = analyzeLogicalUnion(sql);
+        Assertions.assertNotEquals(union.child(0).getOutput(), 
union.getRegularChildOutput(0),
+                "the test must exercise an ordinal mapping that differs from 
child.getOutput()");
+        assertUnionEqualPair(sql, union, 1, 2, true);
+        assertUnionEqualPair(sql, union, 0, 1, false);
+    }
+
+    @Test
+    void testUnionEqualSetChecksEveryConstantRow() {
+        assertUnionEqualPair(
+                "select id, id2 from agg where id = id2 "
+                        + "union all select 1, 1 union all select 2, 2",
+                0, 1, true);
+        assertUnionEqualPair(
+                "select id, id2 from agg where id = id2 "
+                        + "union all select 1, 1 union all select 2, 3",
+                0, 1, false);
+        assertUnionEqualPair(
+                "select id, id2 from agg where id = id2 "
+                        + "union all select 1, 2 union all select 2, 1",
+                0, 1, false);
+    }
+
+    @Test
+    void testConstantOnlyUnionEqualSetUsesNullSafeEquality() {
+        assertUnionEqualPair(
+                "select cast(1 as int), cast(1 as bigint) "
+                        + "union all select cast(2 as int), cast(2 as bigint)",
+                0, 1, true);
+        assertUnionEqualPair(
+                "select 1 + 1, cast(2 as bigint) "
+                        + "union all select 2 * 2, cast(4 as bigint)",
+                0, 1, true);
+        assertUnionEqualPair(
+                "select 1, 1 union all select 2, 3",
+                0, 1, false);
+        assertUnionEqualPair(
+                "select cast(null as int), cast(null as bigint) union all 
select 1, 1",
+                0, 1, true);
+        assertUnionEqualPair(
+                "select cast(null as int), cast(1 as bigint) union all select 
1, 1",
+                0, 1, false);
+    }
+
+    @Test
+    void testConstantOnlyUnionKeepsComparableSubsetOfMixedNullBucket() {
+        String incompatibleNullFirst = "select cast(null as array<int>), 
cast(null as int), "
+                + "cast(null as bigint) union all select cast(null as 
array<int>), "
+                + "cast(1 as int), cast(1 as bigint)";
+        assertUnionEqualPair(incompatibleNullFirst, 1, 2, true);
+        assertUnionEqualPair(incompatibleNullFirst, 0, 1, false);
+
+        String compatibleValuesFirst = "select cast(null as array<int>), 
cast(1 as int), "
+                + "cast(1 as bigint) union all select cast(null as 
array<int>), "
+                + "cast(null as int), cast(null as bigint)";
+        assertUnionEqualPair(compatibleValuesFirst, 1, 2, true);
+        assertUnionEqualPair(compatibleValuesFirst, 0, 1, false);
+    }
+
+    @Test
+    void testWideConstantUnionReusesComparisonProofs() {
+        int columnCount = 256;
+        StringBuilder sql = new StringBuilder("select ");
+        appendNullIntColumns(sql, columnCount);
+        sql.append(" union all select ");
+        appendNullIntColumns(sql, columnCount);
+
+        LogicalUnion union = analyzeLogicalUnion(sql.toString());
+        
Assertions.assertTrue(union.getLogicalProperties().getTrait().isNullSafeEqual(
+                union.getOutput().get(0), union.getOutput().get(columnCount - 
1)));
+    }
+
+    private void appendNullIntColumns(StringBuilder sql, int columnCount) {
+        for (int column = 0; column < columnCount; column++) {
+            if (column > 0) {
+                sql.append(", ");
+            }
+            sql.append("cast(null as int)");
+        }
+    }
+
+    private void assertUnionEqualPair(String sql, int leftIndex, int 
rightIndex, boolean expected) {
+        assertUnionEqualPair(sql, analyzeLogicalUnion(sql), leftIndex, 
rightIndex, expected);
+    }
+
+    private void assertUnionEqualPair(String sql, LogicalUnion logicalUnion,
+            int leftIndex, int rightIndex, boolean expected) {
+        Assertions.assertEquals(expected, 
logicalUnion.getLogicalProperties().getTrait().isNullSafeEqual(
+                logicalUnion.getOutput().get(leftIndex), 
logicalUnion.getOutput().get(rightIndex)));
+
+        Plan physicalPlan = 
PlanChecker.from(connectContext).analyze(sql).rewrite().implement().getPhysicalPlan();
+        PhysicalUnion physicalUnion = findPhysicalUnion(physicalPlan);
+        Assertions.assertNotNull(physicalUnion, "expected a PhysicalUnion in: 
" + physicalPlan.treeString());
+        DataTrait.Builder builder = new DataTrait.Builder();
+        physicalUnion.computeEqualSet(builder);
+        DataTrait physicalTrait = builder.build();
+        Assertions.assertEquals(expected, physicalTrait.isNullSafeEqual(
+                physicalUnion.getOutput().get(leftIndex), 
physicalUnion.getOutput().get(rightIndex)));
+    }
+
+    private LogicalUnion analyzeLogicalUnion(String sql) {
+        Plan rewritten = 
PlanChecker.from(connectContext).analyze(sql).rewrite().getPlan();
+        LogicalUnion logicalUnion = findLogicalUnion(rewritten);
+        Assertions.assertNotNull(logicalUnion, "expected a LogicalUnion in: " 
+ rewritten.treeString());
+        return logicalUnion;
+    }
+
+    private LogicalUnion findLogicalUnion(Plan plan) {
+        if (plan instanceof LogicalUnion) {
+            return (LogicalUnion) plan;
+        }
+        for (Plan child : plan.children()) {
+            LogicalUnion union = findLogicalUnion(child);
+            if (union != null) {
+                return union;
+            }
+        }
+        return null;
+    }
+
+    private PhysicalUnion findPhysicalUnion(Plan plan) {
+        if (plan instanceof PhysicalUnion) {
+            return (PhysicalUnion) plan;
+        }
+        for (Plan child : plan.children()) {
+            PhysicalUnion union = findPhysicalUnion(child);
+            if (union != null) {
+                return union;
+            }
+        }
+        return null;
+    }
+
     @Test
     void testFilterHaving() {
         Plan plan = PlanChecker.from(connectContext)
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
index 937f62505a7..c5387c4cef7 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
@@ -213,6 +213,42 @@ public class EliminateOrderByKeyTest extends 
TestWithFeService implements MemoPa
                                 .getOrderKeys().isEmpty()));
     }
 
+    @Test
+    void testWindowUnionNullSafeEquality() {
+        PlanChecker.from(connectContext)
+                .analyze("select rank() over(order by a, b) from ("
+                        + "select cast(null as int) a, cast(null as bigint) b "
+                        + "union all select cast(1 as int), cast(1 as bigint)) 
t")
+                .rewrite()
+                .printlnTree()
+                .matches(logicalWindow()
+                        .when(window -> ((WindowExpression) 
window.getWindowExpressions().get(0).child(0))
+                                .getOrderKeys().size() == 1));
+    }
+
+    @Test
+    void testWindowUnionMixedNullTypesIndependentOfArmOrder() {
+        PlanChecker.from(connectContext)
+                .analyze("select a, rank() over(order by b, c) from ("
+                        + "select cast(null as array<int>) a, cast(null as 
int) b, cast(null as bigint) c "
+                        + "union all select cast(null as array<int>), cast(1 
as int), cast(1 as bigint)) t")
+                .rewrite()
+                .printlnTree()
+                .matches(logicalWindow()
+                        .when(window -> ((WindowExpression) 
window.getWindowExpressions().get(0).child(0))
+                                .getOrderKeys().size() == 1));
+
+        PlanChecker.from(connectContext)
+                .analyze("select a, rank() over(order by b, c) from ("
+                        + "select cast(null as array<int>) a, cast(1 as int) 
b, cast(1 as bigint) c "
+                        + "union all select cast(null as array<int>), 
cast(null as int), cast(null as bigint)) t")
+                .rewrite()
+                .printlnTree()
+                .matches(logicalWindow()
+                        .when(window -> ((WindowExpression) 
window.getWindowExpressions().get(0).child(0))
+                                .getOrderKeys().size() == 1));
+    }
+
     @Test
     void testWindowPartitionKey() {
         // an order key that repeats the window's own partition key is 
constant within each partition,
diff --git 
a/regression-test/data/nereids_rules_p0/union_equal_set/union_equal_set.out 
b/regression-test/data/nereids_rules_p0/union_equal_set/union_equal_set.out
new file mode 100644
index 00000000000..64ac6a00a6b
--- /dev/null
+++ b/regression-test/data/nereids_rules_p0/union_equal_set/union_equal_set.out
@@ -0,0 +1,28 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !reordered_regular_child_mapping --
+10     1       1       1
+10     1       1       1
+10     2       2       3
+10     2       2       3
+20     1       1       5
+20     1       1       5
+
+-- !mixed_regular_and_constant_rows --
+1      1       1
+1      2       2
+2      1       3
+2      2       4
+
+-- !one_constant_row_breaks_equality --
+1      1       1
+1      1       1
+2      2       3
+2      3       4
+
+-- !all_constant_rows_equal_after_coercion --
+1      1       1
+2      2       2
+
+-- !constant_null_breaks_equality --
+\N     \N      1
+1      1       2
diff --git 
a/regression-test/suites/nereids_rules_p0/union_equal_set/union_equal_set.groovy
 
b/regression-test/suites/nereids_rules_p0/union_equal_set/union_equal_set.groovy
new file mode 100644
index 00000000000..b26d64e674f
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/union_equal_set/union_equal_set.groovy
@@ -0,0 +1,121 @@
+// 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("union_equal_set") {
+    sql "drop table if exists union_equal_reordered_t"
+    sql "drop table if exists union_equal_constant_t"
+
+    sql """
+        create table union_equal_reordered_t (
+            a int not null,
+            b int not null,
+            c int not null
+        ) duplicate key(a)
+        distributed by hash(a) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql """
+        insert into union_equal_reordered_t values
+            (1, 1, 10),
+            (2, 2, 10),
+            (1, 1, 20)
+    """
+
+    explain {
+        sql """
+            select c, a, b, rank() over(order by c, a) rk
+            from (
+                select c, a, b from union_equal_reordered_t where a = b
+                union all
+                select c, a, b from union_equal_reordered_t where a = b
+            ) u
+        """
+        contains "functions: [rank()]"
+        contains "ASC NULLS FIRST, a[#"
+    }
+
+    qt_reordered_regular_child_mapping """
+        select c, a, b, rank() over(order by c, a) rk
+        from (
+            select c, a, b from union_equal_reordered_t where a = b
+            union all
+            select c, a, b from union_equal_reordered_t where a = b
+        ) u
+        order by c, a, b, rk
+    """
+
+    sql """
+        create table union_equal_constant_t (
+            a int not null,
+            b int not null
+        ) duplicate key(a)
+        distributed by hash(a) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql "insert into union_equal_constant_t values (1, 1), (2, 2)"
+
+    explain {
+        sql """
+            select a, b, rank() over(order by a, b) rk
+            from (
+                select a, b from union_equal_constant_t where a = b
+                union all select 1, 2
+                union all select 2, 1
+            ) u
+        """
+        contains "functions: [rank()]"
+        contains "ASC NULLS FIRST, b[#"
+    }
+
+    qt_mixed_regular_and_constant_rows """
+        select a, b, rank() over(order by a, b) rk
+        from (
+            select a, b from union_equal_constant_t where a = b
+            union all select 1, 2
+            union all select 2, 1
+        ) u
+        order by a, b, rk
+    """
+
+    qt_one_constant_row_breaks_equality """
+        select a, b, rank() over(order by a, b) rk
+        from (
+            select a, b from union_equal_constant_t where a = b
+            union all select 1, 1
+            union all select 2, 3
+        ) u
+        order by a, b, rk
+    """
+
+    qt_all_constant_rows_equal_after_coercion """
+        select a, b, rank() over(order by a, b) rk
+        from (
+            select cast(1 as int) a, cast(1 as bigint) b
+            union all select cast(2 as int), cast(2 as bigint)
+        ) u
+        order by a, b, rk
+    """
+
+    qt_constant_null_breaks_equality """
+        select a, b, rank() over(order by a, b) rk
+        from (
+            select cast(null as int) a, cast(null as bigint) b
+            union all select cast(1 as int), cast(1 as bigint)
+        ) u
+        order by a, b, rk
+    """
+}


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

Reply via email to