datafusion_physical_plan/
statistics.rs1use crate::ExecutionPlan;
24use datafusion_common::{
25 Result, Statistics, assert_eq_or_internal_err, assert_or_internal_err,
26};
27use std::cell::RefCell;
28use std::collections::HashMap;
29use std::rc::Rc;
30use std::sync::Arc;
31
32#[derive(Debug, Default)]
41struct StatsCache(HashMap<(usize, Option<usize>), Arc<Statistics>>);
42
43impl StatsCache {
44 fn get(
45 &self,
46 plan: &dyn ExecutionPlan,
47 partition: Option<usize>,
48 ) -> Option<&Arc<Statistics>> {
49 let key = (
50 plan as *const dyn ExecutionPlan as *const () as usize,
51 partition,
52 );
53 self.0.get(&key)
54 }
55
56 fn insert(
57 &mut self,
58 plan: &dyn ExecutionPlan,
59 partition: Option<usize>,
60 stats: Arc<Statistics>,
61 ) {
62 let key = (
63 plan as *const dyn ExecutionPlan as *const () as usize,
64 partition,
65 );
66 self.0.insert(key, stats);
67 }
68}
69
70#[derive(Debug, Default, Clone)]
74pub struct StatisticsArgs {
75 partition: Option<usize>,
76}
77
78impl StatisticsArgs {
79 pub fn new() -> Self {
84 Default::default()
85 }
86
87 pub fn set_partition(&mut self, partition: Option<usize>) {
93 self.partition = partition;
94 }
95
96 pub fn with_partition(mut self, partition: Option<usize>) -> Self {
98 self.set_partition(partition);
99 self
100 }
101
102 pub fn partition(&self) -> Option<usize> {
104 self.partition
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum ChildStats {
112 At(Option<usize>),
114 Skip,
117}
118
119pub struct StatisticsContext {
122 cache: Rc<RefCell<StatsCache>>,
123}
124
125impl Default for StatisticsContext {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131impl StatisticsContext {
132 pub fn new() -> Self {
134 Self {
135 cache: Rc::new(RefCell::new(StatsCache::default())),
136 }
137 }
138
139 pub fn reset_cache(&self) {
146 self.cache.borrow_mut().0.clear();
147 }
148
149 pub fn compute(
155 &self,
156 plan: &dyn ExecutionPlan,
157 args: &StatisticsArgs,
158 ) -> Result<Arc<Statistics>> {
159 let partition = args.partition();
160
161 if let Some(idx) = partition {
162 let partition_count = plan.properties().partitioning.partition_count();
163 assert_or_internal_err!(
164 idx < partition_count,
165 "Invalid partition index: {}, the partition count is {}",
166 idx,
167 partition_count
168 );
169 }
170
171 if let Some(cached) = self.cache.borrow().get(plan, partition) {
172 return Ok(Arc::clone(cached));
173 }
174
175 let children = plan.children();
176 let requests = plan.child_stats_requests(partition);
177 assert_eq_or_internal_err!(
178 requests.len(),
179 children.len(),
180 "{} child_stats_requests returned {} entries for {} children",
181 plan.name(),
182 requests.len(),
183 children.len()
184 );
185 let child_stats = children
186 .iter()
187 .zip(requests)
188 .map(|(child, directive)| match directive {
189 ChildStats::At(p) => {
190 self.compute(child.as_ref(), &StatisticsArgs::new().with_partition(p))
191 }
192 ChildStats::Skip => {
193 Ok(Arc::new(Statistics::new_unknown(child.schema().as_ref())))
194 }
195 })
196 .collect::<Result<Vec<_>>>()?;
197
198 let result = plan.statistics_from_inputs(&child_stats, args)?;
199 self.cache
200 .borrow_mut()
201 .insert(plan, partition, Arc::clone(&result));
202 Ok(result)
203 }
204}
205
206#[cfg(all(test, feature = "test_utils"))]
207mod tests {
208 use super::*;
209 use crate::coalesce_partitions::CoalescePartitionsExec;
210 use crate::test::exec::StatisticsExec;
211 use arrow::datatypes::{DataType, Field, Schema};
212 use datafusion_common::{ColumnStatistics, stats::Precision};
213
214 fn make_stats_leaf(num_rows: usize) -> Arc<dyn ExecutionPlan> {
215 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
216 let col_stats = vec![ColumnStatistics {
217 null_count: Precision::Exact(0),
218 max_value: Precision::Absent,
219 min_value: Precision::Absent,
220 sum_value: Precision::Absent,
221 distinct_count: Precision::Absent,
222 byte_size: Precision::Absent,
223 }];
224 Arc::new(StatisticsExec::new(
225 Statistics {
226 num_rows: Precision::Exact(num_rows),
227 total_byte_size: Precision::Absent,
228 column_statistics: col_stats,
229 },
230 schema,
231 ))
232 }
233
234 #[test]
235 fn coalesce_returns_overall_stats_for_any_partition() {
236 let leaf = make_stats_leaf(100);
237 let plan: Arc<dyn ExecutionPlan> = Arc::new(CoalescePartitionsExec::new(leaf));
238
239 let ctx = StatisticsContext::new();
240 let stats = ctx
241 .compute(
242 plan.as_ref(),
243 &StatisticsArgs::new().with_partition(Some(0)),
244 )
245 .unwrap();
246 assert_eq!(stats.num_rows, Precision::Exact(100));
247
248 let stats_none = ctx.compute(plan.as_ref(), &StatisticsArgs::new()).unwrap();
249 assert_eq!(stats_none.num_rows, Precision::Exact(100));
250 }
251
252 #[test]
253 fn context_caches_within_walk() {
254 let leaf = make_stats_leaf(42);
255 let ctx = StatisticsContext::new();
256 let args = StatisticsArgs::new();
257
258 let s1 = ctx.compute(leaf.as_ref(), &args).unwrap();
259 assert!(!ctx.cache.borrow().0.is_empty());
260
261 let s2 = ctx.compute(leaf.as_ref(), &args).unwrap();
262 assert!(Arc::ptr_eq(&s1, &s2));
263 }
264
265 #[test]
266 fn reset_cache_clears_entries() {
267 let leaf = make_stats_leaf(10);
268 let ctx = StatisticsContext::new();
269 let _ = ctx.compute(leaf.as_ref(), &StatisticsArgs::new()).unwrap();
270 assert!(!ctx.cache.borrow().0.is_empty());
271 ctx.reset_cache();
272 assert!(ctx.cache.borrow().0.is_empty());
273 }
274}