ulysses-you commented on code in PR #58591:
URL: https://github.com/apache/spark/pull/58591#discussion_r3956401302
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -508,6 +523,281 @@ class GroupPartitionsExecSuite extends SharedSparkSession
{
assert(gpe.tryEnableSortedMerge().isEmpty)
}
}
+
+ test("SPARK-59310: basic counts without alignment") {
+ // Keys [1, 2, 1]: 3 input splits, key 1 coalesces partitions 0 and 2, 2
output partitions.
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(1))))
+ val gpe = GroupPartitionsExec(child)
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 0)
+ assert(gpe.metrics("numCoalescedPartitions").value === 1)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 2)
+ // Without expectedPartitionKeys there is no alignment, so its metrics
stay unregistered.
+ assert(!gpe.metrics.contains("numPrunedPartitions"))
+ assert(!gpe.metrics.contains("numReplicatedPartitions"))
+ }
+
+ test("SPARK-59310: zero coalesced without duplicate keys") {
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(3))))
+ val gpe = GroupPartitionsExec(child)
+ gpe.execute()
+
+ assert(gpe.metrics("numCoalescedPartitions").value === 0)
+ assert(gpe.metrics("numPartitions").value === 3)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 1)
+ }
+
+ test("SPARK-59310: distribute alignment pads and never replicates") {
+ // Child splits: key 1 -> [0], key 2 -> [1, 2]. Expected: key 1 x1, key 2
x3, key 3 x1.
+ // Key 2 spreads its 2 splits over 3 expected partitions (one empty pad)
and key 3 has no
+ // split (one more empty), so 5 output partitions with 2 empty and nothing
coalesced.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(2))))
+ val gpe = GroupPartitionsExec(child,
+ expectedPartitionKeys = Some(Seq(keyOf(1) -> 1, keyOf(2) -> 3, keyOf(3)
-> 1)),
+ distributePartitions = true)
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 5)
+ assert(gpe.metrics("numEmptyPartitions").value === 2)
+ assert(gpe.metrics("numPrunedPartitions").value === 0)
+ assert(gpe.metrics("numCoalescedPartitions").value === 0, "distribute
never coalesces")
+ assert(!gpe.metrics.contains("numReplicatedPartitions"), "distribute never
replicates")
+ }
+
+ test("SPARK-59310: alignment prunes unmatched keys, pads missing ones") {
+ // The expected keys carry key 1 and a key-3 slot the child does not hold,
as an inner
+ // join's intersection combined with the other side's layout would. The 2
splits of key 2
+ // cannot produce join output and never enter the alignment; the missing
key 3 pads two
+ // empty output partitions, and being empty, replicates nothing despite
its 2 slots.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(2))))
+ val gpe = GroupPartitionsExec(child,
+ expectedPartitionKeys = Some(Seq(keyOf(1) -> 1, keyOf(3) -> 2)))
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 3)
+ assert(gpe.metrics("numPrunedPartitions").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 2)
+ assert(gpe.metrics("numCoalescedPartitions").value === 0)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 1)
+ assert(gpe.metrics("numReplicatedPartitions").value === 0,
+ "empty groups replicate nothing, and the single-split key 1 has no copy")
+ }
+
+ test("SPARK-59310: replicate alignment counts the reads beyond the first") {
+ // The other join side expects 2 partitions for key 1, so this side's
splits for the key are
+ // replicated to both: the slot beyond the first re-reads both splits, 2
extra input
+ // partition reads. Each output partition also coalesces the 2 splits of
the key.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(1))))
+ val gpe = GroupPartitionsExec(child, expectedPartitionKeys =
Some(Seq(keyOf(1) -> 2)))
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 2)
+ assert(gpe.metrics("numPartitions").value === 2)
+ assert(gpe.metrics("numReplicatedPartitions").value === 2)
+ assert(gpe.metrics("numCoalescedPartitions").value === 2, "both copies
merge the 2 splits")
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 0)
+ assert(gpe.metrics("numPrunedPartitions").value === 0)
+ }
+
+ test("SPARK-59310: an inner join intersection prunes both sides") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 'aa', 41.0, cast('2020-01-15' as timestamp)), " +
+ s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+ s"(2, 'bb', 10.5, cast('2020-01-01' as timestamp)), " +
+ s"(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ s"(1, 42.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 44.0, cast('2020-01-15' as timestamp)), " +
+ s"(1, 45.0, cast('2020-01-15' as timestamp)), " +
+ s"(2, 11.0, cast('2020-01-01' as timestamp)), " +
+ s"(4, 19.5, cast('2020-02-01' as timestamp))")
+
+ // The sides hold different keys ({1,2,3} vs {1,2,4}), so grouping
alone cannot align
+ // them and the join pushes the inner intersection {1,2} down as the
expected keys.
+ // Each side then coalesces its duplicate-key splits (items merges two
groups of two,
+ // purchases one group of three) and prunes the one split of its
unmatched key
+ // (items key 3, purchases key 4); nothing is empty or replicated.
+ // Both AQE arms: the query has no shuffle, so the executed nodes and
their accumulator
+ // ids are the same with and without AQE, and the reporting chain must
work in both.
Review Comment:
Fixed the comment: each arm runs its own query and reads its own execution's
plan graph and metric values (a9a920e489a).
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -508,6 +523,281 @@ class GroupPartitionsExecSuite extends SharedSparkSession
{
assert(gpe.tryEnableSortedMerge().isEmpty)
}
}
+
+ test("SPARK-59310: basic counts without alignment") {
+ // Keys [1, 2, 1]: 3 input splits, key 1 coalesces partitions 0 and 2, 2
output partitions.
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(1))))
+ val gpe = GroupPartitionsExec(child)
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 0)
+ assert(gpe.metrics("numCoalescedPartitions").value === 1)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 2)
+ // Without expectedPartitionKeys there is no alignment, so its metrics
stay unregistered.
+ assert(!gpe.metrics.contains("numPrunedPartitions"))
+ assert(!gpe.metrics.contains("numReplicatedPartitions"))
+ }
+
+ test("SPARK-59310: zero coalesced without duplicate keys") {
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(3))))
+ val gpe = GroupPartitionsExec(child)
+ gpe.execute()
+
+ assert(gpe.metrics("numCoalescedPartitions").value === 0)
+ assert(gpe.metrics("numPartitions").value === 3)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 1)
+ }
+
+ test("SPARK-59310: distribute alignment pads and never replicates") {
+ // Child splits: key 1 -> [0], key 2 -> [1, 2]. Expected: key 1 x1, key 2
x3, key 3 x1.
+ // Key 2 spreads its 2 splits over 3 expected partitions (one empty pad)
and key 3 has no
+ // split (one more empty), so 5 output partitions with 2 empty and nothing
coalesced.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(2))))
+ val gpe = GroupPartitionsExec(child,
+ expectedPartitionKeys = Some(Seq(keyOf(1) -> 1, keyOf(2) -> 3, keyOf(3)
-> 1)),
+ distributePartitions = true)
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 5)
+ assert(gpe.metrics("numEmptyPartitions").value === 2)
+ assert(gpe.metrics("numPrunedPartitions").value === 0)
+ assert(gpe.metrics("numCoalescedPartitions").value === 0, "distribute
never coalesces")
+ assert(!gpe.metrics.contains("numReplicatedPartitions"), "distribute never
replicates")
+ }
+
+ test("SPARK-59310: alignment prunes unmatched keys, pads missing ones") {
+ // The expected keys carry key 1 and a key-3 slot the child does not hold,
as an inner
+ // join's intersection combined with the other side's layout would. The 2
splits of key 2
+ // cannot produce join output and never enter the alignment; the missing
key 3 pads two
+ // empty output partitions, and being empty, replicates nothing despite
its 2 slots.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(2))))
+ val gpe = GroupPartitionsExec(child,
+ expectedPartitionKeys = Some(Seq(keyOf(1) -> 1, keyOf(3) -> 2)))
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 3)
+ assert(gpe.metrics("numPrunedPartitions").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 2)
+ assert(gpe.metrics("numCoalescedPartitions").value === 0)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 1)
+ assert(gpe.metrics("numReplicatedPartitions").value === 0,
+ "empty groups replicate nothing, and the single-split key 1 has no copy")
+ }
+
+ test("SPARK-59310: replicate alignment counts the reads beyond the first") {
+ // The other join side expects 2 partitions for key 1, so this side's
splits for the key are
+ // replicated to both: the slot beyond the first re-reads both splits, 2
extra input
+ // partition reads. Each output partition also coalesces the 2 splits of
the key.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(1))))
+ val gpe = GroupPartitionsExec(child, expectedPartitionKeys =
Some(Seq(keyOf(1) -> 2)))
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 2)
+ assert(gpe.metrics("numPartitions").value === 2)
+ assert(gpe.metrics("numReplicatedPartitions").value === 2)
+ assert(gpe.metrics("numCoalescedPartitions").value === 2, "both copies
merge the 2 splits")
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 0)
+ assert(gpe.metrics("numPrunedPartitions").value === 0)
+ }
+
+ test("SPARK-59310: an inner join intersection prunes both sides") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 'aa', 41.0, cast('2020-01-15' as timestamp)), " +
+ s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+ s"(2, 'bb', 10.5, cast('2020-01-01' as timestamp)), " +
+ s"(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ s"(1, 42.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 44.0, cast('2020-01-15' as timestamp)), " +
+ s"(1, 45.0, cast('2020-01-15' as timestamp)), " +
+ s"(2, 11.0, cast('2020-01-01' as timestamp)), " +
+ s"(4, 19.5, cast('2020-02-01' as timestamp))")
+
+ // The sides hold different keys ({1,2,3} vs {1,2,4}), so grouping
alone cannot align
+ // them and the join pushes the inner intersection {1,2} down as the
expected keys.
+ // Each side then coalesces its duplicate-key splits (items merges two
groups of two,
+ // purchases one group of three) and prunes the one split of its
unmatched key
+ // (items key 3, purchases key 4); nothing is empty or replicated.
+ // Both AQE arms: the query has no shuffle, so the executed nodes and
their accumulator
+ // ids are the same with and without AQE, and the reporting chain must
work in both.
+ Seq(false, true).foreach { aqeEnabled =>
+ withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key ->
aqeEnabled.toString) {
+ val df = sql(s"SELECT i.id FROM testcat.ns.$items i JOIN
testcat.ns.$purchases p " +
+ "ON i.id = p.item_id")
+ val previousExecutionIds = currentExecutionIds()
+ checkAnswer(df, Seq.fill(6)(Row(1L)) ++ Seq.fill(2)(Row(2L)))
+ val executionIds = currentExecutionIds().diff(previousExecutionIds)
+ assert(executionIds.size === 1)
+ val executionId = executionIds.head
+
+ // The metrics must survive the full reporting path: set on the
driver during
+ // doExecute, posted to the listener bus, and readable back from
the status store
+ // the SQL UI renders.
+ val metricValues = statusStore.executionMetrics(executionId)
+ val groupNodes =
+ statusStore.planGraph(executionId).nodes.filter(_.name ==
"GroupPartitions")
+ assert(groupNodes.size === 2, "one GroupPartitionsExec per join
side")
+ groupNodes.foreach { node =>
+ assert(metricValue(metricValues, node, "number of input
partitions") === "5")
+ assert(metricValue(metricValues, node, "number of partitions")
=== "2")
+ assert(metricValue(metricValues, node, "number of empty
partitions") === "0")
+ assert(metricValue(metricValues, node, "number of pruned input
partitions") === "1")
+ assert(metricValue(metricValues, node,
+ "number of replicated input partition reads") === "0",
+ "no expected key carries multiple splits, so nothing is
replicated")
+ }
+ assert(groupNodes.map(metricValue(metricValues, _, "number of
coalesced partitions"))
+ .sorted === Seq("1", "2"))
+ assert(groupNodes.map(metricValue(metricValues, _, "max partitions
per group"))
+ .sorted === Seq("2", "3"))
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-59310: a disjoint inner join prunes both sides to empty end to
end") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp))")
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ s"(3, 42.0, cast('2020-01-01' as timestamp)), " +
+ s"(4, 44.0, cast('2020-01-15' as timestamp))")
+
+ // The key sets {1, 2} and {3, 4} are disjoint, so the inner join's
intersection is
+ // empty: the alignment emits no output partition on either side, each
side prunes both
+ // of its inputs, and doExecute takes the empty-RDD branch. The
metrics are sent before
+ // that branch, so the total pruning still reaches the store.
+ val df = sql(s"SELECT i.id FROM testcat.ns.$items i JOIN
testcat.ns.$purchases p " +
+ "ON i.id = p.item_id")
+ val previousExecutionIds = currentExecutionIds()
+ checkAnswer(df, Nil)
+ val executionIds = currentExecutionIds().diff(previousExecutionIds)
+ assert(executionIds.size === 1)
+ val executionId = executionIds.head
+
+ val metricValues = statusStore.executionMetrics(executionId)
+ val groupNodes =
+ statusStore.planGraph(executionId).nodes.filter(_.name ==
"GroupPartitions")
+ assert(groupNodes.size === 2, "one GroupPartitionsExec per join side")
+ groupNodes.foreach { node =>
+ assert(metricValue(metricValues, node, "number of input partitions")
=== "2")
+ assert(metricValue(metricValues, node, "number of partitions") ===
"0")
+ assert(metricValue(metricValues, node, "number of pruned input
partitions") === "2")
+ assert(metricValue(metricValues, node, "number of empty partitions")
=== "0")
+ assert(metricValue(metricValues, node, "number of coalesced
partitions") === "0")
+ assert(metricValue(metricValues, node, "max partitions per group")
=== "0")
+ assert(metricValue(metricValues, node,
+ "number of replicated input partition reads") === "0")
+ }
+ }
+ }
+ }
+
+ test("SPARK-59310: partial clustering replicates the smaller side") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key ->
"true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 'aa', 41.0, cast('2020-01-02' as timestamp)), " +
+ s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+ s"(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ s"(1, 45.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 50.0, cast('2020-01-02' as timestamp)), " +
+ s"(1, 55.0, cast('2020-01-02' as timestamp)), " +
+ s"(2, 15.0, cast('2020-01-02' as timestamp)), " +
+ s"(2, 20.0, cast('2020-01-03' as timestamp)), " +
+ s"(2, 22.0, cast('2020-01-03' as timestamp)), " +
+ s"(3, 20.0, cast('2020-02-01' as timestamp))")
+
+ // Partial clustering picks the side with fewer splits to replicate:
items (4 splits,
+ // key 1 x2, key 2 x1, key 3 x1) groups per key and copies each group
into every
+ // expected slot, while purchases (7 splits) keeps them, one per slot.
The slots come
+ // from purchases: key 1 x3, key 2 x3, key 3 x1. Items' extra reads:
(3-1) x 2 splits
+ // for key 1, (3-1) x 1 for key 2, none for key 3's single slot.
+ val df = sql(s"SELECT i.id FROM testcat.ns.$items i JOIN
testcat.ns.$purchases p " +
+ "ON i.id = p.item_id")
+ val previousExecutionIds = currentExecutionIds()
+ checkAnswer(df, Seq.fill(6)(Row(1L)) ++ Seq.fill(3)(Row(2L)) ++
Seq(Row(3L)))
+ val executionIds = currentExecutionIds().diff(previousExecutionIds)
+ assert(executionIds.size === 1)
+ val executionId = executionIds.head
+
+ val metricValues = statusStore.executionMetrics(executionId)
+ val groupNodes =
+ statusStore.planGraph(executionId).nodes.filter(_.name ==
"GroupPartitions")
+ assert(groupNodes.size === 2, "one GroupPartitionsExec per join side")
+ val byInputPartitions = groupNodes.map { node =>
+ metricValue(metricValues, node, "number of input partitions") -> node
+ }.toMap
+ assert(byInputPartitions.keySet === Set("4", "7"))
+ val replicatedSide = byInputPartitions("4")
+ val distributeSide = byInputPartitions("7")
+ Seq(replicatedSide, distributeSide).foreach { node =>
+ assert(metricValue(metricValues, node, "number of partitions") ===
"7")
+ assert(metricValue(metricValues, node, "number of empty partitions")
=== "0")
+ assert(metricValue(metricValues, node, "number of pruned input
partitions") === "0")
+ }
+ assert(metricValue(metricValues, replicatedSide,
+ "number of replicated input partition reads") === "6")
+ assert(!distributeSide.metrics.exists(
+ _.name == "number of replicated input partition reads"),
+ "the distribute side holds one split per slot and registers no
replicated metric")
+ assert(metricValue(metricValues, replicatedSide, "number of coalesced
partitions") === "3",
+ "the three copies of key 1's two-split group each merge their
splits")
+ assert(metricValue(metricValues, replicatedSide, "max partitions per
group") === "2")
+ assert(metricValue(metricValues, distributeSide, "number of coalesced
partitions") === "0")
+ assert(metricValue(metricValues, distributeSide, "max partitions per
group") === "1")
+ }
+ }
+ }
+
+ private case class ExecutableKeyedLeaf(kp: KeyedPartitioning)
Review Comment:
Moved to top level with `SparkContext.getActive`, matching the
`DummySparkPlan` / `DummyLeafSparkPlan` convention (a9a920e489a).
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -508,6 +523,281 @@ class GroupPartitionsExecSuite extends SharedSparkSession
{
assert(gpe.tryEnableSortedMerge().isEmpty)
}
}
+
+ test("SPARK-59310: basic counts without alignment") {
+ // Keys [1, 2, 1]: 3 input splits, key 1 coalesces partitions 0 and 2, 2
output partitions.
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(1))))
+ val gpe = GroupPartitionsExec(child)
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 0)
+ assert(gpe.metrics("numCoalescedPartitions").value === 1)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 2)
+ // Without expectedPartitionKeys there is no alignment, so its metrics
stay unregistered.
+ assert(!gpe.metrics.contains("numPrunedPartitions"))
+ assert(!gpe.metrics.contains("numReplicatedPartitions"))
+ }
+
+ test("SPARK-59310: zero coalesced without duplicate keys") {
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(3))))
+ val gpe = GroupPartitionsExec(child)
+ gpe.execute()
+
+ assert(gpe.metrics("numCoalescedPartitions").value === 0)
+ assert(gpe.metrics("numPartitions").value === 3)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 1)
+ }
+
+ test("SPARK-59310: distribute alignment pads and never replicates") {
+ // Child splits: key 1 -> [0], key 2 -> [1, 2]. Expected: key 1 x1, key 2
x3, key 3 x1.
+ // Key 2 spreads its 2 splits over 3 expected partitions (one empty pad)
and key 3 has no
+ // split (one more empty), so 5 output partitions with 2 empty and nothing
coalesced.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(2))))
+ val gpe = GroupPartitionsExec(child,
+ expectedPartitionKeys = Some(Seq(keyOf(1) -> 1, keyOf(2) -> 3, keyOf(3)
-> 1)),
+ distributePartitions = true)
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 5)
+ assert(gpe.metrics("numEmptyPartitions").value === 2)
+ assert(gpe.metrics("numPrunedPartitions").value === 0)
+ assert(gpe.metrics("numCoalescedPartitions").value === 0, "distribute
never coalesces")
+ assert(!gpe.metrics.contains("numReplicatedPartitions"), "distribute never
replicates")
+ }
+
+ test("SPARK-59310: alignment prunes unmatched keys, pads missing ones") {
+ // The expected keys carry key 1 and a key-3 slot the child does not hold,
as an inner
+ // join's intersection combined with the other side's layout would. The 2
splits of key 2
+ // cannot produce join output and never enter the alignment; the missing
key 3 pads two
+ // empty output partitions, and being empty, replicates nothing despite
its 2 slots.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(2), row(2))))
+ val gpe = GroupPartitionsExec(child,
+ expectedPartitionKeys = Some(Seq(keyOf(1) -> 1, keyOf(3) -> 2)))
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 3)
+ assert(gpe.metrics("numPartitions").value === 3)
+ assert(gpe.metrics("numPrunedPartitions").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 2)
+ assert(gpe.metrics("numCoalescedPartitions").value === 0)
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 1)
+ assert(gpe.metrics("numReplicatedPartitions").value === 0,
+ "empty groups replicate nothing, and the single-split key 1 has no copy")
+ }
+
+ test("SPARK-59310: replicate alignment counts the reads beyond the first") {
+ // The other join side expects 2 partitions for key 1, so this side's
splits for the key are
+ // replicated to both: the slot beyond the first re-reads both splits, 2
extra input
+ // partition reads. Each output partition also coalesces the 2 splits of
the key.
+ def keyOf(a: Int): InternalRowComparableWrapper =
+ InternalRowComparableWrapper(row(a), Seq(exprA))
+ val child = ExecutableKeyedLeaf(KeyedPartitioning(Seq(exprA), Seq(row(1),
row(1))))
+ val gpe = GroupPartitionsExec(child, expectedPartitionKeys =
Some(Seq(keyOf(1) -> 2)))
+ gpe.execute()
+
+ assert(gpe.metrics("numInputPartitions").value === 2)
+ assert(gpe.metrics("numPartitions").value === 2)
+ assert(gpe.metrics("numReplicatedPartitions").value === 2)
+ assert(gpe.metrics("numCoalescedPartitions").value === 2, "both copies
merge the 2 splits")
+ assert(gpe.metrics("maxPartitionsPerGroup").value === 2)
+ assert(gpe.metrics("numEmptyPartitions").value === 0)
+ assert(gpe.metrics("numPrunedPartitions").value === 0)
+ }
+
+ test("SPARK-59310: an inner join intersection prunes both sides") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 'aa', 41.0, cast('2020-01-15' as timestamp)), " +
+ s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+ s"(2, 'bb', 10.5, cast('2020-01-01' as timestamp)), " +
+ s"(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ s"(1, 42.0, cast('2020-01-01' as timestamp)), " +
+ s"(1, 44.0, cast('2020-01-15' as timestamp)), " +
+ s"(1, 45.0, cast('2020-01-15' as timestamp)), " +
+ s"(2, 11.0, cast('2020-01-01' as timestamp)), " +
+ s"(4, 19.5, cast('2020-02-01' as timestamp))")
+
+ // The sides hold different keys ({1,2,3} vs {1,2,4}), so grouping
alone cannot align
+ // them and the join pushes the inner intersection {1,2} down as the
expected keys.
+ // Each side then coalesces its duplicate-key splits (items merges two
groups of two,
+ // purchases one group of three) and prunes the one split of its
unmatched key
+ // (items key 3, purchases key 4); nothing is empty or replicated.
+ // Both AQE arms: the query has no shuffle, so the executed nodes and
their accumulator
+ // ids are the same with and without AQE, and the reporting chain must
work in both.
+ Seq(false, true).foreach { aqeEnabled =>
+ withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key ->
aqeEnabled.toString) {
+ val df = sql(s"SELECT i.id FROM testcat.ns.$items i JOIN
testcat.ns.$purchases p " +
+ "ON i.id = p.item_id")
+ val previousExecutionIds = currentExecutionIds()
+ checkAnswer(df, Seq.fill(6)(Row(1L)) ++ Seq.fill(2)(Row(2L)))
+ val executionIds = currentExecutionIds().diff(previousExecutionIds)
+ assert(executionIds.size === 1)
+ val executionId = executionIds.head
+
+ // The metrics must survive the full reporting path: set on the
driver during
+ // doExecute, posted to the listener bus, and readable back from
the status store
+ // the SQL UI renders.
+ val metricValues = statusStore.executionMetrics(executionId)
+ val groupNodes =
+ statusStore.planGraph(executionId).nodes.filter(_.name ==
"GroupPartitions")
+ assert(groupNodes.size === 2, "one GroupPartitionsExec per join
side")
+ groupNodes.foreach { node =>
+ assert(metricValue(metricValues, node, "number of input
partitions") === "5")
+ assert(metricValue(metricValues, node, "number of partitions")
=== "2")
+ assert(metricValue(metricValues, node, "number of empty
partitions") === "0")
+ assert(metricValue(metricValues, node, "number of pruned input
partitions") === "1")
+ assert(metricValue(metricValues, node,
+ "number of replicated input partition reads") === "0",
+ "no expected key carries multiple splits, so nothing is
replicated")
+ }
+ assert(groupNodes.map(metricValue(metricValues, _, "number of
coalesced partitions"))
+ .sorted === Seq("1", "2"))
+ assert(groupNodes.map(metricValue(metricValues, _, "max partitions
per group"))
+ .sorted === Seq("2", "3"))
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-59310: a disjoint inner join prunes both sides to empty end to
end") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp))")
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ s"(3, 42.0, cast('2020-01-01' as timestamp)), " +
+ s"(4, 44.0, cast('2020-01-15' as timestamp))")
+
+ // The key sets {1, 2} and {3, 4} are disjoint, so the inner join's
intersection is
+ // empty: the alignment emits no output partition on either side, each
side prunes both
+ // of its inputs, and doExecute takes the empty-RDD branch. The
metrics are sent before
+ // that branch, so the total pruning still reaches the store.
+ val df = sql(s"SELECT i.id FROM testcat.ns.$items i JOIN
testcat.ns.$purchases p " +
+ "ON i.id = p.item_id")
+ val previousExecutionIds = currentExecutionIds()
+ checkAnswer(df, Nil)
+ val executionIds = currentExecutionIds().diff(previousExecutionIds)
+ assert(executionIds.size === 1)
+ val executionId = executionIds.head
+
+ val metricValues = statusStore.executionMetrics(executionId)
+ val groupNodes =
+ statusStore.planGraph(executionId).nodes.filter(_.name ==
"GroupPartitions")
+ assert(groupNodes.size === 2, "one GroupPartitionsExec per join side")
+ groupNodes.foreach { node =>
+ assert(metricValue(metricValues, node, "number of input partitions")
=== "2")
+ assert(metricValue(metricValues, node, "number of partitions") ===
"0")
+ assert(metricValue(metricValues, node, "number of pruned input
partitions") === "2")
+ assert(metricValue(metricValues, node, "number of empty partitions")
=== "0")
+ assert(metricValue(metricValues, node, "number of coalesced
partitions") === "0")
+ assert(metricValue(metricValues, node, "max partitions per group")
=== "0")
+ assert(metricValue(metricValues, node,
+ "number of replicated input partition reads") === "0")
+ }
+ }
+ }
+ }
+
+ test("SPARK-59310: partial clustering replicates the smaller side") {
+ withSQLConf(
+ SQLConf.V2_BUCKETING_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key ->
"true",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
Review Comment:
Both tests now loop `Seq(false, true)` like the intersection test
(a9a920e489a). Thanks for verifying the AQE-on arms and the shuffle-stage
variant upfront.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -274,6 +307,34 @@ case class GroupPartitionsExec(
@transient private lazy val hasCoalescing: Boolean =
groupedPartitions.exists(_._2.size > 1)
+ // All values are computed on the driver by `grouping`, so they are reported
through
+ // `sendDriverMetrics` rather than task-side accumulators. Registration
reads constructor
+ // parameters only: pruning and replication are facts of the alignment path
and are registered
+ // only in the modes that produce them, while coalescing can happen in any
mode and reports 0
+ // when nothing merged.
+ @transient override lazy val metrics: Map[String, SQLMetric] = Map(
+ "numInputPartitions" -> SQLMetrics.createMetric(sparkContext, "number of
input partitions"),
+ "numPartitions" -> SQLMetrics.createMetric(sparkContext, "number of
partitions"),
+ "numEmptyPartitions" -> SQLMetrics.createMetric(sparkContext, "number of
empty partitions"),
+ "numCoalescedPartitions" ->
+ SQLMetrics.createMetric(sparkContext, "number of coalesced partitions"),
+ "maxPartitionsPerGroup" ->
+ SQLMetrics.createMetric(sparkContext, "max partitions per group")) ++ {
+ if (expectedPartitionKeys.isDefined && !distributePartitions) {
+ Map("numReplicatedPartitions" -> SQLMetrics.createMetric(sparkContext,
+ "number of replicated input partition reads"))
+ } else {
+ Map.empty[String, SQLMetric]
+ }
+ } ++ {
+ if (expectedPartitionKeys.isDefined) {
+ Map("numPrunedPartitions" ->
+ SQLMetrics.createMetric(sparkContext, "number of pruned input
partitions"))
Review Comment:
Added the two rows, plus one for `max partitions per group` since its name
is node-specific (a9a920e489a). The four AQE-shared counting names stay out
until that family gets its own rows.
--
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]