-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59057][SQL] Make KeyedPartitioning.isNarrowed mean actual key collapse, and rename it to isCollapsed #58351
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: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,13 +68,20 @@ case class GroupPartitionsExec( | |
| child.outputPartitioning match { | ||
| case p: Partitioning with Expression => | ||
| // There can be multiple `KeyedPartitioning`s in an output partitioning of a join, but they | ||
| // can only differ in `expressions`; their `partitionKeys` reference is shared (enforced by | ||
| // `PartitioningCollection`), so `groupedPartitions` is computed only once. | ||
| // can only differ in `expressions`; their `partitionKeys` reference and `isCollapsed` flag | ||
| // are shared (enforced by `PartitioningCollection`), so both `groupedPartitions` and the | ||
| // new flag are computed once, outside the transform. | ||
| val partitionKeys = groupedPartitions.map(_._1) | ||
| // `isCollapsed` is sticky: grouping removes the duplicate keys, but it does not make this | ||
| // partitioning any finer than the layout it came from. Projecting onto the join key | ||
| // positions, or reducing the keys onto a coarser transform, can collapse keys in its own | ||
| // right, which `collapsesKeys` answers exactly -- it is computed with the grouping itself, | ||
| // so it costs nothing per call and needs no comparison of key counts. | ||
| val isCollapsed = childIsCollapsed || collapsesKeys | ||
| p.transform { | ||
| case k: KeyedPartitioning => | ||
| val projectedExpressions = joinKeyPositions.fold(k.expressions)(_.map(k.expressions)) | ||
| KeyedPartitioning(projectedExpressions, partitionKeys, isGrouped = isGrouped) | ||
| KeyedPartitioning(projectedExpressions, partitionKeys, isGrouped, isCollapsed) | ||
| }.asInstanceOf[Partitioning] | ||
| case o => o | ||
| } | ||
|
|
@@ -112,26 +119,32 @@ case class GroupPartitionsExec( | |
| keyMap.toSeq.sorted(keyOrdering.on((t: (InternalRowComparableWrapper, _)) => t._1.row)) | ||
| } | ||
|
|
||
| // There must be a `KeyedPartitioning` in the child's output partitioning, as a | ||
| // `GroupPartitionsExec` node is added to a plan only in that case. A collection's members share | ||
| // their `partitionKeys` reference and their `isCollapsed` flag, so any one of them will do. | ||
| @transient private lazy val childKeyedPartitioning: KeyedPartitioning = | ||
| child.outputPartitioning | ||
| .asInstanceOf[Partitioning with Expression] | ||
| .collectFirst { case k: KeyedPartitioning => k } | ||
| .getOrElse( | ||
| throw new SparkException("GroupPartitionsExec requires a child with KeyedPartitioning")) | ||
|
|
||
| /** | ||
| * Computes the grouped partitions by: | ||
| * 1. Projecting partition keys if joinKeyPositions is specified | ||
| * 2. Reducing keys if reducers are specified | ||
| * 3. Grouping input partition indices by their (possibly projected/reduced) keys | ||
| * 4. Sorting or distributing based on whether partial clustering is enabled | ||
| * | ||
| * Returns a tuple of (partitions, isGrouped) where: | ||
| * Returns a tuple of (partitions, isGrouped, collapsesKeys) where: | ||
| * - partitions: sequence of (partitionKey, inputPartitionIndices) pairs representing | ||
| * how input partitions should be grouped together | ||
| * - isGrouped: whether the output partitioning is grouped (no duplicates in partition keys) | ||
| * - collapsesKeys: whether any group this node outputs covers more than one of the child's own | ||
| * partition keys, i.e. whether the projection or the reduction actually merged keys | ||
| */ | ||
| @transient private lazy val groupedPartitionsTuple = { | ||
| // There must be a `KeyedPartitioning` in child's output partitioning as a | ||
| // `GroupPartitionsExec` node is added to a plan only in that case. | ||
| val keyedPartitioning = child.outputPartitioning | ||
| .asInstanceOf[Partitioning with Expression] | ||
| .collectFirst { case k: KeyedPartitioning => k } | ||
| .getOrElse( | ||
| throw new SparkException("GroupPartitionsExec requires a child with KeyedPartitioning")) | ||
| val keyedPartitioning = childKeyedPartitioning | ||
|
|
||
| // Project partition keys if join key positions are specified | ||
| val (projectedDataTypes, projectedKeys) = | ||
|
|
@@ -145,10 +158,26 @@ case class GroupPartitionsExec( | |
|
|
||
| val keyToPartitionIndices = reducedKeys.zipWithIndex.groupMap(_._1)(_._2) | ||
|
|
||
| // Whether this node collapses keys: does any key it keeps stand for more than one of the | ||
| // child's own partition keys? Counting the child's *keys* rather than its partitions is what | ||
| // tells a collapse from a source reporting several splits per key, and asking it of the keys | ||
| // this node keeps is what tells it from `alignToExpectedKeys` dropping keys, which merges | ||
| // nothing. Ask the key groups, not the partitions finally emitted: `distributePartitions` | ||
| // spreads a group's splits over one partition each, which would hide the merge, and | ||
| // replication would ask about the same group repeatedly. | ||
| val childKeys = keyedPartitioning.partitionKeys.toIndexedSeq | ||
| def coversSeveralChildKeys(indices: Seq[Int]): Boolean = | ||
| indices.map(childKeys).distinct.size > 1 | ||
|
|
||
| if (expectedPartitionKeys.isDefined) { | ||
| alignToExpectedKeys(keyToPartitionIndices) | ||
| val (alignedPartitions, grouped) = alignToExpectedKeys(keyToPartitionIndices) | ||
| val keptGroups = expectedPartitionKeys.get.map { case (key, _) => | ||
|
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. Nit, non-blocking: this re-derives the kept groups with its own |
||
| keyToPartitionIndices.getOrElse(key, Seq.empty) | ||
| } | ||
| (alignedPartitions, grouped, keptGroups.exists(coversSeveralChildKeys)) | ||
| } else { | ||
| (groupAndSortByKeys(keyToPartitionIndices, reducedDataTypes), true) | ||
| val sorted = groupAndSortByKeys(keyToPartitionIndices, reducedDataTypes) | ||
| (sorted, true, sorted.map(_._2).exists(coversSeveralChildKeys)) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -157,6 +186,12 @@ case class GroupPartitionsExec( | |
|
|
||
| @transient lazy val isGrouped: Boolean = groupedPartitionsTuple._2 | ||
|
|
||
| /** Whether the grouping this node performs merges keys the child held apart. */ | ||
| @transient private lazy val collapsesKeys: Boolean = groupedPartitionsTuple._3 | ||
|
|
||
| /** The child's own flag, from the same `KeyedPartitioning` the grouping was computed from. */ | ||
| @transient private lazy val childIsCollapsed: Boolean = childKeyedPartitioning.isCollapsed | ||
|
|
||
| @transient private lazy val hasCoalescing: Boolean = groupedPartitions.exists(_._2.size > 1) | ||
|
|
||
| // Whether the child subtree is safe to use with SortedMergeCoalescedRDD (k-way merge). | ||
|
|
||
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.
This
inherited || projected distinct < source distinctpredicate is now hand-rolled at three producer sites: here,KeyedPartitioning.createShuffleSpec, andGroupPartitionsExec.outputPartitioning, each with site-specific inputs and caveats. Given the PR's own observation that one producer laundering the flag is how the protection went missing, a shared helper onKeyedPartitioning, e.g.would keep the correctness rule in one place instead of three files that must stay in sync.
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.
Added, as
KeyedPartitioning.collapsesOnProjection. One correction to the shape you sketched: theisCollapsed ||disjunct would be dead at both call sites, because each already carries the inherited flag outside it -- andAliasAwareOutputExpressionhas to, since it reads the flag from every input rather than only from the one whose keys it counts. So the helper is the count comparison alone, and its scaladoc says that the callers own the inherited term.GroupPartitionsExecdeliberately does not use it: it can answer the question exactly from the key groups it keeps, and the scaladoc points at that as the reference definition.