Skip to content

Commit 9577291

Browse files
branch-4.0:[fix](partition_prune) Move the pruning of predicates that are alwaystrue after partition pruning into the PlanPostProcessor #63111 (#63467)
picked from #63111 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d95f70d commit 9577291

12 files changed

Lines changed: 649 additions & 323 deletions

File tree

fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PlanPostProcessors.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public List<PlanPostProcessor> getProcessors() {
6464
// add processor if we need
6565
Builder<PlanPostProcessor> builder = ImmutableList.builder();
6666
builder.add(new PushDownFilterThroughProject());
67+
builder.add(new PrunePartitionPredicate());
6768
builder.add(new RemoveUselessProjectPostProcessor());
6869
builder.add(new RecomputeLogicalPropertiesProcessor());
6970
/*
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.doris.nereids.processor.post;
19+
20+
import org.apache.doris.analysis.Expr;
21+
import org.apache.doris.analysis.SlotRef;
22+
import org.apache.doris.catalog.Column;
23+
import org.apache.doris.catalog.OlapTable;
24+
import org.apache.doris.nereids.CascadesContext;
25+
import org.apache.doris.nereids.trees.expressions.Expression;
26+
import org.apache.doris.nereids.trees.expressions.Slot;
27+
import org.apache.doris.nereids.trees.expressions.SlotReference;
28+
import org.apache.doris.nereids.trees.plans.PartitionPrunablePredicate;
29+
import org.apache.doris.nereids.trees.plans.Plan;
30+
import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalPlan;
31+
import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
32+
import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
33+
import org.apache.doris.nereids.util.ExpressionUtils;
34+
35+
import java.util.HashMap;
36+
import java.util.HashSet;
37+
import java.util.LinkedHashSet;
38+
import java.util.List;
39+
import java.util.Map;
40+
import java.util.Optional;
41+
import java.util.Set;
42+
43+
/**
44+
* Removes partition-prunable conjuncts that were registered by {@link
45+
* org.apache.doris.nereids.rules.rewrite.PruneOlapScanPartition} but kept in
46+
* the logical plan during cascades. Doing the removal here, after
47+
* materialized-view rewrite has finished, ensures MV matching observes the
48+
* original predicates; otherwise the MV view-predicate may incorrectly cover
49+
* the dropped partition predicate and produce extra rows.
50+
*/
51+
public class PrunePartitionPredicate extends PlanPostProcessor {
52+
53+
@Override
54+
public Plan visitPhysicalFilter(PhysicalFilter<? extends Plan> filter, CascadesContext context) {
55+
filter = (PhysicalFilter<? extends Plan>) super.visit(filter, context);
56+
Plan child = filter.child();
57+
if (!(child instanceof PhysicalOlapScan)) {
58+
return filter;
59+
}
60+
PhysicalOlapScan scan = (PhysicalOlapScan) child;
61+
Optional<PartitionPrunablePredicate> entryOpt = scan.getPartitionPrunablePredicates();
62+
if (!entryOpt.isPresent()) {
63+
return filter;
64+
}
65+
boolean skipPrunePredicate = context.getConnectContext().getSessionVariable().skipPrunePredicate
66+
|| context.getStatementContext().isSkipPrunePredicate();
67+
if (skipPrunePredicate) {
68+
return filter;
69+
}
70+
Set<Long> scanPartitions = new HashSet<>(scan.getSelectedPartitionIds());
71+
Map<String, Slot> nameToOutputSlot = buildNameToSlotMap(scan);
72+
73+
Set<Expression> remaining = new LinkedHashSet<>(filter.getConjuncts());
74+
boolean changed = false;
75+
PartitionPrunablePredicate entry = entryOpt.get();
76+
if (entry.getSelectedPartitionIds().containsAll(scanPartitions)) {
77+
Map<Expression, Expression> slotReplaceMap =
78+
buildSlotReplaceMap(entry.getSnapshotPartitionSlots(), nameToOutputSlot);
79+
if (slotReplaceMap != null) {
80+
for (Expression conjunct : entry.getPrunableConjuncts()) {
81+
Expression rewritten = slotReplaceMap.isEmpty()
82+
? conjunct : ExpressionUtils.replace(conjunct, slotReplaceMap);
83+
if (remaining.remove(rewritten)) {
84+
changed = true;
85+
}
86+
}
87+
}
88+
}
89+
if (!changed) {
90+
return filter;
91+
}
92+
if (remaining.isEmpty()) {
93+
return scan;
94+
}
95+
return filter.withConjunctsAndChild(remaining, scan)
96+
.copyStatsAndGroupIdFrom((AbstractPhysicalPlan) filter);
97+
}
98+
99+
private static Map<String, Slot> buildNameToSlotMap(PhysicalOlapScan scan) {
100+
OlapTable table = scan.getTable();
101+
List<Slot> slots = scan.getOutput();
102+
Map<String, Slot> map = new HashMap<>(slots.size());
103+
if (scan.getSelectedIndexId() == table.getBaseIndexId()) {
104+
for (Slot slot : slots) {
105+
map.put(slot.getName().toLowerCase(), slot);
106+
}
107+
} else {
108+
for (Slot slot : slots) {
109+
if (!(slot instanceof SlotReference)) {
110+
continue;
111+
}
112+
SlotReference slotReference = (SlotReference) slot;
113+
Optional<Column> columnOptional = slotReference.getOriginalColumn();
114+
if (!columnOptional.isPresent()) {
115+
continue;
116+
}
117+
Expr expr = columnOptional.get().getDefineExpr();
118+
if (!(expr instanceof SlotRef)) {
119+
continue;
120+
}
121+
map.put(((SlotRef) expr).getColumnName().toLowerCase(), slot);
122+
}
123+
}
124+
return map;
125+
}
126+
127+
/**
128+
* Map each recorded snapshot slot to the scan's current output slot of the
129+
* same column name. Returns null when any snapshot slot cannot be located,
130+
* so the caller can skip the entry.
131+
*/
132+
private static Map<Expression, Expression> buildSlotReplaceMap(
133+
List<Slot> snapshotSlots, Map<String, Slot> nameToOutputSlot) {
134+
Map<Expression, Expression> replaceMap = new HashMap<>(snapshotSlots.size());
135+
for (Slot snapshot : snapshotSlots) {
136+
Slot current = nameToOutputSlot.get(snapshot.getName().toLowerCase());
137+
if (current == null) {
138+
return null;
139+
}
140+
if (!snapshot.equals(current)) {
141+
replaceMap.put(snapshot, current);
142+
}
143+
}
144+
return replaceMap;
145+
}
146+
}

fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/SyncMaterializationContext.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,18 @@ public Plan getScanPlan(StructInfo queryStructInfo, CascadesContext cascadesCont
122122
return scanPlan.accept(new DefaultPlanRewriter<Void>() {
123123
@Override
124124
public Plan visitLogicalOlapScan(LogicalOlapScan olapScan, Void context) {
125-
if (!queryStructInfoRelations.get(0).getTable().getFullQualifiers().equals(
125+
LogicalOlapScan queryScan = (LogicalOlapScan) queryStructInfoRelations.get(0);
126+
if (!queryScan.getTable().getFullQualifiers().equals(
126127
olapScan.getTable().getFullQualifiers())) {
127128
// Only the same table, we can do partition prue
128129
return olapScan;
129130
}
130-
return olapScan.withSelectedPartitionIds(
131-
((LogicalOlapScan) queryStructInfoRelations.get(0)).getSelectedPartitionIds());
131+
// Carry partition-prunable predicates from the original query scan onto
132+
// the rewritten MV scan so the post-processor can still drop the
133+
// predicates that have already been enforced by partition pruning.
134+
return olapScan
135+
.withSelectedPartitionIds(queryScan.getSelectedPartitionIds())
136+
.withPartitionPrunablePredicates(queryScan.getPartitionPrunablePredicates());
132137
}
133138
}, null);
134139
}

fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionPruner.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,7 @@
3636
import org.apache.doris.nereids.trees.expressions.literal.DateTimeLiteral;
3737
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
3838
import org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter;
39-
import org.apache.doris.nereids.trees.plans.Plan;
40-
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
41-
import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
4239
import org.apache.doris.nereids.types.DateTimeType;
43-
import org.apache.doris.nereids.util.ExpressionUtils;
4440
import org.apache.doris.nereids.util.Utils;
4541

4642
import com.google.common.collect.ImmutableList;
@@ -51,7 +47,6 @@
5147
import com.google.common.collect.RangeSet;
5248
import com.google.common.collect.Sets;
5349

54-
import java.util.LinkedHashSet;
5550
import java.util.List;
5651
import java.util.Map;
5752
import java.util.Map.Entry;
@@ -355,22 +350,4 @@ private static <K> Pair<Boolean, Boolean> canBePrunedOut(Expression partitionPre
355350
return Pair.of(true, false);
356351
}
357352
}
358-
359-
/** remove predicates that are always true*/
360-
public static Plan prunePredicate(boolean skipPrunePredicate, Optional<Expression> prunedPredicates,
361-
LogicalFilter<? extends Plan> filter, LogicalRelation scan) {
362-
if (!skipPrunePredicate && prunedPredicates.isPresent()) {
363-
Set<Expression> conjuncts = new LinkedHashSet<>(filter.getConjuncts());
364-
Expression deletedPredicate = prunedPredicates.get();
365-
Set<Expression> deletedPredicateSet = ExpressionUtils.extractConjunctionToSet(deletedPredicate);
366-
conjuncts.removeAll(deletedPredicateSet);
367-
if (conjuncts.isEmpty()) {
368-
return scan;
369-
} else {
370-
return filter.withConjunctsAndChild(conjuncts, scan);
371-
}
372-
} else {
373-
return filter.withChildren(ImmutableList.of(scan));
374-
}
375-
}
376353
}

fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalOlapScanToPhysicalOlapScan.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public Rule build() {
6868
olapScan.getScoreLimit(),
6969
olapScan.getScoreRangeInfo(),
7070
olapScan.getAnnOrderKeys(),
71-
olapScan.getAnnLimit())
71+
olapScan.getAnnLimit(),
72+
olapScan.getPartitionPrunablePredicates())
7273
).toRule(RuleType.LOGICAL_OLAP_SCAN_TO_PHYSICAL_OLAP_SCAN_RULE);
7374
}
7475

fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,20 @@
3535
import org.apache.doris.nereids.rules.expression.rules.SortedPartitionRanges;
3636
import org.apache.doris.nereids.trees.expressions.Expression;
3737
import org.apache.doris.nereids.trees.expressions.Slot;
38+
import org.apache.doris.nereids.trees.plans.PartitionPrunablePredicate;
3839
import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
3940
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
4041
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
4142
import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
43+
import org.apache.doris.nereids.util.ExpressionUtils;
4244
import org.apache.doris.nereids.util.Utils;
4345
import org.apache.doris.qe.ConnectContext;
4446

4547
import com.google.common.collect.ImmutableList;
4648
import com.google.common.collect.ImmutableSet;
4749

4850
import java.util.ArrayList;
51+
import java.util.HashSet;
4952
import java.util.LinkedHashSet;
5053
import java.util.List;
5154
import java.util.Map;
@@ -89,12 +92,29 @@ public List<Rule> buildRules() {
8992
}
9093
if (rewrittenLogicalRelation instanceof LogicalEmptyRelation) {
9194
return rewrittenLogicalRelation;
92-
} else {
93-
return PartitionPruner.prunePredicate(
94-
ctx.connectContext.getSessionVariable().skipPrunePredicate
95-
|| ctx.statementContext.isSkipPrunePredicate(),
96-
prunedRes.second, filter, rewrittenLogicalRelation);
9795
}
96+
boolean skipPrunePredicate = ctx.connectContext.getSessionVariable().skipPrunePredicate
97+
|| ctx.statementContext.isSkipPrunePredicate();
98+
if (!skipPrunePredicate && prunedRes.second.isPresent()) {
99+
// Defer the predicate removal to PlanPostProcessor so that materialized-view
100+
// rewrite still sees the original predicates. Otherwise, partition predicates
101+
// that are equivalent to the surviving partition list would be silently
102+
// dropped, leading to wrong results when an MV definition predicate matches
103+
// the remaining conjuncts.
104+
LogicalOlapScan prunedScan = (LogicalOlapScan) rewrittenLogicalRelation;
105+
Set<Expression> prunableConjuncts = ExpressionUtils.extractConjunctionToSet(
106+
prunedRes.second.get());
107+
List<Slot> partitionSlots = getPartitionSlots(prunedScan, prunedScan.getTable());
108+
if (partitionSlots != null) {
109+
PartitionPrunablePredicate entry = new PartitionPrunablePredicate(
110+
new HashSet<>(prunedScan.getSelectedPartitionIds()),
111+
partitionSlots,
112+
prunableConjuncts);
113+
rewrittenLogicalRelation = prunedScan.withPartitionPrunablePredicates(
114+
Optional.of(entry));
115+
}
116+
}
117+
return filter.withChildren(ImmutableList.of(rewrittenLogicalRelation));
98118
}).toRule(RuleType.OLAP_SCAN_PARTITION_PRUNE)
99119
);
100120
}
@@ -174,6 +194,28 @@ private Pair<List<Long>, Optional<Expression>> prunePartitionByFilters(LogicalOl
174194
}
175195
}
176196

197+
private List<Slot> getPartitionSlots(LogicalOlapScan scan, OlapTable table) {
198+
List<Slot> output = scan.getOutput();
199+
PartitionInfo partitionInfo = table.getPartitionInfo();
200+
List<Column> partitionColumns = partitionInfo.getPartitionColumns();
201+
List<Slot> partitionSlots = new ArrayList<>(partitionColumns.size());
202+
for (Column column : partitionColumns) {
203+
Slot partitionSlot = null;
204+
// loop search is faster than build a map
205+
for (Slot slot : output) {
206+
if (slot.getName().equalsIgnoreCase(column.getName())) {
207+
partitionSlot = slot;
208+
break;
209+
}
210+
}
211+
if (partitionSlot == null) {
212+
return null;
213+
}
214+
partitionSlots.add(partitionSlot);
215+
}
216+
return partitionSlots;
217+
}
218+
177219
private List<Long> prunePartitionByTabletIds(LogicalOlapScan scan,
178220
OlapTable table,
179221
List<Long> prunedPartitionsByFilters) {

0 commit comments

Comments
 (0)