@@ -541,17 +541,52 @@ case class EnsureRequirements(
541541 def bothUnprojected (l : KeyedShuffleSpec , r : KeyedShuffleSpec ): Boolean =
542542 l.joinKeyPositions.isEmpty && r.joinKeyPositions.isEmpty
543543
544+ // Whether the merged key list below may be narrowed to what the join type allows. A marked
545+ // layout is left alone: only an identity regrouping keeps its claim (see
546+ // `GroupPartitionsExec`), and losing it costs the pair its join at the gate at the end of this
547+ // method.
548+ //
549+ // Filtering is then the only thing that can shrink the merged list *below* the marked side's
550+ // own declared keys. The merging arms take the union, and `KeyedShuffleSpec.areKeysCompatible`
551+ // pairs a marked layout only with one whose keys are a subset of its declared keys, so the
552+ // union is that side's own key set. The count its hash is taken modulo survives the dedup
553+ // because a marked layout is always grouped: `canCreatePartitioning` is the only producer of
554+ // the marker and it refuses an ungrouped one. A reduce is the other way a merged list comes
555+ // out smaller, and it cannot happen here, since the marked arm of `areKeysCompatible` admits
556+ // only positions holding the same transform function and `reducersBothWays` finds nothing to
557+ // reduce between those.
558+ //
559+ // What filtering does instead: an intersection with a strictly smaller partner, or the
560+ // one-sided arm that keeps the *other* side's keys, drops groups the marked side holds, and
561+ // the regrouping stops being the identity. The pair would then trade its whole join for
562+ // pruning those groups.
563+ //
564+ // Sorting is the other way a regrouping stops being the identity, and is not addressed here.
565+ // `mergeAndDedupPartitions` sorts, so a marked layout whose declared order is not the sorted
566+ // one is relabelled even where the set is unchanged. Handing it its own list verbatim looks
567+ // like the same fix and is not, because `KeyedPartitioning.createShuffleSpec` sorts through
568+ // `toGrouped` under `v2BucketingAllowKeysSubsetOfPartitionKeys` while it hands a marked layout
569+ // back unprojected: the two children would hold one partitioning and report two specs that
570+ // `describesSameKeys` calls different, and `ValidateRequirements` rejects the join this method
571+ // just allowed. Measured on the generated sweep in `EnsureRequirementsSuite`, cell
572+ // `left=id/12 right=id/312/marked Inner`.
573+ val partitionFilter = conf.getConf(SQLConf .V2_BUCKETING_PARTITION_FILTER_ENABLED )
574+ def filtersKeys (l : KeyedPartitioning , r : KeyedPartitioning ): Boolean =
575+ partitionFilter && ! l.mayContainUnknownPartitionKeys && ! r.mayContainUnknownPartitionKeys
576+
544577 // How many key groups the pushdown below would leave this pair. `mergeAndDedupPartitions`
545578 // keeps one side's keys and drops the other's for the filtered one-sided join types, and there
546579 // the dropped side's count says nothing, so rank on the side that survives. The arms that
547580 // really merge have no cheap answer, so they take the larger of the two counts. Keep the join
548581 // types here in step with `mergeAndDedupPartitions`.
549- val partitionFilter = conf.getConf(SQLConf .V2_BUCKETING_PARTITION_FILTER_ENABLED )
550- def rank (l : KeyedShuffleSpec , r : KeyedShuffleSpec ): Int = joinType match {
551- case LeftOuter | LeftAnti | LeftSingle | ExistenceJoin (_) if partitionFilter =>
552- l.numPartitions
553- case RightOuter if partitionFilter => r.numPartitions
554- case _ => l.numPartitions.max(r.numPartitions)
582+ def rank (l : KeyedShuffleSpec , r : KeyedShuffleSpec ): Int = {
583+ val filtered = filtersKeys(l.partitioning, r.partitioning)
584+ joinType match {
585+ case LeftOuter | LeftAnti | LeftSingle | ExistenceJoin (_) if filtered =>
586+ l.numPartitions
587+ case RightOuter if filtered => r.numPartitions
588+ case _ => l.numPartitions.max(r.numPartitions)
589+ }
555590 }
556591
557592 // Each side may offer several members, and the right one is the one the other side can pair
@@ -560,7 +595,8 @@ case class EnsureRequirements(
560595 // `ensureDistributionAndOrdering` makes between children when it picks `bestSpecOpt`.
561596 //
562597 // Two things keep `rank` from being what the join actually gets, both on the merging arms.
563- // `InnerLike` and `LeftSemi` intersect under `v2BucketingPartitionFilterEnabled`, and an
598+ // `InnerLike` and `LeftSemi` intersect under `v2BucketingPartitionFilterEnabled`, unless
599+ // `filtersKeys` turned filtering off for the pair, and an
564600 // intersection is not monotone in member granularity: members cover different clustering keys
565601 // rather than nested ones, so a finer pair can rank above a coarser one and still meet the
566602 // other side in fewer groups. And a union does not merely exceed the rank either, because
@@ -655,7 +691,8 @@ case class EnsureRequirements(
655691
656692 // merge values on both sides
657693 var mergedPartitionKeys =
658- mergeAndDedupPartitions(leftReducedKeys, rightReducedKeys, joinType, reducedKeyOrdering)
694+ mergeAndDedupPartitions(leftReducedKeys, rightReducedKeys, joinType, reducedKeyOrdering,
695+ filterPartitions = filtersKeys(leftPartitioning, rightPartitioning))
659696 .map((_, 1 ))
660697
661698 logInfo(log " After merging, there are " +
@@ -1006,18 +1043,22 @@ case class EnsureRequirements(
10061043 /**
10071044 * Merge, dedup and sort partitions keys for SPJ and optionally enable partition filtering.
10081045 * Both sides must have matching partition expressions.
1046+ *
10091047 * @param leftPartitionKeys left side partition keys
10101048 * @param rightPartitionKeys right side partition keys
10111049 * @param joinType join type for optional partition filtering
10121050 * @param keyOrdering ordering to sort partition keys
1051+ * @param filterPartitions whether to narrow the merged list to the keys the join type allows.
1052+ * The caller decides, see `filtersKeys` in `checkKeyGroupCompatible`.
10131053 * @return merged and sorted partition values
10141054 */
10151055 def mergeAndDedupPartitions (
10161056 leftPartitionKeys : Seq [InternalRowComparableWrapper ],
10171057 rightPartitionKeys : Seq [InternalRowComparableWrapper ],
10181058 joinType : JoinType ,
1019- keyOrdering : Ordering [InternalRowComparableWrapper ]): Seq [InternalRowComparableWrapper ] = {
1020- val merged = if (SQLConf .get.getConf(SQLConf .V2_BUCKETING_PARTITION_FILTER_ENABLED )) {
1059+ keyOrdering : Ordering [InternalRowComparableWrapper ],
1060+ filterPartitions : Boolean ): Seq [InternalRowComparableWrapper ] = {
1061+ val merged = if (filterPartitions) {
10211062 // Rows with matching join keys land in the same key group. If a group is absent from one
10221063 // side, whether it can produce output depends on which side's unmatched rows the join
10231064 // preserves. Only equi-joins reach this method, since every SMJ/SHJ takes its keys from
0 commit comments