Repository navigation
fix: declare the input order of a native window over a sorted cache #7256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
68bff2e
eb0a999
8bc8908
94aaa19
16b1add
dc94ccf
9c5dcd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,7 @@ import org.apache.comet.{CometConf, ConfigEntry} | |
| import org.apache.comet.CometSparkSessionExtensions.withFallbackReason | ||
| import org.apache.comet.serde.{AggSerde, CometOperatorSerde, CometSortOrder, LiteralOuterClass, OperatorOuterClass} | ||
| import org.apache.comet.serde.OperatorOuterClass.Operator | ||
| import org.apache.comet.serde.QueryPlanSerde.{aggExprToProto, exprToProto, liftFallbackReasons, scalarFunctionExprToProto, serializeDataType} | ||
| import org.apache.comet.serde.QueryPlanSerde.{aggExprToProto, exprToProto, hasNonDefaultStringCollation, liftFallbackReasons, scalarFunctionExprToProto, serializeDataType} | ||
|
|
||
| object CometWindowExec extends CometOperatorSerde[WindowExec] { | ||
|
|
||
|
|
@@ -81,10 +81,37 @@ object CometWindowExec extends CometOperatorSerde[WindowExec] { | |
|
|
||
| if (windowExprProto.forall(_.isDefined) && partitionExprs.forall(_.isDefined) | ||
| && sortOrders.forall(_.isDefined)) { | ||
| // Spark plans no sort below the window when its input already reports the order the window | ||
| // needs, but rows from the JVM arrive through a ScanExec that declares none. Send that order | ||
| // for the planner to declare (see SortedInputExec), unless a native sort below declares it | ||
| // or it may be false. A key Comet cannot express, or a collated string key, sends the window | ||
| // back to Spark. | ||
| val requiredOrdering = op.requiredChildOrdering.head | ||
| val inputSortOrders = | ||
| if (requiredOrdering.nonEmpty && !op.child.isInstanceOf[CometSortExec] && | ||
| SortOrder.orderingSatisfies(op.child.outputOrdering, requiredOrdering) && | ||
| !CometHashJoinExec.mayReportFalseInputOrder(op.child)) { | ||
| val orders = requiredOrdering.map { order => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we preserve the usable part of the cache's ordering for the next window? On a single-partition cache sorted by
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, good catch. The lower window now sends the required keys and then the rest of the order its child reports, up to the first key that cannot be expressed. In your example it declares I extended the order at the lower window, not at the upper one. A key left out at the lower window is a collated string or cannot be expressed. The upper window would be checking the same attribute, so I expect it to fall back to Spark rather than fail natively.
|
||
| if (hasNonDefaultStringCollation(order.dataType)) None | ||
| else exprToProto(order, op.child.output) | ||
| } | ||
| if (!orders.forall(_.isDefined)) { | ||
| withFallbackReason( | ||
| op, | ||
| "the order of the window's input, which Spark relies on, cannot be expressed " + | ||
| "natively") | ||
| return None | ||
| } | ||
| orders.flatten | ||
| } else { | ||
| Nil | ||
| } | ||
|
|
||
| val windowBuilder = OperatorOuterClass.Window.newBuilder() | ||
| windowBuilder.addAllWindowExpr(windowExprProto.map(_.get).asJava) | ||
| windowBuilder.addAllPartitionByList(partitionExprs.map(_.get).asJava) | ||
| windowBuilder.addAllOrderByList(sortOrders.map(_.get).asJava) | ||
| windowBuilder.addAllInputSortOrders(inputSortOrders.asJava) | ||
| Some(builder.setWindow(windowBuilder).build()) | ||
| } else { | ||
| // Whichever of the window / partition / order expressions failed has already recorded its | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this also declare the order for an empty bucket? I tried a Parquet table written by one writer with
bucketBy(8, "k").sortBy("k", "v"), two distinctkvalues, and legacy bucketed scan output ordering enabled. The head plansCometWindowExecdirectly overCometNativeScanfordense_rank() OVER (PARTITION BY k ORDER BY v)and sends both sort keys. Six of the eight tasks have no files.build_parquet_scan_plangives those tasks anEmptyExec, whichkeeps_source_orderrejects. DataFusion's bounded window checks its partition ordering before polling the empty stream, so one empty bucket is enough to fail the query. Vanilla Spark returns all 16 rows. An empty input satisfies any ordering. Could we handle that case and cover a table with unpopulated buckets?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, an empty input satisfies any order.
keeps_source_ordernow acceptsEmptyExec, so an empty bucket gets the same declaration as a bucket with a file. Fixed in 16b1add.declared_order_reaches_an_empty_bucketinplanner.rschecks the declaration over anEmptyExec.window over a sorted bucketed table with empty buckets runs nativelyinCometWindowExecSuitewrites your table shape: one writer,bucketBy(8, "k").sortBy("k", "v")and two keys. It reads the table with the legacy bucketed scan ordering on, then checksdense_rank() OVER (PARTITION BY k ORDER BY v)against Spark. It also checks that the window sits directly on the bucketedCometNativeScanExecand that no sort is planned. The hash join's probe side uses the same walk. An empty probe input gets the declaration there too, which changes nothing for it.