andygrove commented on code in PR #6835:
URL: https://github.com/apache/datafusion-comet/pull/6835#discussion_r4252061051
##########
spark/src/main/scala/org/apache/spark/sql/comet/operators.scala:
##########
@@ -2876,12 +2921,44 @@ trait CometHashJoin {
else OperatorOuterClass.BuildSide.BuildRight)
.setNullAwareAntiJoin(isNullAwareAntiJoin)
.setDynamicFilterEnabled(CometConf.COMET_EXEC_JOIN_DYNAMIC_FILTER_ENABLED.get(join.conf))
+ .addAllStreamedSortOrders(streamedSortOrders.asJava)
condition.foreach(joinBuilder.setCondition)
Some(builder.setHashJoin(joinBuilder).build())
} else {
None
}
}
+
+ // Whether a Comet join that may move its streamed rows, while reporting
Spark's order for
+ // them, feeds the order that `plan` reports with no sort in between.
Whether an outer join
+ // declared the order on its probe input is up to the native planner, so
every one built that
+ // way counts. A cache reports the order of the plan that wrote it, so the
walk goes on into
+ // that plan. While CometExecRule converts a plan, a native plan's JVM
inputs sit behind a
+ // CometScanWrapper, a leaf, so the walk looks through that too.
+ private def mayReportFalseOrder(plan: SparkPlan): Boolean = plan match {
+ case _ if plan.outputOrdering.isEmpty => false
+ case _: SortExec | _: CometSortExec => false
+ case join: CometBroadcastHashJoinExec
+ if join.isNullAwareAntiJoin || runsAsRightOuterJoin(join.joinType,
join.buildSide) =>
+ true
+ case join: CometHashJoinExec if runsAsRightOuterJoin(join.joinType,
join.buildSide) => true
+ case _: CometBroadcastNestedLoopJoinExec => true
+ case wrapper: CometScanWrapper => mayReportFalseOrder(wrapper.originalPlan)
+ case scan: CometInMemoryTableScanExec =>
+ mayReportFalseOrder(scan.originalPlan.relation.cachedPlan)
+ case scan: InMemoryTableScanExec =>
mayReportFalseOrder(scan.relation.cachedPlan)
+ case stage: QueryStageExec => mayReportFalseOrder(stage.plan)
+ case _ => plan.children.exists(mayReportFalseOrder)
Review Comment:
Right, a checkpoint keeps `executedPlan.outputOrdering` in its `LogicalRDD`,
so the plan that wrote it is gone and the walk could not see a NOT IN or an
undeclared outer join behind it. I took your second suggestion: the walk now
trusts a leaf that reports an order only if it is a file or DSv2 scan, a range
or a Comet native leaf, and treats any other as unknown. The NOT IN test with
`localCheckpoint()` in place of `.cache()` returns 83 groups for 37 keys
without that. Fixed in 79b4d336d0.
##########
native/core/src/execution/planner.rs:
##########
@@ -2937,6 +2951,54 @@ impl PhysicalPlanner {
Ok(Arc::new(prepared))
}
+ /// `plan`, declaring `sort_orders` as the order its rows arrive in,
unless it already
+ /// declares one. Spark's plan knows the order of an input that enters the
native plan from
+ /// the JVM, but the `ScanExec` it arrives through declares none. Only
that order is declared:
+ /// when an operator in between may have moved rows, `plan` is left as it
is.
+ fn with_declared_order(
+ &self,
+ plan: Arc<dyn ExecutionPlan>,
+ sort_orders: &[Expr],
+ ) -> Result<Arc<dyn ExecutionPlan>, ExecutionError> {
+ if plan.output_ordering().is_some() ||
!Self::keeps_jvm_input_order(&plan) {
+ return Ok(plan);
+ }
+ let exprs = sort_orders
+ .iter()
+ .map(|expr| self.create_sort_expr(expr, plan.schema()))
+ .collect::<Result<Vec<_>, _>>()?;
+ match LexOrdering::new(exprs) {
+ Some(ordering) => Ok(Arc::new(SortedInputExec::new(plan,
ordering))),
+ None => Ok(plan),
+ }
+ }
+
+ /// Whether `plan`'s rows come from an input outside the native plan
through operators that
+ /// keep their input's order, so that the order Spark reports for them
still holds. Any other
+ /// operator may have moved rows, such as the null-aware anti join, whose
streamed side Comet
+ /// builds on, and declaring Spark's order over it would be wrong.
+ fn keeps_jvm_input_order(plan: &Arc<dyn ExecutionPlan>) -> bool {
+ if plan.is::<ScanExec>() || plan.is::<ShuffleScanExec>() {
+ return true;
+ }
+ // DataFusion reports every right hash join as keeping its probe
order, but keeps it only
+ // when the probe declares one, and no input on this path declares an
order.
+ if let Some(join) = plan.downcast_ref::<HashJoinExec>() {
+ if join.join_type() != &DFJoinType::Inner {
Review Comment:
Yes. In DataFusion 55.2 `adjust_indices_by_join_type` passes
`preserve_order_for_right` only for `Right`, `get_semi_indices` and
`get_anti_indices` assert ascending input, and `get_mark_indices` keeps the
whole range, so `keeps_jvm_input_order` now walks through Inner, RightSemi,
RightAnti and RightMark and refuses the rest. The planner test covers each
type, and a Comet test puts `k IN (SELECT ...)` under the outer join: it
declares the order and the partitions stay sorted. The undeclared outer join
test wrote its out-of-order cache through the IN subquery, so it now writes it
through a native sample, which the planner declares nothing through. Fixed in
79b4d336d0.
##########
spark/src/main/scala/org/apache/spark/sql/comet/operators.scala:
##########
@@ -2876,12 +2921,44 @@ trait CometHashJoin {
else OperatorOuterClass.BuildSide.BuildRight)
.setNullAwareAntiJoin(isNullAwareAntiJoin)
.setDynamicFilterEnabled(CometConf.COMET_EXEC_JOIN_DYNAMIC_FILTER_ENABLED.get(join.conf))
+ .addAllStreamedSortOrders(streamedSortOrders.asJava)
condition.foreach(joinBuilder.setCondition)
Some(builder.setHashJoin(joinBuilder).build())
} else {
None
}
}
+
+ // Whether a Comet join that may move its streamed rows, while reporting
Spark's order for
+ // them, feeds the order that `plan` reports with no sort in between.
Whether an outer join
+ // declared the order on its probe input is up to the native planner, so
every one built that
+ // way counts. A cache reports the order of the plan that wrote it, so the
walk goes on into
+ // that plan. While CometExecRule converts a plan, a native plan's JVM
inputs sit behind a
+ // CometScanWrapper, a leaf, so the walk looks through that too.
+ private def mayReportFalseOrder(plan: SparkPlan): Boolean = plan match {
Review Comment:
Narrowed. The walk follows only the streamed side of a hash join, and an
outer join in a cache now counts as declared when it sent its order and its
probe input is a scan in its own plan, which is the one case where the planner
is sure to have declared it. A new test caches a sorted outer join and the join
over that cache declares the order. A build side behind an exchange reports no
order, so your NOT IN case only shows when the inner join's build side is a
cache with an order of its own. The new test uses two caches partitioned on the
key and fails against a walk of both children. Two shapes stay conservative, an
outer join in a cache whose probe is a native operator over the scan and an RDD
checkpoint, and I listed them in #6922 and linked it from the description.
Fixed in 79b4d336d0.
##########
spark/src/test/scala/org/apache/comet/exec/CometInMemoryCacheSuite.scala:
##########
@@ -3288,4 +3289,154 @@ class CometInMemoryCacheSuite extends CometTestBase {
}.nonEmpty)
}
}
+
+ test("hash joins over a sorted cache keep the streamed side's order") {
Review Comment:
Added `a broadcast outer hash join over a sorted cache keeps its order with
default settings` to `CometInMemoryCacheSuite`: AQE on, only the native cache
flag set, a cache with no exchange in its plan, and the broadcast chosen by
size. The cached NOT IN tests run with AQE off and on now too, in
`CometJoinSuite` for Spark's scan and in `CometInMemoryCacheSuite` for the
native one. Fixed in 79b4d336d0.
##########
spark/src/main/scala/org/apache/spark/sql/comet/operators.scala:
##########
@@ -2876,12 +2921,44 @@ trait CometHashJoin {
else OperatorOuterClass.BuildSide.BuildRight)
.setNullAwareAntiJoin(isNullAwareAntiJoin)
.setDynamicFilterEnabled(CometConf.COMET_EXEC_JOIN_DYNAMIC_FILTER_ENABLED.get(join.conf))
+ .addAllStreamedSortOrders(streamedSortOrders.asJava)
condition.foreach(joinBuilder.setCondition)
Some(builder.setHashJoin(joinBuilder).build())
} else {
None
}
}
+
+ // Whether a Comet join that may move its streamed rows, while reporting
Spark's order for
+ // them, feeds the order that `plan` reports with no sort in between.
Whether an outer join
+ // declared the order on its probe input is up to the native planner, so
every one built that
+ // way counts. A cache reports the order of the plan that wrote it, so the
walk goes on into
+ // that plan. While CometExecRule converts a plan, a native plan's JVM
inputs sit behind a
+ // CometScanWrapper, a leaf, so the walk looks through that too.
+ private def mayReportFalseOrder(plan: SparkPlan): Boolean = plan match {
+ case _ if plan.outputOrdering.isEmpty => false
+ case _: SortExec | _: CometSortExec => false
+ case join: CometBroadcastHashJoinExec
+ if join.isNullAwareAntiJoin || runsAsRightOuterJoin(join.joinType,
join.buildSide) =>
+ true
+ case join: CometHashJoinExec if runsAsRightOuterJoin(join.joinType,
join.buildSide) => true
+ case _: CometBroadcastNestedLoopJoinExec => true
+ case wrapper: CometScanWrapper => mayReportFalseOrder(wrapper.originalPlan)
+ case scan: CometInMemoryTableScanExec =>
+ mayReportFalseOrder(scan.originalPlan.relation.cachedPlan)
Review Comment:
Confirmed, thanks. `InMemoryTableScanExec` reads `adaptive.executedPlan`
once the plan is final, and the adaptive plan reports no order itself, so the
walk stopped at it. `mayReportFalseOrder` now reads a final adaptive plan
before it looks at the order, as the scan does. The cached NOT IN tests run
with AQE off and on, and without the change the AQE-on run returns 83 groups
for 37 keys. Fixed in 79b4d336d0.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]