Skip to main content

DuckDB Pipelines for Journalism

A fast, in-process database that runs SQL over CSV and Parquet files where they sit — no server, no loading, ideal for one-off analysis under deadline.

Last reviewed: Next review due:

1. What DuckDB is

DuckDB is a free, open-source analytical database that runs insidewhatever you are already using — the command line, Python or R — with no server to install or keep alive. It stores data in columns rather than rows, which makes the counts, sums and group-bys at the heart of journalism very fast, and it speaks standard SQL. If SQLite is the pocket database for transactional work, DuckDB is its analytical cousin.

Its defining feature for reporters: it can query a CSV, Parquet or JSON file directly, without importing it first. Download the single binary from duckdb.org.

2. Query a CSV without loading it

Point a query straight at a file path. DuckDB reads it on the fly, inferring column names and types, and nothing is copied into a database first.

-- read a council spending CSV in place
SELECT service_area, sum(amount) AS total
FROM 'council-spending.csv'
GROUP BY service_area
ORDER BY total DESC;

-- or be explicit with the reader for control over options
SELECT count(*) FROM read_csv_auto('council-spending.csv');

read_csv_auto() auto-detects delimiters, headers and types; drop to read_csv() with explicit options when a messy file needs a firm hand.

3. Parquet, JSON and many files at once

Parquet is a compact columnar file format that DuckDB reads especially fast. You can also glob many files into one virtual table — useful for a folder of monthly exports.

-- a single Parquet file
SELECT * FROM 'transactions.parquet' LIMIT 20;

-- every monthly CSV in a folder, as one table
SELECT * FROM read_csv_auto('spending/2024-*.csv');

-- read line-delimited JSON
SELECT * FROM read_json_auto('records.json');

4. Reading remote files

With the httpfsextension, DuckDB can query a file over HTTPS without downloading the whole thing first — handy for a large published Parquet dataset.

INSTALL httpfs;
LOAD httpfs;

SELECT count(*)
FROM read_parquet('https://example.org/open-data/latest.parquet');

Be considerate with remote sources: filter early so you only pull the rows and columns you need, and cache a local copy if you will query it repeatedly.

5. Using the command-line tool

Launch DuckDB with no arguments for an in-memory session, or pass a filename to persist a database. Dot-commands control output and run scripts.

# start an in-memory session
duckdb

# inside the shell: pretty output, timing, and run a script
.mode box
.timer on
.read pipeline.sql

# or run one query straight from the shell and exit
duckdb -c "SELECT count(*) FROM 'council-spending.csv'"

6. From Python and R

DuckDB has first-class bindings for both languages, so you can run SQL over files and hand the result to pandas or a tibble for charting.

# Python: query a file, get a pandas DataFrame back
import duckdb
df = duckdb.sql("SELECT service_area, sum(amount) AS total "
                "FROM 'council-spending.csv' GROUP BY service_area").df()
# R: connect via DBI and query into a data frame
library(duckdb)
con <- dbConnect(duckdb())
res <- dbGetQuery(con, "SELECT * FROM 'council-spending.csv' LIMIT 100")
dbDisconnect(con, shutdown = TRUE)

7. When DuckDB beats pandas or Postgres

  • 1A one-off question over a large CSV or Parquet file, where standing up a Postgres server is overkill.
  • 2Data larger than your computer's memory, which DuckDB streams rather than loading whole like pandas.
  • 3Aggregations -- sum, count, group-by -- over millions of rows, where columnar storage shines.
  • 4Joining two files of different formats (CSV and Parquet) without importing either.
  • 5A reproducible pipeline you can commit as a .sql file and re-run when the data refreshes.

8. A simple reproducible pipeline

Put the whole analysis in one pipeline.sql file: read the source in place, aggregate, and export the tidy result to a new file. Run it with duckdb -c ".read pipeline.sql" or from inside the shell.

-- pipeline.sql: source stays untouched, output is a new file
COPY (
  SELECT service_area,
         count(*)          AS payments,
         round(sum(amount)) AS total
  FROM 'council-spending.csv'
  WHERE amount > 25000
  GROUP BY service_area
  ORDER BY total DESC
) TO 'spend-by-area.csv' (HEADER, DELIMITER ',');

Swap the output to Parquet with (FORMAT parquet) if the next step is another query. Commit pipeline.sql to version control and anyone can reproduce the numbers.

9. Red flags

  • Trusting auto-detected types on a messy file; check the schema and cast columns like amounts and dates explicitly.
  • Hammering a remote file repeatedly instead of caching a local copy once you know you will reuse it.
  • Forgetting an in-memory session vanishes on exit; pass a filename or export results if you want to keep them.
  • Overwriting the source file with your output; always COPY to a new filename.
  • Assuming a headline number without a sanity check; confirm count(*) against the source row count.

10. Pipeline checklist

  • I have downloaded the DuckDB binary from duckdb.org.
  • I have inspected the inferred schema and cast amount and date columns explicitly where needed.
  • I have confirmed count(*) matches the source file's row count.
  • I have written the analysis as a .sql script rather than ad-hoc console commands.
  • I have exported results to a new file and left the raw download untouched.
  • I have committed the .sql pipeline to version control.

Get the data

Big UK CSV and Parquet exports — council spend, ONS, Land Registry — are exactly what DuckDB reads best. Browse sources, or file an FOI when the data is not published.

Related guides

Frequently asked questions

What is DuckDB, in plain terms?
DuckDB is a free, open-source database designed for analysis rather than for running a website. It is in-process, meaning it runs inside whatever you are already using -- the command line, a Python script, an R session -- with no server to install or keep running. It stores data column by column, which makes the sums, counts and group-bys that dominate journalism extremely fast. Think of it as SQLite's analytical cousin: a single lightweight tool that speaks standard SQL and can read your files where they sit. Download it from duckdb.org; the whole thing is one small binary.
How is DuckDB different from pandas or PostgreSQL?
Pandas loads data into your computer's memory and you manipulate it in Python; DuckDB runs SQL over files and can process datasets larger than memory by streaming them. Postgres is a full server you install, configure and load data into before you can query it. DuckDB sits between the two: the power of SQL and a query planner, but zero setup and nothing to maintain. For a one-off question over a large CSV or Parquet file, DuckDB is often the fastest path from download to answer. For a shared, always-on database behind a live page, Postgres remains the right choice.
Can DuckDB really query a CSV without importing it first?
Yes, and this is its defining trick for journalists. You point a SELECT straight at a file path and DuckDB reads it on the fly, inferring column names and types as it goes. The same works for Parquet and JSON, and for several files at once using a wildcard. Nothing is copied or loaded into a database first, so you can aggregate a multi-gigabyte export in seconds without filling your disk with a second copy. If you want to keep the result, you save just the aggregated output. This read-in-place model is what makes DuckDB feel so light for exploratory work.
When should a journalist reach for DuckDB over the alternatives?
Reach for it when you have a big flat file and a specific question: how much did every council spend on a category, which suppliers appear across datasets, what is the monthly trend in a large export. It excels at one-off and repeatable analysis where the data arrives as CSV or Parquet and you do not want the overhead of a server. It is also a superb glue tool: query remote Parquet, join two files of different formats, and export a tidy CSV for a chart. If you need many users, live writes or a permanent store, use Postgres; if you need heavy statistics or bespoke charts, pair DuckDB with R or Python.
How do I make a DuckDB analysis reproducible?
Write your queries into a plain .sql file and run it as a script rather than typing commands live. The command-line tool can execute a file, so your whole pipeline -- read the source, clean, aggregate, export -- lives in one text file you can commit to version control and re-run whenever the data updates. Because DuckDB reads files in place, the script plus the original download is all anyone needs to reproduce your numbers. Keep the raw file untouched, do every transformation in SQL, and export results to a new file so the source is never overwritten. That is a clean, auditable pipeline.