DS DevShelfHub Projects · AI tools
Cheatsheets / DuckDB
Cheatsheet · Dev tooling

DuckDB: In-process OLAP SQL, Parquet and Python Reference Guide

By DevShelfHub

In-process OLAP SQL — query Parquet, CSV, and JSON in place without loading into a server. Covers LIST/STRUCT/MAP types, QUALIFY, ASOF JOIN, PIVOT, window functions, the Python API, and Arrow/DataFrame interop for duckdb 1.1+.

106 items 8 min OLAP Parquet In-process

Start hereQuick start · 6 you’ll reach for daily

Open a DBduckdb my.duckdb
Query a CSVSELECT * FROM 'sales.csv'
Query ParquetSELECT * FROM 's3://b/*.parquet'
From a DataFramecon.sql("SELECT * FROM df")
ProfileEXPLAIN ANALYZE SELECT ...
Export ParquetCOPY (...) TO 'out.parquet'

Target versions · paceVersions

Targets: duckdb ≥ 1.1 python ≥ 3.9 arrow / polars optional

DuckDB is an in-process columnar SQL engine — like SQLite for analytics. Released 1.0 in mid-2024; the project ships features fast (QUALIFY, ASOF JOIN, PIVOT/UNPIVOT, typed secrets, attach to Postgres/MySQL/SQLite). The on-disk file format is stable across 1.x.

Install · connectSetup

bash
# macOS / Linux — Homebrew
brew install duckdb

# Or grab a static binary (Linux/macOS/Windows)
curl -L https://install.duckdb.org | sh

# Python — pip
pip install "duckdb>=1.1"

# Open in-memory shell
duckdb

# Open or create a persistent DB file
duckdb sales.duckdb

# One-shot — emit CSV from a Parquet file in one line
duckdb -c "COPY (SELECT * FROM 's3://bucket/y=2026/*.parquet') TO 'out.csv'"

# Python check
python -c "import duckdb; print(duckdb.__version__)"

The shellduckdb · dot-commands

duckdbOpen an in-memory database.
duckdb sales.duckdbOpen or create a persistent file.
duckdb -c "SELECT 1"One-shot query, exits after.
.helpList dot-commands.
.mode line | csv | column | markdown | json | parquetSwitch result formatting.
.headers onPrint column names.
.timer onWall-clock per query.
.maxrows 100Cap rows shown (full data still computed).
.maxwidth 80Truncate wide columns.
.read script.sqlRun SQL from a file.
.output result.csvRedirect output. .output stdout resets.
.tables [pat] · .schema [name]List tables / show CREATE.
.quitExit.

CSV · Parquet · JSONQuery files in place

DuckDB treats files as tables. Predicate + projection pushdown means a 100 MB Parquet scan with one filter often reads only a few MB.

SELECT * FROM 'sales.csv'Auto-detected schema, header, delimiter.
read_csv_auto('sales.csv', sample_size=20000)Same with larger detection sample.
read_csv('s.csv', header=true, delim='|', columns={'id':'INTEGER','name':'VARCHAR'})Explicit schema — no auto-detect cost.
SELECT * FROM 'sales.parquet'Parquet via shorthand. Pushdown enabled.
read_parquet('s3://b/y=*/m=*/*.parquet', hive_partitioning=true)Glob + Hive partition columns become real columns.
read_parquet(['a.parquet','b.parquet'])Explicit list works alongside globs.
SELECT * FROM 'data.jsonl'Newline-delimited JSON.
read_json_auto('records.json')JSON array → rows.
COPY t FROM 'data.csv' (HEADER, AUTO_DETECT TRUE)Bulk-load an existing table.
CREATE SECRET s3_demo (TYPE s3, KEY_ID '...', SECRET '...', REGION 'us-east-1')Typed secret for S3 / GCS / Azure / HTTP basic.
sql
-- Query a remote Parquet file in place — no copy, no load.
-- DuckDB pushes filters + column projections down into the Parquet reader.
INSTALL httpfs; LOAD httpfs;

-- (Optional) S3 credentials via the typed secret API
CREATE OR REPLACE SECRET s3_demo (
    TYPE s3,
    KEY_ID    'AKIA...',
    SECRET    '****',
    REGION    'us-east-1'
);

-- Glob across a Hive-partitioned dataset; only the year=2026 files are read.
SELECT region,
       sum(amount) AS revenue,
       count(*)    AS orders
FROM read_parquet(
    's3://acme-data/orders/year=*/month=*/*.parquet',
    hive_partitioning = TRUE
)
WHERE year = 2026
  AND status = 'paid'
GROUP BY region
ORDER BY revenue DESC;

-- Materialize a slice locally for further work
CREATE TABLE orders_q1 AS
SELECT * FROM read_parquet('s3://acme-data/orders/year=2026/month=0[1-3]/*.parquet');

Primitives · compositesData types

TINYINT · SMALLINT · INTEGER · BIGINT · HUGEINTSigned ints (1, 2, 4, 8, 16 bytes).
UTINYINT · USMALLINT · UINTEGER · UBIGINTUnsigned counterparts.
DECIMAL(18, 4)Fixed precision. Use for money.
FLOAT · DOUBLEIEEE-754. Avoid for currency.
VARCHARUTF-8 string — no length limit; VARCHAR(n) is hint-only.
DATE · TIME · TIMESTAMP · TIMESTAMPTZ · INTERVALTemporal types. Microsecond precision.
LIST(INTEGER)   e.g. [1, 2, 3]Homogeneous array, variable length.
STRUCT(a INT, b VARCHAR)   e.g. {'a':1,'b':'x'}Named-field record. Fields are addressable.
MAP(VARCHAR, INTEGER)   e.g. MAP{'a':1}Key-value pairs of one type each.
UNION(num INT, text VARCHAR)Tagged union — one of multiple types per row.
CREATE TYPE size AS ENUM('s','m','l')Bounded set; stored as tiny int.

Tables · attach · copyDDL & DML

CREATE TABLE t (id INT PRIMARY KEY, ...)Standard. PK enforced.
CREATE OR REPLACE TABLE t AS SELECT ...Idempotent rebuild — nice in notebooks.
CREATE TEMP TABLE t (...)Session-scoped; gone on disconnect.
CREATE VIEW v AS SELECT ...Saved query; recomputed on each call.
INSERT INTO t SELECT * FROM 'src.parquet'Bulk load via the file reader.
COPY t TO 'out.parquet' (FORMAT 'parquet', COMPRESSION 'zstd')Single-file export.
COPY (SELECT ...) TO 'out.parquet'Export an ad-hoc query.
COPY t TO 'dir' (FORMAT PARQUET, PARTITION_BY (year, month))Hive-partition the output by columns.
EXPORT DATABASE 'dump_dir'Schema + Parquet data for every table.
IMPORT DATABASE 'dump_dir'Restore from EXPORT DATABASE.
ATTACH 'other.duckdb' AS otherMount a second DuckDB file.
ATTACH 'postgres:dbname=app' AS pg (TYPE POSTGRES)Live-read Postgres in queries (postgres_scanner).

Friendly SQL extrasQueries

SELECT * EXCLUDE (password) FROM usersDrop columns from * without listing the rest.
SELECT * REPLACE (lower(email) AS email) FROM usersRewrite one column in-place.
SELECT * RENAME (created AS created_at) FROM tRename on read; no DDL needed.
SELECT COLUMNS('^amt_') FROM salesRegex column picker — sums for all amt_*.
SELECT * FROM t USING SAMPLE 10%Statistically uniform sample.
SELECT * FROM t USING SAMPLE 1000 ROWS (reservoir)Fixed-size sample — great for dashboards.
SELECT * QUALIFY row_number() OVER (PARTITION BY u ORDER BY ts DESC) = 1Filter on a window result without a subquery.
SELECT u.*, p.* FROM users u, LATERAL (SELECT * FROM posts WHERE user_id=u.id LIMIT 3) pTop-N per group via LATERAL.
PIVOT sales ON region USING sum(amount)Wide-table pivot in one statement.
UNPIVOT wide ON jan, feb, mar INTO NAME month VALUE salesLong-format the opposite direction.
SELECT o.* FROM orders o ASOF JOIN prices p ON o.sym=p.sym AND o.ts >= p.tsAs-of join — pick the latest price ≤ order timestamp.

OVER framesWindow functions

row_number() OVER (PARTITION BY u ORDER BY ts)Sequential index per group.
rank() · dense_rank() · ntile(4)Ranking + quartile bucketing.
lag(col, 1) · lead(col, 1)Prev / next row in the partition.
sum(x) OVER (PARTITION BY u ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)Trailing 7-day total.
avg(x) OVER (ORDER BY ts RANGE BETWEEN INTERVAL 1 HOUR PRECEDING AND CURRENT ROW)Time-range frame.
first_value(x) OVER (... GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW)Group-based frame (3 frame modes: ROWS / RANGE / GROUPS).
... ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROWFrame exclusion clause.
WINDOW w AS (PARTITION BY u ORDER BY ts) ... sum(x) OVER wNamed window — reuse the spec across columns.
sql
-- 7-day trailing revenue per user, with day-over-day delta and quartile bucket.
WITH daily AS (
    SELECT user_id,
           date_trunc('day', ts) AS d,
           sum(amount)           AS rev
    FROM orders
    WHERE status = 'paid'
    GROUP BY user_id, d
)
SELECT user_id, d, rev,
       -- trailing 7 days, including today (sliding window, rows-based)
       sum(rev) OVER w7 AS rev_7d,
       -- yesterday's revenue, for delta plots
       lag(rev, 1) OVER (PARTITION BY user_id ORDER BY d) AS rev_prev,
       -- quartile bucket within each day
       ntile(4) OVER (PARTITION BY d ORDER BY rev) AS rev_q
FROM daily
WINDOW w7 AS (
    PARTITION BY user_id ORDER BY d
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)
QUALIFY rev_q = 4                                 -- keep top-quartile rows only
ORDER BY d DESC, rev_7d DESC;

GROUP BY · FILTERAggregations

GROUP BY GROUPING SETS ((), (u), (u, region))Multiple groupings in one pass.
GROUP BY ROLLUP (region, city)Hierarchical totals + subtotals.
GROUP BY CUBE (a, b)All combinations of grouping cols.
count(*) FILTER (WHERE status='ok')Per-aggregate conditional. Cleaner than CASE WHEN.
array_agg(col ORDER BY ts) · list(col)Collect into a LIST. list() is the DuckDB alias.
string_agg(name, ', ' ORDER BY name)Concatenate.
approx_count_distinct(col)HyperLogLog. Fast, ~2% error.
quantile_cont(col, [0.5, 0.95, 0.99])Multiple quantiles in one expression.
histogram(col, 10)Equal-width bins; returns a LIST(STRUCT).

duckdb packagePython integration

import duckdb; con = duckdb.connect()In-memory connection.
duckdb.connect("file.duckdb")Persistent.
duckdb.sql("SELECT 1")Module-level shortcut on the default in-memory con.
con.execute("SELECT * FROM t WHERE id = ?", [42]).fetchone()Parameterized. ? placeholders.
con.sql("SELECT * FROM df_var")Reference a Python DataFrame by its variable name.
con.register("name", df)Register a DataFrame under a chosen name.
rel = con.sql("...") · rel.df() · rel.arrow() · rel.pl()→ pandas / pyarrow / polars (zero-copy where possible).
rel.fetchnumpy()→ dict of NumPy arrays.
rel.write_parquet("out.parquet", compression="zstd")Direct Parquet write — skips Python.
con.from_query("SELECT ...").show()Inspect interactively in a notebook.
python
import duckdb
import pandas as pd

# In-memory connection (or duckdb.connect("file.duckdb") for persistence)
con = duckdb.connect()

# Pandas → DuckDB: query a DataFrame by its Python variable name
sales = pd.DataFrame({"region": ["EU", "US", "EU"], "amount": [10, 20, 5]})
top = con.sql("""
    SELECT region, sum(amount) AS total
    FROM sales
    GROUP BY region
    ORDER BY total DESC
""")
print(top.df())                          # Result -> DataFrame
print(top.arrow())                       # Result -> pyarrow.Table (zero-copy)

# Parameterized — bind values, never f-string
row = con.execute(
    "SELECT total FROM (SELECT region, sum(amount) AS total FROM sales GROUP BY region) "
    "WHERE region = ?", ["EU"]
).fetchone()
print(row)                               # (15,)

# Register a fresh DataFrame under a chosen name
con.register("orders_df", pd.read_csv("orders.csv"))
con.sql("SELECT count(*) FROM orders_df").show()

# Export query result straight to Parquet — no intermediate file
con.sql("SELECT * FROM sales WHERE amount > 5").write_parquet("hot.parquet")

Lazy-loadedExtensions

INSTALL httpfs; LOAD httpfs;HTTPS / S3 / GCS / Azure Blob.
INSTALL parquet; LOAD parquet;Bundled but lazy. Auto-loaded when you read a .parquet path.
INSTALL spatial; LOAD spatial;GIS types, GeoParquet, Shapefile, Excel via GDAL.
INSTALL fts; LOAD fts;Inverted-index full-text search; BM25 ranking.
INSTALL json; LOAD json;JSON read + path functions.
INSTALL postgres_scanner; LOAD postgres_scanner;ATTACH a live Postgres database.
INSTALL sqlite_scanner; LOAD sqlite_scanner;ATTACH a SQLite file as a foreign DB.
SELECT * FROM duckdb_extensions()List installed / loaded extensions.

EXPLAIN · pragmasPerformance

PRAGMA threads = 8Worker count. Default = physical cores.
PRAGMA memory_limit = '8GB'Soft cap. Engine spills to temp_directory over this.
PRAGMA temp_directory = '/tmp/duck'Where to spill larger-than-RAM queries.
EXPLAIN SELECT ...Plan tree.
EXPLAIN ANALYZE SELECT ...Plan + per-op timings + row counts.
PRAGMA enable_profiling = 'json'; SET profile_output='plan.json'Persist profile to a file.
SET preserve_insertion_order = falseFaster scans; final ordering undefined unless you ORDER BY.
CHECKPOINTFlush WAL into the main file. Safe before backup.

Full pipeline · ~25 linesEnd-to-end · Parquet analytics

Reads a Hive-partitioned dataset on S3, ranks the top-5 regions per month, and writes the result back out as a single Parquet file. Replace the S3 path and you have a working analytics job.

python
# End-to-end: read remote Parquet, aggregate, write Parquet out.
# Replaces a small PySpark/Pandas script — runs single-process, multi-core.
import duckdb

con = duckdb.connect()                                  # in-memory, multi-threaded
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute("PRAGMA threads = 8")
con.execute("PRAGMA memory_limit = '4GB'")

# Hive-partitioned dataset on S3 → ranked top regions per month, in one query
result = con.sql("""
    WITH paid AS (
        SELECT region, year, month, amount
        FROM read_parquet(
            's3://acme-data/orders/year=*/month=*/*.parquet',
            hive_partitioning = TRUE
        )
        WHERE status = 'paid'
    )
    SELECT year, month, region, revenue
    FROM (
        SELECT year, month, region,
               sum(amount)                                            AS revenue,
               row_number() OVER (PARTITION BY year, month
                                  ORDER BY sum(amount) DESC)          AS rk
        FROM paid
        GROUP BY year, month, region
    )
    WHERE rk <= 5
    ORDER BY year DESC, month DESC, rk
""")

result.write_parquet("top_regions_by_month.parquet")    # columnar, compressed
print(result.df().head())                               # peek as a DataFrame

Best practiceGood to know

Read remote Parquet directly — don’t download first. With httpfs loaded, predicate + column pushdown means a WHERE year=2026 on a Hive-partitioned bucket only fetches the matching files. Often beats Spark for one-machine workloads up to ~100 GB.
Use QUALIFY instead of a subquery filter. SELECT * QUALIFY row_number() OVER (...) = 1 is the cleanest top-N-per-group pattern you’ll find in SQL.
Round-trip with Arrow when leaving Python. rel.arrow() is zero-copy into pyarrow; from there, hand-off to Polars / DataFusion / a network sink without re-materializing.

Common trapsWatch out for

DuckDB is single-process. One writer at a time per .duckdb file. For shared writes you need a server (DuckDB Cloud, MotherDuck) or a Postgres in front. It is not a replacement for OLTP.
Auto-detect can guess wrong on messy CSVs. Mixed-typed columns become VARCHAR silently. Pass an explicit columns={'id':'INTEGER',...} for production loads.
Insertion order is preserved by default — at a cost. SET preserve_insertion_order = false can double scan speed on wide analytical queries. Just remember to ORDER BY when order actually matters.

Go deeperSee also

DuckDB FAQ

What is DuckDB and how is it different from SQLite?

DuckDB is an in-process columnar SQL engine optimised for analytical (OLAP) queries — aggregations, window functions, and scans across millions of rows. SQLite is row-oriented and built for transactional (OLTP) workloads. DuckDB is like SQLite for data analysis.

Can DuckDB query Parquet and CSV files directly?

Yes. DuckDB can query Parquet, CSV, JSON, Arrow, and even remote S3 files without importing them first. Use SELECT * FROM read_parquet('s3://bucket/data/*.parquet') or simply FROM 'file.csv'. The scanner infers schema automatically.

Is DuckDB suitable for production use?

DuckDB is production-ready for single-node analytical workloads — ETL pipelines, data exploration, and read-heavy dashboards. It is not a replacement for OLTP databases or distributed systems like Spark. For cloud-scale or multi-writer scenarios, consider MotherDuck or Parquet on S3.

How do I use DuckDB with Python pandas or polars?

Create a connection with duckdb.connect() then use con.sql("SELECT * FROM df") to query a pandas or polars DataFrame directly by name — no import step needed. Fetch results with .df() for pandas, .pl() for polars, or .arrow() for PyArrow.

How does DuckDB compare to BigQuery or Snowflake?

DuckDB runs in-process on your machine or server with no infrastructure cost. BigQuery and Snowflake are managed cloud data warehouses that scale across distributed storage but charge per query. DuckDB is faster for local data and free; cloud warehouses shine for terabyte-scale shared datasets.

Does DuckDB support window functions and advanced SQL?

Yes. DuckDB supports the full window function syntax, QUALIFY (filtering window results inline), ASOF JOIN for time-series alignment, PIVOT/UNPIVOT, LIST/STRUCT/MAP types, and recursive CTEs. It closely follows the SQL standard and PostgreSQL extensions.