Lesson 02 Parquet & Columnar Storage
Theme:

Homework 02 — Parquet & Columnar Storage

Lesson 02 · Data Engineering with Python · Three tasks

Task 1 — Repartition by type and re-benchmark

Re-run the L02 dataset, but this time partition by type instead of date. Use pq.write_to_dataset with partition_cols=["type"]. Your output directory should look like:

data/parquet_by_type/
  type=PushEvent/part-0.parquet
  type=CreateEvent/part-0.parquet
  ...

Then repeat the DuckDB benchmark from the lab:

import duckdb, time
# Query 1: count PushEvents
t = time.perf_counter()
n = duckdb.query(
    "SELECT COUNT(*) FROM 'data/parquet_by_type/type=PushEvent/*.parquet'"
).fetchone()[0]
print(f"by-type count PushEvent: {round(time.perf_counter()-t, 4)} s — {n:,} rows")

# Query 2: count all types
t = time.perf_counter()
res = duckdb.query(
    "SELECT type, COUNT(*) AS n FROM 'data/parquet_by_type/**/*.parquet' GROUP BY type ORDER BY n DESC"
).fetchdf()
print(f"by-type GROUP BY: {round(time.perf_counter()-t, 4)} s")

✓ Self-check: the PushEvent-count query opens exactly one directory and is sub-millisecond (partition pruning). The "top-10 types by count" query has to scan every partition and is roughly as slow as — or slower than — the date-partitioned version.

Task 2 — Compression face-off

Write the same one-hour Arrow table with three different compression codecs and one uncompressed baseline. Record the resulting file sizes and the read latencies:

import pyarrow.parquet as pq, time, pathlib

# Assume `table` is already built from one gharchive hour
for codec in ["snappy", "gzip", "zstd", None]:
    name = codec or "none"
    out  = pathlib.Path(f"data/cmp/{name}.parquet")
    out.parent.mkdir(parents=True, exist_ok=True)
    pq.write_table(table, out, compression=codec)

    t = time.perf_counter()
    pq.read_table(out)
    elapsed = round(time.perf_counter() - t, 4)
    size_mb = round(out.stat().st_size / 1_048_576, 2)
    print(f"{name:8s}  {size_mb:6.2f} MB   read {elapsed} s")

Build a small Markdown or plain-text table of your results. Then answer in one sentence: given a read-heavy analytics workload, which codec would you choose and why?

✓ Self-check: zstd produces the smallest file; snappy is the fastest to read; gzip is typically the worst trade-off for analytics (decent compression, slowest decompression). Uncompressed is fastest to read but significantly larger.

Task 3 — Read with DuckDB directly from JSON.gz

Without converting anything, point DuckDB at your raw JSON.gz files and run the same aggregation query you've been benchmarking:

SELECT type, COUNT(*) AS n
FROM read_json_auto('data/raw/*.json.gz')
GROUP BY type
ORDER BY n DESC;

Time it. Then run the same query against your date-partitioned Parquet dataset:

SELECT type, COUNT(*) AS n
FROM 'data/parquet/date=2024-01-01/*.parquet'
GROUP BY type
ORDER BY n DESC;

Record both elapsed times. Calculate the speedup ratio.

✓ Self-check: Parquet is at least 10× faster, often 50–100×. The JSON.gz version is slow not just because of JSON parsing overhead — it is slow because the row-oriented layout forces DuckDB to materialise every column for every row even though the query only needs type.

Tip: Keep both the data/parquet/ and data/parquet_by_type/ directories. Lesson 03 will add schema validation on top of these same files, so having both partition strategies on disk makes the validation examples more interesting.

View solutions →