|
18 | 18 | package org.apache.doris.nereids.rules.exploration.mv; |
19 | 19 |
|
20 | 20 | import org.apache.doris.catalog.MTMV; |
| 21 | +import org.apache.doris.catalog.TableIf; |
21 | 22 | import org.apache.doris.common.AnalysisException; |
22 | 23 | import org.apache.doris.common.Id; |
23 | 24 | import org.apache.doris.common.Pair; |
|
29 | 30 | import org.apache.doris.nereids.CascadesContext; |
30 | 31 | import org.apache.doris.nereids.StatementContext; |
31 | 32 | import org.apache.doris.nereids.jobs.executor.Rewriter; |
| 33 | +import org.apache.doris.nereids.jobs.joinorder.hypergraph.edge.JoinEdge; |
32 | 34 | import org.apache.doris.nereids.properties.LogicalProperties; |
33 | 35 | import org.apache.doris.nereids.properties.OrderKey; |
34 | 36 | import org.apache.doris.nereids.rules.exploration.ExplorationRuleFactory; |
|
43 | 45 | import org.apache.doris.nereids.rules.rewrite.MergeProjectable; |
44 | 46 | import org.apache.doris.nereids.trees.expressions.ComparisonPredicate; |
45 | 47 | import org.apache.doris.nereids.trees.expressions.Expression; |
| 48 | +import org.apache.doris.nereids.trees.expressions.IsNull; |
46 | 49 | import org.apache.doris.nereids.trees.expressions.NamedExpression; |
47 | 50 | import org.apache.doris.nereids.trees.expressions.Not; |
48 | 51 | import org.apache.doris.nereids.trees.expressions.Slot; |
@@ -821,21 +824,28 @@ protected SplitPredicate predicatesCompensate( |
821 | 824 | Set<Set<Slot>> requireNoNullableViewSlot = comparisonResult.getViewNoNullableSlot(); |
822 | 825 | // check query is use the null reject slot which view comparison need |
823 | 826 | if (!requireNoNullableViewSlot.isEmpty()) { |
| 827 | + // Required null-reject slots are recorded on the view side. Map query slots to view slots |
| 828 | + // before checking whether query predicates or INNER JoinEdges can reject those null rows. |
824 | 829 | SlotMapping queryToViewMapping = viewToQuerySlotMapping.inverse(); |
825 | | - // try to use |
826 | | - boolean valid = containsNullRejectSlot(requireNoNullableViewSlot, |
827 | | - queryStructInfo.getPredicates().getPulledUpPredicates(), queryToViewMapping, queryStructInfo, |
828 | | - viewStructInfo, cascadesContext); |
829 | | - if (!valid) { |
| 830 | + Optional<Set<Expression>> queryBasedNullRejectCompensationPredicates = |
| 831 | + getQueryBasedNullRejectCompensationPredicates( |
| 832 | + requireNoNullableViewSlot, |
| 833 | + queryStructInfo.getPredicates().getPulledUpPredicates(), queryToViewMapping, |
| 834 | + queryStructInfo, viewStructInfo, viewToQuerySlotMapping, cascadesContext); |
| 835 | + if (!queryBasedNullRejectCompensationPredicates.isPresent()) { |
830 | 836 | queryStructInfo = queryStructInfo.withPredicates(queryStructInfo.getPredicates() |
831 | 837 | .mergePulledUpPredicates(comparisonResult.getQueryAllPulledUpExpressions())); |
832 | | - valid = containsNullRejectSlot(requireNoNullableViewSlot, |
833 | | - queryStructInfo.getPredicates().getPulledUpPredicates(), queryToViewMapping, |
834 | | - queryStructInfo, viewStructInfo, cascadesContext); |
| 838 | + queryBasedNullRejectCompensationPredicates = getQueryBasedNullRejectCompensationPredicates( |
| 839 | + requireNoNullableViewSlot, queryStructInfo.getPredicates().getPulledUpPredicates(), |
| 840 | + queryToViewMapping, queryStructInfo, viewStructInfo, viewToQuerySlotMapping, cascadesContext); |
835 | 841 | } |
836 | | - if (!valid) { |
| 842 | + if (!queryBasedNullRejectCompensationPredicates.isPresent()) { |
837 | 843 | return SplitPredicate.INVALID_INSTANCE; |
838 | 844 | } |
| 845 | + if (!queryBasedNullRejectCompensationPredicates.get().isEmpty()) { |
| 846 | + queryStructInfo = queryStructInfo.withPredicates(queryStructInfo.getPredicates() |
| 847 | + .mergePulledUpPredicates(queryBasedNullRejectCompensationPredicates.get())); |
| 848 | + } |
839 | 849 | } |
840 | 850 | // compensate couldNot PulledUp Conjunctions |
841 | 851 | Map<Expression, ExpressionInfo> couldNotPulledUpCompensateConjunctions = |
@@ -863,56 +873,148 @@ protected SplitPredicate predicatesCompensate( |
863 | 873 | } |
864 | 874 |
|
865 | 875 | /** |
866 | | - * Check the queryPredicates contains the required nullable slot |
| 876 | + * Check whether query-side null-reject evidence covers each required view-side slot set. |
| 877 | + * |
| 878 | + * <p>The check is view-based because the required null-reject slots come from the MV join graph. |
| 879 | + * The returned compensation predicates are query-based because they will be merged into queryStructInfo. |
| 880 | + * |
| 881 | + * <p>Return meanings: |
| 882 | + * Optional.empty(): no valid proof, or no safe output slot can carry the compensation predicate. |
| 883 | + * Optional.of(emptySet()): existing query predicates already provide the required null-reject. |
| 884 | + * Optional.of(nonEmptySet): INNER JoinEdge proof must be materialized as these IS NOT NULL predicates. |
867 | 885 | */ |
868 | | - private boolean containsNullRejectSlot(Set<Set<Slot>> requireNoNullableViewSlot, |
| 886 | + private Optional<Set<Expression>> getQueryBasedNullRejectCompensationPredicates( |
| 887 | + Set<Set<Slot>> requireNoNullableViewSlot, |
869 | 888 | Set<Expression> queryPredicates, |
870 | 889 | SlotMapping queryToViewMapping, |
871 | 890 | StructInfo queryStructInfo, |
872 | 891 | StructInfo viewStructInfo, |
| 892 | + SlotMapping viewToQueryMapping, |
873 | 893 | CascadesContext cascadesContext) { |
874 | | - Set<Expression> queryPulledUpPredicates = queryPredicates.stream() |
875 | | - .flatMap(expr -> ExpressionUtils.extractConjunction(expr).stream()) |
876 | | - .map(expr -> { |
877 | | - // NOTICE inferNotNull generate Not with isGeneratedIsNotNull = false, |
878 | | - // so, we need set this flag to false before comparison. |
879 | | - if (expr instanceof Not) { |
880 | | - return ((Not) expr).withGeneratedIsNotNull(false); |
881 | | - } |
882 | | - return expr; |
883 | | - }) |
| 894 | + Set<Slot> predicateNullRejectViewSlots = getViewBasedNullRejectSlots( |
| 895 | + getPredicateNullRejectSlots(queryPredicates, cascadesContext), queryToViewMapping, queryStructInfo); |
| 896 | + Set<Slot> innerJoinNullRejectViewSlots = getViewBasedNullRejectSlots( |
| 897 | + getInnerJoinNullRejectSlots(queryStructInfo, cascadesContext), queryToViewMapping, queryStructInfo); |
| 898 | + Set<Slot> allNullRejectViewSlots = new HashSet<>(predicateNullRejectViewSlots); |
| 899 | + allNullRejectViewSlots.addAll(innerJoinNullRejectViewSlots); |
| 900 | + if (allNullRejectViewSlots.isEmpty()) { |
| 901 | + return Optional.empty(); |
| 902 | + } |
| 903 | + Set<Slot> viewOutputSlots = viewStructInfo.getPlanOutputShuttledExpressions().stream() |
| 904 | + .filter(Slot.class::isInstance) |
| 905 | + .map(Slot.class::cast) |
884 | 906 | .collect(Collectors.toSet()); |
885 | | - Set<Expression> queryNullRejectPredicates = |
886 | | - ExpressionUtils.inferNotNull(queryPulledUpPredicates, cascadesContext); |
887 | | - if (queryPulledUpPredicates.containsAll(queryNullRejectPredicates)) { |
888 | | - // Query has no null reject predicates, return |
889 | | - return false; |
| 907 | + Map<SlotReference, SlotReference> viewToQuerySlotReferenceMap = viewToQueryMapping.toSlotReferenceMap(); |
| 908 | + Set<Expression> compensationPredicates = new HashSet<>(); |
| 909 | + for (Set<Slot> requiredViewSlots : getShuttledRequireNoNullableViewSlots( |
| 910 | + requireNoNullableViewSlot, viewStructInfo)) { |
| 911 | + if (Sets.intersection(requiredViewSlots, allNullRejectViewSlots).isEmpty()) { |
| 912 | + return Optional.empty(); |
| 913 | + } |
| 914 | + if (!Sets.intersection(requiredViewSlots, predicateNullRejectViewSlots).isEmpty()) { |
| 915 | + continue; |
| 916 | + } |
| 917 | + Optional<Slot> compensationViewSlot = findCompensationViewSlot( |
| 918 | + requiredViewSlots, viewOutputSlots, innerJoinNullRejectViewSlots); |
| 919 | + if (!compensationViewSlot.isPresent()) { |
| 920 | + return Optional.empty(); |
| 921 | + } |
| 922 | + Slot querySlot = viewToQuerySlotReferenceMap.get(compensationViewSlot.get()); |
| 923 | + if (querySlot == null) { |
| 924 | + return Optional.empty(); |
| 925 | + } |
| 926 | + compensationPredicates.add(new Not(new IsNull(querySlot), false)); |
| 927 | + } |
| 928 | + return Optional.of(compensationPredicates); |
| 929 | + } |
| 930 | + |
| 931 | + private Set<Slot> getPredicateNullRejectSlots(Set<Expression> queryPredicates, CascadesContext cascadesContext) { |
| 932 | + Set<Slot> nullRejectSlots = new HashSet<>(); |
| 933 | + for (Expression queryPredicate : queryPredicates) { |
| 934 | + TypeUtils.isNotNull(queryPredicate).ifPresent(nullRejectSlots::add); |
| 935 | + } |
| 936 | + for (Expression inferredNotNull : ExpressionUtils.inferNotNull(queryPredicates, cascadesContext)) { |
| 937 | + TypeUtils.isNotNull(inferredNotNull).ifPresent(nullRejectSlots::add); |
890 | 938 | } |
891 | | - // Get query null reject predicate slots |
892 | | - Set<Expression> queryNullRejectSlotSet = new HashSet<>(); |
893 | | - for (Expression queryNullRejectPredicate : queryNullRejectPredicates) { |
894 | | - Optional<Slot> notNullSlot = TypeUtils.isNotNull(queryNullRejectPredicate); |
895 | | - if (!notNullSlot.isPresent()) { |
| 939 | + return nullRejectSlots; |
| 940 | + } |
| 941 | + |
| 942 | + private Set<Slot> getInnerJoinNullRejectSlots(StructInfo queryStructInfo, CascadesContext cascadesContext) { |
| 943 | + Set<Slot> nullRejectSlots = new HashSet<>(); |
| 944 | + // INNER JOIN conditions guarantee NOT NULL on join-key slots. |
| 945 | + // After EliminateOuterJoin converts LEFT to INNER, the JoinEdge objects in the HyperGraph |
| 946 | + // retain the INNER type even though EliminateNotNull removes filter-level NOT NULL predicates. |
| 947 | + for (JoinEdge joinEdge : queryStructInfo.getHyperGraph().getJoinEdges()) { |
| 948 | + if (joinEdge.getJoinType().isInnerJoin()) { |
| 949 | + nullRejectSlots.addAll(ExpressionUtils.inferNotNullSlots( |
| 950 | + ImmutableSet.copyOf(joinEdge.getExpressions()), cascadesContext)); |
| 951 | + } |
| 952 | + } |
| 953 | + return nullRejectSlots; |
| 954 | + } |
| 955 | + |
| 956 | + private Set<Slot> getViewBasedNullRejectSlots(Set<Slot> queryNullRejectSlots, |
| 957 | + SlotMapping queryToViewMapping, StructInfo queryStructInfo) { |
| 958 | + Set<Slot> viewBasedSlots = new HashSet<>(); |
| 959 | + for (Slot queryNullRejectSlot : queryNullRejectSlots) { |
| 960 | + Expression shuttledQuerySlot = ExpressionUtils.shuttleExpressionWithLineage( |
| 961 | + queryNullRejectSlot, queryStructInfo.getTopPlan()); |
| 962 | + if (!(shuttledQuerySlot instanceof Slot)) { |
896 | 963 | continue; |
897 | 964 | } |
898 | | - queryNullRejectSlotSet.add(notNullSlot.get()); |
| 965 | + Expression viewSlot = ExpressionUtils.replace(shuttledQuerySlot, |
| 966 | + queryToViewMapping.toSlotReferenceMap()); |
| 967 | + if (viewSlot instanceof Slot) { |
| 968 | + viewBasedSlots.add((Slot) viewSlot); |
| 969 | + } |
899 | 970 | } |
900 | | - // query slot need shuttle to use table slot, avoid alias influence |
901 | | - Set<Expression> queryUsedNeedRejectNullSlotsViewBased = ExpressionUtils.shuttleExpressionWithLineage( |
902 | | - new ArrayList<>(queryNullRejectSlotSet), queryStructInfo.getTopPlan()).stream() |
903 | | - .map(expr -> ExpressionUtils.replace(expr, queryToViewMapping.toSlotReferenceMap())) |
904 | | - .collect(Collectors.toSet()); |
905 | | - // view slot need shuttle to use table slot, avoid alias influence |
| 971 | + return viewBasedSlots; |
| 972 | + } |
| 973 | + |
| 974 | + private Set<Set<Slot>> getShuttledRequireNoNullableViewSlots(Set<Set<Slot>> requireNoNullableViewSlot, |
| 975 | + StructInfo viewStructInfo) { |
906 | 976 | Set<Set<Slot>> shuttledRequireNoNullableViewSlot = new HashSet<>(); |
907 | 977 | for (Set<Slot> requireNullableSlots : requireNoNullableViewSlot) { |
908 | 978 | shuttledRequireNoNullableViewSlot.add( |
909 | 979 | ExpressionUtils.shuttleExpressionWithLineage(new ArrayList<>(requireNullableSlots), |
910 | 980 | viewStructInfo.getTopPlan()).stream().map(Slot.class::cast) |
911 | 981 | .collect(Collectors.toSet())); |
912 | 982 | } |
913 | | - // query pulledUp predicates should have null reject predicates and contains any require noNullable slot |
914 | | - return shuttledRequireNoNullableViewSlot.stream().noneMatch(viewRequiredNullSlotSet -> |
915 | | - Sets.intersection(viewRequiredNullSlotSet, queryUsedNeedRejectNullSlotsViewBased).isEmpty()); |
| 983 | + return shuttledRequireNoNullableViewSlot; |
| 984 | + } |
| 985 | + |
| 986 | + private Optional<Slot> findCompensationViewSlot(Set<Slot> requiredViewSlots, Set<Slot> viewOutputSlots, |
| 987 | + Set<Slot> innerJoinNullRejectViewSlots) { |
| 988 | + Set<Slot> outputRequiredSlots = Sets.intersection(requiredViewSlots, viewOutputSlots); |
| 989 | + Optional<Slot> compensationViewSlot = outputRequiredSlots.stream() |
| 990 | + .filter(innerJoinNullRejectViewSlots::contains) |
| 991 | + .findFirst(); |
| 992 | + if (compensationViewSlot.isPresent()) { |
| 993 | + return compensationViewSlot; |
| 994 | + } |
| 995 | + return outputRequiredSlots.stream() |
| 996 | + .filter(slot -> isOriginalNonNullableSlotOnInnerJoinProofTable(slot, innerJoinNullRejectViewSlots)) |
| 997 | + .findFirst(); |
| 998 | + } |
| 999 | + |
| 1000 | + private boolean isOriginalNonNullableSlotOnInnerJoinProofTable(Slot slot, Set<Slot> innerJoinNullRejectViewSlots) { |
| 1001 | + if (!(slot instanceof SlotReference)) { |
| 1002 | + return false; |
| 1003 | + } |
| 1004 | + SlotReference slotReference = (SlotReference) slot; |
| 1005 | + if (!slotReference.getOriginalColumn().map(column -> !column.isAllowNull()).orElse(!slot.nullable())) { |
| 1006 | + return false; |
| 1007 | + } |
| 1008 | + Optional<TableIf> originalTable = slotReference.getOriginalTable(); |
| 1009 | + if (!originalTable.isPresent()) { |
| 1010 | + return false; |
| 1011 | + } |
| 1012 | + return innerJoinNullRejectViewSlots.stream() |
| 1013 | + .filter(SlotReference.class::isInstance) |
| 1014 | + .map(SlotReference.class::cast) |
| 1015 | + .map(SlotReference::getOriginalTable) |
| 1016 | + .anyMatch(referenceTable -> referenceTable.isPresent() |
| 1017 | + && referenceTable.get().equals(originalTable.get())); |
916 | 1018 | } |
917 | 1019 |
|
918 | 1020 | /** |
|
0 commit comments