DuckDB Analytics skill for Codex
SQL straight over Parquet and CSV, no load step. Codex profiles files with DESCRIBE and SUMMARIZE before writing SQL, reads CSV with explicit types and a reject table instead of silent skips, and writes results as sorted, partitioned Parquet. It uses DuckDB idioms like QUALIFY, GROUP BY ALL, PIVOT and ASOF joins, sets memory limits and spill directories, and saves every answer as a re-runnable .sql file.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- duckdb-analytics Installs to ~/.agents/skills/duckdb-analytics/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install duckdb-analytics The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project duckdb-analytics Installs to .agents/skills/duckdb-analytics/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/duckdb-analytics.zipunzip -o duckdb-analytics.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) duckdb-analytics PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $duckdb-analytics, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Runs analytical SQL with DuckDB directly over Parquet, CSV and JSON files, local folders, S3 or GCS - read_parquet and read_csv options, hive partitioning, COPY TO partitioned Parquet, SUMMARIZE profiling, window functions, PIVOT, memory limits and the Python and CLI clients. Use when the user wants to query or join data files with SQL, profile a dataset, convert CSV to Parquet, build a local analytics database, replace a slow pandas script with SQL, or asks why a DuckDB query runs out of memory.
Use it when
- You want SQL answers from Parquet, CSV or JSON files without standing up a database
- A dataset needs profiling or conversion to Parquet
- A local analytics database or reproducible report query is needed
- A DuckDB query is slow or exceeds memory
Use something else when
- The data lives in a cloud warehouse already: use snowflake-sql or bigquery-optimization
- You need a server-side real-time analytics store: use clickhouse-analytics
- The question is analyst SQL patterns like funnels and cohorts: use sql-for-analytics
How to check the result
Row counts and a known total match between source files and outputs, and the saved .sql file reproduces the result from a fresh shell.
The mistake it prevents
Trusting CSV auto-detection on a sample, which mistypes ID columns or fails on a malformed row deep in the file.
Done well means: files are queried in place without a load step unless reuse justifies one,
types are explicit wherever inference is shaky, outputs are Parquet with sensible partitions,
and every answer is reproducible from a single .sql file or script.
Before running anything
- Read
AGENTS.md. Check how DuckDB is available:duckdb --version(CLI) orpython -c "import duckdb; print(duckdb.__version__)". This skill targets DuckDB 1.x. - Find the data and its shape before writing the query:
DESCRIBE SELECT * FROM 'data/orders/*.parquet';
SUMMARIZE SELECT * FROM 'data/orders/*.parquet'; -- min, max, nulls, approx distinct per column
SELECT * FROM read_csv('raw/events.csv', sample_size = -1) LIMIT 5;
SELECT * FROM parquet_metadata('data/orders/part-0.parquet'); -- row groups, stats
- Decide where results live:
Situation Choice One-off question over files In-memory connection, query files directly Repeated queries, joins across many sources Persistent analytics.duckdbfile with tables or viewsOutput consumed by other tools COPY ... TOParquetConcurrent writers from several processes Not DuckDB’s model (single writer per file); use a server database
Reading files
-- CSV with explicit schema when auto-detection guesses wrong
SELECT *
FROM read_csv('raw/events_*.csv',
header = true,
delim = ',',
columns = {'event_id': 'VARCHAR', 'user_id': 'VARCHAR', 'ts': 'TIMESTAMP', 'value': 'DOUBLE'},
timestampformat = '%Y-%m-%d %H:%M:%S',
filename = true, -- adds a column with the source file
ignore_errors = false); -- keep false until you know why rows fail
-- Parquet folders with key=value directories, schemas that drift between files
SELECT *
FROM read_parquet('lake/orders/**/*.parquet', hive_partitioning = true, union_by_name = true)
WHERE year = 2025 AND month = 9; -- partition pruning on directory keys
-- JSON lines
SELECT * FROM read_json('logs/*.jsonl', format = 'newline_delimited');
store_rejects = trueonread_csvcollects bad rows intoreject_errorsinstead of failing or silently skipping them. Report the reject count.- IDs with leading zeros: force
VARCHARincolumnsortypes.
Remote storage
httpfs handles S3, GCS, R2 and HTTPS and autoloads on first use; it may need network for
the first INSTALL, which in a Codex sandbox requires approval.
CREATE SECRET aws (TYPE s3, PROVIDER credential_chain); -- env vars, ~/.aws, SSO
SELECT count(*) FROM read_parquet('s3://bucket/events/date=2025-09-*/*.parquet');
Never paste access keys into a SQL file that is committed. Use credential_chain or
environment variables.
Analytical SQL patterns
-- GROUP BY ALL and QUALIFY keep queries short
SELECT customer_id, date_trunc('month', created_at) AS month, sum(amount) AS revenue
FROM orders
GROUP BY ALL;
-- latest row per key
SELECT * FROM orders
QUALIFY row_number() OVER (PARTITION BY order_id ORDER BY updated_at DESC) = 1;
-- pivot months into columns
PIVOT monthly ON month USING sum(revenue) GROUP BY customer_id;
-- as-of join: price in effect at order time
SELECT o.*, p.price
FROM orders o ASOF LEFT JOIN prices p
ON o.sku = p.sku AND o.created_at >= p.valid_from;
-- reuse expressions by alias in the same SELECT, exclude columns
SELECT * EXCLUDE (raw_payload), amount * fx AS amount_eur, amount_eur * 0.19 AS vat
FROM orders;
Writing outputs
COPY (SELECT * FROM cleaned_orders)
TO 'out/orders' (FORMAT parquet, PARTITION_BY (year, month), COMPRESSION zstd,
OVERWRITE_OR_IGNORE true);
COPY (SELECT * FROM summary ORDER BY month) TO 'out/summary.csv' (HEADER, DELIMITER ',');
- Partition by low-cardinality columns that queries filter on (date parts, region). Thousands of tiny files are worse than no partitioning.
ORDER BYthe main filter column before writing a single Parquet file so row-group min/max statistics allow skipping.
Python client
import duckdb
con = duckdb.connect("analytics.duckdb") # or duckdb.connect() for in-memory
con.execute("SET memory_limit = '4GB'; SET threads = 4;")
rel = con.sql("SELECT status, count(*) AS n FROM 'data/orders/*.parquet' GROUP BY ALL")
df = rel.df() # pandas; rel.pl() for Polars, rel.arrow() for Arrow
con.sql("SELECT * FROM df WHERE n > 100") # DataFrames in scope are queryable by name
# parameters, never f-strings with user input
con.execute("SELECT * FROM orders WHERE customer_id = ?", ["C-1042"]).fetchall()
con.close()
Memory and performance
| Symptom | Fix |
|---|---|
| Out of memory on a big join or aggregate | SET memory_limit = '...' below container limit; SET temp_directory = '/tmp/duck' so operators spill |
Still OOM with ORDER BY on huge output | SET preserve_insertion_order = false when order is not needed |
| Slow scans over many small files | Compact into fewer larger Parquet files (target 100 MB to 1 GB) |
| Partition filter ignored | Filter on the hive column itself, not on a function of it |
| Slow repeated reads of CSV | Convert to Parquet or a table once |
Use EXPLAIN ANALYZE <query> to see per-operator time and cardinality. Check that filters
appear inside the scan node (pushdown) rather than above it.
Gotchas
- A
.duckdbfile opened read-write by one process cannot be opened by another; useduckdb.connect(path, read_only=True)for concurrent readers. - Database files are tied to storage format versions; old CLIs may not open newer files.
Exchange data as Parquet, not
.duckdbfiles. read_csvauto-detection samples rows; a bad value late in the file errors at query time.sample_size = -1scans everything for detection.- Integer division:
/returns a double in DuckDB 1.x; use//for integer division. - Timestamps without time zone stay naive. Use
TIMESTAMPTZand setTimeZoneexplicitly when the source carries offsets. SUMMARIZEdistinct counts are approximate.
Verification
- Row counts:
SELECT count(*)on source and output, plus counts per partition. - Spot-check totals against a known figure (finance report, dashboard).
- Re-run the saved
.sqlfile from a clean shell:duckdb analytics.duckdb < queries/report.sql. - Check output files:
SELECT count(*), min(created_at), max(created_at) FROM 'out/orders/**/*.parquet';
Report template
**Question:** monthly revenue by country, Jan-Sep 2025
**Sources:** lake/orders (hive, 212 files, 48.1M rows), customers.csv (88k rows)
**Query:** queries/monthly_revenue.sql (runs in 4.2 s, memory_limit 4GB)
**Output:** out/monthly_revenue.parquet (243 rows)
**Checks:** row counts match source; September total equals finance export within 0.01
**Caveats:** 311 CSV rows rejected (malformed timestamps), listed in out/rejects.csv What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "DuckDB Analytics"
short_description: "SQL straight over Parquet and CSV, no load step"
default_prompt: "$duckdb-analytics Use DuckDB to join these three CSV exports and give me revenue by month" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.