Skip to content

Commit ff774c6

Browse files
refactor(physical-plan): externalize statistics traversal into StatisticsContext (#23051)
## Which issue does this PR close? - Closes #22958. ## Rationale for this change Follow-up to #21815. Per @2010YOUY01's suggestion there, this decouples statistics traversal/caching from each operator's computation: operators now express only local propagation, while the walk and cache live in an external `StatisticsContext`. ## What changes are included in this PR? - `StatisticsContext` owns the bottom-up walk + per-walk cache; `compute(plan, args)` resolves children, then calls the operator. - `ExecutionPlan::statistics_from_inputs(input_stats, args)` — stateless local propagation from pre-resolved child stats; default delegates to deprecated `partition_statistics`. - `ExecutionPlan::child_stats_requests(partition) -> Vec<ChildStats>` — per-child directive (`At(Option<usize>)` / `Skip`) for which partition each child is computed at (e.g. broadcast joins request the build side overall; `UnionExec` skips non-owning inputs). - Removes the unreleased `statistics_with_args`; `partition_statistics` stays deprecated. - Migrates all operators/callers; updates the 55.0.0 upgrade guide. ## Are these changes tested? Yes — existing statistics tests now run through `StatisticsContext::compute`, plus new unit tests for the cache and the `compute_statistics` benchmark. ## Are there any user-facing changes? Yes (public `ExecutionPlan` API): `partition_statistics` deprecated in favor of `statistics_from_inputs`; new `StatisticsContext` / `ChildStats`; unreleased `statistics_with_args` removed. Upgrade guide updated. --- Reviewer note: `statistics_from_inputs` takes `args: &StatisticsArgs` (currently just `partition`) as a non-breaking extension seam rather than a bare `partition` — happy to change if preferred (see #22958). ---- Disclaimer: I used AI to assist in the code generation, I have manually reviewed the output and it matches my intention and understanding. --------- Co-authored-by: xudong.w <wxd963996380@gmail.com>
1 parent 3a29d6b commit ff774c6

54 files changed

Lines changed: 1023 additions & 596 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

datafusion-examples/examples/relation_planner/table_sample.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ use datafusion::{
108108
},
109109
physical_expr::EquivalenceProperties,
110110
physical_plan::{
111-
DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties, StatisticsArgs,
111+
ChildStats, DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties,
112+
StatisticsArgs,
112113
metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet, RecordOutput},
113114
},
114115
physical_planner::{DefaultPhysicalPlanner, ExtensionPlanner, PhysicalPlanner},
@@ -722,10 +723,16 @@ impl ExecutionPlan for SampleExec {
722723
Some(self.metrics.clone_inner())
723724
}
724725

725-
fn statistics_with_args(&self, args: &StatisticsArgs) -> Result<Arc<Statistics>> {
726-
let mut stats = Arc::unwrap_or_clone(
727-
args.compute_child_statistics(&self.input, args.partition())?,
728-
);
726+
fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
727+
vec![ChildStats::At(partition)]
728+
}
729+
730+
fn statistics_from_inputs(
731+
&self,
732+
input_stats: &[Arc<Statistics>],
733+
_args: &StatisticsArgs,
734+
) -> Result<Arc<Statistics>> {
735+
let mut stats = input_stats[0].as_ref().clone();
729736
let ratio = self.upper_bound - self.lower_bound;
730737

731738
// Scale statistics by sampling ratio (inexact due to randomness)

datafusion/core/src/datasource/file_format/csv.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ mod tests {
4545
use datafusion_datasource::file_format::FileFormat;
4646
use datafusion_datasource::write::BatchSerializer;
4747
use datafusion_expr::{col, lit};
48-
use datafusion_physical_plan::statistics::StatisticsArgs;
48+
use datafusion_physical_plan::statistics::{StatisticsArgs, StatisticsContext};
4949
use datafusion_physical_plan::{ExecutionPlan, collect};
5050

5151
use arrow::array::{
@@ -217,11 +217,14 @@ mod tests {
217217

218218
// test metadata
219219
assert_eq!(
220-
exec.statistics_with_args(&StatisticsArgs::new())?.num_rows,
220+
StatisticsContext::new()
221+
.compute(exec.as_ref(), &StatisticsArgs::new())?
222+
.num_rows,
221223
Precision::Absent
222224
);
223225
assert_eq!(
224-
exec.statistics_with_args(&StatisticsArgs::new())?
226+
StatisticsContext::new()
227+
.compute(exec.as_ref(), &StatisticsArgs::new())?
225228
.total_byte_size,
226229
Precision::Absent
227230
);

datafusion/core/src/datasource/file_format/json.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ mod tests {
3636
BatchDeserializer, DecoderDeserializer, DeserializerOutput,
3737
};
3838
use datafusion_datasource::file_format::FileFormat;
39-
use datafusion_physical_plan::statistics::StatisticsArgs;
39+
use datafusion_physical_plan::statistics::{StatisticsArgs, StatisticsContext};
4040
use datafusion_physical_plan::{ExecutionPlan, collect};
4141

4242
use arrow::compute::concat_batches;
@@ -119,11 +119,14 @@ mod tests {
119119

120120
// test metadata
121121
assert_eq!(
122-
exec.statistics_with_args(&StatisticsArgs::new())?.num_rows,
122+
StatisticsContext::new()
123+
.compute(exec.as_ref(), &StatisticsArgs::new())?
124+
.num_rows,
123125
Precision::Absent
124126
);
125127
assert_eq!(
126-
exec.statistics_with_args(&StatisticsArgs::new())?
128+
StatisticsContext::new()
129+
.compute(exec.as_ref(), &StatisticsArgs::new())?
127130
.total_byte_size,
128131
Precision::Absent
129132
);

datafusion/core/src/datasource/file_format/parquet.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ mod tests {
141141
use datafusion_execution::object_store::ObjectStoreUrl;
142142
use datafusion_execution::runtime_env::RuntimeEnv;
143143
use datafusion_expr::dml::InsertOp;
144-
use datafusion_physical_plan::statistics::StatisticsArgs;
144+
use datafusion_physical_plan::statistics::{StatisticsArgs, StatisticsContext};
145145
use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
146146
use datafusion_physical_plan::{ExecutionPlan, collect};
147147

@@ -716,12 +716,15 @@ mod tests {
716716

717717
// test metadata
718718
assert_eq!(
719-
exec.statistics_with_args(&StatisticsArgs::new())?.num_rows,
719+
StatisticsContext::new()
720+
.compute(exec.as_ref(), &StatisticsArgs::new())?
721+
.num_rows,
720722
Precision::Exact(8)
721723
);
722724
// TODO correct byte size: https://github.com/apache/datafusion/issues/14936
723725
assert_eq!(
724-
exec.statistics_with_args(&StatisticsArgs::new())?
726+
StatisticsContext::new()
727+
.compute(exec.as_ref(), &StatisticsArgs::new())?
725728
.total_byte_size,
726729
Precision::Absent,
727730
);
@@ -766,11 +769,14 @@ mod tests {
766769

767770
// note: even if the limit is set, the executor rounds up to the batch size
768771
assert_eq!(
769-
exec.statistics_with_args(&StatisticsArgs::new())?.num_rows,
772+
StatisticsContext::new()
773+
.compute(exec.as_ref(), &StatisticsArgs::new())?
774+
.num_rows,
770775
Precision::Exact(8)
771776
);
772777
assert_eq!(
773-
exec.statistics_with_args(&StatisticsArgs::new())?
778+
StatisticsContext::new()
779+
.compute(exec.as_ref(), &StatisticsArgs::new())?
774780
.total_byte_size,
775781
Precision::Absent,
776782
);

datafusion/core/src/datasource/listing/table.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ mod tests {
145145
use datafusion_physical_expr::expressions::{Column, binary};
146146
use datafusion_physical_expr_common::sort_expr::LexOrdering;
147147
use datafusion_physical_plan::empty::EmptyExec;
148-
use datafusion_physical_plan::statistics::StatisticsArgs;
148+
use datafusion_physical_plan::statistics::{StatisticsArgs, StatisticsContext};
149149
use datafusion_physical_plan::{
150150
ExecutionPlanProperties, Partitioning, RangePartitioning, SplitPoint, collect,
151151
};
@@ -266,11 +266,14 @@ mod tests {
266266

267267
// test metadata
268268
assert_eq!(
269-
exec.statistics_with_args(&StatisticsArgs::new())?.num_rows,
269+
StatisticsContext::new()
270+
.compute(exec.as_ref(), &StatisticsArgs::new())?
271+
.num_rows,
270272
Precision::Exact(8)
271273
);
272274
assert_eq!(
273-
exec.statistics_with_args(&StatisticsArgs::new())?
275+
StatisticsContext::new()
276+
.compute(exec.as_ref(), &StatisticsArgs::new())?
274277
.total_byte_size,
275278
Precision::Absent,
276279
);
@@ -1612,16 +1615,16 @@ mod tests {
16121615

16131616
let exec_default = table_default.scan(&state, None, &[], None).await?;
16141617
assert_eq!(
1615-
exec_default
1616-
.statistics_with_args(&StatisticsArgs::new())?
1618+
StatisticsContext::new()
1619+
.compute(exec_default.as_ref(), &StatisticsArgs::new())?
16171620
.num_rows,
16181621
Precision::Exact(8)
16191622
);
16201623

16211624
// TODO correct byte size: https://github.com/apache/datafusion/issues/14936
16221625
assert_eq!(
1623-
exec_default
1624-
.statistics_with_args(&StatisticsArgs::new())?
1626+
StatisticsContext::new()
1627+
.compute(exec_default.as_ref(), &StatisticsArgs::new())?
16251628
.total_byte_size,
16261629
Precision::Absent
16271630
);
@@ -1638,14 +1641,14 @@ mod tests {
16381641

16391642
let exec_disabled = table_disabled.scan(&state, None, &[], None).await?;
16401643
assert_eq!(
1641-
exec_disabled
1642-
.statistics_with_args(&StatisticsArgs::new())?
1644+
StatisticsContext::new()
1645+
.compute(exec_disabled.as_ref(), &StatisticsArgs::new())?
16431646
.num_rows,
16441647
Precision::Absent
16451648
);
16461649
assert_eq!(
1647-
exec_disabled
1648-
.statistics_with_args(&StatisticsArgs::new())?
1650+
StatisticsContext::new()
1651+
.compute(exec_disabled.as_ref(), &StatisticsArgs::new())?
16491652
.total_byte_size,
16501653
Precision::Absent
16511654
);
@@ -1662,15 +1665,15 @@ mod tests {
16621665

16631666
let exec_enabled = table_enabled.scan(&state, None, &[], None).await?;
16641667
assert_eq!(
1665-
exec_enabled
1666-
.statistics_with_args(&StatisticsArgs::new())?
1668+
StatisticsContext::new()
1669+
.compute(exec_enabled.as_ref(), &StatisticsArgs::new())?
16671670
.num_rows,
16681671
Precision::Exact(8)
16691672
);
16701673
// TODO correct byte size: https://github.com/apache/datafusion/issues/14936
16711674
assert_eq!(
1672-
exec_enabled
1673-
.statistics_with_args(&StatisticsArgs::new())?
1675+
StatisticsContext::new()
1676+
.compute(exec_enabled.as_ref(), &StatisticsArgs::new())?
16741677
.total_byte_size,
16751678
Precision::Absent,
16761679
);

datafusion/core/tests/custom_sources_cases/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,11 @@ impl ExecutionPlan for CustomExecutionPlan {
179179
Ok(Box::pin(TestCustomRecordBatchStream { nb_batch: 1 }))
180180
}
181181

182-
fn statistics_with_args(&self, args: &StatisticsArgs) -> Result<Arc<Statistics>> {
182+
fn statistics_from_inputs(
183+
&self,
184+
_input_stats: &[Arc<Statistics>],
185+
args: &StatisticsArgs,
186+
) -> Result<Arc<Statistics>> {
183187
if args.partition().is_some() {
184188
return Ok(Arc::new(Statistics::new_unknown(&self.schema())));
185189
}

datafusion/core/tests/custom_sources_cases/statistics.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ use datafusion::{
3535
use datafusion_catalog::Session;
3636
use datafusion_common::{project_schema, stats::Precision};
3737
use datafusion_physical_expr::EquivalenceProperties;
38-
use datafusion_physical_plan::StatisticsArgs;
3938
use datafusion_physical_plan::execution_plan::{Boundedness, EmissionType};
39+
use datafusion_physical_plan::{StatisticsArgs, StatisticsContext};
4040

4141
use async_trait::async_trait;
4242

@@ -174,7 +174,11 @@ impl ExecutionPlan for StatisticsValidation {
174174
unimplemented!("This plan only serves for testing statistics")
175175
}
176176

177-
fn statistics_with_args(&self, args: &StatisticsArgs) -> Result<Arc<Statistics>> {
177+
fn statistics_from_inputs(
178+
&self,
179+
_input_stats: &[Arc<Statistics>],
180+
args: &StatisticsArgs,
181+
) -> Result<Arc<Statistics>> {
178182
if args.partition().is_some() {
179183
Ok(Arc::new(Statistics::new_unknown(&self.schema)))
180184
} else {
@@ -233,7 +237,8 @@ async fn sql_basic() -> Result<()> {
233237
// the statistics should be those of the source
234238
assert_eq!(
235239
stats,
236-
*physical_plan.statistics_with_args(&StatisticsArgs::new())?
240+
*StatisticsContext::new()
241+
.compute(physical_plan.as_ref(), &StatisticsArgs::new())?
237242
);
238243

239244
Ok(())
@@ -250,7 +255,8 @@ async fn sql_filter() -> Result<()> {
250255
.unwrap();
251256

252257
let physical_plan = df.create_physical_plan().await.unwrap();
253-
let stats = physical_plan.statistics_with_args(&StatisticsArgs::new())?;
258+
let stats = StatisticsContext::new()
259+
.compute(physical_plan.as_ref(), &StatisticsArgs::new())?;
254260
assert_eq!(stats.num_rows, Precision::Inexact(7));
255261

256262
Ok(())
@@ -265,7 +271,8 @@ async fn sql_limit() -> Result<()> {
265271
let physical_plan = df.create_physical_plan().await.unwrap();
266272
// when the limit is smaller than the original number of lines we mark the statistics as inexact
267273
// and cap NDV at the new row count
268-
let limit_stats = physical_plan.statistics_with_args(&StatisticsArgs::new())?;
274+
let limit_stats = StatisticsContext::new()
275+
.compute(physical_plan.as_ref(), &StatisticsArgs::new())?;
269276
assert_eq!(limit_stats.num_rows, Precision::Exact(5));
270277
// c1: NDV=2 stays at 2 (already below limit of 5)
271278
assert_eq!(
@@ -286,7 +293,8 @@ async fn sql_limit() -> Result<()> {
286293
// when the limit is larger than the original number of lines, statistics remain unchanged
287294
assert_eq!(
288295
stats,
289-
*physical_plan.statistics_with_args(&StatisticsArgs::new())?
296+
*StatisticsContext::new()
297+
.compute(physical_plan.as_ref(), &StatisticsArgs::new())?
290298
);
291299

292300
Ok(())
@@ -304,7 +312,8 @@ async fn sql_window() -> Result<()> {
304312

305313
let physical_plan = df.create_physical_plan().await.unwrap();
306314

307-
let result = physical_plan.statistics_with_args(&StatisticsArgs::new())?;
315+
let result = StatisticsContext::new()
316+
.compute(physical_plan.as_ref(), &StatisticsArgs::new())?;
308317

309318
assert_eq!(stats.num_rows, result.num_rows);
310319
let col_stats = &result.column_statistics;

datafusion/core/tests/parquet/file_statistics.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use datafusion_physical_optimizer::PhysicalOptimizerRule;
4545
use datafusion_physical_optimizer::filter_pushdown::FilterPushdown;
4646
use datafusion_physical_plan::ExecutionPlan;
4747
use datafusion_physical_plan::filter::FilterExec;
48-
use datafusion_physical_plan::statistics::StatisticsArgs;
48+
use datafusion_physical_plan::statistics::{StatisticsArgs, StatisticsContext};
4949
use tempfile::tempdir;
5050

5151
#[tokio::test]
@@ -65,7 +65,8 @@ async fn check_stats_precision_with_filter_pushdown() {
6565
// Scan without filter, stats are exact
6666
let exec = table.scan(&state, None, &[], None).await.unwrap();
6767
assert_eq!(
68-
exec.statistics_with_args(&StatisticsArgs::new())
68+
StatisticsContext::new()
69+
.compute(exec.as_ref(), &StatisticsArgs::new())
6970
.unwrap()
7071
.num_rows,
7172
Precision::Exact(8),
@@ -99,8 +100,8 @@ async fn check_stats_precision_with_filter_pushdown() {
99100
);
100101
// Scan with filter pushdown, stats are inexact
101102
assert_eq!(
102-
optimized_exec
103-
.statistics_with_args(&StatisticsArgs::new())
103+
StatisticsContext::new()
104+
.compute(optimized_exec.as_ref(), &StatisticsArgs::new())
104105
.unwrap()
105106
.num_rows,
106107
Precision::Inexact(8),
@@ -135,15 +136,15 @@ async fn load_table_stats_with_session_level_cache() {
135136
let exec1 = table1.scan(&state1, None, &[], None).await.unwrap();
136137

137138
assert_eq!(
138-
exec1
139-
.statistics_with_args(&StatisticsArgs::new())
139+
StatisticsContext::new()
140+
.compute(exec1.as_ref(), &StatisticsArgs::new())
140141
.unwrap()
141142
.num_rows,
142143
Precision::Exact(8)
143144
);
144145
assert_eq!(
145-
exec1
146-
.statistics_with_args(&StatisticsArgs::new())
146+
StatisticsContext::new()
147+
.compute(exec1.as_ref(), &StatisticsArgs::new())
147148
.unwrap()
148149
.total_byte_size,
149150
// Byte size is absent because we cannot estimate the output size
@@ -157,15 +158,15 @@ async fn load_table_stats_with_session_level_cache() {
157158
assert_eq!(get_static_cache_size(&state2), 0);
158159
let exec2 = table2.scan(&state2, None, &[], None).await.unwrap();
159160
assert_eq!(
160-
exec2
161-
.statistics_with_args(&StatisticsArgs::new())
161+
StatisticsContext::new()
162+
.compute(exec2.as_ref(), &StatisticsArgs::new())
162163
.unwrap()
163164
.num_rows,
164165
Precision::Exact(8)
165166
);
166167
assert_eq!(
167-
exec2
168-
.statistics_with_args(&StatisticsArgs::new())
168+
StatisticsContext::new()
169+
.compute(exec2.as_ref(), &StatisticsArgs::new())
169170
.unwrap()
170171
.total_byte_size,
171172
// Absent because the data contains variable length columns
@@ -178,15 +179,15 @@ async fn load_table_stats_with_session_level_cache() {
178179
assert_eq!(get_static_cache_size(&state1), 1);
179180
let exec3 = table1.scan(&state1, None, &[], None).await.unwrap();
180181
assert_eq!(
181-
exec3
182-
.statistics_with_args(&StatisticsArgs::new())
182+
StatisticsContext::new()
183+
.compute(exec3.as_ref(), &StatisticsArgs::new())
183184
.unwrap()
184185
.num_rows,
185186
Precision::Exact(8)
186187
);
187188
assert_eq!(
188-
exec3
189-
.statistics_with_args(&StatisticsArgs::new())
189+
StatisticsContext::new()
190+
.compute(exec3.as_ref(), &StatisticsArgs::new())
190191
.unwrap()
191192
.total_byte_size,
192193
// Absent because the data contains variable length columns
@@ -252,7 +253,9 @@ async fn anonymous_parquet_stats_cache_with_explicit_wider_schema() {
252253
.await
253254
.unwrap();
254255

255-
let stats = plan.statistics_with_args(&StatisticsArgs::new()).unwrap();
256+
let stats = StatisticsContext::new()
257+
.compute(plan.as_ref(), &StatisticsArgs::new())
258+
.unwrap();
256259
assert_eq!(stats.column_statistics.len(), 2);
257260
assert_eq!(stats.column_statistics[1].null_count, Precision::Exact(1));
258261

0 commit comments

Comments
 (0)