| <!--- | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the | |
| "License"); you may not use this file except in compliance | |
| with the License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, | |
| software distributed under the License is distributed on an | |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| KIND, either express or implied. See the License for the | |
| specific language governing permissions and limitations | |
| under the License. | |
| --> | |
| (configuration)= | |
| # Configuration | |
| Let's look at how we can configure DataFusion. When creating a {py:class}`~datafusion.context.SessionContext`, you can pass in | |
| a {py:class}`~datafusion.context.SessionConfig` and {py:class}`~datafusion.context.RuntimeEnvBuilder` object. These two cover a wide range of options. | |
| ```python | |
| from datafusion import RuntimeEnvBuilder, SessionConfig, SessionContext | |
| # create a session context with default settings | |
| ctx = SessionContext() | |
| print(ctx) | |
| # create a session context with explicit runtime and config settings | |
| runtime = RuntimeEnvBuilder().with_disk_manager_os().with_fair_spill_pool(10000000) | |
| config = ( | |
| SessionConfig() | |
| .with_create_default_catalog_and_schema(True) | |
| .with_default_catalog_and_schema("foo", "bar") | |
| .with_target_partitions(8) | |
| .with_information_schema(True) | |
| .with_repartition_joins(False) | |
| .with_repartition_aggregations(False) | |
| .with_repartition_windows(False) | |
| .with_parquet_pruning(False) | |
| .set("datafusion.execution.parquet.pushdown_filters", "true") | |
| ) | |
| ctx = SessionContext(config, runtime) | |
| print(ctx) | |
| ``` | |
| ## Setting options by key | |
| The `with_*` methods cover the common options, but any option DataFusion declares can be | |
| set by its fully qualified key with {py:meth}`~datafusion.SessionConfig.set`. The value is | |
| always a string, and is parsed according to the type the option declares, so an unknown key | |
| or an unparsable value raises rather than being silently ignored: | |
| ```python | |
| config = SessionConfig().set("datafusion.execution.batch_size", "1024") | |
| ``` | |
| A whole dictionary of options can be applied at once by passing it to the | |
| {py:class}`~datafusion.SessionConfig` constructor, which is the shape a replayed set of | |
| settings usually arrives in: | |
| ```python | |
| config = SessionConfig({"datafusion.execution.batch_size": "1024"}) | |
| ``` | |
| Both routes reject the same keys, so which one you use does not change what is accepted. The | |
| constructor applies its entries in an unspecified order, so a dictionary with more than one | |
| bad key does not report a predictable one first. | |
| One trap is worth knowing about if you read settings back out of a session and replay them | |
| somewhere else, such as onto a worker process or into a test fixture. With | |
| `with_information_schema(True)`, the `information_schema.df_settings` table lists the | |
| `datafusion.runtime.*` keys alongside the rest, but those come from the runtime environment | |
| rather than from `ConfigOptions` and cannot be set this way. Feeding that table's rows back | |
| in verbatim will fail on the first such row, whichever route you use. Configure the runtime | |
| through `RuntimeEnvBuilder` instead, and skip the `datafusion.runtime.` prefix when | |
| replaying. | |
| ## Maximizing CPU Usage | |
| DataFusion uses partitions to parallelize work. For small queries the | |
| default configuration (number of CPU cores) is often sufficient, but to | |
| fully utilize available hardware you can tune how many partitions are | |
| created and when DataFusion will repartition data automatically. | |
| Configure a `SessionContext` with a higher partition count: | |
| ```python | |
| from datafusion import SessionConfig, SessionContext | |
| # allow up to 16 concurrent partitions | |
| config = SessionConfig().with_target_partitions(16) | |
| ctx = SessionContext(config) | |
| ``` | |
| Automatic repartitioning for joins, aggregations, window functions and | |
| other operations can be enabled to increase parallelism: | |
| ```python | |
| config = ( | |
| SessionConfig() | |
| .with_target_partitions(16) | |
| .with_repartition_joins(True) | |
| .with_repartition_aggregations(True) | |
| .with_repartition_windows(True) | |
| ) | |
| ``` | |
| Manual repartitioning is available on DataFrames when you need precise | |
| control: | |
| ```python | |
| from datafusion import col | |
| df = ctx.read_parquet("data.parquet") | |
| # Evenly divide into 16 partitions | |
| df = df.repartition(16) | |
| # Or partition by the hash of a column | |
| df = df.repartition_by_hash(col("a"), num=16) | |
| result = df.collect() | |
| ``` | |
| (checking_partitioning)= | |
| ### Checking what the plan actually does | |
| `repartition` and `repartition_by_hash` are requests, not instructions. The optimizer is | |
| free to drop a repartition nothing downstream needs, to collapse partitions again for an | |
| operator that requires a single stream, or to substitute a repartition of its own sized by | |
| `target_partitions`. So the number you passed is not necessarily the number you get. | |
| {py:attr}`~datafusion.ExecutionPlan.output_partitioning` reports what the built plan does, | |
| as opposed to what was asked of it: | |
| ```python | |
| from datafusion import SessionConfig, SessionContext, col, functions as f | |
| config = SessionConfig().with_target_partitions(16) | |
| ctx = SessionContext(config) | |
| df = ctx.read_parquet("data.parquet").repartition_by_hash(col("a"), num=8) | |
| plan = df.aggregate([col("a")], [f.sum(col("b"))]).execution_plan() | |
| partitioning = plan.output_partitioning | |
| print(partitioning.scheme) # 'Hash' | |
| print(partitioning.partition_count) # 16 -- target_partitions, not the 8 requested | |
| print(partitioning.hash_expressions) # ['a@0'] | |
| ``` | |
| The request for eight partitions did not survive: the optimizer inserted its own hash | |
| repartition at `target_partitions` instead. Had the aggregation been left off, the | |
| repartition would have been removed altogether and the plan would report | |
| `UnknownPartitioning` over the source's own partition count. | |
| `UnknownPartitioning` means the plan knows how many partitions it has but nothing about how | |
| rows are distributed across them, which is the ordinary case for a file scan. | |
| {py:attr}`~datafusion.ExecutionPlan.partition_count` gives the same count on its own when | |
| the scheme does not matter. | |
| Four schemes exist, but only three of them can come out of a plan you built here. | |
| `UnknownPartitioning` comes from a source, and `RoundRobinBatch` and `Hash` from a | |
| repartition — either one you asked for or one the optimizer inserted. `Range`, which spreads | |
| an ordered key space across partitions at chosen split points, has no request form in this | |
| package: `repartition` asks for round-robin, `repartition_by_hash` asks for hash, and SQL | |
| has no range-repartition syntax. | |
| It is still worth handling, because a plan does not have to have been built here. Both | |
| `datafusion-proto` and `datafusion-ffi` carry range partitioning faithfully, so | |
| {py:meth}`~datafusion.ExecutionPlan.from_bytes` can return a plan reporting it, as can an | |
| extension library whose query planner builds one — see {ref}`extension_planners`. Such a | |
| plan executes normally; only the split points are invisible, since | |
| {py:attr}`~datafusion.PhysicalPartitioning.hash_expressions` returns `None` for every scheme | |
| but `Hash`. Read {py:func}`repr` of the partitioning to see them. | |
| ### Benchmark Example | |
| The repository includes a benchmark script that demonstrates how to maximize CPU usage | |
| with DataFusion. The {code}`benchmarks/max_cpu_usage.py` script shows a practical example | |
| of configuring DataFusion for optimal parallelism. | |
| You can run the benchmark script to see the impact of different configuration settings: | |
| ```bash | |
| # Run with default settings (uses all CPU cores) | |
| python benchmarks/max_cpu_usage.py | |
| # Run with specific number of rows and partitions | |
| python benchmarks/max_cpu_usage.py --rows 5000000 --partitions 16 | |
| # See all available options | |
| python benchmarks/max_cpu_usage.py --help | |
| ``` | |
| Here's an example showing the performance difference between single and multiple partitions: | |
| ```bash | |
| # Single partition - slower processing | |
| $ python benchmarks/max_cpu_usage.py --rows=10000000 --partitions 1 | |
| Processed 10000000 rows using 1 partitions in 0.107s | |
| # Multiple partitions - faster processing | |
| $ python benchmarks/max_cpu_usage.py --rows=10000000 --partitions 10 | |
| Processed 10000000 rows using 10 partitions in 0.038s | |
| ``` | |
| This example demonstrates nearly 3x performance improvement (0.107s vs 0.038s) when using | |
| 10 partitions instead of 1, showcasing how proper partitioning can significantly improve | |
| CPU utilization and query performance. | |
| The script demonstrates several key optimization techniques: | |
| 1. **Higher target partition count**: Uses {code}`with_target_partitions()` to set the number of concurrent partitions | |
| 2. **Automatic repartitioning**: Enables repartitioning for joins, aggregations, and window functions | |
| 3. **Manual repartitioning**: Uses {code}`repartition()` to ensure all partitions are utilized | |
| 4. **CPU-intensive operations**: Performs aggregations that can benefit from parallelization | |
| The benchmark creates synthetic data and measures the time taken to perform a sum aggregation | |
| across the specified number of partitions. This helps you understand how partition configuration | |
| affects performance on your specific hardware. | |
| #### Important Considerations | |
| The provided benchmark script demonstrates partitioning concepts using synthetic in-memory data | |
| and simple aggregation operations. While useful for understanding basic configuration principles, | |
| actual performance in production environments may vary significantly based on numerous factors: | |
| **Data Sources and I/O Characteristics:** | |
| - **Table providers**: Performance differs greatly between Parquet files, CSV files, databases, and cloud storage | |
| - **Storage type**: Local SSD, network-attached storage, and cloud storage have vastly different characteristics | |
| - **Network latency**: Remote data sources introduce additional latency considerations | |
| - **File sizes and distribution**: Large files may benefit differently from partitioning than many small files | |
| **Query and Workload Characteristics:** | |
| - **Operation complexity**: Simple aggregations versus complex joins, window functions, or nested queries | |
| - **Data distribution**: Skewed data may not partition evenly, affecting parallel efficiency | |
| - **Memory usage**: Large datasets may require different memory management strategies | |
| - **Concurrent workloads**: Multiple queries running simultaneously affect resource allocation | |
| **Hardware and Environment Factors:** | |
| - **CPU architecture**: Different processors have varying parallel processing capabilities | |
| - **Available memory**: Limited RAM may require different optimization strategies | |
| - **System load**: Other applications competing for resources affect DataFusion performance | |
| **Recommendations for Production Use:** | |
| To optimize DataFusion for your specific use case, it is strongly recommended to: | |
| 1. **Create custom benchmarks** using your actual data sources, formats, and query patterns | |
| 2. **Test with representative data volumes** that match your production workloads | |
| 3. **Measure end-to-end performance** including data loading, processing, and result handling | |
| 4. **Evaluate different configuration combinations** for your specific hardware and workload | |
| 5. **Monitor resource utilization** (CPU, memory, I/O) to identify bottlenecks in your environment | |
| This approach will provide more accurate insights into how DataFusion configuration options | |
| will impact your particular applications and infrastructure. | |
| For more information about available {py:class}`~datafusion.context.SessionConfig` options, see the [rust DataFusion Configuration guide](https://arrow.apache.org/datafusion/user-guide/configs.html), | |
| and about {code}`RuntimeEnvBuilder` options in the rust [online API documentation](https://docs.rs/datafusion/latest/datafusion/execution/runtime_env/struct.RuntimeEnvBuilder.html). |