DuckDB — embedded columnar OLAP, no server
SkillDatabases & dataUse when analytical SQL must run in-process with no server: Parquet/CSV/JSON/Arrow queried in place, OLAP embedded in an app or notebook, a slow pandas groupby on multi-GB data, or S3/lakehouse data read without downloading. NOT a multi-user analytics server (that is clickhouse-analytics), NOT an app's transactional CRUD store (that is postgresdb).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the DuckDB — embedded columnar OLAP, no server skill
What this skill tells your AI
The instructions your AI receives, as published by ericrisco/rsc-harness in skills/duckdb/SKILL.md and read by ahel’s review.
DuckDB is an in-process analytical (OLAP) database: it links into your process like SQLite, but stores
data column-by-column and vectorizes execution for aggregates, joins, and window functions. There is no
server, no port, no daemon — you pip install duckdb (or drop one CLI binary) and query. Its killer move
is reading Parquet/CSV/JSON/Arrow in place, without a load step, so a folder of files becomes a table.
The fact that drives every routing decision below: DuckDB is one writer, many readers, single process. It is brilliant for analysis on one machine and wrong for multi-user serving or transactional app writes.
Latest stable is v1.5.3 (released 2026-05-20). Pin the LTS line (v1.4.x; v1.4.4 LTS shipped 2026-01-26) for anything long-lived — LTS gets ~1 year of patches and a stable storage format. Use 1.5.x for greenfield exploration.
Is DuckDB the tool? (decide first)
| Your workload | Reach for |
|---|---|
| Analytics over local/remote files, one process, one writer | duckdb (this skill) |
| Many concurrent users, production query API, dashboards-as-a-service, petabyte scale | clickhouse-analytics |
| App transactional CRUD: users, orders, many small writes, FKs, connection pool | postgresdb |
| Embedded single-file transactional store / edge / sync | sqlite-turso |
| Pure SQL syntax question, engine-agnostic (window fns, CTEs) | sql |
| Similarity / embedding search as the core workflow | vector-db |
The two you will confuse most: DuckDB vs ClickHouse is embedded-single-node vs server-distributed — under ~10GB on one box DuckDB usually wins; pick ClickHouse when many people query concurrently. DuckDB vs SQLite is same niche, opposite workload — both embedded single-file, but SQLite is row-store OLTP and DuckDB is column-store OLAP. Don't run your app's writes through DuckDB.
Install & version
# Python (replacement-scan + relational API)
pip install 'duckdb==1.4.4' # LTS, for long-lived projects — stable storage format + patches
pip install duckdb # current stable, for greenfield exploration
# CLI (single static binary)
curl https://install.duckdb.org | sh # or: brew install duckdb
duckdb -version
Why pin LTS for production: the on-disk .duckdb format and extension ABI are stable within an LTS line,
so a routine upgrade won't strand a persisted database or break an installed extension mid-project.
Query files directly — the killer feature
Do not load a file into pandas just to query it. Point DuckDB at the path and let it scan only the
columns and row groups it needs (Parquet metadata pushdown). A bare string path is a replacement scan, so
FROM 'data/*.parquet' works without naming a reader.
import duckdb
# BAD: read the whole file into RAM, then aggregate in pandas
import pandas as pd
df = pd.read_parquet("sales/") # pulls every column of every file into memory
out = df.groupby("region")["amount"].sum()
# GOOD: scan in place, only the two needed columns ever touch memory
out = duckdb.sql("""
FROM 'sales/*.parquet'
SELECT region, sum(amount) AS revenue
GROUP BY ALL
ORDER BY revenue DESC
""").df()
Readers and globs you will actually use:
SELECT * FROM read_parquet('s3://bkt/y=*/m=*/*.parquet', filename = true); -- glob + source col
SELECT * FROM read_csv_auto('events.csv'); -- sniff delimiter/types/header
SELECT * FROM read_csv('raw.csv', header = false, types = {'id': 'BIGINT'}); -- when sniffing is wrong
SELECT * FROM read_json_auto('logs/*.ndjson'); -- newline-delimited or array JSON
filename = true adds a filename column — essential when a glob mixes partitions and you need to know
which file a row came from.
In-memory vs persistent
con = duckdb.connect() # in-memory: default, gone when the process exits
con = duckdb.connect("analytics.duckdb") # single file, created if absent; extension is not significant
Persist when: the dataset is reused across runs, an intermediate result is larger than RAM (DuckDB spills to the file), or you are curating a dataset to share. Otherwise stay in-memory — it is the fast path and needs no cleanup. The whole database is one file; copy it to move the database.
Python / dataframe interop
In-scope pandas/Polars/Arrow frames are queryable by variable name — that is a replacement scan, no registration needed. The relational API is lazy; nothing executes until you materialize.
import duckdb, pandas as pd
orders = pd.read_parquet("orders.parquet") # ordinary frame in local scope
rel = duckdb.sql("FROM orders SELECT region, sum(amount) AS rev GROUP BY ALL") # lazy, by name
rel.df() # -> pandas rel.pl() # -> Polars
rel.arrow() # -> Arrow table rel.fetchall() # -> list[tuple]
Use a single connection per thread, never share one cursor across threads. Full client surface — parameterized queries, relational operators, NumPy/torch round-trips, threading rules — is in references/python-and-interop.md.
Friendly SQL — use the dialect
DuckDB's dialect removes the boilerplate that makes analytics SQL tedious. Prefer it in DuckDB-only code.
FROM events SELECT count(*); -- FROM-first: pipe-friendly, valid on its own
SELECT * EXCLUDE (raw_payload) FROM events; -- everything but the noisy column
SELECT * REPLACE (lower(email) AS email) FROM users; -- transform one column, keep the rest
SELECT region, sum(amount) FROM sales GROUP BY ALL; -- no restating non-aggregates
SELECT * FROM sales ORDER BY ALL; -- deterministic order without listing columns
SELECT COLUMNS('amount_.*') FROM sales; -- regex over column names
SELECT 1, 2, 3, -- trailing commas are legal
GROUP BY ALL / ORDER BY ALL are the biggest wins: add a column to the SELECT and the grouping follows
automatically, so the two clauses can't drift out of sync.
Remote + lakehouse data
Read from S3/GCS/HTTP without downloading first: load httpfs and store credentials in a secret.
INSTALL httpfs; LOAD httpfs;
CREATE SECRET s3 (TYPE s3, PROVIDER credential_chain); -- picks up env/role creds
SELECT region, sum(amount) FROM read_parquet('s3://bkt/sales/*.parquet') GROUP BY ALL;
Iceberg, Delta, and DuckLake (DuckDB's own SQL-catalog lakehouse format) are read via extensions. Secret config, hive-partition globs, and the lakehouse one-liners live in references/remote-and-lakehouse.md.
Export & handoff
COPY (SELECT region, sum(amount) AS rev FROM 'sales/*.parquet' GROUP BY ALL)
TO 'summary.parquet' (FORMAT parquet);
COPY sales TO 'out/' (FORMAT parquet, PARTITION_BY (year, month)); -- hive-partitioned dataset
Parquet is the default handoff: it keeps types and reads straight back into DuckDB, Spark, pandas, or
ClickHouse. Use PARTITION_BY so downstream readers can prune partitions.
Outgrowing DuckDB
DuckDB scales up (more RAM/threads, out-of-core spill), not out. Tune within one box:
PRAGMA threads = 8; -- match cores
PRAGMA memory_limit = '12GB'; -- cap RAM; the rest spills to the temp dir / database file
Hand off when you hit the single-process wall: concurrent writers or a multi-user query API or an
always-on service → clickhouse-analytics. Want managed/shared/hybrid
DuckDB without running infra → MotherDuck (managed DuckDB-as-a-service; ATTACH 'md:') — the scale-out
escape hatch, not the default.
Anti-patterns
| Anti-pattern | Do instead |
|---|---|
| Loading a file into pandas, then querying the frame | Pages the whole file through RAM. FROM 'file.parquet' SELECT ... scans only needed columns. |
| Making DuckDB the app's database | One writer, single process — concurrent CRUD writers corrupt the workflow. App OLTP is postgresdb. |
| Serving a dashboard API for 200 users from DuckDB | It's embedded, not a server; concurrent users serialize on the writer. That's clickhouse-analytics. |
| Running the latest version in prod | Pin the LTS (1.4.x; v1.4.4 shipped 2026-01-26) so a future upgrade doesn't break the on-disk format / extension ABI. |
| Downloading the S3 files, then reading them | INSTALL httpfs + CREATE SECRET reads s3://... in place with row-group pushdown. No download. |
SELECT * over a 200-column Parquet | Columnar engine — list the columns you need so it skips the rest. SELECT * reads everything. |
GROUP BY a, b, c — restating every column | Drift bait. GROUP BY ALL tracks the SELECT automatically. |
| Treating "embedded" as "no need to think about RAM" | Set memory_limit / threads; without a cap a runaway aggregate can thrash before it spills. |
| Using DuckDB as the embeddings/vector store | VSS exists but vector search is vector-db's workflow, not DuckDB's home turf. |
Verify
Run scripts/verify.sh from anywhere. It runs a tiny self-contained smoke test — prefers the duckdb
CLI, falls back to python3 -c "import duckdb" — that runs an aggregate and a read_csv_auto over a
generated file and asserts a known scalar, proving the documented commands execute on your installed
version. If neither the CLI nor the Python module is present it prints SKIP and exits 0. No network.
Project grounding (02-DOCS + CLAUDE.md)
In a project with a 02-DOCS/ layer (the harness wiki), record this project's
DuckDB decisions — version pin, file layout, persistent vs in-memory, remote/secret setup — in
02-DOCS/wiki/stack/duckdb.md and index it in 02-DOCS/wiki/index.md (the Knowledge map; root CLAUDE.md
keeps only a short pointer to it). Read it first on
every use and keep choices consistent. No 02-DOCS/? Skip silently. Conventions are recorded, never gated.
Signals
- GitHub stars
- 82
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
duckdb- Source
- github.com/ericrisco/rsc-harness