CSV vs Parquet Benchmark: 6.8x Smaller, 22-60x Faster
6 min read · Last updated BY
This benchmark measured CSV against Parquet on 11.2 million real NYC yellow taxi trips in DuckDB. Parquet with ZSTD compression is 6.8x smaller than CSV, and queries run 22-60x faster depending on how many columns they touch.
Key takeaways
- On 11,198,026 NYC TLC yellow taxi rows, Parquet/ZSTD is 172,016,004 bytes (164 MB) versus 1,172,769,059 bytes (1.09 GB) for CSV — 6.8x smaller. Parquet/Snappy is 228,421,875 bytes (218 MB) — 5.1x smaller.
- Query speedups versus CSV range from ~22x (all 20 columns) to ~60x (single-column aggregate).
count(*)is ~160x because it reads Parquet row-group metadata, not data. - The fair lower-bound headline is 22x, not 160x. DuckDB's parallel CSV reader is already fast (~0.5 s over 1.1 GB). The Parquet win is columnar pruning, metadata, and compression.
- Environment: DuckDB v1.5.5, Apple M3 Max, 36 GB RAM, local SSD, both formats OS-cached. Timings are the median of 3 runs via
.timer on. - The dataset is public. Download, export, and query commands that reproduce this benchmark are below.
What is the dataset?
NYC TLC yellow taxi trips for January–March 2025. Public data from the TLC trip record page.
- 11,198,026 rows
- 20 columns, including
cbd_congestion_fee(added by NYC TLC in the 2025 Q1 data)
Columns: VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, passenger_count, trip_distance, RatecodeID, store_and_fwd_flag, PULocationID, DOLocationID, payment_type, fare_amount, extra, mta_tax, tip_amount, tolls_amount, improvement_surcharge, total_amount, congestion_surcharge, Airport_fee, cbd_congestion_fee.
How the benchmark works
Official TLC Parquet files for 2025-01, 2025-02, and 2025-03 were loaded into one table, then exported once to CSV (with header) and to Parquet with Snappy and ZSTD via COPY … TO … (FORMAT PARQUET, COMPRESSION …).
Five queries were timed against the CSV file and the Parquet/ZSTD file.
- Engine: DuckDB v1.5.5
- Hardware: Apple M3 Max, 36 GB RAM, local SSD
- Cache: both formats OS-cached
- Timing: median of 3 runs via
.timer on
Not measured: gzip CSV, other hardware, other DuckDB versions, cloud object storage.
File size and query speed
Same 11,198,026 rows in every file.
| Format | Size | vs CSV |
|---|---|---|
| CSV | 1,172,769,059 bytes (1.09 GB) | — |
| Parquet (Snappy) | 228,421,875 bytes (218 MB) | 5.1x smaller |
| Parquet (ZSTD) | 172,016,004 bytes (164 MB) | 6.8x smaller |
Query speed, CSV vs Parquet-ZSTD, median of 3:
| Query | CSV | Parquet | Speedup |
|---|---|---|---|
SELECT count(*) | 0.485 s | 0.003 s | ~160x |
SELECT avg(trip_distance) | 0.490 s | 0.008 s | ~60x |
GROUP BY passenger_count (count + avg) | 0.506 s | 0.021 s | ~24x |
Filtered sum (WHERE trip_distance > 10) | 0.496 s | 0.013 s | ~38x |
Top-5 by total_amount (all 20 columns) | 0.705 s | 0.032 s | ~22x |
count(*) answers from Parquet row-group metadata without scanning data. That is why it is ~160x, and why 22x (all columns) is the number to quote as a lower bound.
Why is Parquet faster?
Three mechanisms, not “CSV is slow.”
Columnar pruning. A Parquet read loads only the columns the query names. avg(trip_distance) decodes one column; the all-columns top-5 decodes all 20. That is why speedup falls from ~60x to ~22x as more columns are touched. CSV parses every field of every row either way. This is the same column-skipping behavior described in the columnar storage guide.
Row-group metadata. Parquet stores a row count per row group. SELECT count(*) returns from that metadata in 0.003 s versus 0.485 s on CSV — ~160x. That is a real format advantage. It is not a general “Parquet reads 160x faster” claim.
Compression. Parquet/ZSTD is 6.8x smaller than the CSV (164 MB vs 1.09 GB), so there is less data to read even when a column is needed.
What did not help: row-group skipping. Parquet can skip whole row groups when a filter falls outside a row group's min/max footer stats. On this file it never fires: the data is in pickup-time order, long trips appear in all 92 row groups, and not one has a trip_distance max below 10. The filtered sum's ~38× is column pruning and decompression only — the same mechanisms as every other row. Pushdown pays off when the filter column correlates with row-group order, such as a date filter on time-partitioned files.
DuckDB's parallel CSV reader is already fast in absolute terms: about 0.5 s over 1.1 GB on this machine. The Parquet win is architectural — pruning, metadata, and compression — not a slow CSV implementation.
The fair lower-bound headline is 22x (all 20 columns). Use 22-60x when the question is “how much faster is Parquet,” and reserve ~160x for an explanation of count(*). For a broader look at choosing the format, see why choose Parquet.
Reproduce it yourself
Source files (three months):
Copy code
curl -O https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet
curl -O https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-02.parquet
curl -O https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-03.parquet
Load and export to CSV, Snappy, and ZSTD:
Copy code
CREATE TABLE taxi AS
SELECT * FROM read_parquet('yellow_tripdata_2025-*.parquet', union_by_name = true);
SELECT count(*) FROM taxi;
-- 11198026
COPY taxi TO 'taxi.csv' (FORMAT CSV, HEADER);
COPY taxi TO 'taxi_snappy.parquet' (FORMAT PARQUET, COMPRESSION SNAPPY);
COPY taxi TO 'taxi_zstd.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
-- ls -la taxi.csv taxi_snappy.parquet taxi_zstd.parquet
Five timed queries (median of 3 runs each):
Copy code
.timer on
-- Query 1: count(*)
SELECT count(*) FROM read_csv_auto('taxi.csv');
SELECT count(*) FROM read_parquet('taxi_zstd.parquet');
-- Query 2: single-column aggregate
SELECT avg(trip_distance) FROM read_csv_auto('taxi.csv');
SELECT avg(trip_distance) FROM read_parquet('taxi_zstd.parquet');
-- Query 3: GROUP BY (count + avg)
SELECT passenger_count, count(*), avg(total_amount)
FROM read_csv_auto('taxi.csv')
GROUP BY passenger_count ORDER BY 1;
SELECT passenger_count, count(*), avg(total_amount)
FROM read_parquet('taxi_zstd.parquet')
GROUP BY passenger_count ORDER BY 1;
-- Query 4: filtered aggregate
SELECT sum(total_amount) FROM read_csv_auto('taxi.csv') WHERE trip_distance > 10;
SELECT sum(total_amount) FROM read_parquet('taxi_zstd.parquet') WHERE trip_distance > 10;
-- Query 5: top-5, all 20 columns
SELECT * FROM read_csv_auto('taxi.csv') ORDER BY total_amount DESC LIMIT 5;
SELECT * FROM read_parquet('taxi_zstd.parquet') ORDER BY total_amount DESC LIMIT 5;
File sizes and the 11,198,026 row count should match this page exactly. Timings will vary by machine; the table above is the median of 3 runs on the hardware listed in the method.
When CSV still wins
CSV is the better file when you need:
- Interchange with tools that do not support columnar formats
- Human-readable diffs
- Streaming or appending rows without rewriting a file
This benchmark does not measure those workflows. It measures size and query time on a local, OS-cached copy of the same 11.2 million rows.
Start using MotherDuck now!
FAQS
On this dataset, yes. Parquet with Snappy is 228,421,875 bytes (218 MB) versus 1,172,769,059 bytes (1.09 GB) for CSV — 5.1x smaller. Parquet with ZSTD is 172,016,004 bytes (164 MB) — 6.8x smaller. Other datasets, codecs, and encodings were not measured, so this page does not claim Parquet is smaller in every case.
On this benchmark, Parquet/ZSTD is about 22x to 60x faster than CSV depending on how many columns the query touches. The all-columns top-5 query is ~22x; a single-column average is ~60x. count(*) is ~160x because it answers from row-group metadata. DuckDB's CSV reader itself finishes these queries in about half a second.
Because it does not scan the data. Parquet stores a row count in each row group's metadata, so SELECT count(*) returns from that metadata in 0.003 s versus 0.485 s on CSV — about 160x. That is a real format advantage, not a general claim that every Parquet read is 160x faster.
On this dataset, ZSTD produces the smaller file: 172,016,004 bytes (164 MB, 6.8x smaller than CSV) versus 228,421,875 bytes (218 MB, 5.1x) for Snappy. Query timings in this benchmark are against the ZSTD file only. Snappy query speed was not measured, so this page does not rank the two codecs on speed.
Yes. The source is public NYC TLC yellow taxi Parquet for January–March 2025. Download those three files, COPY them to CSV, Snappy Parquet, and ZSTD Parquet, then run the five queries with .timer on. The commands are in the reproduction section above. File sizes and row count should match exactly; timings will vary by machine.
No. These numbers are from 11,198,026 rows — 1,172,769,059 bytes of CSV and 172,016,004 bytes of Parquet/ZSTD — on DuckDB v1.5.5, an Apple M3 Max with 36 GB RAM, and a local SSD with both formats OS-cached. Small files, other hardware, other DuckDB versions, and cloud object storage were not tested.
Because the queries do different amounts of work. count(*) is ~160x because Parquet answers from row-group metadata without scanning data. A single-column average is ~60x because only that column is read. The all-columns top-5 is ~22x, the fair lower bound, because every column must be decoded. CSV parses the whole row either way.
DuckDB v1.5.5 on an Apple M3 Max with 36 GB RAM and a local SSD. Both the CSV and Parquet files were OS-cached. Timings are the median of three runs using .timer on. Other DuckDB versions, other machines, and cloud object storage were not measured.
