Class AbstractSubqueryJoin
- All Implemented Interfaces:
NamedWriteable,Writeable,PostAnalysisVerificationAware,PostOptimizationVerificationAware,Resolvable,ExecutesOn,ExecutesOn.Coordinator,SortAgnostic,SortPreserving
field IN (subquery) and field NOT IN (subquery): SemiJoin (SEMI),
AntiJoin (ANTI) and MarkJoin (MARK). The three are siblings — they share this dedup pipeline but differ in the terminal
plan they produce.
The right side is an independent subquery that must be executed first. Once the subquery result is available as a LocalRelation,
inlineData(org.elasticsearch.xpack.esql.plan.logical.join.AbstractSubqueryJoin, org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation, int, org.elasticsearch.compute.data.BlockFactory, java.util.concurrent.atomic.AtomicReference<org.elasticsearch.compute.data.Page>) converts the node into an In list predicate or a hash join. The SEMI / ANTI / MARK differences are isolated into the
small set of protected hooks below; the default implementations here encode the SEMI (IN) behavior, AntiJoin
flips a couple of hooks for NOT IN, and MarkJoin returns Eval-based plans that preserve every left row and produce a
boolean mark attribute with three-valued logic.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final recordTuple holding the subplan to execute and the original plan node for identity matching.Nested classes/interfaces inherited from class org.elasticsearch.xpack.esql.plan.logical.LogicalPlan
LogicalPlan.StageNested classes/interfaces inherited from class org.elasticsearch.xpack.esql.core.tree.Node
Node.NodeStringFormatNested classes/interfaces inherited from interface org.elasticsearch.xpack.esql.plan.logical.ExecutesOn
ExecutesOn.Coordinator, ExecutesOn.ExecuteLocation, ExecutesOn.RemoteNested classes/interfaces inherited from interface org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware
PostOptimizationVerificationAware.CoordinatorOnlyNested classes/interfaces inherited from interface org.elasticsearch.common.io.stream.Writeable
Writeable.Reader<V>, Writeable.Writer<V> -
Field Summary
Fields inherited from class org.elasticsearch.xpack.esql.plan.logical.join.Join
ENTRY, UNSUPPORTED_TYPESFields inherited from class org.elasticsearch.xpack.esql.core.tree.Node
TO_STRING_MAX_LINES, TO_STRING_MAX_WIDTH -
Constructor Summary
ConstructorsModifierConstructorDescriptionprotectedAbstractSubqueryJoin(Source source, LogicalPlan left, LogicalPlan right, JoinConfig config) protectedAbstractSubqueryJoin(Source source, LogicalPlan left, LogicalPlan right, JoinType type, List<Attribute> leftFields, List<Attribute> rightFields) -
Method Summary
Modifier and TypeMethodDescriptionprotected abstract LogicalPlanbuildEmptyRightSidePlan(Source source) Build the terminal plan when the right side has zero rows.protected LogicalPlanbuildFilterPathPlan(Block dedupKeys, DataType keyType, Attribute leftField, Source source, boolean rightHadNulls) Build the terminal plan for the small-dedup "filter" path.protected LogicalPlanbuildHashJoinPathPlan(LogicalPlan leftSide, LocalRelation deduplicatedData, JoinConfig leftJoinConfig, Attribute sentinelAttr, Source source, boolean rightHadNulls) Build the terminal plan for the large-dedup "hash-join" path.protected LogicalPlanbuildShortCircuitPlan(Source source, boolean allRightNull) Build the terminal plan when the right side contains a NULL value that forces the predicate to a constant for every left row.computeOutputExpressions(List<? extends NamedExpression> left, List<? extends NamedExpression> right) booleanprotected booleanWhether the hash-join path needs to drop NULL-keyed left rows before the join.firstSubPlan(LogicalPlan optimizedPlan, Set<LocalRelation> subPlansResults) Finds the first subquery join in the plan whose right side has not yet been replaced with results.static LogicalPlaninlineData(AbstractSubqueryJoin subqueryJoin, LocalRelation data, int hashJoinThreshold, BlockFactory blockFactory, AtomicReference<Page> pageHolder) Converts this subquery join (SEMI / ANTI / MARK) into an executable plan once the subquery result is available.protected static IninListFromDedupKeys(Source source, Attribute leftField, Block dedupKeys, DataType keyType) static booleanSame asJoin.UNSUPPORTED_TYPESbut TEXT and VERSION are allowed in IN/NOT IN subquery joins because IN/NOT IN can compare these types via equality.static LogicalPlannewMainPlan(LogicalPlan optimizedPlan, AbstractSubqueryJoin.LogicalPlanTuple subPlans, LocalRelation resultWrapper, int hashJoinThreshold, BlockFactory blockFactory, AtomicReference<Page> pageHolder) voidpostAnalysisVerification(Failures failures) Allows the implementer to validate itself.protected ExpressionsentinelFilterCondition(Source source, Attribute sentinel) Sentinel-column filter that drives the hash-join path.protected booleanWhether any NULL on the right side makes the predicate non-TRUE for every left row.protected ExpressionwrapInExpression(Source source, Expression in) Wraps theInexpression built from the dedup keys for the inline-filter path.voidwriteTo(StreamOutput out) Methods inherited from class org.elasticsearch.xpack.esql.plan.logical.join.Join
computeOutputExpressions, config, equals, executesOn, getRightToSerialize, hashCode, info, isRemote, leftReferences, makeReference, output, postOptimizationVerification, replaceChildren, resolved, rightOutputFields, rightReferences, withConfigMethods inherited from class org.elasticsearch.xpack.esql.plan.logical.BinaryPlan
left, replaceChildren, replaceLeft, replaceRight, rightMethods inherited from class org.elasticsearch.xpack.esql.plan.logical.LogicalPlan
analyzed, childrenResolved, optimized, preAnalyzed, preOptimized, setAnalyzed, setOptimized, setPreAnalyzed, setPreOptimizedMethods inherited from class org.elasticsearch.xpack.esql.plan.QueryPlan
computeExpressions, computeReferences, expressions, forEachExpression, forEachExpression, forEachExpressionDown, forEachExpressionUp, inputSet, outputSet, references, transformExpressionsDown, transformExpressionsDown, transformExpressionsOnly, transformExpressionsOnly, transformExpressionsOnlyUp, transformExpressionsUp, transformExpressionsUpMethods inherited from class org.elasticsearch.xpack.esql.core.tree.Node
allMatch, anyMatch, children, collect, collect, collect, collectFirstChildren, collectLeaves, doCollectFirst, forEachDown, forEachDown, forEachDownMayReturnEarly, forEachProperty, forEachPropertyDown, forEachPropertyOnly, forEachPropertyUp, forEachUp, forEachUp, nodeName, nodeProperties, nodeString, nodeString, propertiesToString, replaceChildrenSameSize, source, sourceLocation, sourceText, toString, toString, toString, transformChildren, transformChildren, transformDown, transformDown, transformDown, transformDown, transformDownSkipBranch, transformNodeProps, transformPropertiesDown, transformPropertiesOnly, transformPropertiesUp, transformUp, transformUp, transformUpMethods inherited from class java.lang.Object
clone, finalize, getClass, notify, notifyAll, wait, wait, waitMethods inherited from interface org.elasticsearch.xpack.esql.plan.logical.ExecutesOn.Coordinator
executesOnMethods inherited from interface org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware
postAnalysisVerification
-
Constructor Details
-
AbstractSubqueryJoin
protected AbstractSubqueryJoin(Source source, LogicalPlan left, LogicalPlan right, JoinConfig config) -
AbstractSubqueryJoin
protected AbstractSubqueryJoin(Source source, LogicalPlan left, LogicalPlan right, JoinType type, List<Attribute> leftFields, List<Attribute> rightFields)
-
-
Method Details
-
writeTo
- Specified by:
writeToin interfaceWriteable- Overrides:
writeToin classJoin- Throws:
IOException
-
getWriteableName
- Specified by:
getWriteableNamein interfaceNamedWriteable- Overrides:
getWriteableNamein classJoin
-
computeOutputExpressions
public List<NamedExpression> computeOutputExpressions(List<? extends NamedExpression> left, List<? extends NamedExpression> right) - Overrides:
computeOutputExpressionsin classJoin
-
expressionsResolved
public boolean expressionsResolved()- Overrides:
expressionsResolvedin classJoin
-
buildEmptyRightSidePlan
Build the terminal plan when the right side has zero rows.x IN ()≡ FALSE for SEMI / MARK;x NOT IN ()≡ TRUE for ANTI. MarkJoin produces an Eval that sets the mark attribute to FALSE. -
buildShortCircuitPlan
Build the terminal plan when the right side contains a NULL value that forces the predicate to a constant for every left row. For SEMI this only fires when every right value is NULL (allRightNull == true) and producesFilter(FALSE). For ANTI any NULL on the right is fatal, so it overridesshortCircuitOnAnyRightNull()and likewise producesFilter(FALSE). MarkJoin does not short-circuit on any NULL — it keeps the row counts unchanged and emitsEval($m = NULL)when every right value is NULL. -
shortCircuitOnAnyRightNull
protected boolean shortCircuitOnAnyRightNull()Whether any NULL on the right side makes the predicate non-TRUE for every left row. For ANTI (x NOT IN (..., NULL, ...)) this is always the case, so onceinlineData(org.elasticsearch.xpack.esql.plan.logical.join.AbstractSubqueryJoin, org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation, int, org.elasticsearch.compute.data.BlockFactory, java.util.concurrent.atomic.AtomicReference<org.elasticsearch.compute.data.Page>)detects a NULL position in the BlockHash dedup output it short-circuits toFilter(FALSE). For SEMI the NULL stays in the dedup output for the filter path (x IN (a, b, NULL)≡x IN (a, b)under WHERE semantics) and is stripped before constructing theLocalRelationon the hash-join path. For MARK therightHadNullsflag is forwarded to the hash-join path's CASE expression; the filter path uses the NULL position in the dedup output directly. -
buildFilterPathPlan
protected LogicalPlan buildFilterPathPlan(Block dedupKeys, DataType keyType, Attribute leftField, Source source, boolean rightHadNulls) Build the terminal plan for the small-dedup "filter" path.dedupKeyscontains the BlockHash-deduplicated distinct values from the right side and may include a single NULL position at index 0 if any input position was NULL or multi-valued (BlockHash collapses all NULL inputs into its reserved group 0).rightHadNullsis equivalent todedupKeys.isNull(0)and is passed through for documentation / hash-join-path symmetry. SEMI returnsFilter(In(...)), ANTI returnsFilter(NOT In(...))(ANTI never reaches this path withrightHadNulls = true— it short-circuits earlier), and MarkJoin builds anEvalwhose IN list is the dedup positions verbatim, relying onIn's three-valued semantics to compute the mark. -
inListFromDedupKeys
protected static In inListFromDedupKeys(Source source, Attribute leftField, Block dedupKeys, DataType keyType) Build theInexpression for the filter path by turning each BlockHash dedup position into aLiteral. A NULL position (BlockHash's reserved group 0) becomes a NULL literal naturally, soIn's three-valued semantics carry the correct predicate/mark value into the surroundingFilter(SEMI/ANTI) orEval(MARK). -
buildHashJoinPathPlan
protected LogicalPlan buildHashJoinPathPlan(LogicalPlan leftSide, LocalRelation deduplicatedData, JoinConfig leftJoinConfig, Attribute sentinelAttr, Source source, boolean rightHadNulls) Build the terminal plan for the large-dedup "hash-join" path. The deduplicated key column has already been wrapped in aLocalRelationalongside a constant TRUE sentinel column. SEMI/ANTI add a sentinel filter and Project that drops the right-side column; MarkJoin instead computes the mark via a CASE expression and projects the mark column out alongside the original left output. -
wrapInExpression
Wraps theInexpression built from the dedup keys for the inline-filter path. SEMI returns it as-is; ANTI wraps it inNot. -
sentinelFilterCondition
Sentinel-column filter that drives the hash-join path. SEMI keeps matched rows (IS NOT NULLon the sentinel); ANTI keeps unmatched rows (IS NULL). -
filterNullLeftKeysBeforeHashJoin
protected boolean filterNullLeftKeysBeforeHashJoin()Whether the hash-join path needs to drop NULL-keyed left rows before the join. SEMI/ANTI filter on the sentinel after the join; ANTI in particular would otherwise keep NULL-keyed rows (sentinel NULL → "no match") even thoughNULL NOT IN (...)should yield NULL. MARK keeps every left row and handles the NULL-key case explicitly inside the CASE expression that produces the mark, so it suppresses the filter. -
postAnalysisVerification
Description copied from interface:PostAnalysisVerificationAwareAllows the implementer to validate itself. This usually involves checking its internal setup, which often means checking the parameters it received on construction: their data or syntactic type, class, their count, expressions' structure etc. The discovered failures are added to the givenFailuresobject.It is often more useful to perform the checks as extended as it makes sense, over stopping at the first failure. This will allow the author to progress faster to a correct query.
Example: the
Filterclass, which models the WHERE command, checks that the expression it filters on -condition- is of a Boolean or NULL type:@Override void postAnalysisVerification(Failures failures) { if (condition.dataType() != NULL && condition.dataType() != BOOLEAN) { failures.add(fail(condition, "Condition expression needs to be boolean, found [{}]", condition.dataType())); } }- Specified by:
postAnalysisVerificationin interfacePostAnalysisVerificationAware- Overrides:
postAnalysisVerificationin classJoin- Parameters:
failures- the object to add failures to.
-
isSubqueryJoinUnsupported
Same asJoin.UNSUPPORTED_TYPESbut TEXT and VERSION are allowed in IN/NOT IN subquery joins because IN/NOT IN can compare these types via equality. -
inlineData
public static LogicalPlan inlineData(AbstractSubqueryJoin subqueryJoin, LocalRelation data, int hashJoinThreshold, BlockFactory blockFactory, AtomicReference<Page> pageHolder) Converts this subquery join (SEMI / ANTI / MARK) into an executable plan once the subquery result is available.The subquery key column is always run through a
BlockHash(backed by the suppliedblockFactory, so the dedup blocks are tracked by the request circuit breaker) before deciding the output shape. Doing dedup up-front lets duplicate-heavy subqueries collapse to the cheaper inline-filter path even when their raw position count exceeds the threshold, and it shrinks the IN list (and therefore both the planning and runtime cost) for the filter path itself.Once dedup is done:
- If the deduplicated key count is
<= hashJoinThreshold, the dedup keys are converted toLiterals and the join becomesFilter(IN(...))(orFilter(NOT IN(...))for ANTI). The dedup blocks are released immediately. - Otherwise, the dedup keys plus a constant TRUE sentinel become a
LocalRelationon the right of a LEFTJoin; aFilteron the sentinel (IS NOT NULLfor SEMI,IS NULLfor ANTI) and aProjectthat drops the right-side column complete the rewrite. The mapper turns this into aHashJoinExec.
SQL NULL / MV semantics for IN / NOT IN. Under
WHERE,x IN (...)returns NULL (filtered out) whenxis NULL or whenxmatches no non-null value and the list contains a NULL;x NOT IN (..., NULL, ...)is never TRUE for any row. Multi-valued operands on either side fold to NULL with the standard "single-value function encountered multi-value" warning, matching theInoperator's MV behavior. The filter path inherits these semantics fromIndirectly. The hash-join path needs extra work because its runtimeBlockHash(insideRowInTableLookupOperator) treatsnull = nullas a match, and a non-match always yields sentinel NULL (kept by ANTI). We close every gap as part of dedup canonicalization and join construction:- Right side: convert MV positions to NULL via
convertMvPositionsToNull(org.elasticsearch.compute.data.Block, org.elasticsearch.compute.data.BlockFactory)(mirrors the left-sideMvSingleValueOrNullguard), then hand the result to BlockHash. BlockHash collapses every NULL — original or MV-derived — into its reserved group 0, which surfaces as a single NULL position at index 0 of the dedup output. The post-dedup logic derivesrightHadNullsfrom that position and drives the short-circuit decisions:- SEMI:
x IN (a, b, NULL)≡x IN (a, b)onceWHEREdrops NULL results, so the filter path keeps the NULL literal (it's harmless) and the hash-join path strips it from theLocalRelation. - ANTI: any NULL on the right makes the predicate non-TRUE for every row, so we short-circuit to
Filter(FALSE)immediately after the dedup. - MARK: keeps
rightHadNullsonly as a hint for the hash-join path's CASE expression — the filter path uses the NULL position in the dedup output directly to driveIn's three-valued mark.
- SEMI:
- Left side (hash-join path only): insert
Eval(svKey = MvSingleValueOrNull(leftField))and (for SEMI/ANTI)Filter(IsNotNull(svKey))abovesubqueryJoin.left()so NULL-keyed and MV-keyed left rows can never reach the lookup. MARK keeps the SV guard but skips the IsNotNull filter so MV/NULL left rows survive into the output with the CASE-driven mark.
Page lifecycle: when a non-null
pageHolderis supplied, the source page held there is released eagerly (the dedup step copied every distinct value into a fresh, CB-tracked block), and the holder is either cleared (filter / empty / short-circuit paths) or swapped to point at the new dedup page (hash-join path) so the caller's existing cleanup releases the right page at end of plan execution.- Parameters:
blockFactory- CB-tracked factory used to allocate the dedup key column and sentinelpageHolder- optional holder that initially references the source page fromdata.supplier().get(); updated to reference the new dedup page (hash-join path) or cleared (filter / empty path) so the caller's existing cleanup releases the correct page. May benullfor callers that do not need this swap (e.g. unit tests).
- If the deduplicated key count is
-
firstSubPlan
public static AbstractSubqueryJoin.LogicalPlanTuple firstSubPlan(LogicalPlan optimizedPlan, Set<LocalRelation> subPlansResults) Finds the first subquery join in the plan whose right side has not yet been replaced with results. Unlike InlineJoin, the right side is an independent subquery that doesn't use StubRelation, no replaceStub is needed, deep copy is not required. -
newMainPlan
public static LogicalPlan newMainPlan(LogicalPlan optimizedPlan, AbstractSubqueryJoin.LogicalPlanTuple subPlans, LocalRelation resultWrapper, int hashJoinThreshold, BlockFactory blockFactory, AtomicReference<Page> pageHolder)
-