Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
import org.apache.doris.nereids.util.JoinUtils;
import org.apache.doris.nereids.util.Utils;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
Expand Down Expand Up @@ -63,9 +62,23 @@ public class LeadingHint extends Hint {

private final Map<ExprId, String> exprIdToTableNameMap = Maps.newLinkedHashMap();

private final List<Pair<Long, Expression>> filters = new ArrayList<>();
/** A filter occurrence with its table bitmap, expression, and the original join type
* from which it was collected. The original type is used to prevent a filter from
* being consumed at the wrong join level (e.g., an inner-join predicate with a
* narrow bitmap being consumed by a lower anti join). */
public static class FilterEntry {
public final long bitmap;
public final Expression expr;
public final JoinType originalType;

private final Map<Expression, JoinType> conditionJoinType = Maps.newLinkedHashMap();
public FilterEntry(long bitmap, Expression expr, JoinType originalType) {
this.bitmap = bitmap;
this.expr = expr;
this.originalType = originalType;
}
}

private final List<FilterEntry> filters = new ArrayList<>();

private final List<JoinConstraint> joinConstraintList = new ArrayList<>();

Expand Down Expand Up @@ -279,32 +292,24 @@ public Map<ExprId, String> getExprIdToTableNameMap() {
return exprIdToTableNameMap;
}

public List<Pair<Long, Expression>> getFilters() {
public List<FilterEntry> getFilters() {
return filters;
}

public void putConditionJoinType(Expression filter, JoinType joinType) {
conditionJoinType.put(filter, joinType);
/** Add a filter occurrence with its original join type. The type is stored per
* occurrence so that makeJoinPlan can reject conditions whose original type
* is incompatible with the generated join, putting them back for later use. */
public void addFilter(long bitmap, Expression expr, JoinType originalType) {
filters.add(new FilterEntry(bitmap, expr, originalType));
}

/**
* find out whether conditions can match original joinType
* @param conditions conditions needs to put on this join
* @param joinType join type computed by join constraint
* @return can conditions matched
*/
public boolean isConditionJoinTypeMatched(List<Expression> conditions, JoinType joinType) {
for (Expression condition : conditions) {
JoinType originalJoinType = conditionJoinType.get(condition);
if (originalJoinType.equals(joinType)
|| originalJoinType.isOneSideOuterJoin() && joinType.isOneSideOuterJoin()
|| originalJoinType.isSemiJoin() && joinType.isSemiJoin()
|| originalJoinType.isAntiJoin() && joinType.isAntiJoin()) {
continue;
}
return false;
}
return true;
/** Check whether a filter originally collected from a join of {@code filterType}
* can legally be consumed as a condition of a join of {@code joinType}. */
public static boolean isJoinTypeCompatible(JoinType filterType, JoinType joinType) {
return filterType.equals(joinType)
|| filterType.isOneSideOuterJoin() && joinType.isOneSideOuterJoin()
|| filterType.isSemiJoin() && joinType.isSemiJoin()
|| filterType.isAntiJoin() && joinType.isAntiJoin();
}

public List<JoinConstraint> getJoinConstraintList() {
Expand Down Expand Up @@ -416,6 +421,36 @@ public Pair<JoinConstraint, Boolean> getJoinConstraint(Long joinTableBitmap, Lon
continue;
}

// Semi/anti join constraints require the constrained side (the right
// table for left semi/anti, or the left table for right semi/anti)
// to remain intact on one child — it cannot be split across children
// or mixed with extra tables. The first guard (isSubset
// minLeftHand/minRightHand) ensures the constraint is only evaluated
// when all required tables are present in the current join. Once
// present, if the constrained side is violated, the leading hint can
// never satisfy this constraint (tables only merge, never split), so
// we must return failure immediately.
//
// Example 1 — LEFT SEMI JOIN where the constrained side gets mixed:
// SELECT /*+ leading(a2 a5 { a3 a1 }) */ ...
// FROM a1 JOIN a2 ON ...
// LEFT SEMI JOIN a3 ON a2.c2 = a2.c1
// JOIN a5 ON a2.c2 = a2.c1;
// Original tree: ((a1 JOIN a2) LEFT SEMI JOIN a3) JOIN a5
// Leading asks: ((a2 JOIN a5) JOIN (a3 JOIN a1))
// At the top join: left={a2,a5}, right={a1,a3}
// Constrained side (right table a3 for LEFT SEMI) is mixed with
// a1 on the right child → violated, leading must be UNUSED.
//
// Example 2 — RIGHT ANTI JOIN where constrained side gets mixed:
// SELECT /*+ leading(a3 a1 a2) */ ...
// FROM a1 RIGHT ANTI JOIN a2 ON a1.c4 = a1.c
// JOIN a3 ON a2.c = a3.c3;
// Original tree: (a1 RIGHT ANTI JOIN a2) JOIN a3
// Leading asks: ((a3 JOIN a1) JOIN a2)
// At the top join: left={a1,a3}, right={a2}
// Constrained side (left table a1 for RIGHT ANTI) is mixed with
// a3 → violated, leading must be UNUSED.
if (joinConstraint.getJoinType().isSemiOrAntiJoin()) {
if (!LongBitmap.isSubset(joinConstraint.getMinLeftHand(), joinTableBitmap)
|| !LongBitmap.isSubset(joinConstraint.getMinRightHand(), joinTableBitmap)) {
Expand All @@ -426,11 +461,11 @@ public Pair<JoinConstraint, Boolean> getJoinConstraint(Long joinTableBitmap, Lon
? joinConstraint.getLeftHand() : joinConstraint.getRightHand();
if (LongBitmap.isOverlap(constrainedSide, leftTableBitmap)
&& !constrainedSide.equals(leftTableBitmap)) {
continue;
return Pair.of(null, false);
}
if (LongBitmap.isOverlap(constrainedSide, rightTableBitmap)
&& !constrainedSide.equals(rightTableBitmap)) {
continue;
return Pair.of(null, false);
}
}

Expand Down Expand Up @@ -524,23 +559,45 @@ private LogicalPlan makeJoinPlan(LogicalPlan leftChild, LogicalPlan rightChild,
if (!distributeJoinType.equals("join")) {
distributeHint = strToHint.get(distributeJoinType);
}
List<Expression> conditions = getJoinConditions(
// Collect candidate conditions whose bitmap is covered by this join.
// Each entry carries the original join type from CollectJoinConstraint.
List<FilterEntry> candidateEntries = collectJoinConditions(
getFilters(), leftChild, rightChild);
Pair<List<Expression>, List<Expression>> pair = JoinUtils.extractExpressionForHashTable(
leftChild.getOutput(), rightChild.getOutput(), conditions);
// leading hint would set status inside if not success
List<Expression> candidateExprs = new ArrayList<>();
for (FilterEntry e : candidateEntries) {
candidateExprs.add(e.expr);
}
// Determine join type using all candidate expressions for constraint matching.
JoinType joinType = computeJoinType(getBitmap(leftChild),
getBitmap(rightChild), conditions);
getBitmap(rightChild), candidateExprs);
if (joinType == null) {
// Put back all candidates since we cannot build this join.
filters.addAll(candidateEntries);
this.setStatus(HintStatus.SYNTAX_ERROR);
this.setErrorMessage("JoinType can not be null");
} else if (!isConditionJoinTypeMatched(conditions, joinType)) {
this.setStatus(HintStatus.UNUSED);
this.setErrorMessage("condition does not matched joinType");
return null;
}
// Keep only entries whose original type is compatible with the generated
// join type. Incompatible entries are returned to the global filter list
// so they can be consumed at a later (higher) join level.
List<Expression> conditions = new ArrayList<>();
for (FilterEntry entry : candidateEntries) {
if (isJoinTypeCompatible(entry.originalType, joinType)) {
conditions.add(entry.expr);
} else {
filters.add(entry);
Comment thread
starocean999 marked this conversation as resolved.
}
}
// Rejected candidates must not shape the join type: if all compatible
// conditions were filtered out, demote INNER_JOIN to CROSS_JOIN.
if (conditions.isEmpty() && joinType == JoinType.INNER_JOIN) {
joinType = JoinType.CROSS_JOIN;
}
if (!this.isSuccess()) {
return null;
}
Pair<List<Expression>, List<Expression>> pair = JoinUtils.extractExpressionForHashTable(
leftChild.getOutput(), rightChild.getOutput(), conditions);
// get joinType
LogicalJoin logicalJoin = new LogicalJoin<>(joinType, pair.first,
pair.second,
Expand Down Expand Up @@ -576,39 +633,45 @@ public Plan generateLeadingJoinPlan() {
}

LogicalJoin finalJoin = (LogicalJoin) stack.pop();
// we want all filters been removed
if (Utils.enableAssert && !filters.isEmpty()) {
throw new IllegalStateException(
"Leading hint process failed: filter should be empty, but meet: " + filters
);
// Any filter that was put back due to incompatible join type but never
// consumed by a later join means the leading hint cannot preserve the
// original semantics. Fail unconditionally.
if (!filters.isEmpty()) {
this.setStatus(HintStatus.UNUSED);
this.setErrorMessage("leading plan cannot consume all join predicates, leftover: "
+ filters);
return null;
}
if (finalJoin != null) {
this.setStatus(HintStatus.SUCCESS);
}
return finalJoin;
}

private List<Expression> getJoinConditions(List<Pair<Long, Expression>> filters,
LogicalPlan left, LogicalPlan right) {
List<Expression> joinConditions = new ArrayList<>();
/** Collect filter entries whose bitmap is a subset of the given join's tables.
* Removes them from the global filter list; the caller must put back any entry
* whose original type is incompatible with the final join type. */
private List<FilterEntry> collectJoinConditions(List<FilterEntry> filters,
LogicalPlan left, LogicalPlan right) {
List<FilterEntry> collected = new ArrayList<>();
Long tablesBitMap = LongBitmap.or(getBitmap(left), getBitmap(right));
for (int i = filters.size() - 1; i >= 0; i--) {
Pair<Long, Expression> filterPair = filters.get(i);
Long tablesBitMap = LongBitmap.or(getBitmap(left), getBitmap(right));
// left one is smaller set
if (LongBitmap.isSubset(filterPair.first, tablesBitMap)) {
joinConditions.add(filterPair.second);
FilterEntry entry = filters.get(i);
if (LongBitmap.isSubset(entry.bitmap, tablesBitMap)) {
collected.add(entry);
filters.remove(i);
}
}
return joinConditions;
return collected;
}

private LogicalPlan makeFilterPlanIfExist(List<Pair<Long, Expression>> filters, LogicalPlan scan) {
private LogicalPlan makeFilterPlanIfExist(List<FilterEntry> filters, LogicalPlan scan) {
Set<Expression> newConjuncts = new HashSet<>();
Long scanBitmap = getBitmap(scan);
for (int i = filters.size() - 1; i >= 0; i--) {
Pair<Long, Expression> filterPair = filters.get(i);
if (LongBitmap.isSubset(filterPair.first, getBitmap(scan))) {
newConjuncts.add(filterPair.second);
FilterEntry entry = filters.get(i);
if (LongBitmap.isSubset(entry.bitmap, scanBitmap)) {
newConjuncts.add(entry.expr);
filters.remove(i);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.apache.doris.nereids.rules.analysis;

import org.apache.doris.common.Pair;
import org.apache.doris.nereids.hint.Hint;
import org.apache.doris.nereids.hint.JoinConstraint;
import org.apache.doris.nereids.hint.LeadingHint;
Expand Down Expand Up @@ -72,8 +71,7 @@ public List<Rule> buildRules() {
if (join.getJoinType().isLeftJoin()) {
filterBitMap = LongBitmap.or(filterBitMap, rightHand);
}
leading.getFilters().add(Pair.of(filterBitMap, expression));
leading.putConditionJoinType(expression, join.getJoinType());
leading.addFilter(filterBitMap, expression, join.getJoinType());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still lets an upper inner predicate be consumed before a lower right outer join has introduced its preserved side. In a reduced plan like:

InnerJoin ON a.x = c.x
  RightOuterJoin ON a.k = b.k
    a
    b
  c

CollectJoinConstraint records the upper hash predicate as {a,c}. With leading(a c b), the {a,c} join is built first; the right-outer constraint is skipped there because {b} is not in the join bitmap yet, so computeJoinType() uses the {a,c} condition for an inner join. The final join then matches the original right outer constraint and preserves b rows. That is not equivalent: if a is empty, the original upper inner join eliminates the unmatched b rows, while the generated (a JOIN c) RIGHT OUTER JOIN b returns them. Please make the right-outer constraint block this premature {a,c} consumption, for example by propagating upper inner predicates that reference the nullable left side into the constraint or adding the symmetric partial-overlap guard needed for right outer joins, and cover it with an end-to-end leading-hint test.

}
expressions = join.getOtherJoinConjuncts();
for (Expression expression : expressions) {
Expand All @@ -84,8 +82,7 @@ public List<Rule> buildRules() {
if (join.getJoinType().isLeftJoin()) {
filterBitMap = LongBitmap.or(filterBitMap, rightHand);
}
leading.getFilters().add(Pair.of(filterBitMap, expression));
leading.putConditionJoinType(expression, join.getJoinType());
leading.addFilter(filterBitMap, expression, join.getJoinType());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterBitMap is still just the predicate's input tables here, so one-sided otherJoinConjuncts can be consumed as scan filters before the outer join level where they are semantically valid.

One case is a lower preserved-side ON predicate: a FULL OUTER JOIN b ON a.v > 0 records the predicate as {a}, so makeFilterPlanIfExist() turns it into Filter(a.v > 0, a) before the full outer join; rows from a with a.v <= 0 should be preserved as unmatched full-outer rows, but the generated plan drops them. The same applies to right-preserving joins such as a RIGHT OUTER JOIN b ON b.v > 0 or a RIGHT ANTI JOIN b ON b.v > 0.

A second case is an upper predicate over the nullable side of a lower outer join: for (a LEFT OUTER JOIN b ON a.k = b.k) INNER JOIN c ON b.v > 0, leading(b a c) visits scan b first and consumes {b} as a scan filter. The regenerated b RIGHT OUTER JOIN a then preserves a rows that the original upper inner predicate would eliminate when b is NULL or fails b.v > 0.

Please encode these outer-join dependencies in the collected bitmap/constraint state, or otherwise block makeFilterPlanIfExist() from consuming predicates that are only valid after the outer join has been applied. PushDownJoinOtherCondition already models these side restrictions, and this path needs the same semantic guard plus end-to-end leading-hint coverage.

}
collectJoinConstraintList(leading, leftHand, rightHand, join, totalFilterBitMap, nonNullableSlotBitMap);

Expand Down
Loading
Loading