tschwarzinger commented on PR #27170:
URL: https://github.com/apache/datafusion/pull/27170#issuecomment-6738869507

   After spending more time with this particular part of the code, I still 
think this is a good (but not the only) option for fixing the bug. `SortExec` 
can return, IMO, a wrong output ordering if `preserve_partitioning` is false 
until the optimizer changes the inner plan to return a single partition. During 
this window `COPY` / `DataSinkExec` obtains the output ordering and holds on to 
it.
   
   The cause of the bug:
   1. `SortExec` is being planned from the `Sort` logical node in the physical 
planner (sort expressions: [id, ts])
   2. At this point, `SortExec`s equivalence properties show and output 
ordering of [ts] and a heterogenous constant for id. Each partition has a 
different id and each partition is sorted on [ts]. `SortExec` does not change 
the output ordering to `[id, ts]` even though this will eventually be the 
output ordering after the optimizer has changes the input of `SortExec`. 
`OutputRequirements::reorder` does not contain `id` because it's a heterogenous 
constant.
   3. `COPY` uses the output ordering of `SortExec` (`[ts]`) as required input 
ordering during logical -> physical planning.
   4. The optimizer satisfies the sink’s input ordering requirement (`[ts]`). A 
`SortPreservingMergeExec: [ts]` over the union is enough, so the `SortExec` is 
removed. 
   
   There are at least two appraoches on how we could fix this:
   - Make the output ordering function of `SortExec` aware that it will be 
changed by the optimizer. So even though `SortExec` still has multiple input 
partitions, it reports its output ordering as if it would have only a single 
input partition, if `preserve_partitioning` is false. This is what this PR is 
trying to achieve and it's similar to how it's done with the output 
partitioning where we also have a special casing based on 
`preserve_partitioning`. See below.
   - We could use the mechanism that is used for `OutputRequirementsExec` 
(i.e., traversing the plan and downcasting `SortExec`) also for `COPY`. 
However, this only works for `SortExec` and `SortPreservingMergeExec`.
   
   #### Existing Special Case based on `preserve_partitioning`:
   
   ```rust
   fn output_partitioning_helper(
           input: &Arc<dyn ExecutionPlan>,
           preserve_partitioning: bool,
       ) -> Partitioning {
           // Get output partitioning:
           if preserve_partitioning {
               input.output_partitioning().clone()
           } else {
               Partitioning::UnknownPartitioning(1)
           }
       }
   ```
   
   #### Logical to Phyiscal Planner for Writes:
   
   ```rust
           // ...
        LogicalPlan::Copy(CopyTo {
                        input,
                        output_url,
                        file_type,
                        partition_by,
                        options: source_option_tuples,
                        output_schema: _,
                    }) => {
                   // ...
                   
                let ordering = 
input_exec.properties().output_ordering().cloned(); // <--- here the not yet 
finalized output ordering is stored
   
                sink_format
                    .create_writer_physical_plan(
                        input_exec,
                        session,
                        config,
                        ordering.map(Into::into),
                    )
                    .await?
   
                  // ...
           }
   ```
   
   #### Why only `COPY` and not every query?
   
   The issue doesn't get raised with `OutputRequirementExec` because it uses a 
custom traversal of the execution plan and uses the sort expressions of 
`SortExec` by downcasting it. It even has a comment for constants in 
`require_top_ordering_helper`.
   
   ```text
   // In case of constant columns, output ordering of the `SortExec` would
   // be an empty set. Therefore; we check the sort expression field to
   // assign the requirements.
   let req_dist = sort_exec
        .input_distribution_requirements()
        .into_per_child()
        .swap_remove(0);
   let req_ordering = sort_exec.expr();
   let reqs = OrderingRequirements::from(req_ordering.clone());
   ```
   
   `EXPLAIN VERBOSE` at main without `COPY`: 
   ```text
   physical_plan after OutputRequirements
    // The order_by below looks good!
   01)OutputRequirementExec: order_by=[(id@0, asc), (ts@1, asc)], 
dist_by=SinglePartition
   02)--SortExec: expr=[id@0 ASC NULLS LAST, ts@1 ASC NULLS LAST], 
preserve_partitioning=[false]
   03)----UnionExec
   04)------ProjectionExec: expr=[b as id, ts@0 as ts, x@1 as x]
   05)--------DataSourceExec: file_groups={1 group: 
[[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/copy/union_const_order/sorted.parquet]]},
 projection=[ts, x], output_ordering=[ts@0 ASC NULLS LAST], file_type=parquet
   06)------ProjectionExec: expr=[a as id, ts@0 as ts, x@1 as x]
   07)--------DataSourceExec: file_groups={1 group: 
[[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/copy/union_const_order/sorted.parquet]]},
 projection=[ts, x], output_ordering=[ts@0 ASC NULLS LAST], file_type=parquet
   ```
   
   `EXPLAIN VERBOSE` at main with `COPY` and a change that shows the input 
ordering requirements of `DataSinkExec`:
   
   initial_physical_plan_with_stats
   ```text
   // The `order=` loses the `id` which causes the observed bug
   01)DataSinkExec: sink=ParquetSink(file_groups=[]) order=[ts@1 ASC NULLS 
LAST], statistics=[Rows=Absent, Bytes=Absent, [(Col[0]:)]]
   02)--SortExec: expr=[id@0 ASC NULLS LAST, ts@1 ASC NULLS LAST], 
preserve_partitioning=[false], statistics=[Rows=Exact(6), Bytes=Inexact(96), 
[(Col[0]: Min=Exact(Utf8("a")) Max=Exact(Utf8("b")) Null=Exact(0) 
Distinct=Inexact(2)),(Col[1]: Min=Exact(Int64(1)) Max=Exact(Int64(3)) 
Null=Exact(0) ScanBytes=Exact(48)),(Col[2]: Min=Exact(Float64(10)) 
Max=Exact(Float64(30)) Null=Exact(0) ScanBytes=Exact(48))]]
   03)----UnionExec, statistics=[Rows=Exact(6), Bytes=Inexact(96), [(Col[0]: 
Min=Exact(Utf8("a")) Max=Exact(Utf8("b")) Null=Exact(0) 
Distinct=Inexact(2)),(Col[1]: Min=Exact(Int64(1)) Max=Exact(Int64(3)) 
Null=Exact(0) ScanBytes=Exact(48)),(Col[2]: Min=Exact(Float64(10)) 
Max=Exact(Float64(30)) Null=Exact(0) ScanBytes=Exact(48))]]
   04)------ProjectionExec: expr=[b as id, ts@0 as ts, x@1 as x], 
statistics=[Rows=Exact(3), Bytes=Inexact(48), [(Col[0]: Min=Exact(Utf8("b")) 
Max=Exact(Utf8("b")) Null=Exact(0) Distinct=Exact(1)),(Col[1]: 
Min=Exact(Int64(1)) Max=Exact(Int64(3)) Null=Exact(0) 
ScanBytes=Exact(24)),(Col[2]: Min=Exact(Float64(10)) Max=Exact(Float64(30)) 
Null=Exact(0) ScanBytes=Exact(24))]]
   05)--------DataSourceExec: file_groups={1 group: 
[[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/copy/union_const_order/sorted.parquet]]},
 projection=[ts, x], output_ordering=[ts@0 ASC NULLS LAST], file_type=parquet, 
statistics=[Rows=Exact(3), Bytes=Exact(48), [(Col[0]: Min=Exact(Int64(1)) 
Max=Exact(Int64(3)) Null=Exact(0) ScanBytes=Exact(24)),(Col[1]: 
Min=Exact(Float64(10)) Max=Exact(Float64(30)) Null=Exact(0) 
ScanBytes=Exact(24))]]
   06)------ProjectionExec: expr=[a as id, ts@0 as ts, x@1 as x], 
statistics=[Rows=Exact(3), Bytes=Inexact(48), [(Col[0]: Min=Exact(Utf8("a")) 
Max=Exact(Utf8("a")) Null=Exact(0) Distinct=Exact(1)),(Col[1]: 
Min=Exact(Int64(1)) Max=Exact(Int64(3)) Null=Exact(0) 
ScanBytes=Exact(24)),(Col[2]: Min=Exact(Float64(10)) Max=Exact(Float64(30)) 
Null=Exact(0) ScanBytes=Exact(24))]]
   07)--------DataSourceExec: file_groups={1 group: 
[[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/copy/union_const_order/sorted.parquet]]},
 projection=[ts, x], output_ordering=[ts@0 ASC NULLS LAST], file_type=parquet, 
statistics=[Rows=Exact(3), Bytes=Exact(48), [(Col[0]: Min=Exact(Int64(1)) 
Max=Exact(Int64(3)) Null=Exact(0) ScanBytes=Exact(24)),(Col[1]: 
Min=Exact(Float64(10)) Max=Exact(Float64(30)) Null=Exact(0) 
ScanBytes=Exact(24))]]
   ```
   


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