Skip to content

feat: report the ordering and partitioning of materialized CTE scans - #26428

Open
adriangb wants to merge 9 commits into
apache:mainfrom
adriangb:mcte-body-first-properties
Open

adriangb wants to merge 9 commits into
apache:mainfrom
adriangb:mcte-body-first-properties

Conversation

@adriangb

@adriangb adriangb commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Follow-up to #26271, which is merged. Implements the plan in #26271 (comment).

Rationale for this change

A reference to a MATERIALIZED CTE reports no ordering and unknown partitioning. The continuation then sorts or repartitions rows that the body already sorted or partitioned:

WITH c AS MATERIALIZED (SELECT g, count(*) AS n FROM p GROUP BY g)
SELECT g, sum(n) FROM c GROUP BY g;

Before:

AggregateExec: mode=FinalPartitioned, gby=[g@0 as g], aggr=[sum(c.n)]
  RepartitionExec: partitioning=Hash([g@0], 12), input_partitions=12
    AggregateExec: mode=Partial, gby=[g@0 as g], aggr=[sum(c.n)]
      MaterializedCteScanExec: name=c

After:

AggregateExec: mode=SinglePartitioned, gby=[g@0 as g], aggr=[sum(c.n)]
  MaterializedCteScanExec: name=c, partitioning=Hash([g@0], 12)

On 10 million rows with 1 million groups (12 partitions, datafusion-cli, release build, median of 30 runs), the query takes about 35 ms instead of about 41 ms.

What changes are included in this PR?

DefaultPhysicalPlanner::optimize_physical_plan now does these steps for a plan with materialized CTEs:

  1. It optimizes each body one time, on its own, as the child of a MaterializedCteExec with an empty continuation, so the body has the same parent as in the full plan. A body that reads or defines another CTE goes after that CTE.
  2. It gives each scan the properties of its optimized body:
    • the statistics of the whole body and of each body partition
    • the partition count and hash partitioning
    • the equivalence properties: orderings, equivalent columns, constants and constraints
  3. It optimizes the rest of the plan one time. During this run each MaterializedCteExec is replaced by a private planning-only node, FrozenMaterializedCteExec, whose only child is the continuation. So the optimizers cannot see or change the optimized body. Then the MaterializedCteExec nodes are put back.

The body never changes after the scans copy its properties, so there is no check of the final plan and no fallback plan.

A scan with the properties of the body replays one body partition per partition. At execution it checks that the body has the partitioning and equivalence properties the scan reports. If they differ, it returns an internal error rather than wrong results. This check is cheap and guards against a future change that breaks the planning-time guarantee.

Partition p of a scan with n partitions reports the merged statistics of the body partitions it replays: p, p + n, and so on.

A scan that comes from a separate planning run, such as a view, is bound again, so it loses the properties from that run.

EXPLAIN VERBOSE shows the frozen body. In each physical_plan after <rule> step, the CTE shows as FrozenMaterializedCteExec: name=c, with the optimized body as its first child. So the output is clear that rules do not touch the body in that run. The final plan shows MaterializedCteExec with its body and continuation, as before. This is similar to OutputRequirementExec, which also shows only in the per-rule steps. The display copy is built only when EXPLAIN VERBOSE asks for each step, so normal planning does not pay for it.

Planning time. Measured with new sql_planner benchmarks that are part of this PR. Each kind of CTE is planned read once, read twice, and as a chain of six CTEs that each read the previous one twice. Results are Criterion means:

Case main This PR
unmarked, one reference 242 µs 243 µs
unmarked, two references 474 µs 475 µs
unmarked, chain of 6 8.25 ms 8.17 ms
MATERIALIZED, one reference 264 µs 263 µs
MATERIALIZED, two references 360 µs 359 µs
MATERIALIZED, chain of 6 924 µs 925 µs
NOT MATERIALIZED, one reference 245 µs 244 µs
NOT MATERIALIZED, two references 481 µs 473 µs
NOT MATERIALIZED, chain of 6 8.33 ms 8.11 ms
one CTE of each kind 1.36 ms 1.32 ms

With EXPLAIN in datafusion-cli (12 partitions, 2000 queries per file), the per-partition statistics add about 3%: 0.254 to 0.263 ms for one CTE, and 0.975 to 1.004 ms for a chain of 6.

Limits:

  • EnforceSorting removes an ORDER BY at the top of a body without LIMIT, as before. So the scan has no ordering in that case.

Comparison with PostgreSQL and DuckDB.

  • Planning: this is what PostgreSQL does. It plans each CTE before the main query (planner.c). The CTE scan reports the sort order of the CTE plan (allpaths.c).
  • Hash partitioning: PostgreSQL and DuckDB have no hash partitioning of a scan, so that part has no precedent.
  • EXPLAIN: DuckDB shows a CTE node with the body and the continuation as children, the same shape as the final plan here.

Are these changes tested?

Yes.

  • cte_materialized.slt:
    • a sort that is removed
    • a sort that is still needed
    • a body ORDER BY without LIMIT
    • a repartition and a partial aggregate that are removed
    • a partitioned join of two references
    • a chain of two CTEs
    • a CTE defined inside a CTE body
    • a scalar subquery
    • a view
  • Unit tests:
    • the copied properties and the replay order
    • the partition-count error
    • the error when the body is not sorted or has other hash keys
    • the per-partition statistics when the scan has the same partitions as the body, fewer, one, or more
  • Planner tests:
    • the optimizer runs one time on the body and one time on the rest of the plan
    • a rule that would rewrite the body in the second run cannot reach it
    • EXPLAIN VERBOSE shows the frozen body in every changed step
    • each scan partition has the statistics of its body partition
  • View test: a view that is not inlined.

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. EXPLAIN VERBOSE shows FrozenMaterializedCteExec in the per-rule steps.

New public functions: MaterializedCteScanExec::with_body_properties, MaterializedCteExec::register and MaterializedCteBuffer::name.

MaterializedCteScanExec::with_body_statistics now also takes the statistics of each body partition. It is new in #26271 and not released yet. Scan statistics now come from the optimized body.

🤖 Generated with Claude Code

@github-actions github-actions Bot added core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) physical-plan Changes to the physical-plan crate labels Oct 16, 2026
@codecov-commenter

codecov-commenter commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.60342% with 99 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.90%. Comparing base (484183d) to head (ba2c820).
⚠️ Report is 162 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/core/src/physical_planner.rs 83.38% 19 Missing and 34 partials ⚠️
datafusion/physical-plan/src/materialized_cte.rs 86.52% 5 Missing and 33 partials ⚠️
datafusion/core/src/datasource/view_test.rs 80.95% 0 Missing and 8 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #26428    +/-   ##
========================================
  Coverage   82.89%   82.90%            
========================================
  Files        1154     1154            
  Lines      457630   458240   +610     
  Branches   457630   458240   +610     
========================================
+ Hits       379370   379892   +522     
- Misses      55256    55269    +13     
- Partials    23004    23079    +75     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

adriangb and others added 4 commits October 27, 2026 19:13
Move the optimizer loop into run_physical_optimizers, so that the planner can run it on more than one plan.
Add materialized_ctes and as_materialized_cte, and use them in MaterializedCteBinder.
No change in behavior.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add MaterializedCteScanExec::with_body_properties, which gives a scan the partition count, the output ordering and the hash partitioning of a CTE body.
Partition p of such a scan replays partition p of the body, and the scan returns an internal error if the partition counts differ at execution.
Add MaterializedCteExec::register and MaterializedCteBuffer::name for the planner.
The planner does not use these yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… body (RED)

Add sqllogictests and a view test that expect a scan of a MATERIALIZED CTE to report the ordering and the hash partitioning of its body, so that the continuation does not sort or repartition again.
These tests fail until the planner copies the properties of the body to the scans.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…(GREEN)

optimize_physical_plan now optimizes each MATERIALIZED CTE body one time, on its own, as the child of a MaterializedCteExec, after the bodies that it reads or defines.
Each scan then gets the statistics, the partition count, the output ordering and the hash partitioning of its optimized body.
While the rest of the plan is optimized, each MaterializedCteExec is replaced by a planning-only node whose only child is the continuation, so the optimizers cannot change the body and the properties of the scans stay correct.

Closes apache#26424.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@adriangb
adriangb force-pushed the mcte-body-first-properties branch from e59b111 to efa27b5 Compare October 28, 2026 00:18
@adriangb

Copy link
Copy Markdown
Contributor Author

cc @asolimando

@asolimando

Copy link
Copy Markdown
Member

I started checking and it looks good to me, there are a couple of points to discuss but the direction is good. I was a bit optimistic I could review by EOD today, but it's more realistic I will manage to provide a proper review by the end of this week, thanks for the ping, Adrian!

@asolimando asolimando left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice approach, Adrian, the design makes sense to me. I wish DF had support for DAG plans, it would make reasoning on CTEs and diamond plans much easier.

A few comments inline, the only one I'd like to settle before merging is the safety check in execute (either the check or a doc note), as it can lead to wrong results.

let output = std::future::poll_fn(|cx| output.get_shared(cx)).await?;
// The ordering and the hash partitioning of the scan are only
// correct when one scan partition replays one body partition.
if mirrors_body && output.partitions.len() != output_partitions {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we check only the partition count, but if you have a body with the same count but a different ordering or hash keys, it goes through with wrong results. I have checked with a variant of mirror_scan_rejects_other_body_partition_count: same buffer, unsorted body, the scan still reports [a ASC] and returns [3, 1, 2].

I don't think the problem can reproduce in DF core, but projects doing post-planning rewrites like Ballista or datafusion distributed could, we should probably handle this to be on the safe side.

A cheap fix would be checking the buffer's body in execute has the same hash partitioning, and an ordering that satisfies the scan's.

If you prefer not to fix it, I think we should at least put a note on MaterializedCteExec::new's documentation saying the body must keep the copied properties.

WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This points to the issue this PR closes, but statistics are still only known for single-partition scans. Shall we file a new issue and repoint it?

if !same_columns {
return None;
}
let ordering = body.output_ordering().cloned();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this keeps only the first ordering, and drops the equivalences and constants of the body, but they would still be valid for the scan. Copying the body’s equivalence properties (with the scan schema) would preserve them.

Comment thread datafusion/core/src/physical_planner.rs Outdated
fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => {
write!(f, "MaterializedCteExec: name={}", self.buffer.name())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand we don't want to leak the freezing of CTEs in plans, but plan display is the quickest debugging tool: in EXPLAIN VERBOSE the body disappears after the first rule while the node still shows as a regular MaterializedCteExec, so it looks like the body was dropped, which can be confusing.

Is there a way we can mark the node as frozen, or still show its body, somehow?

adriangb and others added 5 commits November 1, 2026 19:00
…es its scans report

A scan that has the properties of its body checked only the partition
count when it executed. A rewrite of the plan after planning that kept the
count but changed the ordering or the hash keys of the body gave wrong
results. The scan now also checks that the final body has its hash
partitioning, orderings, equivalences, constants and constraints, and
returns an internal error otherwise.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ts scans

A scan with the properties of its body reported only the first output
ordering of the body. It now reports all equivalence properties of the
body (orderings, equivalences and constants), moved to the scan schema.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
While the optimizers run on the rest of the plan, each optimized CTE body
is hidden in a planning-only `FrozenMaterializedCteExec`. That node called
itself `MaterializedCteExec`, so in `EXPLAIN VERBOSE` the body seemed to
disappear after the first rule.

The node now shows its own name, like `OutputRequirementExec`, and the
plans that the observer sees show the body as its first child. Only the
copy shown to the observer has the body as a child; the optimizers still
cannot reach it. Planning without an observer does not make the copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A scan of a materialized CTE with more than one partition reported
unknown statistics for each partition. The planner now computes the
statistics of each partition of the optimized body, with the same
statistics context as the statistics of the whole body, and copies them
to the scans. Partition `p` of a scan with `n` partitions merges the
statistics of the body partitions `p, p + n, ...` that it replays.

`MaterializedCteScanExec::with_body_statistics` (not released yet) takes
the statistics of each body partition.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add sql_planner cases that plan a CTE read once, read twice, and a
chain of six CTEs that each read the previous one twice, for each kind
of CTE, plus one query that mixes all three.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning 636590bf4f4b4f792d3f2fdacedc5fee0ba54950
    Building datafusion v55.1.0 (current)
       Built [  41.762s] (current)
     Parsing datafusion v55.1.0 (current)
      Parsed [   0.026s] (current)
    Building datafusion v55.1.0 (baseline)
       Built [  13.689s] (baseline)
     Parsing datafusion v55.1.0 (baseline)
      Parsed [   0.026s] (baseline)
    Checking datafusion v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.462s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  57.970s] datafusion
    Building datafusion-physical-plan v55.1.0 (current)
       Built [  12.425s] (current)
     Parsing datafusion-physical-plan v55.1.0 (current)
      Parsed [   0.163s] (current)
    Building datafusion-physical-plan v55.1.0 (baseline)
       Built [   9.052s] (baseline)
     Parsing datafusion-physical-plan v55.1.0 (baseline)
      Parsed [   0.168s] (baseline)
    Checking datafusion-physical-plan v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.510s] 229 checks: 228 pass, 1 fail, 0 warn, 31 skip

--- failure method_parameter_count_changed: pub method parameter count changed ---

Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/method_parameter_count_changed.ron

Failed in:
  datafusion_physical_plan::materialized_cte::MaterializedCteScanExec::with_body_statistics takes 1 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks-shared/semver-checks/git-636590bf4f4b4f792d3f2fdacedc5fee0ba54950/e2a3e96438e593e600b6e79c1af4aa2a321a8c97/datafusion/physical-plan/src/materialized_cte.rs:722, but now takes 2 parameters in /home/runner/work/datafusion/datafusion/datafusion/physical-plan/src/materialized_cte.rs:787

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  23.517s] datafusion-physical-plan

@adriangb

adriangb commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

run benchmark sql_planner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c6569850863-4228-fw6fh 6.12.105+ #1 SMP Sat Sep 19 07:55:25 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing mcte-body-first-properties (4824866) to 985bbbf (merge-base) diff

Run configuration
run benchmark sql_planner

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing mcte-body-first-properties (4824866) to 985bbbf (merge-base) diff

Run configuration
run benchmark sql_planner
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                 HEAD                                   mcte-body-first-properties
-----                                                 ----                                   --------------------------
logical_aggregate_with_join                           1.00    343.4±1.71µs        ? ?/sec    1.01    345.6±2.06µs        ? ?/sec
logical_correlated_subquery_exists                    1.00    205.1±0.79µs        ? ?/sec    1.02    208.4±1.47µs        ? ?/sec
logical_correlated_subquery_in                        1.01    209.6±1.48µs        ? ?/sec    1.00    208.3±2.04µs        ? ?/sec
logical_distinct_many_columns                         1.00    291.6±1.30µs        ? ?/sec    1.00    292.6±1.16µs        ? ?/sec
logical_join_4_with_agg_and_filter                    1.01    198.0±0.83µs        ? ?/sec    1.00    195.3±0.99µs        ? ?/sec
logical_join_8_with_agg_sort_limit                    1.01    344.2±1.90µs        ? ?/sec    1.00    341.1±1.97µs        ? ?/sec
logical_join_chain_16                                 1.00    611.3±1.82µs        ? ?/sec    1.01    614.9±2.61µs        ? ?/sec
logical_join_chain_4                                  1.02     75.3±0.31µs        ? ?/sec    1.00     73.5±0.56µs        ? ?/sec
logical_join_chain_8                                  1.00    197.6±0.78µs        ? ?/sec    1.00    197.9±0.63µs        ? ?/sec
logical_multiple_subqueries                           1.00    391.5±1.27µs        ? ?/sec    1.00    390.7±1.80µs        ? ?/sec
logical_nested_cte_4_levels                           1.00    199.9±0.81µs        ? ?/sec    1.00    199.8±0.67µs        ? ?/sec
logical_plan_struct_join_agg_sort                     1.00    125.6±0.78µs        ? ?/sec    1.00    125.2±0.87µs        ? ?/sec
logical_plan_tpcds_all                                1.00     73.6±0.28ms        ? ?/sec    1.00     73.7±0.29ms        ? ?/sec
logical_plan_tpch_all                                 1.00      5.3±0.04ms        ? ?/sec    1.02      5.4±0.02ms        ? ?/sec
logical_scalar_subquery                               1.01    217.6±0.99µs        ? ?/sec    1.00    215.6±0.93µs        ? ?/sec
logical_select_all_from_1000                          1.00      6.8±0.03ms        ? ?/sec    1.04      7.1±0.02ms        ? ?/sec
logical_select_one_from_700                           1.00    227.0±1.75µs        ? ?/sec    1.00    227.1±1.83µs        ? ?/sec
logical_trivial_join_high_numbered_columns            1.00    205.8±1.41µs        ? ?/sec    1.01    208.5±1.51µs        ? ?/sec
logical_trivial_join_low_numbered_columns             1.00    195.1±1.34µs        ? ?/sec    1.00    195.9±1.27µs        ? ?/sec
logical_union_4_branches                              1.00    306.4±2.40µs        ? ?/sec    1.00    307.4±1.58µs        ? ?/sec
logical_union_8_branches                              1.00    619.3±2.86µs        ? ?/sec    1.01    625.6±6.82µs        ? ?/sec
logical_wide_aggregate_1000_exprs                     1.00     38.5±0.11ms        ? ?/sec    1.01     38.9±0.13ms        ? ?/sec
logical_wide_aggregate_100_exprs                      1.00   1562.4±4.94µs        ? ?/sec    1.01   1577.0±5.77µs        ? ?/sec
logical_wide_case_50_exprs                            1.00   1709.3±5.81µs        ? ?/sec    1.01   1731.2±4.17µs        ? ?/sec
logical_wide_filter_200_predicates                    1.00   1292.7±5.13µs        ? ?/sec    1.01   1310.5±5.90µs        ? ?/sec
logical_wide_filter_50_predicates                     1.00    340.9±1.31µs        ? ?/sec    1.01    345.4±1.24µs        ? ?/sec
optimizer_correlated_exists                           1.00    214.4±1.38µs        ? ?/sec    1.01    217.4±0.60µs        ? ?/sec
optimizer_join_4_with_agg_filter                      1.00    393.0±1.32µs        ? ?/sec    1.01    396.5±1.47µs        ? ?/sec
optimizer_join_chain_4                                1.00    153.0±0.36µs        ? ?/sec    1.01    153.8±0.50µs        ? ?/sec
optimizer_join_chain_8                                1.00    528.0±1.22µs        ? ?/sec    1.00    530.2±1.31µs        ? ?/sec
optimizer_select_all_from_1000                        1.01      6.8±0.02ms        ? ?/sec    1.00      6.7±0.01ms        ? ?/sec
optimizer_select_one_from_700                         1.00    236.8±0.61µs        ? ?/sec    1.01    238.0±1.56µs        ? ?/sec
optimizer_tpcds_all                                   1.00    249.3±0.85ms        ? ?/sec    1.00    250.5±2.13ms        ? ?/sec
optimizer_tpch_all                                    1.00     15.1±0.05ms        ? ?/sec    1.00     15.0±0.06ms        ? ?/sec
optimizer_wide_aggregate_100                          1.00  1739.4±11.51µs        ? ?/sec    1.00   1745.1±2.74µs        ? ?/sec
optimizer_wide_filter_200                             1.00      4.0±0.01ms        ? ?/sec    1.01      4.0±0.01ms        ? ?/sec
physical_cte_materialized_chain_6                                                            1.00      2.8±0.01ms        ? ?/sec
physical_cte_materialized_one_ref                                                            1.00    758.5±2.23µs        ? ?/sec
physical_cte_materialized_two_refs                                                           1.00   1024.8±2.54µs        ? ?/sec
physical_cte_mixed                                                                           1.00      3.2±0.01ms        ? ?/sec
physical_cte_not_materialized_chain_6                                                        1.00     17.9±0.05ms        ? ?/sec
physical_cte_not_materialized_one_ref                                                        1.00    689.8±1.75µs        ? ?/sec
physical_cte_not_materialized_two_refs                                                       1.00   1203.1±3.35µs        ? ?/sec
physical_cte_unmarked_chain_6                                                                1.00     17.9±0.12ms        ? ?/sec
physical_cte_unmarked_one_ref                                                                1.00    667.5±2.12µs        ? ?/sec
physical_cte_unmarked_two_refs                                                               1.00   1189.0±2.69µs        ? ?/sec
physical_intersection                                 1.00    501.1±2.12µs        ? ?/sec    1.04    519.1±2.23µs        ? ?/sec
physical_join_consider_sort                           1.00    940.9±2.66µs        ? ?/sec    1.03    965.1±1.73µs        ? ?/sec
physical_join_distinct                                1.00    187.0±1.45µs        ? ?/sec    1.00    187.8±1.41µs        ? ?/sec
physical_many_self_joins                              1.00      7.3±0.05ms        ? ?/sec    1.00      7.3±0.01ms        ? ?/sec
physical_plan_clickbench_all                          1.02    134.0±1.27ms        ? ?/sec    1.00    131.7±0.59ms        ? ?/sec
physical_plan_clickbench_q1                           1.00   1288.2±7.21µs        ? ?/sec    1.00   1291.5±7.98µs        ? ?/sec
physical_plan_clickbench_q10                          1.02   1882.6±4.98µs        ? ?/sec    1.00  1839.8±13.12µs        ? ?/sec
physical_plan_clickbench_q11                          1.01   1997.4±6.08µs        ? ?/sec    1.00   1975.7±8.19µs        ? ?/sec
physical_plan_clickbench_q12                          1.00      2.0±0.01ms        ? ?/sec    1.01      2.1±0.01ms        ? ?/sec
physical_plan_clickbench_q13                          1.00   1896.7±5.99µs        ? ?/sec    1.01  1912.4±15.33µs        ? ?/sec
physical_plan_clickbench_q14                          1.01      2.0±0.01ms        ? ?/sec    1.00      2.0±0.01ms        ? ?/sec
physical_plan_clickbench_q15                          1.00   1953.7±6.25µs        ? ?/sec    1.00  1955.2±11.57µs        ? ?/sec
physical_plan_clickbench_q16                          1.00   1630.9±3.52µs        ? ?/sec    1.05  1708.1±10.82µs        ? ?/sec
physical_plan_clickbench_q17                          1.00   1688.6±4.43µs        ? ?/sec    1.00   1693.7±5.86µs        ? ?/sec
physical_plan_clickbench_q18                          1.00   1514.5±5.47µs        ? ?/sec    1.03  1566.4±13.46µs        ? ?/sec
physical_plan_clickbench_q19                          1.00   1843.3±5.60µs        ? ?/sec    1.06  1949.2±17.30µs        ? ?/sec
physical_plan_clickbench_q2                           1.00   1652.4±6.61µs        ? ?/sec    1.00   1645.5±8.24µs        ? ?/sec
physical_plan_clickbench_q20                          1.00   1467.5±5.42µs        ? ?/sec    1.01   1481.4±9.34µs        ? ?/sec
physical_plan_clickbench_q21                          1.00   1652.4±4.31µs        ? ?/sec    1.04  1715.1±23.79µs        ? ?/sec
physical_plan_clickbench_q22                          1.00      2.0±0.01ms        ? ?/sec    1.02      2.0±0.02ms        ? ?/sec
physical_plan_clickbench_q23                          1.00      2.1±0.01ms        ? ?/sec    1.01      2.1±0.01ms        ? ?/sec
physical_plan_clickbench_q24                          1.00      4.8±0.01ms        ? ?/sec    1.03      4.9±0.06ms        ? ?/sec
physical_plan_clickbench_q25                          1.00   1742.1±6.10µs        ? ?/sec    1.04   1817.7±7.02µs        ? ?/sec
physical_plan_clickbench_q26                          1.00   1606.9±6.67µs        ? ?/sec    1.02   1642.2±5.32µs        ? ?/sec
physical_plan_clickbench_q27                          1.00   1774.0±6.14µs        ? ?/sec    1.02   1814.7±8.89µs        ? ?/sec
physical_plan_clickbench_q28                          1.00      2.1±0.01ms        ? ?/sec    1.01      2.1±0.01ms        ? ?/sec
physical_plan_clickbench_q29                          1.00      2.2±0.01ms        ? ?/sec    1.02      2.2±0.01ms        ? ?/sec
physical_plan_clickbench_q3                           1.01   1540.3±6.23µs        ? ?/sec    1.00   1531.6±4.85µs        ? ?/sec
physical_plan_clickbench_q30                          1.00     12.6±0.11ms        ? ?/sec    1.01     12.7±0.07ms        ? ?/sec
physical_plan_clickbench_q31                          1.00      2.2±0.01ms        ? ?/sec    1.02      2.2±0.01ms        ? ?/sec
physical_plan_clickbench_q32                          1.00      2.2±0.01ms        ? ?/sec    1.00      2.2±0.02ms        ? ?/sec
physical_plan_clickbench_q33                          1.00   1849.7±8.06µs        ? ?/sec    1.02   1886.2±8.06µs        ? ?/sec
physical_plan_clickbench_q34                          1.00   1664.1±6.55µs        ? ?/sec    1.03  1707.5±11.51µs        ? ?/sec
physical_plan_clickbench_q35                          1.00   1690.5±4.37µs        ? ?/sec    1.01   1712.6±6.17µs        ? ?/sec
physical_plan_clickbench_q36                          1.00   1918.8±5.19µs        ? ?/sec    1.01  1945.9±10.09µs        ? ?/sec
physical_plan_clickbench_q37                          1.00      2.4±0.01ms        ? ?/sec    1.01      2.4±0.02ms        ? ?/sec
physical_plan_clickbench_q38                          1.00      2.4±0.01ms        ? ?/sec    1.01      2.4±0.01ms        ? ?/sec
physical_plan_clickbench_q39                          1.00      2.4±0.01ms        ? ?/sec    1.01      2.4±0.02ms        ? ?/sec
physical_plan_clickbench_q4                           1.00  1384.3±12.24µs        ? ?/sec    1.01   1396.0±5.73µs        ? ?/sec
physical_plan_clickbench_q40                          1.00      3.1±0.03ms        ? ?/sec    1.00      3.1±0.02ms        ? ?/sec
physical_plan_clickbench_q41                          1.01      2.8±0.03ms        ? ?/sec    1.00      2.7±0.01ms        ? ?/sec
physical_plan_clickbench_q42                          1.03      2.8±0.02ms        ? ?/sec    1.00      2.7±0.01ms        ? ?/sec
physical_plan_clickbench_q43                          1.02      3.0±0.03ms        ? ?/sec    1.00      2.9±0.01ms        ? ?/sec
physical_plan_clickbench_q44                          1.00   1473.2±6.71µs        ? ?/sec    1.00   1467.5±7.63µs        ? ?/sec
physical_plan_clickbench_q45                          1.00   1472.0±5.70µs        ? ?/sec    1.00   1473.8±5.72µs        ? ?/sec
physical_plan_clickbench_q46                          1.00  1734.9±10.74µs        ? ?/sec    1.00   1742.8±7.66µs        ? ?/sec
physical_plan_clickbench_q47                          1.00      2.2±0.01ms        ? ?/sec    1.01      2.3±0.02ms        ? ?/sec
physical_plan_clickbench_q48                          1.02      2.5±0.01ms        ? ?/sec    1.00      2.4±0.01ms        ? ?/sec
physical_plan_clickbench_q49                          1.03      2.5±0.02ms        ? ?/sec    1.00      2.5±0.01ms        ? ?/sec
physical_plan_clickbench_q5                           1.02  1504.8±14.08µs        ? ?/sec    1.00   1479.7±9.28µs        ? ?/sec
physical_plan_clickbench_q50                          1.03      2.6±0.03ms        ? ?/sec    1.00      2.6±0.01ms        ? ?/sec
physical_plan_clickbench_q51                          1.00  1801.3±10.75µs        ? ?/sec    1.00   1803.7±7.25µs        ? ?/sec
physical_plan_clickbench_q52                          1.01      2.3±0.02ms        ? ?/sec    1.00      2.3±0.01ms        ? ?/sec
physical_plan_clickbench_q53                          1.00  1695.3±10.68µs        ? ?/sec    1.00   1687.8±7.25µs        ? ?/sec
physical_plan_clickbench_q54                          1.00  1687.6±14.54µs        ? ?/sec    1.01   1711.7±7.75µs        ? ?/sec
physical_plan_clickbench_q55                          1.04  1684.3±24.62µs        ? ?/sec    1.00   1616.2±7.76µs        ? ?/sec
physical_plan_clickbench_q56                          1.02  1638.9±23.48µs        ? ?/sec    1.00   1599.2±5.24µs        ? ?/sec
physical_plan_clickbench_q57                          1.00  1736.6±20.52µs        ? ?/sec    1.00  1740.3±17.03µs        ? ?/sec
physical_plan_clickbench_q58                          1.04      2.2±0.05ms        ? ?/sec    1.00      2.1±0.01ms        ? ?/sec
physical_plan_clickbench_q59                          1.04  1687.5±29.61µs        ? ?/sec    1.00   1628.6±8.42µs        ? ?/sec
physical_plan_clickbench_q6                           1.00   1478.6±6.81µs        ? ?/sec    1.03   1517.6±8.74µs        ? ?/sec
physical_plan_clickbench_q60                          1.03  1663.4±22.71µs        ? ?/sec    1.00   1612.0±5.14µs        ? ?/sec
physical_plan_clickbench_q7                           1.00   1329.9±5.73µs        ? ?/sec    1.00   1335.0±4.15µs        ? ?/sec
physical_plan_clickbench_q8                           1.00   1828.3±6.42µs        ? ?/sec    1.00   1834.5±7.62µs        ? ?/sec
physical_plan_clickbench_q9                           1.00   1756.1±4.78µs        ? ?/sec    1.01   1777.8±6.62µs        ? ?/sec
physical_plan_struct_join_agg_sort                    1.00   1193.6±4.05µs        ? ?/sec    1.00   1196.1±2.13µs        ? ?/sec
physical_plan_tpcds_all                               1.00    561.4±3.46ms        ? ?/sec    1.01    567.3±2.81ms        ? ?/sec
physical_plan_tpch_all                                1.00     39.5±0.08ms        ? ?/sec    1.01     40.1±0.17ms        ? ?/sec
physical_plan_tpch_q1                                 1.01   1408.1±4.40µs        ? ?/sec    1.00   1398.1±2.58µs        ? ?/sec
physical_plan_tpch_q10                                1.00      2.1±0.00ms        ? ?/sec    1.00      2.1±0.00ms        ? ?/sec
physical_plan_tpch_q11                                1.00      2.1±0.00ms        ? ?/sec    1.01      2.1±0.01ms        ? ?/sec
physical_plan_tpch_q12                                1.00   1188.5±2.58µs        ? ?/sec    1.02   1216.3±3.14µs        ? ?/sec
physical_plan_tpch_q13                                1.00    947.3±2.52µs        ? ?/sec    1.02    968.3±1.99µs        ? ?/sec
physical_plan_tpch_q14                                1.00   1216.3±2.63µs        ? ?/sec    1.03   1250.7±2.45µs        ? ?/sec
physical_plan_tpch_q16                                1.02   1554.2±2.89µs        ? ?/sec    1.00   1527.6±2.26µs        ? ?/sec
physical_plan_tpch_q17                                1.00   1507.7±3.26µs        ? ?/sec    1.00   1504.0±3.81µs        ? ?/sec
physical_plan_tpch_q18                                1.00   1741.0±4.55µs        ? ?/sec    1.00   1735.3±3.27µs        ? ?/sec
physical_plan_tpch_q19                                1.00   1958.2±4.69µs        ? ?/sec    1.00   1959.7±3.36µs        ? ?/sec
physical_plan_tpch_q2                                 1.00      3.2±0.02ms        ? ?/sec    1.00      3.2±0.00ms        ? ?/sec
physical_plan_tpch_q20                                1.00   1931.9±4.71µs        ? ?/sec    1.03  1988.2±16.60µs        ? ?/sec
physical_plan_tpch_q21                                1.00      2.4±0.00ms        ? ?/sec    1.01      2.4±0.00ms        ? ?/sec
physical_plan_tpch_q22                                1.00   1468.2±3.03µs        ? ?/sec    1.00   1472.4±2.39µs        ? ?/sec
physical_plan_tpch_q3                                 1.00   1600.6±4.59µs        ? ?/sec    1.02   1630.1±3.15µs        ? ?/sec
physical_plan_tpch_q4                                 1.00   1055.2±6.61µs        ? ?/sec    1.01   1069.1±3.02µs        ? ?/sec
physical_plan_tpch_q5                                 1.01      2.2±0.02ms        ? ?/sec    1.00      2.2±0.00ms        ? ?/sec
physical_plan_tpch_q6                                 1.00    694.0±1.81µs        ? ?/sec    1.01    701.9±1.49µs        ? ?/sec
physical_plan_tpch_q7                                 1.01      2.5±0.01ms        ? ?/sec    1.00      2.5±0.00ms        ? ?/sec
physical_plan_tpch_q8                                 1.01      3.3±0.03ms        ? ?/sec    1.00      3.3±0.01ms        ? ?/sec
physical_plan_tpch_q9                                 1.00      2.5±0.01ms        ? ?/sec    1.00      2.5±0.00ms        ? ?/sec
physical_select_aggregates_from_200                   1.01      7.4±0.03ms        ? ?/sec    1.00      7.3±0.01ms        ? ?/sec
physical_select_all_from_1000                         1.00     17.0±0.04ms        ? ?/sec    1.02     17.3±0.06ms        ? ?/sec
physical_select_one_from_700                          1.01    643.7±5.75µs        ? ?/sec    1.00    637.6±7.37µs        ? ?/sec
physical_sorted_union_order_by_10_int64               1.00      3.5±0.01ms        ? ?/sec    1.00      3.5±0.00ms        ? ?/sec
physical_sorted_union_order_by_10_uint64              1.03      6.9±0.05ms        ? ?/sec    1.00      6.7±0.01ms        ? ?/sec
physical_sorted_union_order_by_50_int64               1.02     75.8±0.43ms        ? ?/sec    1.00     74.6±0.18ms        ? ?/sec
physical_sorted_union_order_by_50_uint64              1.02    257.7±1.79ms        ? ?/sec    1.00    252.6±0.90ms        ? ?/sec
physical_theta_join_consider_sort                     1.00    968.3±3.02µs        ? ?/sec    1.03    993.0±1.43µs        ? ?/sec
physical_unnest_to_join                               1.00    552.7±2.45µs        ? ?/sec    1.04    572.6±1.29µs        ? ?/sec
physical_window_function_partition_by_12_on_values    1.00    633.3±2.20µs        ? ?/sec    1.01    640.5±1.16µs        ? ?/sec
physical_window_function_partition_by_30_on_values    1.00   1198.4±9.60µs        ? ?/sec    1.01   1214.6±2.78µs        ? ?/sec
physical_window_function_partition_by_4_on_values     1.00    395.0±0.90µs        ? ?/sec    1.00    396.7±1.43µs        ? ?/sec
physical_window_function_partition_by_7_on_values     1.00    484.2±1.61µs        ? ?/sec    1.00    483.3±1.19µs        ? ?/sec
physical_window_function_partition_by_8_on_values     1.02    519.6±1.85µs        ? ?/sec    1.00    509.1±1.40µs        ? ?/sec
with_param_values_many_columns                        1.00    368.3±1.80µs        ? ?/sec    1.02    376.0±1.96µs        ? ?/sec

Resource Usage

sql_planner — base (merge-base)

Metric Value
Wall time 2025.4s
Peak memory 139.6 MiB
Avg memory 90.1 MiB
CPU user 1972.4s
CPU sys 1.5s
Peak spill 0 B

sql_planner — branch

Metric Value
Wall time 2125.5s
Peak memory 140.0 MiB
Avg memory 94.3 MiB
CPU user 2104.5s
CPU sys 1.5s
Peak spill 0 B

File an issue against this benchmark runner

@asolimando asolimando left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Adrian, I saw you addressed all my comments and went even further, with the full equivalence properties checked at execution and per-partition statistics, LGTM!

galaxytemple pushed a commit to galaxytemple/datafusion that referenced this pull request Nov 3, 2026
…27005)

## Which issue does this PR close?

- Related to apache#26424 and apache#26428. No
issue of its own: this PR only adds benchmarks.

## Rationale for this change

`MATERIALIZED` CTEs (apache#26271) are planned differently
from inlined CTEs: each body is optimized on its own before the rest of
the plan. apache#26428 adds more work to that step. No
planner benchmark plans a materialized CTE today, so there is no
standard way to see what this costs, or to compare it with inlining.

## What changes are included in this PR?

New `sql_planner` cases, which plan a query from SQL to a physical plan.
The PostgreSQL dialect is used, because sqlparser only parses `[NOT]
MATERIALIZED` for that dialect. For each kind of CTE (unmarked,
`MATERIALIZED`, `NOT MATERIALIZED`) there are three cases:

- `physical_cte_<kind>_one_ref`: the CTE is read once.
- `physical_cte_<kind>_two_refs`: the CTE is joined with itself.
- `physical_cte_<kind>_chain_6`: six CTEs, each joining the previous one
with itself.

`physical_cte_mixed` has one CTE of each kind, each read twice.

Results on `main` (Criterion means):

| Case | unmarked | `MATERIALIZED` | `NOT MATERIALIZED` |
| --- | --- | --- | --- |
| one reference | 242 µs | 264 µs | 245 µs |
| two references | 474 µs | 360 µs | 481 µs |
| chain of 6 | 8.25 ms | 0.92 ms | 8.33 ms |

`physical_cte_mixed` takes 1.36 ms.

An inlined chain copies the first CTE 32 times. So it takes about 9
times as long to plan as the materialized chain.

## What is the testing strategy for this PR?

This PR has only benchmarks. They run with:

```bash
cargo bench -p datafusion --bench sql_planner -- physical_cte
```

The `sql_planner` benchmark needs ClickBench data. The checked-in
fixture works: put
`datafusion/core/tests/data/clickbench_hits_10.parquet` in a directory,
and set `CLICKBENCH_DATA_PATH` to that directory.

## Are there any user-facing changes?

No.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change core Core DataFusion crate physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report the ordering and partitioning of MATERIALIZED CTE scans by optimizing each CTE body first

4 participants