How to Query Parquet Files With DuckDB

DuckDB reads Parquet files directly with a single SQL call - no server, no schema setup, no pandas loading. Point it at a glob, turn on hive partitioning, and query across hundreds of files in milliseconds.

Data Engineerduckdbparquetsql

Why DuckDB for Parquet

Parquet is the standard storage format for data lakes, but querying it traditionally meant spinning up Spark or loading files into pandas (which pulls everything into memory first). DuckDB solves both problems: it's an embedded SQL engine that reads .parquet natively, pushes filters down to the file level (column + row group pruning), and handles tens of GB on a single machine without a cluster.

Opening a partitioned lake

A Hive-partitioned lake stores files in directories like lake/events/year=2024/month=01/part-0.parquet. DuckDB detects those path segments and exposes them as real columns when you pass hive_partitioning=1:

import duckdb

con = duckdb.connect()

# 'year' and 'month' become queryable columns automatically
rows = con.execute("""
    SELECT year, month, user_id, SUM(payload_size) AS total_size
    FROM read_parquet('lake/events/**/*.parquet', hive_partitioning=1)
    WHERE year = 2024
    GROUP BY year, month, user_id
    LIMIT 5
""").fetchall()

No file scanning loops, no pd.read_parquet, no schema file - just SQL against the glob.

Top-N per group with a window function

To find the top 5 users by spend for each month, use ROW_NUMBER() inside a CTE. A Python loop over query results is fragile and slow; the window function does it in one SQL pass:

import json
import duckdb

con = duckdb.connect()

rows = con.execute("""
    WITH per_month AS (
      SELECT
        year,
        month,
        user_id,
        SUM(payload_size) AS total_size
      FROM read_parquet('lake/events/**/*.parquet', hive_partitioning=1)
      WHERE year = 2024
      GROUP BY year, month, user_id
    ),
    ranked AS (
      SELECT
        printf('%04d-%02d', year, month) AS month,
        user_id,
        total_size,
        ROW_NUMBER() OVER (
          PARTITION BY year, month
          ORDER BY total_size DESC
        ) AS rn
      FROM per_month
    )
    SELECT month, user_id, total_size
    FROM ranked
    WHERE rn <= 5
    ORDER BY month, total_size DESC
""").fetchall()

result = [{"month": m, "user_id": int(u), "total_size": int(t)} for m, u, t in rows]
print(json.dumps(result))

PARTITION BY year, month ORDER BY total_size DESC restarts the rank counter for each month, so WHERE rn <= 5 keeps exactly the top 5 per month.

Partition pruning in practice

The WHERE year = 2024 clause is not just a row filter - DuckDB reads the path segments and skips entire directories for other years before opening a single file. On a large lake (thousands of Parquet files), this makes the difference between a 10-second scan and a sub-second one.

Running the same query on the cloud

The exact same SQL runs unchanged on MotherDuck (DuckDB-as-a-service) over Parquet in S3, or via Athena/Trino against the same files. The lakehouse pattern decouples storage (open Parquet format, you own it) from compute - swap the engine without touching the data.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How do I read Parquet files in Python with DuckDB?

Use duckdb.connect() and read_parquet('path/**/*.parquet') in a SQL string. DuckDB reads Parquet natively with columnar projection and partition pruning - no pandas, no Spark, no server needed.

What is hive_partitioning in DuckDB?

hive_partitioning=1 tells DuckDB to parse path segments like year=2024/month=01 as real SQL columns. This lets you filter by year or month in WHERE and GROUP BY, and DuckDB prunes directories it doesn't need to read.

How do I get the top N rows per group in DuckDB SQL?

Use ROW_NUMBER() OVER (PARTITION BY <group> ORDER BY <metric> DESC) inside a CTE, then filter WHERE rn <= N in the outer query. This is more efficient than looping over results in Python.

Keep learning

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectMake a CSV Importer Resilient to Bad DataData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →