KevsRobots Learning Platform
70% Percent Complete
By Kevin McAleer, 7 Minutes
Parquet is a columnar file format ideal for analytics. DuckDB reads and writes Parquet natively and efficiently.
New to terms like Parquet, partitioning, or predicate pushdown? See the Beginner glossary.
Why this matters:
CREATE TABLE trips AS SELECT * FROM read_parquet('data/trips/*.parquet');
Notes:
*.parquet to read multiple files at once.What is a “glob”? A glob is a simple wildcard pattern for matching files and folders:
*.parquet— all Parquet files in a foldertrips_2024-*.parquet— files starting withtrips_2024-year=*/month=*/*.parquet— partitioned folders by year/month DuckDB accepts these patterns in functions likeread_parquet()andread_csv_auto().
Select only the columns you need and filter early to reduce IO:
SELECT vendor_id, pickup_date, fare_amount
FROM trips
WHERE pickup_date >= DATE '2024-01-01';
Produce compact, portable outputs for downstream tools.
COPY (
SELECT vendor_id, DATE_TRUNC('month', pickup_date) AS month,
SUM(fare_amount) AS revenue
FROM trips
GROUP BY vendor_id, month
) TO 'exports/trips_monthly.parquet' (FORMAT 'parquet');
Notes:
exports/ folder exists first.*_monthly).Partitioning creates subfolders by keys; DuckDB can skip folders when those keys are filtered.
COPY (
SELECT *, strftime(pickup_date, '%Y') AS year,
strftime(pickup_date, '%m') AS month
FROM trips
) TO 'exports/trips_by_year_month'
(FORMAT 'parquet', PARTITION_BY (year, month));
Now you can scan selected partitions quickly:
SELECT COUNT(*)
FROM read_parquet('exports/trips_by_year_month/year=2024/month=09/*.parquet');
Notes:
Partitioning, explained
- Imagine a filing cabinet: you put trips into drawers by
year, then folders bymonth. If you only need 2024‑09, you open just that folder.- On disk this looks like
year=2024/month=09/…. DuckDB understands this pattern and can skip all other folders.- A good partition key is something you filter by often (date, region) and has limited distinct values (12 months, a handful of regions).
When to partition
- Datasets larger than a few hundred MB, or when you regularly query slices (by month, by region).
- Don’t over‑partition (e.g., by minute or by user ID) — it creates thousands of tiny files.
How to query partitions
- By folder path:
SELECT * FROM read_parquet('…/year=2024/month=09/*.parquet');- Or by predicate using virtual columns:
SELECT COUNT(*) FROM read_parquet('…/year=*/month=*/*.parquet') WHERE year = 2024 AND month = 9;
What is “predicate pushdown”?
- A predicate is your filter (the
WHEREclause). Pushdown means DuckDB sends that filter into the file reader, so it only reads the parts that could match.- Parquet stores min/max stats per row group. If
fare_amount > 0and a row group’s max is 0, DuckDB can skip that group entirely.- Result: less data read from disk/network and faster queries.
Without pushdown (conceptually): read data -> then filter -> keep a few rows. With pushdown: tell the reader the filter first -> skip non‑matching chunks -> read much less.
SELECT vendor_id, SUM(fare_amount) AS revenue
FROM read_parquet('exports/trips_by_year_month/year=2024/*.parquet')
WHERE fare_amount > 0
GROUP BY vendor_id;
Notes:
What it is:
Why it matters:
Safe patterns:
NULL and optionally fill with a default using COALESCE.CAST(id AS BIGINT) everywhere) before combining.Inspect schemas quickly:
-- Peek at columns and types via DESCRIBE on a scan
DESCRIBE SELECT * FROM read_parquet('exports/trips_by_year_month/year=2024/*.parquet');
Normalize when combining different vintages:
-- Example: newer files have `surcharge`, older files do not
CREATE OR REPLACE VIEW trips_all AS
SELECT vendor_id, pickup_date, fare_amount, surcharge
FROM read_parquet('data/new/*.parquet')
UNION ALL
SELECT vendor_id, pickup_date, fare_amount, CAST(NULL AS DOUBLE) AS surcharge
FROM read_parquet('data/old/*.parquet');
Persist a curated, consistent table:
CREATE OR REPLACE TABLE trips_curated AS
SELECT
CAST(vendor_id AS VARCHAR) AS vendor_id,
CAST(pickup_date AS TIMESTAMP) AS pickup_ts,
fare_amount,
COALESCE(surcharge, 0) AS surcharge
FROM trips_all;
Notes:
UNION ALL with aligned columns.CAST and COALESCE to harmonize types and defaults.schema.md in your repo.Common pitfalls:
COPY.year, month, day and query only one week.SELECT * on the same dataset.COPY makes exporting analytical tables easy and portable.SELECT COUNT(*)
FROM read_parquet('data/sales/part-*.parquet');
You try it
- Replace with any folder you have; confirm count matches number of rows across files
COPY (
SELECT * FROM my_table
) TO 'exports/my_table.parquet' (FORMAT PARQUET);
You try it
- Export a filtered subset (e.g.,
WHERE date >= '2024-01-01')
COPY (
SELECT * FROM my_table
) TO 'exports/sales_by_year_month'
(FORMAT PARQUET, PARTITION_BY (year, month));
You try it
- List the created folder tree and spot
year=YYYY/month=MMfolders
SELECT customer_id, SUM(amount) AS revenue
FROM read_parquet('exports/sales_by_year_month')
WHERE year = 2024 AND month = 6
GROUP BY customer_id;
You try it
- Add a second filter on
amount > 0and runEXPLAINto see filtered scans
-- Old files without column `coupon`
-- New files add nullable `coupon` string
SELECT COUNT(*), COUNT(coupon) AS with_coupon
FROM read_parquet('exports/sales_by_year_month');
You try it
- Add one small Parquet with an extra nullable column and confirm queries still work
year, month, country).
You can use the arrows ← → on your keyboard to navigate between lessons.
Comments