Skip to main content

datafusion_physical_plan/
analyze.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Defines the ANALYZE operator
19
20use std::sync::Arc;
21
22use super::stream::{RecordBatchReceiverStream, RecordBatchStreamAdapter};
23use super::{
24    DisplayAs, Distribution, ExecutionPlanProperties, PlanProperties,
25    SendableRecordBatchStream,
26};
27use crate::display::DisplayableExecutionPlan;
28use crate::execution_plan::EvaluationType;
29use crate::metrics::{MetricCategory, MetricType};
30use crate::{
31    ChildrenPropertiesMode, DisplayFormatType, ExecutionPlan, Partitioning,
32    ReplaceChildrenOptions,
33};
34
35use arrow::{array::StringBuilder, datatypes::SchemaRef, record_batch::RecordBatch};
36use datafusion_common::format::ExplainFormat;
37use datafusion_common::instant::Instant;
38use datafusion_common::tree_node::TreeNodeRecursion;
39use datafusion_common::{
40    DataFusionError, Result, assert_eq_or_internal_err, internal_err,
41};
42use datafusion_execution::TaskContext;
43use datafusion_physical_expr::EquivalenceProperties;
44use datafusion_physical_expr::PhysicalExpr;
45
46use futures::StreamExt;
47
48/// `EXPLAIN ANALYZE` execution plan operator. This operator runs its input,
49/// discards the results, and then prints out an annotated plan with metrics
50#[derive(Debug, Clone)]
51pub struct AnalyzeExec {
52    /// Control how much extra to print
53    verbose: bool,
54    /// If statistics should be displayed
55    show_statistics: bool,
56    /// Which metric categories should be displayed
57    metric_types: Vec<MetricType>,
58    /// Optional filter by semantic category (rows / bytes / timing).
59    metric_categories: Option<Vec<MetricCategory>>,
60    /// Output format for the rendered plan + metrics.
61    format: ExplainFormat,
62    /// The input plan (the plan being analyzed)
63    pub(crate) input: Arc<dyn ExecutionPlan>,
64    /// The output schema for RecordBatches of this exec node
65    schema: SchemaRef,
66    cache: Arc<PlanProperties>,
67}
68
69/// Builder for [`AnalyzeExec`].
70///
71/// Builder for [AnalyzeExec].
72pub struct AnalyzeExecBuilder {
73    verbose: bool,
74    show_statistics: bool,
75    input: Arc<dyn ExecutionPlan>,
76    schema: SchemaRef,
77    metric_types: Vec<MetricType>,
78    metric_categories: Option<Vec<MetricCategory>>,
79    format: ExplainFormat,
80}
81
82impl AnalyzeExecBuilder {
83    pub fn new(
84        verbose: bool,
85        show_statistics: bool,
86        input: Arc<dyn ExecutionPlan>,
87        schema: SchemaRef,
88    ) -> Self {
89        Self {
90            verbose,
91            show_statistics,
92            input,
93            schema,
94            metric_types: vec![MetricType::Summary, MetricType::Dev],
95            metric_categories: None,
96            format: ExplainFormat::Indent,
97        }
98    }
99
100    pub fn with_metric_types(mut self, metric_types: Vec<MetricType>) -> Self {
101        self.metric_types = metric_types;
102        self
103    }
104
105    pub fn with_metric_categories(
106        mut self,
107        metric_categories: Option<Vec<MetricCategory>>,
108    ) -> Self {
109        self.metric_categories = metric_categories;
110        self
111    }
112
113    pub fn with_format(mut self, format: ExplainFormat) -> Self {
114        self.format = format;
115        self
116    }
117
118    pub fn build(self) -> AnalyzeExec {
119        let cache =
120            AnalyzeExec::compute_properties(&self.input, Arc::clone(&self.schema));
121        AnalyzeExec {
122            verbose: self.verbose,
123            show_statistics: self.show_statistics,
124            metric_types: self.metric_types,
125            metric_categories: self.metric_categories,
126            format: self.format,
127            input: self.input,
128            schema: self.schema,
129            cache: Arc::new(cache),
130        }
131    }
132}
133
134impl AnalyzeExec {
135    /// Returns a builder for constructing an [`AnalyzeExec`].
136    pub fn builder(
137        verbose: bool,
138        show_statistics: bool,
139        input: Arc<dyn ExecutionPlan>,
140        schema: SchemaRef,
141    ) -> AnalyzeExecBuilder {
142        AnalyzeExecBuilder::new(verbose, show_statistics, input, schema)
143    }
144
145    /// Access to verbose
146    pub fn verbose(&self) -> bool {
147        self.verbose
148    }
149
150    /// Access to show_statistics
151    pub fn show_statistics(&self) -> bool {
152        self.show_statistics
153    }
154
155    /// Access to metric_categories
156    pub fn metric_categories(&self) -> Option<&[MetricCategory]> {
157        self.metric_categories.as_deref()
158    }
159
160    /// Access to format
161    pub fn format(&self) -> &ExplainFormat {
162        &self.format
163    }
164
165    /// The input plan
166    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
167        &self.input
168    }
169
170    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
171    fn compute_properties(
172        input: &Arc<dyn ExecutionPlan>,
173        schema: SchemaRef,
174    ) -> PlanProperties {
175        PlanProperties::new(
176            EquivalenceProperties::new(schema),
177            Partitioning::UnknownPartitioning(1),
178            input.pipeline_behavior(),
179            input.boundedness(),
180        )
181        .with_evaluation_type(EvaluationType::Eager)
182    }
183}
184
185impl DisplayAs for AnalyzeExec {
186    fn fmt_as(
187        &self,
188        t: DisplayFormatType,
189        f: &mut std::fmt::Formatter,
190    ) -> std::fmt::Result {
191        match t {
192            DisplayFormatType::Default | DisplayFormatType::Verbose => {
193                write!(f, "AnalyzeExec verbose={}", self.verbose)
194            }
195            DisplayFormatType::TreeRender => {
196                // TODO: collect info
197                write!(f, "")
198            }
199        }
200    }
201}
202
203impl ExecutionPlan for AnalyzeExec {
204    fn name(&self) -> &'static str {
205        "AnalyzeExec"
206    }
207
208    /// Return a reference to Any that can be used for downcasting
209    fn properties(&self) -> &Arc<PlanProperties> {
210        &self.cache
211    }
212
213    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
214        vec![&self.input]
215    }
216
217    fn required_input_distribution(&self) -> Vec<Distribution> {
218        self.input_distribution_requirements().into_per_child()
219    }
220
221    fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
222        crate::InputDistributionRequirements::new(vec![
223            Distribution::UnspecifiedDistribution,
224        ])
225    }
226
227    fn apply_expressions(
228        &self,
229        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
230    ) -> Result<TreeNodeRecursion> {
231        Ok(TreeNodeRecursion::Continue)
232    }
233
234    fn replace_children(
235        self: Arc<Self>,
236        mut children: Vec<Arc<dyn ExecutionPlan>>,
237        _: ReplaceChildrenOptions,
238    ) -> Result<Arc<dyn ExecutionPlan>> {
239        Ok(Arc::new(
240            AnalyzeExec::builder(
241                self.verbose,
242                self.show_statistics,
243                children.pop().unwrap(),
244                Arc::clone(&self.schema),
245            )
246            .with_metric_types(self.metric_types.clone())
247            .with_metric_categories(self.metric_categories.clone())
248            .with_format(self.format.clone())
249            .build(),
250        ))
251    }
252
253    fn with_new_children(
254        self: Arc<Self>,
255        children: Vec<Arc<dyn ExecutionPlan>>,
256    ) -> Result<Arc<dyn ExecutionPlan>> {
257        self.replace_children(
258            children,
259            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
260        )
261    }
262
263    fn execute(
264        &self,
265        partition: usize,
266        context: Arc<TaskContext>,
267    ) -> Result<SendableRecordBatchStream> {
268        assert_eq_or_internal_err!(
269            partition,
270            0,
271            "AnalyzeExec invalid partition. Expected 0, got {partition}"
272        );
273
274        // Gather futures that will run each input partition in
275        // parallel (on a separate tokio task) using a JoinSet to
276        // cancel outstanding futures on drop
277        let num_input_partitions = self.input.output_partitioning().partition_count();
278        let mut builder =
279            RecordBatchReceiverStream::builder(self.schema(), num_input_partitions);
280
281        for input_partition in 0..num_input_partitions {
282            builder.run_input(
283                Arc::clone(&self.input),
284                input_partition,
285                Arc::clone(&context),
286            );
287        }
288
289        // Create future that computes the final output
290        let start = Instant::now();
291        let captured_input = Arc::clone(&self.input);
292        let captured_schema = Arc::clone(&self.schema);
293        let verbose = self.verbose;
294        let show_statistics = self.show_statistics;
295        let metric_types = self.metric_types.clone();
296        let metric_categories = self.metric_categories.clone();
297        let format = self.format.clone();
298
299        // future that gathers the results from all the tasks in the
300        // JoinSet that computes the overall row count and final
301        // record batch
302        let mut input_stream = builder.build();
303        let output = async move {
304            let mut total_rows = 0;
305            while let Some(batch) = input_stream.next().await.transpose()? {
306                total_rows += batch.num_rows();
307            }
308            drop(input_stream);
309
310            let duration = Instant::now() - start;
311            create_output_batch(
312                verbose,
313                show_statistics,
314                total_rows,
315                duration,
316                &captured_input,
317                &captured_schema,
318                &metric_types,
319                metric_categories.as_deref(),
320                &format,
321            )
322        };
323
324        Ok(Box::pin(RecordBatchStreamAdapter::new(
325            Arc::clone(&self.schema),
326            futures::stream::once(output),
327        )))
328    }
329
330    #[cfg(feature = "proto")]
331    fn try_to_proto(
332        &self,
333        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
334    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
335        use datafusion_proto_models::protobuf;
336
337        // Exhaustive destructure: adding a field to `AnalyzeExec` without
338        // deciding how it is serialized is a compile error, not a silent
339        // round-trip gap.
340        let Self {
341            verbose,
342            show_statistics,
343            // TODO: not on the wire. `AnalyzeExecBuilder` always resets this to
344            // `[Summary, Dev]`, so a non-default selection is lost on
345            // round-trip. Fixing it needs a new proto field.
346            metric_types: _,
347            metric_categories,
348            format,
349            input,
350            schema,
351            // Derived at construction from `input` and `schema`.
352            cache: _,
353        } = self;
354
355        let input = ctx.encode_child(input)?;
356        let (has_metric_categories, metric_categories) = match metric_categories {
357            Some(categories) => {
358                (true, categories.iter().map(ToString::to_string).collect())
359            }
360            None => (false, vec![]),
361        };
362        let format = match format {
363            ExplainFormat::Indent => protobuf::ExplainFormat::Indent,
364            ExplainFormat::Tree => protobuf::ExplainFormat::Tree,
365            ExplainFormat::PostgresJSON => protobuf::ExplainFormat::Pgjson,
366            ExplainFormat::Graphviz => protobuf::ExplainFormat::Graphviz,
367        } as i32;
368        Ok(Some(protobuf::PhysicalPlanNode {
369            physical_plan_type: Some(
370                protobuf::physical_plan_node::PhysicalPlanType::Analyze(Box::new(
371                    protobuf::AnalyzeExecNode {
372                        verbose: *verbose,
373                        show_statistics: *show_statistics,
374                        input: Some(Box::new(input)),
375                        schema: Some(schema.as_ref().try_into()?),
376                        has_metric_categories,
377                        metric_categories,
378                        format,
379                    },
380                )),
381            ),
382        }))
383    }
384}
385
386#[cfg(feature = "proto")]
387impl AnalyzeExec {
388    /// Reconstruct an [`AnalyzeExec`] from its protobuf representation.
389    pub fn try_from_proto(
390        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
391        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
392    ) -> Result<Arc<dyn ExecutionPlan>> {
393        use datafusion_proto_models::protobuf;
394
395        let analyze = crate::expect_plan_variant!(
396            node,
397            protobuf::physical_plan_node::PhysicalPlanType::Analyze,
398            "AnalyzeExec",
399        );
400        // Exhaustive destructure: a new field on `AnalyzeExecNode` is a compile
401        // error here rather than a silently ignored wire field.
402        let protobuf::AnalyzeExecNode {
403            verbose,
404            show_statistics,
405            input,
406            schema,
407            has_metric_categories,
408            metric_categories,
409            format,
410        } = analyze.as_ref();
411
412        let input =
413            ctx.decode_required_child(input.as_deref(), "AnalyzeExec", "input")?;
414        let metric_categories = if *has_metric_categories {
415            Some(
416                metric_categories
417                    .iter()
418                    .map(|category| category.parse::<MetricCategory>())
419                    .collect::<Result<Vec<_>>>()?,
420            )
421        } else {
422            None
423        };
424        let proto_format = protobuf::ExplainFormat::try_from(*format).map_err(|_| {
425            DataFusionError::Internal(format!(
426                "Received an AnalyzeExecNode message with unknown ExplainFormat {format}"
427            ))
428        })?;
429        let format = match proto_format {
430            protobuf::ExplainFormat::Indent => ExplainFormat::Indent,
431            protobuf::ExplainFormat::Tree => ExplainFormat::Tree,
432            protobuf::ExplainFormat::Pgjson => ExplainFormat::PostgresJSON,
433            protobuf::ExplainFormat::Graphviz => ExplainFormat::Graphviz,
434        };
435        let schema = schema.as_ref().ok_or_else(|| {
436            datafusion_common::internal_datafusion_err!(
437                "AnalyzeExec is missing required field 'schema'"
438            )
439        })?;
440        Ok(Arc::new(
441            AnalyzeExec::builder(
442                *verbose,
443                *show_statistics,
444                input,
445                Arc::new(arrow::datatypes::Schema::try_from(schema)?),
446            )
447            .with_metric_categories(metric_categories)
448            .with_format(format)
449            .build(),
450        ))
451    }
452}
453
454/// Creates the output of AnalyzeExec as a RecordBatch
455#[expect(clippy::too_many_arguments)]
456fn create_output_batch(
457    verbose: bool,
458    show_statistics: bool,
459    total_rows: usize,
460    duration: std::time::Duration,
461    input: &Arc<dyn ExecutionPlan>,
462    schema: &SchemaRef,
463    metric_types: &[MetricType],
464    metric_categories: Option<&[MetricCategory]>,
465    format: &ExplainFormat,
466) -> Result<RecordBatch> {
467    let mut type_builder = StringBuilder::with_capacity(1, 1024);
468    let mut plan_builder = StringBuilder::with_capacity(1, 1024);
469
470    match format {
471        ExplainFormat::Indent => {
472            // TODO use some sort of enum rather than strings?
473            type_builder.append_value("Plan with Metrics");
474            let annotated_plan = DisplayableExecutionPlan::with_metrics(input.as_ref())
475                .set_metric_types(metric_types.to_vec())
476                .set_metric_categories(metric_categories.map(|c| c.to_vec()))
477                .set_show_statistics(show_statistics)
478                .indent(verbose)
479                .to_string();
480            plan_builder.append_value(annotated_plan);
481            // Verbose output
482            // TODO make this more sophisticated
483            if verbose {
484                type_builder.append_value("Plan with Full Metrics");
485                let annotated_plan =
486                    DisplayableExecutionPlan::with_full_metrics(input.as_ref())
487                        .set_metric_types(metric_types.to_vec())
488                        .set_metric_categories(metric_categories.map(|c| c.to_vec()))
489                        .set_show_statistics(show_statistics)
490                        .indent(verbose)
491                        .to_string();
492                plan_builder.append_value(annotated_plan);
493                type_builder.append_value("Output Rows");
494                plan_builder.append_value(total_rows.to_string());
495                type_builder.append_value("Duration");
496                plan_builder.append_value(format!("{duration:?}"));
497            }
498        }
499        ExplainFormat::PostgresJSON => {
500            // `show_statistics` is intentionally not forwarded here: the pgjson
501            // renderer does not emit statistics, and the planner rejects the
502            // `show_statistics` + pgjson combination up front.
503            type_builder.append_value("Plan with Metrics");
504            let mut displayable = if verbose {
505                DisplayableExecutionPlan::with_full_metrics(input.as_ref())
506            } else {
507                DisplayableExecutionPlan::with_metrics(input.as_ref())
508            };
509            displayable = displayable
510                .set_metric_types(metric_types.to_vec())
511                .set_metric_categories(metric_categories.map(|c| c.to_vec()));
512            if verbose {
513                displayable = displayable.set_summary(Some(total_rows), Some(duration));
514            }
515            plan_builder.append_value(displayable.pgjson(verbose).to_string());
516        }
517        ExplainFormat::Tree | ExplainFormat::Graphviz => {
518            return internal_err!("AnalyzeExec does not support {format} output format");
519        }
520    }
521
522    RecordBatch::try_new(
523        Arc::clone(schema),
524        vec![
525            Arc::new(type_builder.finish()),
526            Arc::new(plan_builder.finish()),
527        ],
528    )
529    .map_err(DataFusionError::from)
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::{
536        collect,
537        test::{
538            assert_is_pending,
539            exec::{BlockingExec, assert_strong_count_converges_to_zero},
540        },
541    };
542
543    use arrow::datatypes::{DataType, Field, Schema};
544    use futures::FutureExt;
545
546    #[tokio::test]
547    async fn test_drop_cancel() -> Result<()> {
548        let task_ctx = Arc::new(TaskContext::default());
549        let schema =
550            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, true)]));
551
552        let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 1));
553        let refs = blocking_exec.refs();
554        let analyze_exec =
555            Arc::new(AnalyzeExec::builder(true, false, blocking_exec, schema).build());
556
557        let fut = collect(analyze_exec, task_ctx);
558        let mut fut = fut.boxed();
559
560        assert_is_pending(&mut fut);
561        drop(fut);
562        assert_strong_count_converges_to_zero(refs).await;
563
564        Ok(())
565    }
566}