adriangb opened a new pull request, #26271:
URL: https://github.com/apache/datafusion/pull/26271

   ## Which issue does this PR close?
   
   - Closes apache/datafusion#26230.
   
   Stacked on apache/datafusion#25580. Only the last commit is new.
   
   ## Rationale for this change
   
   A reference to a `MATERIALIZED` CTE reported unknown statistics, no ordering 
and unknown partitioning. A query could then get a worse plan than the same 
query with the CTE inlined.
   
   ```sql
   SET datafusion.sql_parser.dialect = 'PostgreSQL';
   CREATE TABLE small AS SELECT value AS k FROM generate_series(1, 5);
   CREATE TABLE huge  AS SELECT value AS k FROM generate_series(1, 100000);
   
   EXPLAIN WITH c AS MATERIALIZED (SELECT k FROM small)
   SELECT count(*) FROM huge JOIN c ON huge.k = c.k;
   ```
   
   Before, the hash table is built on the 100000 rows of `huge`:
   
   ```
   HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)], 
projection=[]
     DataSourceExec: partitions=1, partition_sizes=[13]
     MaterializedCteScanExec: name=c
   ```
   
   After, it is built on the 5 rows of the CTE:
   
   ```
   HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)], 
projection=[]
     MaterializedCteScanExec: name=c
     DataSourceExec: partitions=1, partition_sizes=[13]
   ```
   
   The continuation also sorted or repartitioned rows that the body already 
sorted or partitioned. For `WITH c AS MATERIALIZED (SELECT g, count(*) AS n 
FROM p GROUP BY g) SELECT g, sum(n) FROM c GROUP BY g`, the `RepartitionExec` 
and the partial aggregate above the scan are removed, and the scan shows 
`partitioning=Hash([g@0], 4)`.
   
   ## What changes are included in this PR?
   
   The scan is a leaf and the body is a child of `MaterializedCteExec`, so the 
planner copies the properties to the scan in 
`DefaultPhysicalPlanner::optimize_physical_plan`. The EXPLAIN paths use the 
same function.
   
   **Statistics.** Each scan gets the statistics of the body of the initial 
plan, computed with `StatisticsContext` before the optimizer runs. The 
statistics API is not changed.
   
   **Ordering and hash partitioning.** These are only known after the optimizer 
has rewritten the body, and a wrong claim gives wrong results:
   
   1. The plan is optimized with scans that have statistics only. This plan is 
always correct.
   2. If an optimized body has an output ordering or a hash partitioning, each 
scan of the initial plan gets the partition count, ordering and partitioning of 
that body, and the plan is optimized again.
   3. The second plan is used only if each body in it has the properties that 
its scans report. If not, step 2 is done again, at most 3 times. If no plan 
passes the check, the plan of step 1 is used.
   
   A scan with the properties of the body replays one body partition per 
partition, and returns an internal error if the partition counts are not the 
same at execution.
   
   Limits:
   
   - A query with a `MATERIALIZED` CTE whose body has an ordering or a hash 
partitioning runs the physical optimizer two times, or up to four times for a 
chain of CTEs. Queries without `MATERIALIZED` are not changed. If this cost is 
not acceptable, the statistics part can be split out.
   - An `ORDER BY` at the top of a body without `LIMIT` is removed by 
`EnforceSorting`, as before, so the scan has no ordering in that case.
   - `EXPLAIN VERBOSE` shows the rules of the optimizer run that made the final 
plan.
   
   Other plan changes that follow from the statistics: an exact row count of 
the body can answer `count(*)`, and the body then does not run. One pinned plan 
in `cte_materialized.slt` has a new join order.
   
   ## Are these changes tested?
   
   Yes. `cte_materialized.slt`: the join-order plan, a sort that is removed, a 
sort that is still needed, a repartition and a partial aggregate that are 
removed, a partitioned join of two references, a chain of two CTEs, a scalar 
subquery and a view. Unit tests for the statistics, the copied properties, the 
replay order and the partition-count error. A planner test with an optimizer 
rule that changes the body after the scans got its properties checks the 
fallback.
   
   ## Are there any user-facing changes?
   
   Plans of queries with `MATERIALIZED` CTEs can change. 
`MaterializedCteScanExec` shows `partitioning=` and `output_ordering=` in 
EXPLAIN when it has them. New public functions: 
`MaterializedCteScanExec::with_body_statistics`, `with_body_properties`, 
`body_properties_hold`, and `MaterializedCteExec::body`, `register`.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


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