XiDuo You created SPARK-58996:
---------------------------------

             Summary: Partially clustered storage-partitioned join duplicates 
rows when EnsureRequirements re-runs
                 Key: SPARK-58996
                 URL: https://issues.apache.org/jira/browse/SPARK-58996
             Project: Spark
          Issue Type: Bug
          Components: SQL
    Affects Versions: 5.0.0
            Reporter: XiDuo You


`EnsureRequirements` is not idempotent for a storage-partitioned join that uses 
a partially
clustered distribution. Re-running it on a plan it already produced stacks a 
second
`GroupPartitionsExec` on top of the first, and the result duplicates rows.

AQE hands the whole plan back to `EnsureRequirements` after rewriting some 
other join --
`ConvertSortMergeJoinToShuffledHashJoin` and `OptimizeSkewedJoin` both do this. 
On that second
pass a join child is `SortExec(GroupPartitionsExec(...))` rather than a bare 
scan. A partially
clustered `KeyedPartitioning` reports `isGrouped = false` by design, so the 
distribution step
treats it as satisfied "only after grouping" and adds a plain 
`GroupPartitionsExec` on top;
`applyGroupPartitions` then writes the join's `expectedPartitionKeys` and 
`distributePartitions`
into that fresh outer node.

The alignment is therefore re-derived from an already-aligned layout. The inner 
node replicates
an input partition across the expected partitions, and the outer node 
concatenates those replicas
back into a single partition before replicating again, so every row of the 
replicated side is
emitted twice.

Reproduction, against a DSv2 catalog that reports `KeyGroupedPartitioning`:

{code}
spark.sql.sources.v2.bucketing.enabled=true
spark.sql.sources.v2.bucketing.pushPartValues.enabled=true
spark.sql.sources.v2.bucketing.partiallyClusteredDistribution.enabled=true
spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold=100m

CREATE TABLE sp1 (id BIGINT, data STRING) PARTITIONED BY (id);
INSERT INTO sp1 VALUES (1, 'aa'), (2, 'bb');
ALTER TABLE sp1 ADD COLUMN extra STRING;   -- opens a second split for id = 1
INSERT INTO sp1 VALUES (1, 'ab', 'x');

CREATE TABLE sp2 (id BIGINT, data STRING) PARTITIONED BY (id);
INSERT INTO sp2 VALUES (1, 'p'), (2, 'q');

CREATE TABLE np1 (id BIGINT, data STRING);
INSERT INTO np1 VALUES (7, 'x');
CREATE TABLE np2 (id BIGINT, data STRING);
INSERT INTO np2 VALUES (7, 'y');

SELECT /*+ MERGE(a, b) */ a.id AS k FROM sp1 a JOIN sp2 b ON a.id = b.id
UNION ALL
SELECT c.id AS k FROM np1 c JOIN np2 d ON c.id = d.id;
{code}

This returns {{(1, 1, 1, 1, 2, 7)}}; the correct answer is {{(1, 1, 2, 7)}}.

Two details are load-bearing in the reproduction:

* The {{np1}}/{{np2}} branch exists only to create a materialized shuffle 
stage, which is what
  makes `ConvertSortMergeJoinToShuffledHashJoin` fire and re-run 
`EnsureRequirements` over the
  whole plan. The storage-partitioned join has no shuffle of its own, so it 
cannot trigger the
  re-run by itself.
* {{id = 1}} must map to two input splits. In the in-memory test catalog the 
`ALTER TABLE`
  between the two inserts changes the write schema and opens the second split; 
on a real
  connector this corresponds to a partition value with more than one data file, 
where the files
  are not combined into a single task.

A single pass is self-consistent: within one `ensureDistributionAndOrdering` 
call the
distribution step creates the `GroupPartitionsExec` and `applyGroupPartitions` 
rewrites that same
node. The stacking only appears once the rule is applied to its own output.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to