Skip to content

Commit d65de54

Browse files
committed
[SPARK-59289][SQL] Internal review
1 parent 0f7a0d8 commit d65de54

5 files changed

Lines changed: 111 additions & 127 deletions

File tree

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,8 @@ case class KeyLayout(
582582
*
583583
* - `keysSatisfy()`: do the keys as they stand co-locate every cluster key, with nothing left for
584584
* a node to project away? This is the strict question, and `satisfies()` is it plus `isGrouped`.
585+
* Strict for a `ClusteredDistribution`; its `OrderedDistribution` arm is a gate on what may claim
586+
* a global ordering, and the order itself is still the caller's to check.
585587
* - `keysCanSatisfy()`: `keysSatisfy()`, or the keys co-locate them after a node has projected
586588
* away the expressions that carry none. Only
587589
* `spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys` admits the second half.
@@ -818,22 +820,18 @@ case class KeyedPartitioning(
818820
/**
819821
* The counts `numPartitionsProjectedOn` has already worked out, by position set.
820822
*
821-
* Memoized here rather than at a caller because there is no one caller. `satisfies` asks this
822-
* question now, and `EnsureRequirements`, `ValidateRequirements` and every `AQEShuffleReadRule`
823-
* application all ask `satisfies` of the same partitioning, so a memo scoped to one rule pass
824-
* would miss most of the repeats. The partitioning is immutable and the answer depends only on
825-
* the keys and the position set, so the cache cannot go stale.
823+
* Kept on the partitioning rather than at a caller because there is no one caller: `satisfies`
824+
* asks this question, and the planner, `ValidateRequirements` and every `AQEShuffleReadRule`
825+
* application all ask `satisfies` of the same partitioning. The partitioning is immutable and the
826+
* answer depends only on the keys and the position set, so the cache cannot go stale.
826827
*
827828
* Not a constructor field, so it stays out of the product and leaves equality, `hashCode` and
828-
* canonicalization alone. The map is built on first use, and only for a partitioning something
829-
* actually asks a narrowing question of. On the default configuration nothing does, since
830-
* `mayProjectToClusterKeys` requires
829+
* canonicalization alone. The map is built on first use, and on the default configuration nothing
830+
* asks a narrowing question at all, since `mayProjectToClusterKeys` requires
831831
* `spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys`.
832832
*
833-
* [[TransientBestEffortLazyVal]] rather than a `lazy val`, for the reason that class exists: a
834-
* Scala 2 `lazy val` locks the instance to initialize it, and this is read from whichever thread
835-
* is planning or validating. Two threads racing here each build an empty map and one is dropped,
836-
* which is all the best-effort part costs.
833+
* [[TransientBestEffortLazyVal]] because a Scala 2 `lazy val` locks the instance to initialize
834+
* it, and this is read from whichever thread is planning or validating.
837835
*/
838836
private val projectedPartitionCounts =
839837
new TransientBestEffortLazyVal[ConcurrentHashMap[BitSet, java.lang.Integer]](
@@ -935,8 +933,8 @@ case class KeyedPartitioning(
935933
expressions.forall(_.references.size == 1)
936934

937935
/**
938-
* The strict question of the family the class doc lists. `true` only when the
939-
* keys as they stand co-locate every cluster key, with nothing left for a
936+
* The strict question of the family the class doc lists, for a [[ClusteredDistribution]]. `true`
937+
* only when the keys as they stand co-locate every cluster key, with nothing left for a
940938
* `GroupPartitionsExec` to do about it.
941939
*
942940
* Two ways to be true. Every partition expression is a function of cluster keys alone, so
@@ -945,6 +943,12 @@ case class KeyedPartitioning(
945943
* second is the only branch that reads a partition key, and only
946944
* `spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys` admits it, so the ordinary
947945
* configuration answers structurally.
946+
*
947+
* The strictness claim stops at [[ClusteredDistribution]]. The [[OrderedDistribution]] arm is a
948+
* local gate on what may claim a global ordering at all, and it does not read the key order, so a
949+
* side whose keys run the wrong way still answers `true` here. `EnsureRequirements.resolveChild`
950+
* is what compares the keys against the required ordering and builds the sorting node, and it has
951+
* to stay: nothing below tells it the keys are already in order.
948952
*/
949953
private def keysSatisfy(required: Distribution): Boolean = {
950954
required match {
@@ -1383,24 +1387,18 @@ case class PartitioningCollection(partitionings: Seq[Partitioning])
13831387
override def createShuffleSpec(distribution: ClusteredDistribution): ShuffleSpec = {
13841388
// `maySatisfyAfterProjection`, not `satisfies`. A spec says what its partitioning could
13851389
// co-partition on, and a grouped `KeyedPartitioning` whose keys are coarser than the
1386-
// operation's still can, through the projection a `GroupPartitionsExec` performs. The strict
1387-
// question would drop it, which is narrower than what this filter admitted before `satisfies`
1388-
// became strict.
1389-
//
1390-
// It is not wider either. `ValidateRequirements` builds a spec from a finished plan through
1391-
// here, and is the gate that makes AQE drop a rewrite that breaks co-partitioning, so this
1392-
// admits exactly what it admitted before and no more. `checkKeyedPartitioningInvariant` forces
1393-
// one `KeyLayout` on all keyed members, so `isGrouped` is uniform across them and the filter
1394-
// either keeps every keyed member or none: the latter only for a partitioning that does not
1395-
// serve the distribution through a keyed member at all, which neither caller passes.
1390+
// operation's still can, through the projection a `GroupPartitionsExec` performs. That is the
1391+
// admission set this filter had before `satisfies` became strict, up to one shape it now also
1392+
// keeps, a partition expression that *is* a cluster key, which `areKeysCompatible` turns away
1393+
// anyway. The set matters because `ValidateRequirements` builds a spec from a finished plan
1394+
// through here.
13961395
//
1397-
// Every admitted member has to stay, because `isCompatibleWith` answers for any of them and
1398-
// the collection cannot know which one the other side matched. That has a cost worth knowing:
1399-
// `KeyedShuffleSpec.canCreatePartitioning` is false without `v2BucketingShuffleEnabled` and
1400-
// `ShuffleSpecCollection.canCreatePartitioning` is a `forall`, so a groupable keyed member
1401-
// beside a usable non-keyed one would cost the collection its role as a shuffle template. No
1402-
// operator is known to report that mixture, since `EnsureRequirements` groups a keyed child
1403-
// before it can reach a join's output.
1396+
// Every admitted member stays, because `isCompatibleWith` answers for any of them and the
1397+
// collection cannot know which one the other side matched. The cost is that
1398+
// `ShuffleSpecCollection.canCreatePartitioning` is a `forall` over members whose keyed half is
1399+
// false without `v2BucketingShuffleEnabled`, so a groupable keyed member beside a usable
1400+
// non-keyed one would cost the collection its role as a shuffle template. No operator is known
1401+
// to report that mixture.
14041402
val filtered =
14051403
partitionings.filter(PartitioningCollection.maySatisfyAfterProjection(_, distribution))
14061404
ShuffleSpecCollection(filtered.map(_.createShuffleSpec(distribution)))
@@ -1446,18 +1444,15 @@ object PartitioningCollection {
14461444
* finished plan is checked against. `EnsureRequirements.createKeyedShuffleSpecs` is where the
14471445
* planner asks the wider question, for a child it is about to group itself.
14481446
*
1449-
* A required partition count still has to match, which is what `satisfies` applies to every other
1450-
* partitioning. A node changes the count, so the pre-grouping one is not a bound on what it will
1451-
* be. This clause is here for consistency with the strict question, not as a prediction.
1447+
* The partition count clause is here for consistency with the strict question. A node changes the
1448+
* count, so the pre-grouping one is no prediction of it.
14521449
*/
14531450
private[sql] def maySatisfyAfterProjection(
14541451
p: Partitioning,
1455-
required: ClusteredDistribution): Boolean = p match {
1452+
required: ClusteredDistribution): Boolean = flatten(p).exists {
14561453
case k: KeyedPartitioning =>
14571454
k.isGrouped && required.requiredNumPartitions.forall(_ == k.numPartitions) &&
14581455
k.keysMaySatisfy(required)
1459-
case pc: PartitioningCollection =>
1460-
pc.partitionings.exists(maySatisfyAfterProjection(_, required))
14611456
case other => other.satisfies(required)
14621457
}
14631458

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,28 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
6363
}
6464
}
6565

66-
test("SPARK-59289: createShuffleSpec drops a member that is not grouped") {
66+
test("SPARK-59289: createShuffleSpec drops a keyed member that is not grouped") {
6767
val a = AttributeReference("a", IntegerType)()
6868
val clustered = ClusteredDistribution(Seq(a))
6969
// A node would group this one too, but the admission set here is what a finished plan is
7070
// checked against by `ValidateRequirements`, so it stays what the strict question admitted
71-
// before a projection was allowed to answer it. Asked of the predicate rather than of a mixed
72-
// collection, since `checkKeyedPartitioningInvariant` forces one `KeyLayout` on all keyed
73-
// members and `isGrouped` is therefore uniform across them.
71+
// before a projection was allowed to answer it.
7472
val ungrouped = KeyedPartitioning(Seq(a), Seq(InternalRow(1), InternalRow(1), InternalRow(2)))
7573
val grouped = KeyedPartitioning(Seq(a), Seq(InternalRow(1), InternalRow(2), InternalRow(3)))
7674
assert(!ungrouped.isGrouped && grouped.isGrouped, "test setup")
7775

7876
assert(!PartitioningCollection.maySatisfyAfterProjection(ungrouped, clustered))
7977
assert(PartitioningCollection.maySatisfyAfterProjection(grouped, clustered))
78+
79+
// And through the filter itself, which is the predicate's one caller. The keyed member goes and
80+
// the hash member stays, so the collection keeps a spec and its `require` is not tripped. The
81+
// pairing is asked of a mixed collection rather than of two keyed members, because
82+
// `checkKeyedPartitioningInvariant` forces one `KeyLayout` on all keyed members and `isGrouped`
83+
// is therefore uniform across them: a collection cannot hold one of each.
84+
val mixed = PartitioningCollection(Seq(ungrouped, HashPartitioning(Seq(a), 3)))
85+
val specs = mixed.createShuffleSpec(clustered).asInstanceOf[ShuffleSpecCollection].specs
86+
assert(specs.size == 1, s"only the hash member survives the filter, got $specs")
87+
assert(specs.head.isInstanceOf[HashShuffleSpec], s"and it is the hash one, got $specs")
8088
}
8189

8290
protected def checkCompatible(

sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ import org.apache.spark.sql.vectorized.ColumnarBatch
5353
* the child still reports what it was planned over, see
5454
* `outputPartitioning`.
5555
* @param childPartitioning The child's partitioning this node was decided for. Kept so that a node
56-
* handed a different child can say so. A `Partitioning` that is an
57-
* `Expression` is in the case class product on purpose, so that
58-
* `QueryPlan.doCanonicalize` normalizes its exprIds along with
59-
* `plannedPartitioning`'s and the comparison stays meaningful.
56+
* handed a different child can say so, and in the case class product so
57+
* that `QueryPlan.doCanonicalize` normalizes its exprIds along with
58+
* `plannedPartitioning`'s, which is what keeps the comparison meaningful.
6059
* @param joinKeyPositions Optional projection selecting a subset of the partitioning key positions,
6160
* so that partitions sharing the projected key are coalesced. Used whenever
6261
* the cluster keys are a subset of the partition keys, either the join
@@ -103,17 +102,18 @@ case class GroupPartitionsExec(
103102
* node a child reporting something else invalidates both, so the keyed claim is dropped and only
104103
* the physical output count is reported. `ValidateRequirements` then finds the parent's
105104
* distribution unsatisfied and `AdaptiveSparkPlanExec` reverts the rewrite, which is what an
106-
* `AQEShuffleReadExec` landing over a keyed shuffle stage relies on.
105+
* `AQEShuffleReadExec` landing over a keyed shuffle stage relies on. That revert is also what
106+
* keeps `doExecute` off the changed child, since it still coalesces on the planning-time
107+
* `grouping.partitions`. The base relied on the same revert, having re-derived to get there.
107108
*
108-
* Re-deriving from the new child instead would be worse. The recipe was chosen for a pairing, and
109-
* applying it to a different child yields a claim nothing validated against the other side. It
110-
* would also buy nothing today: no rule in the tree is known to report a *different*
111-
* `KeyedPartitioning`, so the two reachable outcomes are an equal one, where this check passes,
112-
* and `UnknownPartitioning`, where re-deriving reports exactly what this reports.
109+
* Re-deriving instead would apply a recipe chosen for one pairing to a different child, yielding
110+
* a claim nothing validated against the other side. It would also buy nothing today: no rule is
111+
* known to report a *different* `KeyedPartitioning`, so the reachable outcomes are an equal one,
112+
* where this check passes, and `UnknownPartitioning`, where re-deriving reports the same thing.
113113
*
114-
* Asked on the read rather than in `withNewChildInternal` because plan canonicalization rebuilds
115-
* this node over a canonicalized child and never reads its partitioning, which it cannot: a
116-
* canonicalized `BatchScanExec` throws from `reportedKeyedPartitioning`.
114+
* Asked on the read rather than in `withNewChildInternal` because canonicalization rebuilds this
115+
* node over a canonicalized child whose partitioning cannot be read at all: a canonicalized
116+
* `BatchScanExec` throws from `reportedKeyedPartitioning`.
117117
*/
118118
@transient override lazy val outputPartitioning: Partitioning =
119119
if (child.outputPartitioning == childPartitioning) {
@@ -417,15 +417,11 @@ case class GroupPartitionsExec(
417417
* @param isIdentity whether the grouping leaves the declared keys and every partition where they
418418
* were. Nothing rewrote the keys, output partition i holds exactly input
419419
* partition i, and there is one output per input. That is the only grouping that
420-
* keeps a marked layout's undeclared rows at hash(key) % numPartitions. A
421-
* projection or reduction re-labels the groups into a different key space, so
422-
* even a grouping whose indices line up would pin the claim to keys it no longer
423-
* declares. A rewrite is rejected up front, covering a narrowing
424-
* projection, a reordering one, and any reducer slot. A reducer slot counts as
425-
* key-changing even though a conforming self-reducer cannot rewrite a reachable
426-
* key value, so the give-up there loses at most an optimization. A grouping that
427-
* drops trailing declared keys reads identity for every group it keeps, but the
428-
* partition count shrinks and the hash modulus with it.
420+
* keeps a marked layout's undeclared rows at hash(key) % numPartitions, so a
421+
* projection, a reduction or a dropped trailing key all fail it: each moves the
422+
* claim into a different key space or changes the modulus. A reducer slot counts
423+
* as key-changing even where a conforming self-reducer could not rewrite a
424+
* reachable key value, so the give-up there loses at most an optimization.
429425
* @param numPrunedPartitions the alignment's effect on the reads of the child's splits, 0 outside
430426
* the alignment path. See `alignToExpectedKeys`.
431427
* @param numReplicatedPartitionReads as above.
@@ -501,15 +497,16 @@ private[sql] object GroupPartitionsExec {
501497
// describes the keys, so the reduce marks it (see `KeyedShuffleSpec.reducersBothWays`).
502498
//
503499
// A marked claim pins undeclared rows to hash(key) % numPartitions (see [[KeyLayout]]'s
504-
// `mayContainUnknownPartitionKeys`). Only an identity grouping keeps that relationship: a
505-
// reorder, a coalesce, or a resize moves those rows, and a projection or reduction rewrites
506-
// the keys the claim speaks for (see `PartitionGrouping.isIdentity`). Clearing only the marker
507-
// would misreport the undeclared rows that remain, so give up the keyed partitioning at the
508-
// physical output count (one per group, padding included) that a parent's
509-
// `PartitioningCollection` requires for uniformity. The give-up deliberately under-reports.
510-
// The node still physically groups the partitions but no longer claims a keyed layout, so a
511-
// join planned over it does not see its required distribution satisfied. `EnsureRequirements`
512-
// asks this before it commits to a pairing and falls back to a shuffle when it happens.
500+
// `mayContainUnknownPartitionKeys`), and only an identity grouping keeps that relationship (see
501+
// `PartitionGrouping.isIdentity`). Clearing the marker alone would misreport the undeclared
502+
// rows that remain, so the keyed partitioning goes and only the physical output count is
503+
// reported, one per group with padding included, which is the count a parent's
504+
// `PartitioningCollection` needs for uniformity. `EnsureRequirements` asks this before it
505+
// commits to a pairing and falls back to a shuffle when it happens.
506+
//
507+
// The marker read off `p` is the same bit the `else` branch publishes through
508+
// `grouping.layout`, since `computeGrouping` copies the child's layout without touching it. A
509+
// change to that copy has to move this guard with it.
513510
if (PartitioningCollection.keyedMarkerOf(p).contains(true) && !grouping.isIdentity) {
514511
UnknownPartitioning(grouping.partitions.size)
515512
} else {

0 commit comments

Comments
 (0)