jayshrivastava commented on PR #26593:
URL: https://github.com/apache/datafusion/pull/26593#issuecomment-6697040472

   @mbutrovich I've thought about partitioning satisfaction and layouts a 
little bit. I this is the main issue to address before merging.
   
   ### Background
   - `symmetric_join_output_partitioning` sets the output partitioning of the 
join based on the join type only (does not care about split points, 
distribution etc.)
   - `EnforceDistribution` optimizer rule
     - The join declares `input_distribution_requirements`, so we assert the 
inputs are key partitioned. However, we don't check split points, bounds, or 
partition count. We insert a `RepartitionExec` if this failes
     - `enforce_distribution_relationships` check that all join inputs are 
co-partitioned
       - `adapt` - Imo this should be called `adapt_schema` and modify the 
schema temporarily to check co-partitioned ness. I have reverted the previous 
change were we strip the bounds.
       - `has_same_layout` - This is the main thing that checks partition 
count, split points etc. If the layout is not the same, we insert a 
`RepartitionExec`
   
   So the question is - now that there's bounds, are there soundness / 
correctness issues?
   
   ### Join Example
   For the most part, everything in vanilla datafusion is correct. The 
`symmetric_join_output_partitioning` does the heavy lifting, choosing the right 
bounds as needed.
   
   The only suspicious case is this:
   ```
   HashJoinExec: mode=Partitioned, join_type=Inner, on=[(a@0, a@0)]
     RepartitionExec: partitioning=Range([a@0 ASC], [(10), (20), (30)], 4), 
input_partitions=1
       DataSourceExec: partitions=1, partition_sizes=[1]
     DataSourceExec: partitions=4, partition_sizes=[1, 1, 1, 1]
   
   Left partitioning: Range([a@0 ASC], [(10), (20), (30)], 4)
   Right partitioning: Range([a@0 ASC], [(10), (20), (30)], 4, start=(0), 
end=(35))
   Output partitioning: Range([a@1 ASC], [(10), (20), (30)], 4, start=(0), 
end=(35))
   ``` 
   
   We repartition **without bounds** into a join that **has bounds**. In this 
scenario, because the operator is a join, **it's expected that the join drops 
rows outside the bounds**. The key thing to note is bounds are not considered 
by the optimizer rule
   - `input_distribution_requirements` enforces distributions, which are not 
aware of partition counts, split points, nor bounds
   - `has_same_layout` - considers everything but bounds
   
   The `ExecutionPlan` must truncate to the bounds.
   
   ## Custom Plan
   
   Say I create a custom plan which enforces an input distribution, I can 
introduce an issue where we emit rows outside the bounds:
   ```
   Output partitioning: Range([a@0 ASC], [(20)], 2, start=(10), end=(30))
   Input partitioning: Hash([a@0], 2)
   
   TestProjectionExec // configures the `input_distribution_requirements` to be 
key partitioned on `a`
     RepartitionExec: partitioning=Hash([a@0], 2), input_partitions=2
       DataSourceExec: partitions=2, partition_sizes=[1, 1]
   ```
   This emits rows outside of [10, 30]. This is a real problem.
   
   ## Ideas
   
   My two best ideas are as follows:
   1. Leave this as is, defer to the operator to handle bounds. If an operator 
declares `Partitioning::Range` with bounds and its input doesn't have bounds, 
the operator should handle this. This is reasonable because the plan node has 
to **opt in**. There wouldn't be a correctness issue without that. This is 
currently true for all joins and works fine.
   
   2. We could make `has_same_layout` consider bounds, but for joins, the 
operator handles rows outside the bounds already. Updating `has_same_layout` to 
return false when the input has no bounds and the output is bounded would 
insert repartitions which are unnecessary. For example here, we don't need to 
repartition the left
   ```
   HashJoinExec: mode=Partitioned, join_type=Inner, on=[(a@0, a@0)]
       DataSourceExec: partitions=4, partition_sizes=[1, 1, 1, 1]
       DataSourceExec: partitions=4, partition_sizes=[1, 1, 1, 1]
   
   Left partitioning: Range([a@0 ASC], [(10), (20), (30)], 4)
   Right partitioning: Range([a@0 ASC], [(10), (20), (30)], 4, start=(0), 
end=(35))
   Output partitioning: Range([a@1 ASC], [(10), (20), (30)], 4, start=(0), 
end=(35))
   ``` 
   If I were to make `has_same_layout` return false on a bounds mismatch, I 
would add a `FilterExec` like so just to be safe in lieu of a repartition (an 
in some cases you would need both a repartition and filter).
   ```
   FilterExec: 0 <= a AND a < 30
   ```
   I wouldn't mind putting that in this PR if it makes sense.
   
   3. This one is related to https://github.com/apache/datafusion/issues/25483 
and is a bigger change. I would say we can do this whenever work on 
https://github.com/apache/datafusion/issues/25483 begins and not put it in this 
PR.
   
   The solution there would be to probably add partitions to 
`input_distribution_requirements` (or `RangeLayoutRelationshipKind` as the 
issue proposes).
   
   This change feels like it may be overdue. I'm a new contributor, but I feel 
like the distributions were added for things like aggregations, where you only 
care that there's a distinct value in each partition but you don't care about 
anything else. We're kind of piggybacking on that mechanism for strong 
partitioning guarantees, like for co-partitioned joins where the actual set of 
values in each partition matters.
   
   I'm going to push a commit with some tests which show the above examples.
   
   Please let me know what you think! 
   
   
   
   
   
   
    


-- 
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]

Reply via email to