Lesson 09 DuckDB Locally
Theme:
Lesson 09 · Slide 1

Lesson 09 — DuckDB Locally

The analytical engine that lives next to your Parquet.

Data Engineering Module 4 Modern Lakehouse
Lesson 09 · Slide 2

Where Module 4 fits

Modules 1–3 (done)

Produced Parquet partitions (L02), validated by Great Expectations (L03), orchestrated by Airflow (L04–L06), and modelled by dbt (L08). You have data at rest in a well-structured directory.

Module 4 (this lesson)

Flips the script: instead of pushing data into a database, the engine reads Parquet (and later Iceberg) in place. Same data, no copy, no load step.

Lesson 09 · Slide 3

What DuckDB is

Lesson 09 · Slide 4

Why it matters for pipelines

Lesson 09 · Slide 5

Hello world — query Parquet directly

import duckdb

con = duckdb.connect(":memory:")
con.execute("""
    SELECT type, count(*) AS n
    FROM read_parquet('data/parquet/date=*/*.parquet',
                      hive_partitioning=true)
    GROUP BY type
    ORDER BY n DESC
""").fetchall()

No LOAD, no COPY, no DDL. DuckDB reads the partition files in place and respects the Hive-style date=YYYY-MM-DD/ directory layout for predicate pushdown.

Lesson 09 · Slide 6

Predicate pushdown in 2 minutes

SQL: WHERE date = '2024-01-01'
        |
        v
  DuckDB partition discovery
  (reads directory listing only)
        |
        v
  Opens ONLY date=2024-01-01/*.parquet
        |
        v
  Reads Parquet row-group statistics
  (min/max per column per chunk)
        |
        v
  Decompresses ONLY matching pages
  for the columns in SELECT

The whole-file-scan alternative reads ∼1000× more bytes. Partition pruning + row-group skipping + column projection stack together.

Lesson 09 · Slide 7

DuckDB vs SQLite vs pandas

NeedSQLitepandasDuckDB
OLTP (writes, transactions)yesnonot the focus
OLAP (GROUP BY, joins on millions of rows)slowdepends on RAMyes
Reads Parquet nativelynowith read_parquetyes, with pushdown
Vector / columnar enginenopartialyes
One-line installyesyesyes

The takeaway: DuckDB is the SQLite-of-analytics. Same install footprint, OLAP semantics.

Lesson 09 · Slide 8

The pipeline-embedding pattern

Bad pattern

Every Airflow task spins up its own DuckDB connection and re-discovers the partitions. Schema changes require updating every task individually. Tests use a different code path from production.

Good pattern

A thin query.py helper exposes open_pipeline_db() returning a connection with the events view pre-registered. Every task imports the helper; tests point the same helper at fixtures.

Lesson 09 · Slide 9

The shared helper

from sample_pipeline.duckdb.query import open_pipeline_db

with open_pipeline_db() as con:
    rows = con.execute(
        "SELECT type, count(*) FROM events GROUP BY type"
    ).fetchall()

The same open_pipeline_db() pattern is used in count_by_type.py and top_repos.py in the sample pipeline. One view definition, tested once, reused everywhere.

Lesson 09 · Slide 10

What DuckDB is not

Lesson 09 · Slide 11

Concurrency notes

A single .duckdb file supports one writer at a time; multiple readers are fine. In Airflow you typically run pipeline scans against the in-memory connection (no .duckdb file at all) so concurrent task instances never contend. The persistent .duckdb form is for analyst-facing dashboards or long-lived local exploratory sessions.

The in-memory pattern used in query.py is always safe in multi-worker Airflow: each task process gets its own Python interpreter and its own DuckDB connection object.

Lesson 09 · Slide 12

Tying it to the pipeline

Lesson 09 · Slide 13

Recap & what's next

Coming up: Lesson 10 — Iceberg tables. Snapshots, time travel, ACID on a data lake — without leaving DuckDB.
1 / ?