Skip to content

DataFusion DataFrame Platform

DataFusion is an Apache Arrow-native query engine with a Rust backend and Python bindings. BenchBox supports benchmarking DataFusion using its native DataFrame API through the datafusion-df platform.

Overview

AttributeValue
CLI Namedatafusion-df
FamilyExpression
ExecutionLazy
Best ForMedium to large datasets with complex analytical queries
Min Version40.0.0

Features

  • Arrow-native - Zero-copy data sharing with other Arrow tools
  • Lazy evaluation - Query optimization before execution
  • Predicate pushdown - Efficient Parquet filtering
  • Rust performance - High-performance query execution
  • SQL support - Hybrid DataFrame/SQL queries

Installation

Terminal window
uv add datafusion
pip install datafusion
python -c "import datafusion; print(f'DataFusion {datafusion.__version__}')"

Quick Start

Terminal window
benchbox run --platform datafusion-df --benchmark tpch --scale 0.01
benchbox run --platform datafusion-df --benchmark tpch --scale 1 \
--platform-option target_partitions=8

The second command runs with custom parallelism.

Configuration Options

OptionDefaultDescription
target_partitionsCPU countNumber of partitions for parallelism
repartition_joinstrueEnable automatic repartitioning for joins
parquet_pushdowntrueEnable predicate/projection pushdown for Parquet
batch_size8192Batch size for query execution
memory_limitNoneMemory limit for fair spill pool (e.g., ‘8G’, ‘16GB’)
temp_dirSystem tempTemporary directory for disk spilling

target_partitions

Controls parallelism by setting the number of partitions for query execution:

Terminal window
benchbox run --platform datafusion-df --benchmark tpch --scale 1 \
--platform-option target_partitions=4
benchbox run --platform datafusion-df --benchmark tpch --scale 10

The first command uses 4 partitions, which suits limited memory. The second uses all available CPUs (the default).

parquet_pushdown

DataFusion can push predicates and projections down to the Parquet reader:

Terminal window
benchbox run --platform datafusion-df --benchmark tpch --scale 1 \
--platform-option parquet_pushdown=false

Disable pushdown for debugging.

Scale Factor Guidelines

DataFusion uses lazy evaluation and can handle larger datasets than eager frameworks:

Scale FactorData SizeMemory RequiredUse Case
0.01~10 MB~200 MBUnit testing, CI/CD
0.1~100 MB~500 MBIntegration testing
1.0~1 GB~2 GBStandard benchmarking
10.0~10 GB~8 GBPerformance testing
100.0~100 GB~32 GBLarge-scale testing

Note: Execution times vary based on query complexity, hardware, and configuration. Run benchmarks to establish baselines for your environment.

Performance Characteristics

Strengths

  • Query optimization - Lazy evaluation enables automatic optimization
  • Memory efficiency - Streaming execution reduces memory pressure
  • Parquet performance - Native Parquet support with pushdown
  • Arrow integration - Zero-copy interop with PyArrow ecosystem

Limitations

  • Single-node - No distributed execution (unlike Spark)
  • Python overhead - Some overhead in Python bindings
  • Smaller community - Less documentation than Pandas/Polars

Performance Tips

  1. Enable parallelism for large datasets:

    Terminal window
    --platform-option target_partitions=16
  2. Use Parquet format for best performance:

    Terminal window
    benchbox run --platform datafusion-df --benchmark tpch --scale 10
  3. Compare with Polars for single-node workloads:

    Terminal window
    --platform polars-df

Query Implementation

DataFusion queries use expression-based operations:

This is TPC-H Q1, the Pricing Summary Report. It filters with the expression API, computes derived columns and then aggregates.

def q1_datafusion_impl(ctx: DataFrameContext) -> Any:
lineitem = ctx.get_table("lineitem")
cutoff = ctx.cast_date(ctx.lit("1998-09-02"))
filtered = lineitem.filter(ctx.col("l_shipdate") <= cutoff)
disc_price = ctx.col("l_extendedprice") * (ctx.lit(1) - ctx.col("l_discount"))
charge = disc_price * (ctx.lit(1) + ctx.col("l_tax"))
result = (
filtered
.aggregate(
[ctx.col("l_returnflag"), ctx.col("l_linestatus")],
[
f.sum(ctx.col("l_quantity")).alias("sum_qty"),
f.avg(ctx.col("l_quantity")).alias("avg_qty"),
f.sum(disc_price).alias("sum_disc_price"),
f.sum(charge).alias("sum_charge"),
]
)
.sort(ctx.col("l_returnflag").sort(ascending=True))
)
return result

Comparison: DataFusion vs Polars DataFrame

AspectDataFusion (datafusion-df)Polars (polars-df)
BackendRust (via Arrow)Rust
ExecutionLazyLazy
MemoryEfficientVery efficient
SpeedSub-second queriesSub-100ms queries
SQL SupportFull SQL engineBasic SQL
Best forSQL + DataFrame hybridPure DataFrame

Window Functions

DataFusion supports SQL-style window functions:

from benchbox.platforms.dataframe import DataFusionDataFrameAdapter
adapter = DataFusionDataFrameAdapter()
row_num = adapter.window_row_number(
order_by=[("sale_date", True)],
partition_by=["category"]
)
running_total = adapter.window_sum(
column="amount",
partition_by=["category"],
order_by=[("sale_date", True)]
)

Troubleshooting

Memory Issues

DataFusion error: Memory exhausted

Solutions:

  1. Set a memory limit with spilling: --platform-option memory_limit=8G
  2. Reduce partitions: --platform-option target_partitions=4
  3. Reduce batch size: --platform-option batch_size=4096
  4. Set a temp directory for spilling: --platform-option temp_dir=/tmp/datafusion
  5. Reduce scale factor: --scale 1

Slow Parquet Reads

If Parquet file reads are slow:

Terminal window
--platform-option parquet_pushdown=true

Type Errors

DataFusion is strict about types. Ensure date columns are properly cast:

date_col = adapter.cast_date(adapter.col("date_string"))

Python API

from benchbox.platforms.dataframe import DataFusionDataFrameAdapter
adapter = DataFusionDataFrameAdapter(
working_dir="./benchmark_data",
target_partitions=8,
repartition_joins=True,
parquet_pushdown=True,
batch_size=8192,
memory_limit="8G",
temp_dir="/tmp/datafusion"
)
ctx = adapter.create_context()
adapter.load_tables(ctx, data_dir="./tpch_data")
df = adapter.sql("SELECT * FROM lineitem WHERE l_quantity > 10")
result = adapter.collect(df)
from benchbox.core.tpch.dataframe_queries import TPCH_DATAFRAME_QUERIES
query = TPCH_DATAFRAME_QUERIES.get_query("Q1")
result = adapter.execute_query(ctx, query)
print(result)

memory_limit enables memory management with spilling, and temp_dir is the directory for spilled data.