This is an automated email from the ASF dual-hosted git repository.
englefly pushed a commit to branch topn-rf-opt
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/topn-rf-opt by this push:
new 3d807efe64b [improvement](nereids) Apply conservative source
eligibility to strict TopN filters
3d807efe64b is described below
commit 3d807efe64b2221f1f82e9822f06e05b550d8d51
Author: englefly <[email protected]>
AuthorDate: Thu Nov 12 17:54:23 2026 +0800
[improvement](nereids) Apply conservative source eligibility to strict TopN
filters
### Background
TopN runtime filters use a populated heap's frontier to prune scan input.
The development branch supports strict bounds per target, with eligibility
derived from translated plan nodes.
### What problem does this PR solve?
Issue Number: None
Related PR: None
PR Type: Enhancement
Problem Summary:
Restrict strict bounds to single-key TopN sources whose actual scan targets
and complete paths are all proven safe. Preserve existing inclusive filters
when a source, expression, operator or reader cannot be certified, and show the
selected comparison in EXPLAIN.
### Details
- **Change map:** 284 changed lines in five production Java files and 411
in four test files (three FE classes and one regression suite); no generated or
documentation files. Production code carries eligibility through Nereids,
aggregates target paths and checks final execution metadata. Tests cover
planner/serialization behavior and EXPLAIN. Obsolete translated-plan proof
commentary is removed.
- **Before and after:** Previously translated node classes could authorize
computed probes, external scans, ASOF/null-aware paths and only some targets of
a source. Now immutable pushdown contexts downgrade monotonically, registration
ANDs every actual path, and a source emits either all target IDs or an empty
strict-target list. Explicitly handle the storage-aggregate visitor bypass and
unwrap lazy scans before checking the selected storage model.
- **Configs and defaults:** No new settings. Retain the existing source
phase, type whitelist, filter ratio and closed-filter discovery. Check positive
LIMIT, nonnegative OFFSET and overflow without imposing uniqueness, LIMIT=1 or
OFFSET=0.
- **Impact on existing users:** Safe direct-slot DUP_KEYS/MOW scans can
prune boundary ties. Aggregates, storage merges, computed keys, external
readers and unproved join paths retain inclusive bounds across every target of
their source.
- **Behavior change:** Strict eligibility is source-wide and conservative.
At serialization recheck the final heap algorithm, ordering keys, candidate
count, reader models and every translated target path, including repeated and
disconnected targets.
- **Compatibility:** Reuse strict_target_node_ids without changing Thrift,
BE or persisted data formats. Empty lists retain inclusive bounds; older BEs
ignore the existing optional field.
### Testing
- ./run-fe-ut.sh --run
org.apache.doris.nereids.postprocess.TopNRuntimeFilterTest,org.apache.doris.nereids.postprocess.TopnFilterStrictBoundsTest,org.apache.doris.nereids.postprocess.TopnFilterPushDownVisitorTest:
passed locally; 35 tests, zero failures/errors/skips. Includes complete
planner/translation tests in both directions and direct visitor/final-plan
tests.
- ./build.sh --fe --exclude-obs-dependencies -j4: passed locally, including
full Checkstyle and FE packaging. The default build was stopped at unavailable
Huawei OBS artifact-source verification; the supported exclusion flag omits
that optional module. The temporary ASAN build setting was restored afterward.
- Standalone fe-core Checkstyle passed with zero violations; git diff
--cached --check passed.
- Added regression EXPLAIN checks to topn_runtime_filter_target_mode.
Cluster regression was not run; no golden result files were edited.
### Notes for Reviewers
Self-review checkpoints:
- Goal, scope and reuse: strict qualification refines existing filters;
shared contexts and join/scan helpers cover parallel paths without widening
pushdown.
- Correctness and conditions: preserve grouping contributions, storage
merges, key bindings and LIMIT+OFFSET candidates. Do not alter visibility,
delete bitmaps, error behavior or NULL semantics.
- Concurrency and lifecycle: state remains query-local; branch contexts are
immutable. No new threads, locks, static initialization, resources or
persistence are introduced.
- Configuration, protocols and writes: no new settings, schema fields,
transaction or data-write paths. Compatibility uses the branch's existing
optional target list.
- Coverage and results: source/target conjunction, child order, aliases,
unsafe operators, joins, lazy mappings, selected indexes, final sort metadata
and inclusive fallback have FE coverage. Regression EXPLAIN assertions do not
require changed golden results.
- Observability and performance: EXPLAIN displays <, <=, > or >= from final
eligibility; qualification runs during planning/serialization, with no new
execution-time work or metrics framework.
### Release note
Strict TopN runtime filters now require all targets of a source to follow
proven-safe direct-column OLAP paths. Other paths keep inclusive bounds, and
EXPLAIN shows the chosen comparison.
### Check List (For Author)
- Behavior changed: Yes (see Details)
- Does this need documentation: No
- Pre-submission status: Partially verified: FE tests, Checkstyle and FE
build passed; optional OBS packaging and cluster regression were not verified.
### AI Disclosure
- Tool: Codex (GPT-6)
- Generated: Code, tests and this description
- Human review: Pending
---
.../doris/nereids/processor/post/TopNScanOpt.java | 17 +-
.../nereids/processor/post/TopnFilterContext.java | 8 +-
.../processor/post/TopnFilterPushDownVisitor.java | 97 +++++++-
.../nereids/trees/plans/physical/TopnFilter.java | 151 ++++---------
.../java/org/apache/doris/planner/SortNode.java | 11 +-
.../nereids/postprocess/TopNRuntimeFilterTest.java | 78 ++++++-
.../postprocess/TopnFilterPushDownVisitorTest.java | 245 +++++++++++++++++++++
.../postprocess/TopnFilterStrictBoundsTest.java | 69 ++++--
.../topn_runtime_filter_target_mode.groovy | 19 +-
9 files changed, 554 insertions(+), 141 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopNScanOpt.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopNScanOpt.java
index 705562a45b3..f954ed2bef7 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopNScanOpt.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopNScanOpt.java
@@ -19,11 +19,13 @@ package org.apache.doris.nereids.processor.post;
import org.apache.doris.nereids.CascadesContext;
import
org.apache.doris.nereids.processor.post.TopnFilterPushDownVisitor.PushDownContext;
+import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.SortPhase;
import org.apache.doris.nereids.trees.plans.algebra.TopN;
import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.util.Utils;
/**
* topN opt
@@ -42,12 +44,23 @@ public class TopNScanOpt extends PlanPostProcessor {
TopnFilterPushDownVisitor pusher = new
TopnFilterPushDownVisitor(ctx.getTopnFilterContext());
TopnFilterPushDownVisitor.PushDownContext pushdownContext = new
PushDownContext(topN,
topN.getOrderKeys().get(0).getExpr(),
- topN.getOrderKeys().get(0).isNullFirst());
+ topN.getOrderKeys().get(0).isNullFirst(),
checkStrictTopN(topN));
topN.accept(pusher, pushdownContext);
}
return topN;
}
+ // Strict eligibility refines the existing filter; multiple keys still get
inclusive bounds.
+ static boolean checkStrictTopN(TopN topN) {
+ return topN instanceof PhysicalTopN
+ && ((PhysicalTopN<?>) topN).getSortPhase() ==
SortPhase.LOCAL_SORT
+ && topN.getOrderKeys().size() == 1
+ && topN.getOrderKeys().get(0).getExpr() instanceof Slot
+ &&
isSupportedTopNRuntimeFilterType(topN.getOrderKeys().get(0).getExpr().getDataType())
+ && topN.getLimit() > 0 && topN.getOffset() >= 0
+ && !Utils.addOverflows(topN.getLimit(), topN.getOffset());
+ }
+
boolean checkTopN(TopN topN) {
if (!(topN instanceof PhysicalTopN)) {
return false;
@@ -65,7 +78,7 @@ public class TopNScanOpt extends PlanPostProcessor {
return isSupportedTopNRuntimeFilterType(firstKeyType);
}
- private boolean isSupportedTopNRuntimeFilterType(DataType dataType) {
+ private static boolean isSupportedTopNRuntimeFilterType(DataType dataType)
{
return dataType.isBooleanType()
|| dataType.isIntegralType()
|| dataType.isDecimalLikeType()
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterContext.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterContext.java
index d52c5b71ac4..cb52caf6dfa 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterContext.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterContext.java
@@ -45,12 +45,12 @@ public class TopnFilterContext {
/**
* add topN filter
*/
- public void addTopnFilter(TopN topn, PhysicalRelation scan, Expression
expr) {
+ public void addTopnFilter(TopN topn, PhysicalRelation scan, Expression
expr, boolean strictEligible) {
TopnFilter filter = filters.get(topn.getObjectId());
if (filter == null) {
- filters.put(topn.getObjectId(), new TopnFilter(topn, scan, expr));
+ filters.put(topn.getObjectId(), new TopnFilter(topn, scan, expr,
strictEligible));
} else {
- filter.addTarget(scan, expr);
+ filter.addTarget(scan, expr, strictEligible);
}
}
@@ -97,7 +97,7 @@ public class TopnFilterContext {
return;
}
filter.legacySortNode = sortNode;
- sortNode.setUseTopnOpt(true);
+ sortNode.setTopnFilter(filter);
Preconditions.checkArgument(!filter.legacyTargets.isEmpty(), "missing
targets: " + filter);
for (ScanNode scan : filter.legacyTargets.keySet()) {
scan.addTopnFilterSortNode(sortNode);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
index 772655e50b4..9e3a2fec898 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
@@ -17,6 +17,9 @@
package org.apache.doris.nereids.processor.post;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.MaterializedIndexMeta;
+import org.apache.doris.catalog.OlapChangelogTableWrapper;
import
org.apache.doris.nereids.processor.post.TopnFilterPushDownVisitor.PushDownContext;
import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -26,22 +29,27 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitors;
import org.apache.doris.nereids.trees.plans.JoinType;
import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
import org.apache.doris.nereids.trees.plans.algebra.TopN;
import org.apache.doris.nereids.trees.plans.algebra.Union;
import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalJoin;
import org.apache.doris.nereids.trees.plans.physical.PhysicalAssertNumRows;
import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEAnchor;
import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEProducer;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalDistribute;
import org.apache.doris.nereids.trees.plans.physical.PhysicalFileScan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
import
org.apache.doris.nereids.trees.plans.physical.PhysicalLazyMaterializeOlapScan;
import org.apache.doris.nereids.trees.plans.physical.PhysicalNestedLoopJoin;
import org.apache.doris.nereids.trees.plans.physical.PhysicalOdbcScan;
import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalQuickSort;
import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
+import
org.apache.doris.nereids.trees.plans.physical.PhysicalStorageLayerAggregate;
import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
import org.apache.doris.nereids.trees.plans.physical.PhysicalWindow;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -72,30 +80,65 @@ public class TopnFilterPushDownVisitor extends
PlanVisitor<Boolean, PushDownCont
final Expression probeExpr;
final TopN topn;
final boolean nullsFirst;
+ final boolean strictEligible;
public PushDownContext(TopN topn, Expression probeExpr, boolean
nullsFirst) {
+ this(topn, probeExpr, nullsFirst,
TopNScanOpt.checkStrictTopN(topn));
+ }
+
+ public PushDownContext(TopN topn, Expression probeExpr, boolean
nullsFirst, boolean strictEligible) {
this.topn = topn;
this.probeExpr = probeExpr;
this.nullsFirst = nullsFirst;
+ this.strictEligible = strictEligible;
}
public PushDownContext withNewProbeExpression(Expression newProbe) {
- return new PushDownContext(topn, newProbe, nullsFirst);
+ return new PushDownContext(topn, newProbe, nullsFirst,
strictEligible
+ && newProbe instanceof Slot &&
newProbe.getDataType().equals(probeExpr.getDataType()));
}
+ public PushDownContext disableStrict() {
+ return new PushDownContext(topn, probeExpr, nullsFirst, false);
+ }
}
@Override
public Boolean visit(Plan plan, PushDownContext ctx) {
// A bound on a grouping key can exclude whole groups. Stopping at
unlisted operators
// here would disable existing scan filters below aggregates and their
join inputs.
+ // A late strict bound could remove only part of a group and change
its aggregate values.
+ // Unlisted operators therefore preserve closed pushdown without
certifying strict paths.
boolean pushed = false;
+ PushDownContext inclusiveContext = ctx.disableStrict();
for (Plan child : plan.children()) {
- pushed |= child.accept(this, ctx);
+ pushed |= child.accept(this, inclusiveContext);
}
return pushed;
}
+ @Override
+ public Boolean visitPhysicalFilter(PhysicalFilter<? extends Plan> filter,
PushDownContext ctx) {
+ return filter.child().accept(this, ctx);
+ }
+
+ @Override
+ public Boolean visitPhysicalDistribute(PhysicalDistribute<? extends Plan>
distribute, PushDownContext ctx) {
+ return distribute.child().accept(this, ctx);
+ }
+
+ @Override
+ public Boolean visitPhysicalQuickSort(PhysicalQuickSort<? extends Plan>
sort, PushDownContext ctx) {
+ // PhysicalQuickSort is a full sort; truncation and OFFSET are
represented by other nodes.
+ return sort.child().accept(this, ctx);
+ }
+
+ @Override
+ public Boolean
visitPhysicalStorageLayerAggregate(PhysicalStorageLayerAggregate aggregate,
PushDownContext ctx) {
+ // The base visitor unwraps this relation directly, bypassing
visit(Plan, ...).
+ return aggregate.getRelation().accept(this, ctx.disableStrict());
+ }
+
@Override
public Boolean visitPhysicalProject(
PhysicalProject<? extends Plan> project, PushDownContext ctx) {
@@ -118,6 +161,9 @@ public class TopnFilterPushDownVisitor extends
PlanVisitor<Boolean, PushDownCont
@Override
public Boolean visitPhysicalSetOperation(
PhysicalSetOperation setOperation, PushDownContext ctx) {
+ if (!(setOperation instanceof Union) || setOperation.getQualifier() !=
Qualifier.ALL) {
+ ctx = ctx.disableStrict();
+ }
boolean pushedDown = false;
if (setOperation.arity() > 0) {
pushedDown = pushDownFilterToSetOperatorChild(setOperation, ctx,
0);
@@ -233,6 +279,26 @@ public class TopnFilterPushDownVisitor extends
PlanVisitor<Boolean, PushDownCont
return true;
}
+ private PushDownContext strictJoinContext(AbstractPhysicalJoin<? extends
Plan, ? extends Plan> join,
+ PushDownContext ctx) {
+ if (join.isMarkJoin() || !(ctx.probeExpr instanceof Slot)
+ || !join.left().getOutputSet().contains(ctx.probeExpr)) {
+ return ctx.disableStrict();
+ }
+ // Existing pushdown only targets the left input. Equality-key
substitution from the right
+ // and all ASOF, null-aware, mark and non-preserved-side paths remain
inclusive.
+ switch (join.getJoinType()) {
+ case INNER_JOIN:
+ case CROSS_JOIN:
+ case LEFT_OUTER_JOIN:
+ case LEFT_SEMI_JOIN:
+ case LEFT_ANTI_JOIN:
+ return ctx;
+ default:
+ return ctx.disableStrict();
+ }
+ }
+
@Override
public Boolean visitPhysicalHashJoin(PhysicalHashJoin<? extends Plan, ?
extends Plan> join,
PushDownContext ctx) {
@@ -246,6 +312,7 @@ public class TopnFilterPushDownVisitor extends
PlanVisitor<Boolean, PushDownCont
if (!canPushDownToLeftChild(join, ctx.probeExpr)) {
return false;
}
+ ctx = strictJoinContext(join, ctx);
if
(join.left().getOutputSet().containsAll(ctx.probeExpr.getInputSlots())) {
PushDownContext childPushDownContext =
adjustProbeExprNullableThroughOuterJoin(join.left(), ctx);
return join.left().accept(this, childPushDownContext);
@@ -282,7 +349,7 @@ public class TopnFilterPushDownVisitor extends
PlanVisitor<Boolean, PushDownCont
// push to left child. right child is blocking operator, do not need
topn-filter
if
(join.left().getOutputSet().containsAll(ctx.probeExpr.getInputSlots())
&& canPushDownToLeftChild(join, ctx.probeExpr)) {
- return join.left().accept(this, ctx);
+ return join.left().accept(this, strictJoinContext(join, ctx));
}
return false;
}
@@ -301,13 +368,35 @@ public class TopnFilterPushDownVisitor extends
PlanVisitor<Boolean, PushDownCont
|| Math.max(relation.getStats().getRowCount(), 1)
*
ConnectContext.get().getSessionVariable().topnFilterRatio > ctx.topn.getLimit()
+ ctx.topn.getOffset()) {
- topnFilterContext.addTopnFilter(ctx.topn, relation,
ctx.probeExpr);
+ topnFilterContext.addTopnFilter(ctx.topn, relation,
ctx.probeExpr,
+ ctx.strictEligible && ctx.probeExpr instanceof Slot &&
allowsStrictScan(relation));
return true;
}
}
return false;
}
+ private boolean allowsStrictScan(PhysicalRelation relation) {
+ if (relation instanceof PhysicalLazyMaterializeOlapScan) {
+ relation = ((PhysicalLazyMaterializeOlapScan) relation).getScan();
+ }
+ if (!(relation instanceof PhysicalOlapScan)) {
+ return false;
+ }
+ PhysicalOlapScan scan = (PhysicalOlapScan) relation;
+ if (scan.getTable() instanceof OlapChangelogTableWrapper ||
scan.getScanParams().isPresent()
+ || scan.getTableSample().isPresent() ||
scan.getAnnLimit().isPresent()
+ || scan.getScoreLimit().isPresent()) {
+ return false;
+ }
+ // The selected rollup can aggregate even when the base table uses
DUP_KEYS.
+ MaterializedIndexMeta index =
scan.getTable().getIndexMetaByIndexId(scan.getSelectedIndexId());
+ KeysType keysType = index.getKeysType();
+ return !index.isChangelogIndex()
+ && (keysType == KeysType.DUP_KEYS || (keysType ==
KeysType.UNIQUE_KEYS
+ && scan.getTable().getEnableUniqueKeyMergeOnWrite()));
+ }
+
@Override
public Boolean
visitPhysicalLazyMaterializeOlapScan(PhysicalLazyMaterializeOlapScan lazyScan,
PushDownContext ctx) {
return visitPhysicalRelation(lazyScan, ctx);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/TopnFilter.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/TopnFilter.java
index 27ad9eb0b6b..d9285f1df81 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/TopnFilter.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/TopnFilter.java
@@ -20,28 +20,28 @@ package org.apache.doris.nereids.trees.plans.physical;
import org.apache.doris.analysis.Expr;
import org.apache.doris.analysis.ExprToThriftVisitor;
import org.apache.doris.catalog.KeysType;
-import org.apache.doris.datasource.scan.FileScanNode;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.plans.algebra.TopN;
+import org.apache.doris.nereids.util.Utils;
import org.apache.doris.planner.ExchangeNode;
import org.apache.doris.planner.JoinNodeBase;
import org.apache.doris.planner.LocalExchangeNode;
import org.apache.doris.planner.MaterializationNode;
import org.apache.doris.planner.OlapScanNode;
import org.apache.doris.planner.PlanNode;
+import org.apache.doris.planner.RepeatNode;
import org.apache.doris.planner.ScanNode;
import org.apache.doris.planner.SelectNode;
import org.apache.doris.planner.SetOperationNode;
import org.apache.doris.planner.SortNode;
-import org.apache.doris.planner.TableFunctionNode;
import org.apache.doris.thrift.TSortAlgorithm;
import org.apache.doris.thrift.TTopnFilterDesc;
import com.google.common.collect.Maps;
import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
/**
* topn filter
@@ -52,15 +52,25 @@ public class TopnFilter {
public Map<PhysicalRelation, Expression> targets = Maps.newHashMap();
public Map<ScanNode, Expr> legacyTargets = Maps.newHashMap();
- public TopnFilter(TopN topn, PhysicalRelation rel, Expression expr) {
+ private boolean strictEligible;
+
+ public TopnFilter(TopN topn, PhysicalRelation rel, Expression expr,
boolean strictEligible) {
this.topn = topn;
+ this.strictEligible = strictEligible;
targets.put(rel, expr);
}
- public void addTarget(PhysicalRelation rel, Expression expr) {
+ public void addTarget(PhysicalRelation rel, Expression expr, boolean
strictEligible) {
+ // AND every registered path, including repeated registrations of the
same scan.
+ this.strictEligible &= strictEligible;
targets.put(rel, expr);
}
+ /** Eligibility accumulated during pushdown; translated execution metadata
is checked separately. */
+ public boolean isStrictEligible() {
+ return strictEligible;
+ }
+
public boolean hasTargetRelation(PhysicalRelation rel) {
return targets.containsKey(rel);
}
@@ -68,7 +78,7 @@ public class TopnFilter {
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
- builder.append(topn).append("->[ ");
+ builder.append(topn).append("
strictEligible=").append(strictEligible).append("->[ ");
for (PhysicalRelation rel : targets.keySet()) {
builder.append("(").append(rel).append(":").append(targets.get(rel)).append(")
");
}
@@ -88,74 +98,48 @@ public class TopnFilter {
tFilter.putToTargetNodeIdToTargetExpr(scan.getId().asInt(),
ExprToThriftVisitor.treeToThrift(legacyTargets.get(scan)));
}
- List<Integer> strictTargets = new ArrayList<>();
- // Scheduling can change the sort algorithm after translation. Inspect
the final
- // execution plan here, after
ThriftPlansBuilder.setRuntimePredicateIfNeed().
- // Multiple keys must keep ties: (1, 2) can still beat (1, 9) with the
same leading key.
- if (legacySortNode.getSortAlgorithm() == TSortAlgorithm.HEAP_SORT
- && legacySortNode.getSortInfo().getOrderingExprs().size() ==
1) {
- for (PlanNode child : legacySortNode.getChildren()) {
- collectStrictTargets(child, strictTargets);
- }
- }
- tFilter.setStrictTargetNodeIds(strictTargets);
+ // Reuse the branch's per-target wire format, but upgrade a source
only as a whole.
+ tFilter.setStrictTargetNodeIds(allowsStrictBounds()
+ ? legacyTargets.keySet().stream().map(scan ->
scan.getId().asInt())
+ .collect(Collectors.toList()) : new ArrayList<>());
return tFilter;
}
- private void collectStrictTargets(PlanNode node, List<Integer>
strictTargets) {
- if (node.hasLimit() || !allowsStrictBounds(node)) {
- // Removing a tie below an intermediate LIMIT can let a different
row pass it.
- return;
+ /** Check the final execution plan, since scheduling can change the sort
algorithm and limits. */
+ public boolean allowsStrictBounds() {
+ if (!strictEligible || legacyTargets.isEmpty()
+ || legacySortNode.getSortAlgorithm() !=
TSortAlgorithm.HEAP_SORT
+ || legacySortNode.getSortInfo().getOrderingExprs().size() != 1
+ || legacySortNode.getLimit() <= 0 ||
legacySortNode.getOffset() < 0
+ || Utils.addOverflows(legacySortNode.getLimit(),
legacySortNode.getOffset())) {
+ return false;
}
+ Map<ScanNode, Boolean> strictTargets = Maps.newHashMap();
+ for (PlanNode child : legacySortNode.getChildren()) {
+ collectStrictTargets(child, true, strictTargets);
+ }
+ return strictTargets.size() == legacyTargets.size()
+ && strictTargets.values().stream().allMatch(allowed ->
allowed);
+ }
+
+ private void collectStrictTargets(PlanNode node, boolean pathStrict,
Map<ScanNode, Boolean> strictTargets) {
+ // Removing a tie below an intermediate LIMIT/OFFSET can let a
different row pass it.
+ pathStrict &= !node.hasLimit() && node.getOffset() == 0 &&
allowsStrictNode(node);
if (legacyTargets.containsKey(node)) {
- strictTargets.add(node.getId().asInt());
+ strictTargets.merge((ScanNode) node, pathStrict, (previous,
current) -> previous && current);
}
// FE retains the sending fragment below ExchangeNode, unlike the
serialized BE tree.
for (PlanNode child : node.getChildren()) {
- collectStrictTargets(child, strictTargets);
+ collectStrictTargets(child, pathStrict, strictTargets);
}
}
- /**
- * Whether this node preserves the safety of discarding boundary ties at a
descendant scan.
- *
- * <p>This refines an already accepted TopN filter; it does not authorize
new predicate pushdown.
- * The caller checks the final single-key HEAP_SORT algorithm and rejects
intermediate LIMITs.
- * Once the source heap holds LIMIT + OFFSET candidates, another equal key
cannot improve that
- * ordering. However, dropping an input tie must not change a surviving
output row, create a new
- * output row, suppress an error, or remove input needed to produce a
better-ranked row later.
- * This property must hold at every node between the source and the target.
- *
- * <p>The examples below explain local operator semantics, not necessarily
plans that current
- * pushdown rules can produce: some shapes are already rejected upstream.
Unless stated otherwise,
- * use ascending order with boundary b = 1, so the additional rows
rejected by {@code k < b}
- * instead of {@code k <= b} have k = 1. A bound may arrive after some
input has been consumed,
- * for example from a safe sibling of a UNION. Thus a merge can lose only
part of a boundary group.
- * Descending order has the same issue with the comparison reversed.
- *
- * <p>This is a whitelist: false means that the path retains its inclusive
filter, not that every
- * instance of the operator is unsafe. New operator/model variants need
their own proof before
- * being admitted, even if a particular example would happen to produce
the same result.
- */
- private boolean allowsStrictBounds(PlanNode node) {
+ // Nereids proves expression bindings and complete path semantics before
registration.
+ // Recheck translated readers and limits, which can change after that
proof.
+ private boolean allowsStrictNode(PlanNode node) {
if (node instanceof OlapScanNode) {
OlapScanNode scan = (OlapScanNode) node;
KeysType keysType = scan.getSelectedIndexKeyType();
- // Use the selected index's model: a DUP_KEYS base table may have
an AGG_KEYS rollup.
- // Rejected storage cases, even when the SQL plan contains no
AggregationNode:
- // - Changelog: +(k=1,v=10), -(k=1,v=10) should cancel when
folded. If the insert was
- // consumed before the bound and the delete is filtered
afterwards, a spurious insert
- // survives. All changelog scan modes retain inclusive bounds;
this rule does not try
- // to prove individual modes such as DETAIL independent of
storage folding.
- // - AGG_KEYS: stored SUM contributions (k=1,v=10), (k=1,v=20)
must produce (1,30).
- // Reading 10 before the bound and dropping 20 afterwards would
instead produce (1,10).
- // - UNIQUE_KEYS without MOW (MOR): old (k=1,v=10), new (k=1,v=20)
must resolve to (1,20).
- // Dropping the new version after the old one was read can
expose the obsolete (1,10).
- // - PRIMARY_KEYS/UNKNOWN, or a future model: even a scan
containing just (k=1,v=10)
- // remains inclusive. These labels do not establish the
DUP_KEYS/MOW guarantee of
- // finalized, independently discardable rows in this whitelist;
this is conservative.
- // Ordinary DUP_KEYS rows and resolved MOW rows need no such
read-time merge of versions
- // or contributions, so their boundary ties can be discarded when
the rest of the path allows it.
return !scan.isReadChangelog() && (keysType == KeysType.DUP_KEYS
|| (keysType == KeysType.UNIQUE_KEYS &&
scan.getOlapTable().getEnableUniqueKeyMergeOnWrite()));
}
@@ -166,55 +150,14 @@ public class TopnFilter {
case LEFT_OUTER_JOIN:
case LEFT_SEMI_JOIN:
case LEFT_ANTI_JOIN:
- case NULL_AWARE_LEFT_ANTI_JOIN:
- case NULL_AWARE_LEFT_SEMI_JOIN:
- case ASOF_LEFT_INNER_JOIN:
- case ASOF_LEFT_OUTER_JOIN:
return true;
default:
- // The registered scan filter is pushed into the
left/probe side. For equality
- // joins, take L={(k=1,v=10)}, R={(k=1)} and remove the
sole matching L row:
- // - RIGHT_OUTER_JOIN: the result changes from
(L.v=10,R.k=1) to (NULL,1).
- // - FULL_OUTER_JOIN: the same example creates the
previously absent (NULL,1).
- // - RIGHT_ANTI_JOIN: the empty result becomes R.k=1,
since R is now unmatched.
- // - RIGHT_SEMI_JOIN: plain semi output {R.k=1} becomes
empty; with a mark it
- // changes (R.k=1,mark=true) to (1,false). The mark case
changes a surviving row.
- // Plain right-semi is also excluded conservatively; no
separate proof for its
- // build-side output is made here. This does not claim
all plain semi cases fail.
- // ASOF examples use the nearest L.t <= R.t, with R.t=2:
- // - ASOF_RIGHT_INNER_JOIN: L.t={1,2} originally selects
2. Dropping L.t=2
- // (boundary b=2 on L.t) selects 1 instead, creating a
different joined row.
- // - ASOF_RIGHT_OUTER_JOIN: with only L.t=2, dropping that
boundary row changes
- // (L.t=2,R.t=2) to (NULL,2). With L.t={1,2}, it can
also switch matches as above.
- // Future join types stay excluded until their
probe-removal semantics are proved.
return false;
}
}
- // Rejected operator families (all fall through the whitelist below):
- // - AggregationNode/BucketedAggregationNode: GROUP BY k with SUM(v)
over (1,10), (1,20)
- // must return (1,30), not (1,10) if the second contribution is
dropped by a late bound.
- // A scalar COUNT(*) is also sensitive: removing its two inputs
changes the result 2 to 0.
- // - RepeatNode: for grouping sets ((k),()), input k=1 expands to k=1
and k=NULL. Dropping
- // that input also removes the NULL copy, which could rank before
the boundary with NULLS FIRST.
- // - AnalyticEvalNode: SUM(v) OVER () on (k,v)={(1,10),(0,20)} gives
the k=0 row a sum of 30.
- // Dropping k=1 changes that surviving, better-ranked row's sum to
20.
- // - AssertNumRowsNode: an at-most-one-row assertion must fail on
input k={1,1}. Filtering
- // both rows hides the error; scalar-subquery handling may also
synthesize a new NULL row.
- // - PartitionSortNode: in one partition, ORDER BY t with partition
limit 1 chooses (k=1,t=0)
- // over (k=0,t=1). Filtering k=1 first makes the previously
unselected k=0 row survive.
- // - RecursiveCteNode/RecursiveCteScanNode: seed k=1 followed by k-1
while k>0 produces {1,0}.
- // Dropping the boundary seed loses the later k=0 row, which would
beat the bound.
- // - CTEScanNode/shared CTE paths need consumer-specific proof. For
CTE input {k=1}, pushing
- // one consumer's strict bound into the shared producer could change
another's COUNT(*) from 1 to 0.
- // - Other unlisted nodes, including unsupported scans and future
operators, have no proof here.
- // For example, TopN -> SchemaScanNode is not an accepted target
path; EmptySetNode has no rows
- // or useful scan target to mark. Neither needs a strict mode. A new
operator could instead
- // behave like the window example above, so it must not be accepted
merely for having children.
- // The admitted nodes preserve the established pushdown semantics
without requiring additional
- // boundary contributions. Set-operation targets and join probe sides
were selected upstream.
- return node instanceof FileScanNode || node instanceof SelectNode ||
node instanceof SortNode
+ // Repeat is admitted only after the common grouping-key check in
Nereids.
+ return node instanceof SelectNode || node instanceof SortNode || node
instanceof RepeatNode
|| node instanceof ExchangeNode || node instanceof
LocalExchangeNode
- || node instanceof SetOperationNode || node instanceof
TableFunctionNode
- || node instanceof MaterializationNode;
+ || node instanceof SetOperationNode || node instanceof
MaterializationNode;
}
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/SortNode.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/SortNode.java
index ae094e88db8..40bb0b1fd03 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/SortNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/SortNode.java
@@ -26,6 +26,7 @@ import org.apache.doris.analysis.SortInfo;
import org.apache.doris.analysis.ToSqlParams;
import org.apache.doris.common.Pair;
import org.apache.doris.nereids.glue.translator.PlanTranslatorContext;
+import org.apache.doris.nereids.trees.plans.physical.TopnFilter;
import org.apache.doris.planner.LocalExchangeNode.LocalExchangeType;
import org.apache.doris.planner.LocalExchangeNode.LocalExchangeTypeRequire;
import org.apache.doris.qe.ConnectContext;
@@ -52,6 +53,7 @@ public class SortNode extends PlanNode {
private final SortInfo info;
private final boolean useTopN;
private boolean useTopnOpt = false;
+ private TopnFilter topnFilter;
private boolean hasRuntimePredicate = false;
// If mergeByexchange is set to true, the sort information is pushed to the
@@ -142,8 +144,9 @@ public class SortNode extends PlanNode {
}
}
- public void setUseTopnOpt(boolean useTopnOpt) {
- this.useTopnOpt = useTopnOpt;
+ public void setTopnFilter(TopnFilter topnFilter) {
+ this.topnFilter = topnFilter;
+ this.useTopnOpt = true;
}
@Override
@@ -170,6 +173,10 @@ public class SortNode extends PlanNode {
if (useTopnOpt) {
output.append(detailPrefix + "TOPN filter targets:
").append(topnFilterTargets).append("\n");
+ boolean strict = topnFilter.allowsStrictBounds();
+ output.append(detailPrefix).append("TOPN filter boundary: ")
+ .append(info.getIsAscOrder().get(0) ? (strict ? "<" :
"<=") : (strict ? ">" : ">="))
+ .append("\n");
}
output.append(detailPrefix + "algorithm: ");
if (algorithm == TSortAlgorithm.HEAP_SORT) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
index f459b25e999..4d9b4ec6710 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
@@ -52,7 +52,6 @@ import
org.apache.doris.nereids.trees.plans.physical.TopnFilter;
import org.apache.doris.nereids.util.MemoPatternMatchSupported;
import org.apache.doris.nereids.util.PlanChecker;
import org.apache.doris.nereids.util.PlanConstructor;
-import org.apache.doris.planner.OlapScanNode;
import org.apache.doris.planner.ScanNode;
import org.apache.doris.thrift.TExplainLevel;
import org.apache.doris.thrift.TSortAlgorithm;
@@ -142,12 +141,9 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
.translatePlan(plan);
TTopnFilterDesc desc = filter.toThrift();
Assertions.assertEquals(2,
desc.getTargetNodeIdToTargetExprSize());
- for (ScanNode target : filter.legacyTargets.keySet()) {
- String tableName = ((OlapScanNode)
target).getOlapTable().getName();
- boolean strict =
tableName.equals("topn_storage_dup") || source.equals("topn_storage_mow");
- Assertions.assertEquals(strict,
desc.getStrictTargetNodeIds().contains(target.getId().asInt()),
- sql + "\n" + plan.treeString());
- }
+ boolean strict = source.equals("topn_storage_mow") &&
key.equals("k");
+ Assertions.assertEquals(strict ? 2 : 0,
desc.getStrictTargetNodeIdsSize(),
+ sql + "\n" + plan.treeString());
}
}
}
@@ -169,7 +165,8 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
.analyze("SELECT k,v FROM topn_storage_dup ORDER BY k
LIMIT 1").rewrite().implement();
PhysicalPlan plan = checker.getPhysicalPlan();
PhysicalOlapScan scan =
plan.<PhysicalOlapScan>collectToList(PhysicalOlapScan.class::isInstance).get(0);
- PhysicalTopN<?> topn =
plan.<PhysicalTopN<?>>collectToList(PhysicalTopN.class::isInstance).get(0);
+ PhysicalTopN<?> topn = plan.<PhysicalTopN<?>>collectToList(node ->
node instanceof PhysicalTopN
+ && ((PhysicalTopN<?>) node).getSortPhase() ==
SortPhase.LOCAL_SORT).get(0);
OlapTable table = PlanConstructor.newOlapTable(10001,
"topn_index_model", 0, KeysType.DUP_KEYS);
table.setIndexMeta(10002, "aggregate_index",
table.getFullSchema(), 0, 0, (short) 0,
TStorageType.COLUMN, KeysType.AGG_KEYS);
@@ -184,6 +181,8 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
new PushDownContext(topn, scan.getOutput().get(0),
false));
Assertions.assertTrue(accepted);
Assertions.assertEquals(1,
context.getTopnFilters().size());
+ Assertions.assertEquals(indexId == table.getBaseIndexId(),
+
context.getTopnFilters().get(0).isStrictEligible());
}
}
} finally {
@@ -232,6 +231,55 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
}
}
+ @Test
+ public void testStrictBoundaryExplainAndMixedUnionPaths() {
+ double previousRatio =
connectContext.getSessionVariable().topnFilterRatio;
+ String previousAlgorithm =
connectContext.getSessionVariable().forceSortAlgorithm;
+ boolean previousPushTopn =
connectContext.getSessionVariable().pushTopnToAgg;
+ String previousRules = String.join(",",
connectContext.getSessionVariable().getDisableNereidsRuleNames());
+ connectContext.getSessionVariable().topnFilterRatio = 1000000;
+ connectContext.getSessionVariable().forceSortAlgorithm = "heap";
+ connectContext.getSessionVariable().pushTopnToAgg = false;
+
connectContext.getSessionVariable().setDisableNereidsRules(previousRules
+ + ",PRUNE_EMPTY_PARTITION,PUSH_DOWN_TOP_N_THROUGH_UNION");
+ try {
+ String scan = "SELECT k,ts AS v FROM topn_safety";
+ String aggregate = "SELECT k,SUM(ts) AS v FROM topn_safety GROUP
BY k HAVING SUM(ts)>0";
+ Map<String, Boolean> queries = Map.of(
+ "SELECT k AS alias_key,ts FROM topn_safety WHERE ts=999
ORDER BY alias_key %s LIMIT 1 OFFSET 1", true,
+ "SELECT k,ts FROM topn_safety ORDER BY ABS(k) %s LIMIT 1",
false,
+ "SELECT k,ts FROM topn_safety ORDER BY k %s,ts LIMIT 1",
false,
+ "SELECT k,v FROM (" + scan + " UNION ALL " + scan + ") u
ORDER BY k %s LIMIT 1", true,
+ "SELECT k,v FROM (" + scan + " UNION ALL " + aggregate +
") u ORDER BY k %s LIMIT 1", false,
+ "SELECT k,v FROM (" + aggregate + " UNION ALL " + scan +
") u ORDER BY k %s LIMIT 1", false);
+ for (Map.Entry<String, Boolean> query : queries.entrySet()) {
+ for (String direction : List.of("ASC", "DESC")) {
+
PlanChecker.from(connectContext).checkPlannerResult(String.format(query.getKey(),
direction),
+ planner -> {
+ String explain =
planner.getFragments().stream()
+ .map(fragment ->
fragment.getExplainString(TExplainLevel.NORMAL))
+ .collect(Collectors.joining("\n"));
+ Assertions.assertTrue(explain.contains("TOPN
OPT:"), explain);
+ String boundary = direction.equals("ASC") ?
"<" : ">";
+ boundary += query.getValue() ? "" : "=";
+ Assertions.assertTrue(explain.contains("TOPN
filter boundary: " + boundary + "\n"),
+ explain);
+ for (TopnFilter filter :
planner.getTopnFilters()) {
+ TTopnFilterDesc desc = filter.toThrift();
+ Assertions.assertEquals(query.getValue() ?
filter.legacyTargets.size() : 0,
+ desc.getStrictTargetNodeIdsSize(),
explain);
+ }
+ });
+ }
+ }
+ } finally {
+ connectContext.getSessionVariable().topnFilterRatio =
previousRatio;
+ connectContext.getSessionVariable().forceSortAlgorithm =
previousAlgorithm;
+ connectContext.getSessionVariable().pushTopnToAgg =
previousPushTopn;
+
connectContext.getSessionVariable().setDisableNereidsRules(previousRules);
+ }
+ }
+
@Test
public void testTopNFilterDoesNotRequireTotalSecondaryExpressions() {
double previousRatio =
connectContext.getSessionVariable().topnFilterRatio;
@@ -420,7 +468,7 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
.collect(Collectors.toList());
Assertions.assertEquals(1, outerFilters.size(),
plan.treeString());
// An inner TopN remains a barrier for the outer
source. Otherwise BE
- // keeps equality on the aggregate target and may
prune ties on its sibling.
+ // keeps equality on every target of the source.
int expectedTargets = innerOrder.isEmpty() ? 2 : 1;
Assertions.assertEquals(expectedTargets,
outerFilters.get(0).targets.size(), plan.treeString());
Assertions.assertEquals(innerOrder.isEmpty(),
outerFilters.get(0).targets.keySet().stream()
@@ -436,6 +484,7 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
.translatePlan(plan);
Assertions.assertEquals(expectedTargets,
outerFilters.get(0).legacyTargets.size());
Assertions.assertFalse(outerFilters.get(0).toThrift().getTargetNodeIdToTargetExpr().isEmpty());
+
Assertions.assertTrue(outerFilters.get(0).toThrift().getStrictTargetNodeIds().isEmpty());
}
}
}
@@ -534,7 +583,7 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
Expression probeExpr = filter.targets.get(lazyScan);
TopnFilterContext topnFilterContext = new TopnFilterContext();
- topnFilterContext.addTopnFilter(filter.topn, lazyScan.getScan(),
probeExpr);
+ topnFilterContext.addTopnFilter(filter.topn, lazyScan.getScan(),
probeExpr, false);
PlanTranslatorContext translatorContext = new PlanTranslatorContext();
probeExpr.getInputSlots().forEach(slot -> translatorContext
.createSlotDesc(translatorContext.generateTupleDesc(),
(SlotReference) slot));
@@ -542,6 +591,10 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
topnFilterContext.translateTarget(lazyScan, legacyScan,
translatorContext);
Assertions.assertTrue(topnFilterContext.getTopnFilter(filter.topn).legacyTargets.containsKey(legacyScan));
+ topnFilterContext.addTopnFilter(filter.topn, lazyScan, probeExpr,
true);
+ topnFilterContext.translateTarget(lazyScan, legacyScan,
translatorContext);
+
Assertions.assertFalse(topnFilterContext.getTopnFilter(filter.topn).isStrictEligible());
+ Assertions.assertEquals(1,
topnFilterContext.getTopnFilter(filter.topn).legacyTargets.size());
}
@Test
@@ -647,8 +700,13 @@ public class TopNRuntimeFilterTest extends SSBTestBase
implements MemoPatternMat
Assertions.assertTrue(sort.isSetAlgorithm());
Assertions.assertEquals(expectedAlgorithm, sort.getAlgorithm(),
plan.treeString());
Assertions.assertEquals(expectedKeyCount,
sort.getSortInfo().getOrderingExprsSize());
+ // The local heap must retain the final LIMIT + OFFSET candidates
(3 + 2).
+ Assertions.assertEquals(5L, filter.legacySortNode.getLimit() +
filter.legacySortNode.getOffset());
TTopnFilterDesc desc = filter.toThrift();
Assertions.assertTrue(desc.isSetStrictTargetNodeIds());
+ boolean strict = expectedAlgorithm == TSortAlgorithm.HEAP_SORT &&
expectedKeyCount == 1;
+
Assertions.assertTrue(filter.legacySortNode.getNodeExplainString("",
TExplainLevel.NORMAL)
+ .contains("TOPN filter boundary: " + (strict ? "<\n" :
"<=\n")));
Assertions.assertEquals(expectedAlgorithm ==
TSortAlgorithm.HEAP_SORT && expectedKeyCount == 1
? desc.getTargetNodeIdToTargetExprSize() : 0,
desc.getStrictTargetNodeIdsSize());
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterPushDownVisitorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterPushDownVisitorTest.java
new file mode 100644
index 00000000000..77f11a1f9e3
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterPushDownVisitorTest.java
@@ -0,0 +1,245 @@
+// 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.postprocess;
+
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.nereids.processor.post.TopnFilterContext;
+import org.apache.doris.nereids.processor.post.TopnFilterPushDownVisitor;
+import
org.apache.doris.nereids.processor.post.TopnFilterPushDownVisitor.PushDownContext;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.DistributionSpecGather;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.properties.OrderKey;
+import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.LimitPhase;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PreAggStatus;
+import org.apache.doris.nereids.trees.plans.SortPhase;
+import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalAssertNumRows;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEAnchor;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEProducer;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalDistribute;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalLimit;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalNestedLoopJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalQuickSort;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
+import
org.apache.doris.nereids.trees.plans.physical.PhysicalStorageLayerAggregate;
+import
org.apache.doris.nereids.trees.plans.physical.PhysicalStorageLayerAggregate.PushDownAggOp;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalUnion;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalWindow;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.util.PlanConstructor;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.BitSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public class TopnFilterPushDownVisitorTest {
+ private final SlotReference key = new SlotReference("k",
IntegerType.INSTANCE);
+
+ private PhysicalOlapScan scan(SlotReference slot) {
+ OlapTable table = PlanConstructor.newOlapTable(10001, "strict_scan",
0);
+ LogicalProperties properties = new LogicalProperties(() ->
List.of(slot), () -> DataTrait.EMPTY_TRAIT);
+ return new PhysicalOlapScan(PlanConstructor.getNextRelationId(), table,
+ List.of("test"), -1L, List.of(), List.of(), null,
PreAggStatus.on(), List.of(), Optional.empty(),
+ properties, Optional.empty(), List.of(), List.of(), List.of(),
Optional.empty(), Optional.empty(),
+ List.of(), Optional.empty());
+ }
+
+ private PhysicalTopN<Plan> source(Slot slot) {
+ PhysicalTopN<Plan> topn = Mockito.mock(PhysicalTopN.class);
+ Mockito.when(topn.getOrderKeys()).thenReturn(List.of(new
OrderKey(slot, true, false)));
+ Mockito.when(topn.getSortPhase()).thenReturn(SortPhase.LOCAL_SORT);
+ Mockito.when(topn.getLimit()).thenReturn(3L);
+ Mockito.when(topn.getOffset()).thenReturn(2L);
+ return topn;
+ }
+
+ private TopnFilterContext push(Plan plan, PhysicalTopN<?> source) {
+ TopnFilterContext context = new TopnFilterContext();
+ plan.accept(new TopnFilterPushDownVisitor(context),
+ new PushDownContext(source,
source.getOrderKeys().get(0).getExpr(), false));
+ return context;
+ }
+
+ private void assertEligibility(Plan plan, PhysicalTopN<?> source, boolean
strict) {
+ TopnFilterContext context = push(plan, source);
+ Assertions.assertEquals(1, context.getTopnFilters().size());
+ Assertions.assertEquals(strict,
context.getTopnFilters().get(0).isStrictEligible());
+ }
+
+ @Test
+ public void testSourceCandidateCountAndOrderKeys() {
+ PhysicalOlapScan scan = scan(key);
+ PhysicalTopN<?> topn = source(key);
+ assertEligibility(scan, topn, true);
+ for (long[] counts : List.of(new long[] {0, 0}, new long[] {1, -1},
+ new long[] {Long.MAX_VALUE, 1}, new long[] {3, 2}, new long[]
{Long.MAX_VALUE, 0})) {
+ Mockito.when(topn.getLimit()).thenReturn(counts[0]);
+ Mockito.when(topn.getOffset()).thenReturn(counts[1]);
+ assertEligibility(scan, topn, counts[0] > 0 && counts[1] >= 0
+ && counts[0] <= Long.MAX_VALUE - counts[1]);
+ }
+ Mockito.when(topn.getOrderKeys()).thenReturn(List.of(new OrderKey(key,
true, false),
+ new OrderKey(new IntegerLiteral(1), true, false)));
+ assertEligibility(scan, topn, false);
+ Mockito.when(topn.getOrderKeys()).thenReturn(List.of(new OrderKey(new
Add(key, new IntegerLiteral(1)),
+ true, false)));
+ assertEligibility(scan, topn, false);
+ Mockito.when(topn.getOrderKeys()).thenReturn(List.of(new OrderKey(key,
true, false)));
+ Mockito.when(topn.getSortPhase()).thenReturn(SortPhase.MERGE_SORT);
+ assertEligibility(scan, topn, false);
+ }
+
+ @Test
+ public void testSafeUnaryNodesAndExactAliases() {
+ PhysicalOlapScan scan = scan(key);
+ Alias alias = new Alias(key, "alias");
+ Plan plan = new PhysicalProject<>(List.of(alias), null,
+ new PhysicalFilter<>(Set.of(new EqualTo(key, new
IntegerLiteral(1))), null,
+ new
PhysicalDistribute<>(DistributionSpecGather.INSTANCE,
+ new PhysicalQuickSort<>(List.of(new
OrderKey(key, true, false)),
+ SortPhase.LOCAL_SORT, null, scan))));
+ assertEligibility(plan, source(alias.toSlot()), true);
+ Alias computed = new Alias(new Add(key, new IntegerLiteral(1)),
"computed");
+ assertEligibility(new PhysicalProject<>(List.of(computed), null,
scan), source(computed.toSlot()), false);
+ }
+
+ @Test
+ public void testUnknownLimitAndHiddenStorageAggregateKeepClosedFilters() {
+ PhysicalOlapScan scan = scan(key);
+ PhysicalTopN<?> topn = source(key);
+ assertEligibility(new PhysicalLimit<>(10, 0, LimitPhase.LOCAL, null,
scan), topn, false);
+ assertEligibility(new PhysicalLimit<>(10, 1, LimitPhase.LOCAL, null,
scan), topn, false);
+ // No child is exposed by this wrapper: PlanVisitor unwraps its
relation directly.
+ assertEligibility(new PhysicalStorageLayerAggregate(scan,
PushDownAggOp.MIN_MAX), topn, false);
+ Plan unknown = Mockito.mock(Plan.class);
+ Mockito.when(unknown.children()).thenReturn(List.of(scan));
+ TopnFilterContext context = new TopnFilterContext();
+ new TopnFilterPushDownVisitor(context).visit(unknown, new
PushDownContext(topn, key, false));
+
Assertions.assertFalse(context.getTopnFilters().get(0).isStrictEligible());
+ // Rebinding an alias after a downgrade must not re-enable strict
bounds.
+ Alias alias = new Alias(key, "alias");
+ PhysicalProject<?> project = new PhysicalProject<>(List.of(alias),
null, scan);
+ context = new TopnFilterContext();
+ project.accept(new TopnFilterPushDownVisitor(context),
+ new PushDownContext(source(alias.toSlot()), alias.toSlot(),
false).disableStrict());
+
Assertions.assertFalse(context.getTopnFilters().get(0).isStrictEligible());
+ }
+
+ @Test
+ public void testRepeatOnlyForCommonGroupingKeys() {
+ PhysicalOlapScan scan = scan(key);
+ SlotReference groupingId = new SlotReference("grouping_id",
IntegerType.INSTANCE);
+ PhysicalRepeat<?> common = new PhysicalRepeat<>(List.of(List.of(key),
List.of(key, groupingId)),
+ List.of(key, groupingId), groupingId, null, scan);
+ assertEligibility(common, source(key), true);
+ PhysicalRepeat<?> erased = new PhysicalRepeat<>(List.of(List.of(key),
List.of()),
+ List.of(key, groupingId), groupingId, null, scan);
+ Assertions.assertTrue(push(erased,
source(key)).getTopnFilters().isEmpty());
+ }
+
+ @Test
+ public void testExistingBarriersStayClosedToPushdown() {
+ for (Class<? extends Plan> type : List.of(PhysicalTopN.class,
PhysicalWindow.class,
+ PhysicalAssertNumRows.class, PhysicalCTEAnchor.class,
PhysicalCTEProducer.class)) {
+ Plan barrier = Mockito.mock(type);
+ Mockito.doCallRealMethod().when(barrier).accept(Mockito.any(),
Mockito.any());
+ Assertions.assertTrue(push(barrier,
source(key)).getTopnFilters().isEmpty(), type.getSimpleName());
+ }
+ }
+
+ @Test
+ public void testUnionActualTargetsAndChildOrder() {
+ SlotReference other = new SlotReference("other", IntegerType.INSTANCE);
+ SlotReference output = new SlotReference("output",
IntegerType.INSTANCE);
+ PhysicalOlapScan first = scan(key);
+ PhysicalOlapScan second = scan(other);
+ for (boolean reverse : List.of(false, true)) {
+ for (boolean unsafe : List.of(false, true)) {
+ Plan firstBranch = unsafe ? new
PhysicalStorageLayerAggregate(first, PushDownAggOp.MIN_MAX) : first;
+ PhysicalUnion union = new PhysicalUnion(Qualifier.ALL,
List.of(output),
+ reverse ? List.of(List.of(other), List.of(key)) :
List.of(List.of(key), List.of(other)),
+ List.of(), null, reverse ? List.of(second,
firstBranch) : List.of(firstBranch, second));
+ TopnFilterContext context = push(union, source(output));
+ Assertions.assertEquals(2,
context.getTopnFilters().get(0).targets.size());
+ Assertions.assertEquals(!unsafe,
context.getTopnFilters().get(0).isStrictEligible());
+ }
+ }
+ PhysicalWindow<?> window = Mockito.mock(PhysicalWindow.class);
+ Mockito.doCallRealMethod().when(window).accept(Mockito.any(),
Mockito.any());
+ Mockito.when(window.getAllChildrenTypes()).thenReturn(new BitSet());
+ PhysicalUnion union = new PhysicalUnion(Qualifier.ALL, List.of(output),
+ List.of(List.of(key), List.of(other)), List.of(), null,
List.of(first, window));
+ TopnFilterContext context = push(union, source(output));
+ Assertions.assertEquals(1,
context.getTopnFilters().get(0).targets.size());
+
Assertions.assertTrue(context.getTopnFilters().get(0).isStrictEligible());
+ }
+
+ @Test
+ public void testJoinTypesAndEqualitySubstitution() {
+ SlotReference rightKey = new SlotReference("right_k",
IntegerType.INSTANCE);
+ PhysicalOlapScan left = scan(key);
+ PhysicalOlapScan right = scan(rightKey);
+ Set<JoinType> strictJoins = Set.of(JoinType.INNER_JOIN,
JoinType.CROSS_JOIN, JoinType.LEFT_OUTER_JOIN,
+ JoinType.LEFT_SEMI_JOIN, JoinType.LEFT_ANTI_JOIN);
+ for (JoinType type : JoinType.values()) {
+ for (boolean mark : List.of(false, true)) {
+ PhysicalHashJoin<Plan, Plan> hash =
Mockito.mock(PhysicalHashJoin.class);
+ Mockito.when(hash.getJoinType()).thenReturn(type);
+ Mockito.when(hash.isMarkJoin()).thenReturn(mark);
+ Mockito.when(hash.left()).thenReturn(left);
+ Mockito.when(hash.right()).thenReturn(right);
+
Mockito.when(hash.getHashJoinConjuncts()).thenReturn(List.of(new EqualTo(key,
rightKey)));
+ Mockito.doCallRealMethod().when(hash).accept(Mockito.any(),
Mockito.any());
+ PhysicalNestedLoopJoin<Plan, Plan> nested =
Mockito.mock(PhysicalNestedLoopJoin.class);
+ Mockito.when(nested.getJoinType()).thenReturn(type);
+ Mockito.when(nested.isMarkJoin()).thenReturn(mark);
+ Mockito.when(nested.left()).thenReturn(left);
+ Mockito.doCallRealMethod().when(nested).accept(Mockito.any(),
Mockito.any());
+ for (Plan join : List.of(hash, nested)) {
+ TopnFilterContext context = push(join, source(key));
+ if (!context.getTopnFilters().isEmpty()) {
+ Assertions.assertEquals(!mark &&
strictJoins.contains(type),
+
context.getTopnFilters().get(0).isStrictEligible(), type + " mark=" + mark);
+ }
+ }
+ // Retain existing equality-substituted filters, but never
grant them strict bounds.
+ TopnFilterContext substituted = push(hash, source(rightKey));
+
Assertions.assertTrue(substituted.getTopnFilters().stream().noneMatch(f ->
f.isStrictEligible()));
+ }
+ }
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterStrictBoundsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterStrictBoundsTest.java
index cb439ff467f..a33362589d7 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterStrictBoundsTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopnFilterStrictBoundsTest.java
@@ -43,7 +43,6 @@ import org.apache.doris.planner.OlapScanNode;
import org.apache.doris.planner.PartitionSortNode;
import org.apache.doris.planner.PlanNode;
import org.apache.doris.planner.PlanNodeId;
-import org.apache.doris.planner.RepeatNode;
import org.apache.doris.planner.ScanContext;
import org.apache.doris.planner.ScanNode;
import org.apache.doris.planner.SelectNode;
@@ -69,15 +68,23 @@ public class TopnFilterStrictBoundsTest {
return node;
}
+ private OlapScanNode scan(int id) {
+ OlapScanNode scan = node(OlapScanNode.class, id);
+
Mockito.when(scan.getSelectedIndexKeyType()).thenReturn(KeysType.DUP_KEYS);
+ return scan;
+ }
+
private TopnFilter filter(PlanNode child, ScanNode... targets) {
TopN topn = Mockito.mock(TopN.class);
Mockito.when(topn.getOrderKeys()).thenReturn(List.of(new OrderKey(new
IntegerLiteral(1), true, false)));
- TopnFilter filter = new TopnFilter(topn,
Mockito.mock(PhysicalRelation.class), new IntegerLiteral(1));
+ TopnFilter filter = new TopnFilter(topn,
Mockito.mock(PhysicalRelation.class), new IntegerLiteral(1), true);
SortNode source = node(SortNode.class, 10, child);
SortInfo sortInfo = Mockito.mock(SortInfo.class);
Mockito.when(sortInfo.getOrderingExprs()).thenReturn(List.<Expr>of(new
IntLiteral(1)));
Mockito.when(source.getSortInfo()).thenReturn(sortInfo);
Mockito.when(source.getSortAlgorithm()).thenReturn(TSortAlgorithm.HEAP_SORT);
+ Mockito.when(source.getLimit()).thenReturn(3L);
+ Mockito.when(source.getOffset()).thenReturn(2L);
filter.legacySortNode = source;
for (ScanNode target : targets) {
filter.legacyTargets.put(target, new IntLiteral(1));
@@ -86,21 +93,49 @@ public class TopnFilterStrictBoundsTest {
}
@Test
- public void
testCompletePathsAcrossExchangesKeepAggregateTiesAndPruneSiblingTies() {
- ScanNode aggregateScan = node(FileQueryScanNode.class, 20);
- ScanNode siblingScan = node(FileQueryScanNode.class, 30);
- ScanNode disconnectedScan = node(FileQueryScanNode.class, 99);
+ public void testEveryActualTargetMustAllowStrictBounds() {
+ ScanNode aggregateScan = scan(20);
+ ScanNode siblingScan = scan(30);
+ ScanNode disconnectedScan = scan(99);
PlanNode aggregate = node(AggregationNode.class, 21, aggregateScan);
PlanNode union = node(UnionNode.class, 11, node(ExchangeNode.class,
12, aggregate),
node(ExchangeNode.class, 13, siblingScan));
TopnFilter filter = filter(union, aggregateScan, siblingScan,
disconnectedScan);
- Assertions.assertEquals(List.of(30),
filter.toThrift().getStrictTargetNodeIds());
+
Assertions.assertTrue(filter.toThrift().getStrictTargetNodeIds().isEmpty());
Assertions.assertEquals(3,
filter.toThrift().getTargetNodeIdToTargetExprSize());
}
+ @Test
+ public void testExternalTargetsAndUnreachableTargetsKeepSourceInclusive() {
+ ScanNode scan = scan(20);
+ ScanNode file = node(FileQueryScanNode.class, 30);
+ Assertions.assertTrue(filter(file,
file).toThrift().getStrictTargetNodeIds().isEmpty());
+ Assertions.assertTrue(filter(node(UnionNode.class, 11, scan, file),
scan, file)
+ .toThrift().getStrictTargetNodeIds().isEmpty());
+ Assertions.assertTrue(filter(scan, scan,
scan(99)).toThrift().getStrictTargetNodeIds().isEmpty());
+ // A non-target branch must not disable the only actual target.
+ Assertions.assertEquals(List.of(20), filter(node(UnionNode.class, 11,
scan,
+ node(AggregationNode.class, 31, scan(30))),
scan).toThrift().getStrictTargetNodeIds());
+ }
+
+ @Test
+ public void testRepeatedTargetRegistrationNeverRestoresStrictEligibility()
{
+ ScanNode scan = scan(20);
+ for (boolean unsafeFirst : List.of(false, true)) {
+ TopnFilter filter = filter(scan, scan);
+ PhysicalRelation target =
filter.targets.keySet().iterator().next();
+ filter.addTarget(target, new IntegerLiteral(1), !unsafeFirst);
+ filter.addTarget(target, new IntegerLiteral(1), unsafeFirst);
+ filter.addTarget(target, new IntegerLiteral(1), true);
+ Assertions.assertFalse(filter.isStrictEligible());
+
Assertions.assertTrue(filter.toThrift().getStrictTargetNodeIds().isEmpty());
+ Assertions.assertEquals(1, filter.targets.size());
+ }
+ }
+
@Test
public void testFinalAlgorithmAndOrderingKeys() {
- ScanNode scan = node(FileQueryScanNode.class, 20);
+ ScanNode scan = scan(20);
TopnFilter filter = filter(scan, scan);
for (TSortAlgorithm algorithm : TSortAlgorithm.values()) {
Mockito.when(filter.legacySortNode.getSortAlgorithm()).thenReturn(algorithm);
@@ -122,7 +157,7 @@ public class TopnFilterStrictBoundsTest {
@Test
public void testLimitsAndRowChangingOperatorsRetainTies() {
- ScanNode scan = node(FileQueryScanNode.class, 20);
+ ScanNode scan = scan(20);
PlanNode select = node(SelectNode.class, 11, scan);
TopnFilter filter = filter(select, scan);
Assertions.assertEquals(List.of(20),
filter.toThrift().getStrictTargetNodeIds());
@@ -132,7 +167,15 @@ public class TopnFilterStrictBoundsTest {
Mockito.when(scan.hasLimit()).thenReturn(true);
Assertions.assertTrue(filter.toThrift().getStrictTargetNodeIds().isEmpty());
Mockito.when(scan.hasLimit()).thenReturn(false);
- for (Class<? extends PlanNode> type : List.of(AggregationNode.class,
RepeatNode.class,
+ Mockito.when(select.getOffset()).thenReturn(1L);
+
Assertions.assertTrue(filter.toThrift().getStrictTargetNodeIds().isEmpty());
+ Mockito.when(select.getOffset()).thenReturn(0L);
+ PlanNode limitedPath = node(SelectNode.class, 12, scan);
+ Mockito.when(limitedPath.hasLimit()).thenReturn(true);
+ // The same target reached by an unsafe path cannot be rescued by its
safe occurrence.
+ Assertions.assertTrue(filter(node(UnionNode.class, 13, select,
limitedPath), scan)
+ .toThrift().getStrictTargetNodeIds().isEmpty());
+ for (Class<? extends PlanNode> type : List.of(AggregationNode.class,
AnalyticEvalNode.class, AssertNumRowsNode.class,
PartitionSortNode.class, PlanNode.class)) {
Assertions.assertTrue(filter(node(type, 11, scan),
scan).toThrift().getStrictTargetNodeIds().isEmpty(),
type.getSimpleName());
@@ -142,12 +185,10 @@ public class TopnFilterStrictBoundsTest {
@Test
public void testJoinModes() {
Set<JoinOperator> allowed = Set.of(JoinOperator.INNER_JOIN,
JoinOperator.CROSS_JOIN,
- JoinOperator.LEFT_OUTER_JOIN, JoinOperator.LEFT_SEMI_JOIN,
JoinOperator.LEFT_ANTI_JOIN,
- JoinOperator.NULL_AWARE_LEFT_ANTI_JOIN,
JoinOperator.NULL_AWARE_LEFT_SEMI_JOIN,
- JoinOperator.ASOF_LEFT_INNER_JOIN,
JoinOperator.ASOF_LEFT_OUTER_JOIN);
+ JoinOperator.LEFT_OUTER_JOIN, JoinOperator.LEFT_SEMI_JOIN,
JoinOperator.LEFT_ANTI_JOIN);
for (Class<? extends JoinNodeBase> type : List.of(HashJoinNode.class,
NestedLoopJoinNode.class)) {
for (JoinOperator join : JoinOperator.values()) {
- ScanNode scan = node(FileQueryScanNode.class, 20);
+ ScanNode scan = scan(20);
JoinNodeBase joinNode = node(type, 11, scan,
node(PlanNode.class, 31));
Mockito.when(joinNode.getJoinOp()).thenReturn(join);
Assertions.assertEquals(allowed.contains(join) ? List.of(20) :
List.of(),
diff --git
a/regression-test/suites/topn_optimize/topn_runtime_filter_target_mode.groovy
b/regression-test/suites/topn_optimize/topn_runtime_filter_target_mode.groovy
index 3d4ca2b036a..267ea070d50 100644
---
a/regression-test/suites/topn_optimize/topn_runtime_filter_target_mode.groovy
+++
b/regression-test/suites/topn_optimize/topn_runtime_filter_target_mode.groovy
@@ -58,8 +58,9 @@ suite("topn_runtime_filter_target_mode") {
if (sqlAggregate) {
contains "AGGREGATE"
}
- // Both targets remain enabled. BE keeps equality only on the
aggregate path.
+ // Both targets remain enabled, and an aggregate target keeps the
entire source inclusive.
multiContains "TOPN OPT:", 2
+ contains "TOPN filter boundary: ${query.contains('DESC') ? '>=' :
'<='}"
}
"order_qt_${name}_enabled"(checked)
}
@@ -90,6 +91,22 @@ suite("topn_runtime_filter_target_mode") {
UNION ALL SELECT k,v FROM topn_target_control
) u ORDER BY k NULLS LAST LIMIT 1""", "135", true)
+ for (String direction : ["ASC", "DESC"]) {
+ explain {
+ sql "SELECT k AS alias_key,v FROM topn_target_control WHERE v=999
" +
+ "ORDER BY alias_key ${direction} NULLS LAST LIMIT 1 OFFSET
1"
+ contains "TOPN OPT:"
+ contains "TOPN filter boundary: ${direction == 'ASC' ? '<' :
'>'}\n"
+ }
+ for (String orderKey : ["k ${direction},v", "ABS(k) ${direction}"]) {
+ explain {
+ sql "SELECT k,v FROM topn_target_control ORDER BY ${orderKey}
LIMIT 1"
+ contains "TOPN OPT:"
+ contains "TOPN filter boundary: ${direction == 'ASC' ? '<=' :
'>='}"
+ }
+ }
+ }
+
// An aggregate result is not a grouping key. It cannot become a scan
predicate.
explain {
sql "SELECT k,SUM(v) AS s FROM topn_target_input GROUP BY k ORDER BY s
LIMIT 1"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]