# Benchmarks
We compared Delta Arrow Reader with delta-rs, DuckDB, Polars, and Daft on four
workloads. Delta Arrow Reader had the lowest median time on every workload an
alternative could run. Polars completed all four workloads. Daft completed the
text projection but rejected the other three because it does not support
deletion vectors.
These numbers describe the workloads and machine shown below. Different tables,
queries, and hardware can produce different results.
## Results
The table below shows how long each reader took to load the Delta snapshot and
stream the full result. Process startup and Python import time are not included.
Each value is the median after one warmup run.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-wall-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-wall-light.svg">
<img alt="Wall-time comparison across five Delta readers and four workloads" src="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-wall-light.svg">
</picture>
| Mixed-column projection | 0.873 s | 1.143 s | 12.246 s | 3.152 s | Unsupported |
| 3 GB text projection | 2.254 s | 2.584 s | 13.063 s | 3.866 s | 4.750 s |
| Read one row from a table with deletion vectors | 0.0269 s | 0.0820 s | 0.3920 s | 0.5371 s | Unsupported |
| Scan a full table with deletion vectors | 0.1548 s | 1.0834 s | 1.0818 s | 0.8838 s | Unsupported |
Compared with Delta Arrow Reader, Polars took 3.61 times as long on the
mixed-column projection, 1.71 times as long on the text projection, 19.95 times
as long on the one-row deletion-vector read, and 5.71 times as long on the full
deletion-vector scan. Daft took 2.11 times as long on the one workload it could
run.
On the text projection, Delta Arrow Reader took 2.206-2.791 seconds and delta-rs
took 2.239-4.902 seconds. Because those ranges overlap, we do not treat the
difference between their medians as conclusive. The Delta Arrow Reader range
did not overlap the Polars or Daft range on that workload.
Speed is only part of the picture. The charts and table below show median CPU
time and median peak memory use:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-cpu-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-cpu-light.svg">
<img alt="CPU-time comparison across five Delta readers and four workloads" src="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-cpu-light.svg">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-memory-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-memory-light.svg">
<img alt="Peak-memory comparison across five Delta readers and four workloads" src="https://raw.githubusercontent.com/mag1cfrog/delta-arrow-reader/main/docs/content/assets/reader-benchmark-memory-light.svg">
</picture>
| Mixed-column projection | 6.53 s / 334 MiB | 6.09 s / 293 MiB | 10.25 s / 721 MiB | 13.72 s / 1,359 MiB | Unsupported |
| 3 GB text projection | 18.17 s / 1,760 MiB | 16.33 s / 1,677 MiB | 31.07 s / 5,186 MiB | 18.87 s / 3,209 MiB | 43.65 s / 6,263 MiB |
| Read one row with deletion vectors | 0.051 s / 58 MiB | 0.275 s / 266 MiB | 0.816 s / 306 MiB | 0.942 s / 447 MiB | Unsupported |
| Full deletion-vector scan | 1.04 s / 67 MiB | 2.27 s / 266 MiB | 1.39 s / 316 MiB | 1.81 s / 580 MiB | Unsupported |
Each cell shows CPU time followed by peak memory. Delta Arrow Reader used
slightly more CPU and memory than delta-rs on the two projection workloads. It
used less memory than DuckDB, Polars, and Daft on every comparable workload. On
the deletion-vector workloads, it also used less CPU and memory than every
alternative.
## Workloads
The mixed-column workload is based on a real application query. It reads 28
string, numeric, boolean, and time columns from a 14.5-million-row table. It
then reads 14 columns from a small reference table with two deletion vectors.
The larger table contains 1,151 active Parquet files and 448 MB of compressed
data.
The original schema is private, so the published
[query shapes](https://github.com/mag1cfrog/delta-arrow-reader/blob/main/benches/query_shapes.sql)
use neutral table and column names. They preserve the number and types of
projected columns without revealing the source data.
The text workload reads six columns from the public version-4 RedPajama
StackExchange Delta snapshot. It contains 64 Parquet files, 3.03 GB of
compressed data, and 2,697,774 rows.
Deletion vectors mark rows as deleted without rewriting their Parquet files.
To test them, we generated a table with 200 files and 200 million physical
rows. Each file has one deleted row. One query stops after the first available
row. The other reads all 199,999,800 rows that remain. Together, these queries
show both the startup cost of handling deletion vectors and the cost of applying
them across a full scan.
## Method
All readers used the same Delta snapshots stored in local MinIO. Each reader
used the machine's 16 logical CPUs. Results were consumed as Arrow batches
without retaining the full result, and output batches were limited to 8,192
rows.
For each workload, we ran every supported reader once to warm up the system and
discarded those results. We then used balanced, process-isolated run orders:
eight measured rounds for each four-reader workload and ten rounds for the
five-reader text workload. Every reader appeared in every position twice, and
every ordered pair of adjacent readers appeared twice. The exact
[run-order generator](https://github.com/mag1cfrog/delta-arrow-reader/blob/main/benches/run_order.py)
is checked in.
Delta Arrow Reader and delta-rs used the same Rust harness with DataFusion
54.1.0 and Arrow/Parquet 58.4.0. DuckDB, Polars, and Daft each ran in a separate
Python process. Their measured time covers the path from opening the Delta
table to producing Arrow batches, but not interpreter startup or imports. Linux
process accounting recorded wall time, CPU use, memory use, page faults, and
context switches.
All 146 measured processes completed successfully. Their row counts and logical
schemas matched the frozen workload expectations. Both deletion vectors in the
reference table removed the same 43 rows, and the generated table removed the
expected 200 rows.
The readers did not always represent values in memory in exactly the same way.
We allowed differences in string encoding, timestamp units, dictionary
encoding, and where each reader split its Arrow batches because none of them
changed the logical result.
Daft was not timed on the mixed-column or deletion-vector workloads. A separate
capability probe confirmed that Daft 0.7.24 rejects tables that advertise the
Delta deletion-vector reader feature. We did not bypass that check or ask Daft
to ignore deleted rows.
## Environment
We ran the benchmark on August 27, 2026, using one machine with the following
hardware and software:
- AMD Ryzen 7 8845HS, with 8 cores and 16 threads
- 27.95 GiB of memory
- Crucial P3 Plus NVMe SSD
- Fedora Linux 43 with Linux 6.19.14
| Delta Arrow Reader | 0.3.0 (`aaf6699`) |
| delta-rs | `fd7e96910243f9e67b4eae994d52ef246cfcea38` |
| DuckDB | 1.5.5, Delta extension `45c4087` |
| Polars | 1.44.1, with deltalake 1.6.3 |
| Daft | 0.7.24, with deltalake 1.6.3 |
We kept all benchmark data in local MinIO so that public-network delays would
not affect the results. Hardware and system load still affect absolute times,
so we will rerun these comparisons when the readers or their execution settings
change.
## Compare lazy and eager scan metadata
Use the provider-execution benchmark to compare repeated queries against one
loaded table. Run a matched pair in which only the scan metadata mode and output
path differ:
```bash
cargo bench --locked -p delta-arrow-reader --bench reader --all-features -- \
--mode provider-exec \
--seed 0 \
--output target/eager-metadata-lazy.csv \
--provider-exec-temp-dir target/eager-metadata-bench \
--provider-exec-storage-profile local \
--provider-exec-workload provider_many_unequal_files \
--provider-exec-query filter_tail_ids \
--provider-exec-backend direct-parquet \
--provider-exec-scan-metadata-mode lazy \
--provider-exec-queries-per-table 3 \
--provider-exec-scheduling-profile prefetch_2_ap_target_scan_3x \
--provider-exec-parquet-metadata-size-hint-bytes 65536 \
--provider-exec-parquet-full-file-read-threshold-bytes disabled \
--provider-exec-repetitions 5
cargo bench --locked -p delta-arrow-reader --bench reader --all-features -- \
--mode provider-exec \
--seed 0 \
--output target/eager-metadata-eager.csv \
--provider-exec-temp-dir target/eager-metadata-bench \
--provider-exec-storage-profile local \
--provider-exec-workload provider_many_unequal_files \
--provider-exec-query filter_tail_ids \
--provider-exec-backend direct-parquet \
--provider-exec-scan-metadata-mode eager \
--provider-exec-queries-per-table 3 \
--provider-exec-scheduling-profile prefetch_2_ap_target_scan_3x \
--provider-exec-parquet-metadata-size-hint-bytes 65536 \
--provider-exec-parquet-full-file-read-threshold-bytes disabled \
--provider-exec-repetitions 5
```
Each repetition loads and registers a new table, then runs the configured query
three times against that same table. Keep `--seed`, the temporary directory,
workload, query, backend, storage, scheduling, Parquet settings, repetition
count, and query count identical between the two commands. Use at least three
queries per table so the comparison includes metadata reuse rather than only
the initialization tradeoff.
The CSV separates the relevant timing boundaries:
- `table_initialization_micros_*` measures table loading. It ends before
DataFusion table registration.
- `planning_micros_*` measures SQL and physical-plan creation for each query,
including Delta scan planning. Its percentiles include every query from every
repetition.
- `total_micros_*` measures planning and execution for each query, but excludes
table initialization and registration.
- `session_total_micros_*` starts immediately before table loading and ends
after all benchmark queries, output checks, and metrics collection finish. It
includes initialization, registration, planning, and execution. Its
percentiles use one complete session per repetition.
Before comparing timings, confirm that both CSV rows have the same
`fixture_fingerprint`, `seed`, workload, query, storage, backend, scheduling,
repetition count, and query count. `scan_metadata_mode` should be the only
lifecycle setting that differs.
Eager mode moves Delta log/checkpoint replay into table initialization, so first
compare `table_initialization_micros_*` and `planning_micros_*` to confirm that
the work moved between phases. Then compare `session_total_micros_*` to judge
the end-to-end effect for the chosen number of queries. The per-query
`total_micros_*` fields help show whether execution time masks a planning-time
difference.
Do not treat one query count as a universal break-even point. The result depends
on Delta history and checkpoint shape, active-file count, object-store latency,
query selectivity, available statistics, memory pressure, hardware load, and
the number of queries that reuse the loaded table. The local fixture above is a
reproducible comparison of the two lifecycles; measure representative tables on
their real storage before choosing a production default.
### Representative real-S3 result
On August 27, 2026, we also compared the modes against the latest snapshots of
two production-shaped tables in S3. This used the direct Parquet backend through
DataFusion, the unreleased eager-metadata implementation at commit `a6adb83`,
16 target partitions, an 8,192-row batch size, and the machine described in
[Environment](#environment). One warmup pair was discarded. Four measured
pairs alternated whether lazy or eager mode ran first; each isolated process
loaded both tables once, then ran the HIP and schedule queries three times each.
Table initialization and complete-session values below are medians across the
four processes for each mode. Planning values are medians across the 12 query
executions for each table and mode.
| Table initialization | 1.196 s | 4.089 s | +2.894 s |
| Complete six-query session | 50.205 s | 42.451 s | -7.754 s (-15.4%) |
| HIP physical planning | 2.097 s | 5.1 ms | -99.76% |
| Schedule physical planning | 756 ms | 1.1 ms | -99.86% |
Both modes returned the same results and performed the same Parquet I/O. Each
HIP query returned 15,252,906 rows in 2,483 batches and read 505,642,366 bytes
with 1,044 full-file GETs. Each schedule query returned 58,161 rows in 12
batches and read 1,217,686 bytes with four range GETs and four full-file GETs.
We measured memory separately because allocator state from a completed query can
hide the retained-cache cost. Across six counterbalanced initialization-only
pairs, loading both tables increased median resident memory from 38.5 MiB to
68.7 MiB, a 30.2 MiB increase. Across four counterbalanced pairs that each ran
one HIP and one schedule query, median peak resident memory increased from
233.9 MiB to 248.9 MiB, a 15.0 MiB or 6.4% increase. These Linux measurements
used `VmRSS` and `VmHWM` from `/proc/self/status`.
This is a dated case study, not a universal performance promise or a pinned
public fixture. The live table snapshots can advance. Cache memory scales with
active-file metadata, statistics width, partition values, and deletion-vector
metadata rather than table row count or total Parquet data size.