dongjoon-hyun commented on code in PR #58523:
URL: https://github.com/apache/spark/pull/58523#discussion_r3935169708
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -880,9 +880,13 @@ object KeyedPartitioning {
* `PartitioningCollection`, whose invariant requires equal partition keys
-- but join types that
* expose only one side's partitioning (e.g. LEFT OUTER) run nothing that
compares the two
* orders, and silently return wrong results.
+ *
+ * It is the keys' own ordering, the one
`InternalRowComparableWrapper.equals` compares with, so
+ * one definition answers both. `EnsureRequirements`' `OrderedDistribution`
arm is the one place
+ * that lays grouped keys out in another order, the distribution's own.
*/
def groupedKeyRowOrdering(dataTypes: Seq[DataType]): BaseOrdering =
- RowOrdering.createNaturalAscendingOrdering(dataTypes)
+ InternalRowComparableWrapper.orderingFor(dataTypes)
Review Comment:
The caveat in the description is accurate, and I checked that nothing in the
current suites hits it: every `SharedSparkSession` suite runs under
`CODEGEN_ONLY` with fallback off, `NO_CODEGEN` appears only inside
`withSQLConf` blocks that never build a `KeyedPartitioning`, and no other
construction-time conf affects comparison results (collation is already part of
`StringType`). Two things it does change for these four sites, which used to
re-read the mode per call: the first loader in the JVM decides codegen vs
interpreted for the rest of the JVM's life, and in `FALLBACK` mode a transient
codegen failure pins the interpreted instance permanently. If you'd rather keep
per-mode fidelity, keying the cache on `(SQLConf.get.codegenFactoryMode,
dataTypes)` in the load path is cheap and keeps the identity the new test
asserts within any one mode. Accepting the caveat as written is also defensible.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/InternalRowComparableWrapper.scala:
##########
@@ -115,6 +115,9 @@ object InternalRowComparableWrapper {
new InternalRowComparableWrapper(partitionRow,
partitionExpression.map(_.dataType))
}
+ /** The cached ordering a wrapper of these `dataTypes` compares its rows
with in `equals`. */
+ def orderingFor(dataTypes: Seq[DataType]): BaseOrdering =
orderingCache.get(dataTypes)
Review Comment:
One pre-existing property of this cache that the PR extends to the
sort/group sites: the key is `Seq[DataType]`, but
`PythonUserDefinedType.equals`/`hashCode` compare only `pyUDT` and ignore
`sqlType` (`sql/api/.../UserDefinedType.scala`). `supportsExpressions` does not
gate UDTs out and `OrderUtils.isOrderable` accepts them via `sqlType`, so two
Python UDTs with the same class name but different `sqlType` (e.g. two Connect
clients on different versions of the same class) share one entry, and the
second gets an ordering built for the first's `sqlType`. Before this PR the
sort sites built a fresh ordering from the exact types passed; now they can get
the mismatched one. Very much an edge case and the root cause is UDT equality,
not the cache, but worth knowing that `groupedKeyRowOrdering` now inherits it.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2ScanExecBase.scala:
##########
@@ -92,8 +92,7 @@ trait DataSourceV2ScanExecBase
keyGroupedPartitioning match {
case Some(exprs) if conf.v2BucketingEnabled &&
KeyedPartitioning.supportsExpressions(exprs) &&
inputPartitions.nonEmpty &&
inputPartitions.forall(_.isInstanceOf[HasPartitionKey]) =>
- val dataTypes = exprs.map(_.dataType)
- val rowOrdering = RowOrdering.createNaturalAscendingOrdering(dataTypes)
+ val rowOrdering =
KeyedPartitioning.groupedKeyRowOrdering(exprs.map(_.dataType))
val partitionKeys =
inputPartitions.map(_.asInstanceOf[HasPartitionKey].partitionKey()).sorted(rowOrdering)
KeyedPartitioning(exprs, partitionKeys)
Review Comment:
Minor: `KeyedPartitioning.apply` recomputes `expressions.map(_.dataType)`
and its factory does a second `orderingCache.get` for the same key, so per call
this path builds the type list twice and takes the key lock three times. Since
`apply` does not need pre-sorted input (`isGrouped` is order-independent),
building the partitioning first and sorting with its own `keyOrdering`, the way
`PushDownUtils` already does with `keyRowOrdering`, would drop the duplicate.
Micro-level next to the sort itself, so only if you are touching this anyway.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2ScanExecBase.scala:
##########
@@ -92,8 +92,7 @@ trait DataSourceV2ScanExecBase
keyGroupedPartitioning match {
Review Comment:
The 27-calls-per-scan count that motivates this PR comes from
`outputPartitioning` being a `def`. After this change each call still maps
every input partition to its key, sorts them, and then
`KeyedPartitioning.apply` wraps each key and runs a Murmur3-hashed `.distinct`,
so for scans with hundreds or thousands of partitions the per-call work is
still well above the ~100us removed here. Making it `@transient override lazy
val outputPartitioning` looks safe: all four concrete scans define
`inputPartitions` as a `lazy val`, `keyGroupedPartitioning` is a constructor
parameter, `doCanonicalize` goes through `copy`, and
`BatchScanExec.filteredPartitions` consumes the pre-filter partitioning, which
is exactly the cached value. `FileSourceScanExec` already does this (`lazy val
(outputPartitioning, outputOrdering)`). Fine as a follow-up if you'd rather
keep this PR minimal, but it would subsume most of the benefit claimed here.
--
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]