- 1. Introduction
- 2. Understanding the DuckDB Architecture
- 3. The Vectorized Execution Engine
- 4. Installation and Environment Setup
- 5. Analytical SQL Queries in Practice
- 6. Python Integration
- 7. Data Lake Integration (Parquet, S3, Delta Lake)
- 8. Comparing Analytics Tools
- 9. Making Use of Extensions
- 10. MotherDuck: DuckDB in the Cloud
- 11. Performance Optimization Tips
- 12. Troubleshooting
- 13. Production Operating Patterns
- 14. Failure Cases and Responses
- 15. When Not to Use DuckDB
- 16. References

1. Introduction
The data analysis landscape is changing fast. In the past, running a single analytical query meant spinning up a Spark cluster, loading the data into BigQuery, or provisioning a Redshift instance. But in an era where a single laptop has more than 32GB of RAM and SSD speeds reach 3GB/s, people started asking: "do we really need a distributed system just to run analytics?"
DuckDB is the most persuasive answer to that question. Started by Mark Raasveldt and Hannes Muehleisen at CWI (Centrum Wiskunde and Informatica) in the Netherlands, the project applies SQLite's embedded philosophy to the OLAP world as an in-process analytical database. With no separate server process, a single pip install duckdb lets you aggregate Parquet files of hundreds of millions of rows in sub-second time.
The scenarios where you should choose DuckDB are as follows.
- You want to run analytical queries over a few GB to a few tens of GB quickly on a local machine
- You want to query Parquet, CSV, and JSON files directly with SQL, without an ETL pipeline
- You have hit the performance limits of a Pandas DataFrame and are looking for an alternative
- You want to analyze files in an S3 data lake directly, without a server
- You need analytical queries for data quality checks in a CI/CD pipeline
The cases where DuckDB is not a good fit are equally clear. For OLTP workloads that need concurrent writes, for service backends with hundreds of simultaneous connections, and for petabyte-scale distributed processing, tools such as PostgreSQL, ClickHouse, and Spark are the better choice.
2. Understanding the DuckDB Architecture
DuckDB's remarkable performance comes from the synergy of three core design principles: columnar storage, a vectorized execution engine, and morsel-driven parallelism (Morsel-Driven Parallelism).
In-Process Architecture
Like SQLite, DuckDB runs directly inside the application process with no separate server daemon. There is no network communication overhead at all, and no runtime dependencies. It loads as a library in a range of languages including Python, R, Java, and Node.js.
Columnar Storage
A traditional row-oriented database stores all the column values belonging to a single row in a contiguous block. When an analytical query needs only some of the columns, the rest of the data has to be read too, which wastes a lot of I/O.
DuckDB stores data column by column, so it scans only the columns the query needs. Because values of the same type sit next to one another, the compression ratio improves dramatically as well.
Row-oriented (SQLite/PostgreSQL):
┌──────┬─────────┬─────────┬──────────┐
│ id │ name │ country │ revenue │ -- every column of row 1
├──────┼─────────┼─────────┼──────────┤
│ id │ name │ country │ revenue │ -- every column of row 2
└──────┴─────────┴─────────┴──────────┘
↑ even for SELECT country, SUM(revenue)
the id and name column data is read unnecessarily
Columnar (DuckDB):
┌────────────────────────┐
│ id: 1, 2, 3, ... │ -- column block 1 (skipped)
├────────────────────────┤
│ name: A, B, C, ... │ -- column block 2 (skipped)
├────────────────────────┤
│ country: KR, US, ... │ -- column block 3 (scanned!)
├────────────────────────┤
│ revenue: 100, 200, ... │ -- column block 4 (scanned!)
└────────────────────────┘
↑ only the needed columns are read from disk
CPU cache efficiency is maximized
3. The Vectorized Execution Engine
The core mechanism behind DuckDB's performance is its vectorized execution engine (Vectorized Execution Engine). Understanding how it differs from the traditional Volcano (iterator) model makes it clear why DuckDB is fast.
Volcano Model vs Vectorized Model
In the traditional Volcano model, each operator in the query tree passes one row at a time up to the operator above it. Every row processed brings a virtual function call and a branch misprediction, so the function call overhead ends up costing more than the actual computation.
The vectorized model processes 1,024 to 2,048 values (a vector) as a bundle. The function call overhead is amortized across the vector, and the CPU's SIMD instructions and cache lines can be used to the full.
┌─────────────────────────────────────────────────────┐
│ DuckDB vectorized execution engine │
├─────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ SQL Parser │ SQL text → AST │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Binder │ resolve table/column names │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Query Optimizer │ join order, filter pushdown │
│ │ (Cost-based) │ subquery flattening │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Physical Plan (Pipeline-based) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────────┐ │ │
│ │ │Scan │→ │Filter│→ │HashAggr │ │ │
│ │ │(Vec) │ │(Vec) │ │ (Vec) │ │ │
│ │ └──────┘ └──────┘ └──────────┘ │ │
│ │ ↑ data passed in vectors (1024 rows) │ │
│ └─────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Morsel-Driven Parallelism │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Thread 1│ │Thread 2│ │Thread N│ │ │
│ │ │Morsel A│ │Morsel B│ │Morsel C│ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ each thread handles its own data chunk │ │
│ └─────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Result │ vector → DataFrame / Arrow Table │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
Morsel-Driven Parallelism
DuckDB splits table data into chunks called "morsels" (roughly 100,000 rows each). Each worker thread takes one morsel independently, processes it, and picks up the next one when it finishes. This work-stealing approach balances the load across threads naturally and is far more flexible than conventional partition-based parallelism.
4. Installation and Environment Setup
One of DuckDB's biggest advantages is how simple installation is. It has no external dependencies at all, and binaries are provided per platform.
CLI Installation
# macOS (Homebrew)
brew install duckdb
# Linux (apt)
sudo apt-get install duckdb
# Or download the binary directly
wget https://github.com/duckdb/duckdb/releases/latest/download/duckdb_cli-linux-amd64.zip
unzip duckdb_cli-linux-amd64.zip
chmod +x duckdb
sudo mv duckdb /usr/local/bin/
# Check the version
duckdb --version
Installing the Python Package
# pip
pip install duckdb
# conda
conda install -c conda-forge python-duckdb
# Install a specific version (for example, 1.2.x)
pip install duckdb==1.2.1
Confirming Basic Usage
-- Launch the DuckDB CLI
-- In-memory mode (data is discarded when the session ends)
duckdb
-- File-based persistent mode
duckdb my_analytics.db
-- Basic query test
SELECT version();
-- v1.2.1
-- Check system information
SELECT * FROM duckdb_settings()
WHERE name IN ('threads', 'memory_limit', 'temp_directory');
5. Analytical SQL Queries in Practice
DuckDB supports PostgreSQL-compatible SQL and offers a range of SQL extensions geared towards analytics.
Querying Files Directly
One of DuckDB's most powerful features is that you can query a file directly without a CREATE TABLE.
-- Query a CSV file directly
SELECT
product_category,
COUNT(*) AS order_count,
ROUND(AVG(amount), 2) AS avg_amount,
SUM(amount) AS total_revenue
FROM read_csv('sales_2025.csv', auto_detect=true)
WHERE order_date >= '2025-01-01'
GROUP BY product_category
ORDER BY total_revenue DESC
LIMIT 10;
-- Query several CSV files at once with a glob pattern
SELECT
filename,
COUNT(*) AS row_count
FROM read_csv('logs/access_*.csv', auto_detect=true, filename=true)
GROUP BY filename
ORDER BY row_count DESC;
-- Query a JSON file
SELECT
json_extract_string(data, '$.user.name') AS user_name,
json_extract(data, '$.events') AS events
FROM read_json('user_activity.json', auto_detect=true);
-- Query a Parquet file (the fastest format)
SELECT
region,
YEAR(event_timestamp) AS event_year,
COUNT(*) AS event_count
FROM read_parquet('events/*.parquet')
GROUP BY region, event_year
ORDER BY event_count DESC;
Using Window Functions
-- Moving average and ranking over sales data
WITH daily_sales AS (
SELECT
sale_date,
product_id,
SUM(amount) AS daily_amount
FROM read_parquet('sales_data.parquet')
GROUP BY sale_date, product_id
)
SELECT
sale_date,
product_id,
daily_amount,
-- 7-day moving average
ROUND(AVG(daily_amount) OVER (
PARTITION BY product_id
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_7d,
-- Sales rank per product (per day)
RANK() OVER (
PARTITION BY sale_date
ORDER BY daily_amount DESC
) AS daily_rank,
-- Change versus the previous day
ROUND(
(daily_amount - LAG(daily_amount) OVER (
PARTITION BY product_id ORDER BY sale_date
)) * 100.0 / NULLIF(LAG(daily_amount) OVER (
PARTITION BY product_id ORDER BY sale_date
), 0), 2
) AS pct_change
FROM daily_sales
ORDER BY sale_date DESC, daily_rank
LIMIT 20;
PIVOT and UNPIVOT
-- Monthly sales pivot table by category
PIVOT (
SELECT
MONTHNAME(order_date) AS month,
category,
SUM(revenue) AS total
FROM read_parquet('orders.parquet')
WHERE YEAR(order_date) = 2025
GROUP BY month, category
)
ON category
USING SUM(total)
GROUP BY month
ORDER BY month;
6. Python Integration
DuckDB's Python integration goes beyond a simple DB driver: it offers the distinctive ability to query Pandas and Polars DataFrames directly with SQL.
Querying a Pandas DataFrame Directly
import duckdb
import pandas as pd
# Create a Pandas DataFrame
sales_df = pd.DataFrame({
'product': ['A', 'B', 'C', 'A', 'B', 'C'] * 1000,
'region': ['Seoul', 'Busan', 'Seoul', 'Busan', 'Seoul', 'Busan'] * 1000,
'amount': [100, 200, 150, 300, 250, 180] * 1000,
'date': pd.date_range('2025-01-01', periods=6000, freq='h')
})
# Query the DataFrame directly with SQL (the variable name is the table name)
result = duckdb.sql("""
SELECT
product,
region,
COUNT(*) AS order_count,
ROUND(AVG(amount), 2) AS avg_amount,
SUM(amount) AS total_revenue,
MIN(date) AS first_order,
MAX(date) AS last_order
FROM sales_df
GROUP BY product, region
ORDER BY total_revenue DESC
""").df() # return the result as a Pandas DataFrame again
print(result)
# Join a large Parquet file with a DataFrame
products_df = pd.DataFrame({
'product': ['A', 'B', 'C'],
'category': ['Electronics', 'Clothing', 'Food'],
'margin_pct': [0.15, 0.40, 0.25]
})
enriched = duckdb.sql("""
SELECT
s.product,
p.category,
SUM(s.amount) AS revenue,
ROUND(SUM(s.amount) * p.margin_pct, 2) AS estimated_profit
FROM sales_df s
JOIN products_df p ON s.product = p.product
GROUP BY s.product, p.category, p.margin_pct
ORDER BY estimated_profit DESC
""").df()
print(enriched)
Persistent Databases and the Relation API
import duckdb
# Connect to a persistent database
con = duckdb.connect('analytics.db')
# Create the table and load data from Parquet
con.execute("""
CREATE TABLE IF NOT EXISTS events AS
SELECT * FROM read_parquet('raw_events/*.parquet');
""")
# Relation API (method chaining)
result = (
con.table('events')
.filter("event_type = 'purchase'")
.aggregate("product_id, COUNT(*) AS cnt, SUM(amount) AS total")
.order("total DESC")
.limit(10)
.df()
)
# Export the result to Parquet
con.execute("""
COPY (
SELECT product_id, COUNT(*) AS cnt, SUM(amount) AS total
FROM events
WHERE event_type = 'purchase'
GROUP BY product_id
ORDER BY total DESC
) TO 'top_products.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
""")
con.close()
Apache Arrow Integration
import duckdb
import pyarrow as pa
import pyarrow.parquet as pq
# Query an Arrow Table directly
arrow_table = pq.read_table('large_dataset.parquet')
result = duckdb.sql("""
SELECT
category,
COUNT(*) AS cnt,
APPROX_QUANTILE(value, 0.95) AS p95
FROM arrow_table
GROUP BY category
""").arrow() # return the result as an Arrow Table (zero copy)
# Arrow's zero-copy handoff maximizes memory efficiency
print(f"Result rows: {result.num_rows}")
7. Data Lake Integration (Parquet, S3, Delta Lake)
One of the key reasons DuckDB has grown explosively in the data engineering world is that it can query a data lake directly, without complex infrastructure.
Querying S3 Parquet Files Directly
-- Install and load the httpfs extension
INSTALL httpfs;
LOAD httpfs;
-- AWS S3 authentication setup (the Secrets approach - recommended)
CREATE SECRET my_s3_secret (
TYPE S3,
KEY_ID 'AKIAIOSFODNN7EXAMPLE',
SECRET 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
REGION 'ap-northeast-2'
);
-- Query Parquet files on S3 directly
SELECT
event_date,
user_segment,
COUNT(DISTINCT user_id) AS dau,
COUNT(*) AS total_events,
ROUND(AVG(session_duration_sec), 1) AS avg_session_sec
FROM read_parquet('s3://my-data-lake/events/year=2025/month=12/*.parquet')
GROUP BY event_date, user_segment
ORDER BY event_date DESC, dau DESC;
-- Query hive-partitioned data (partition pruning applied automatically)
SELECT *
FROM read_parquet(
's3://my-data-lake/events/**/*.parquet',
hive_partitioning=true
)
WHERE year = 2025 AND month = 12
AND event_type = 'purchase';
-- Save the query result back to S3 as Parquet
COPY (
SELECT
date_trunc('hour', event_timestamp) AS hour,
COUNT(*) AS event_count,
COUNT(DISTINCT user_id) AS unique_users
FROM read_parquet('s3://my-data-lake/raw_events/**/*.parquet',
hive_partitioning=true)
WHERE year = 2025
GROUP BY hour
)
TO 's3://my-data-lake/aggregated/hourly_summary.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);
Reading Delta Lake
-- Install the Delta Lake extension
INSTALL delta;
LOAD delta;
-- Read a Delta Lake table
SELECT *
FROM delta_scan('s3://my-data-lake/delta_tables/user_events/')
WHERE event_date >= '2025-12-01'
LIMIT 100;
GCS (Google Cloud Storage) Integration
-- GCS authentication setup
CREATE SECRET gcs_secret (
TYPE GCS,
KEY_ID 'my-gcs-key-id',
SECRET 'my-gcs-secret'
);
-- Query GCS Parquet files
SELECT COUNT(*), SUM(revenue)
FROM read_parquet('gs://my-bucket/analytics/sales_*.parquet');
8. Comparing Analytics Tools
To understand DuckDB's position accurately, comparing it with competing tools is essential.
| Comparison item | DuckDB | SQLite | ClickHouse | BigQuery |
|---|---|---|---|---|
| Architecture | In-process, columnar | In-process, row-oriented | Client-server, columnar | Serverless, columnar |
| Best-fit workload | Local/embedded OLAP | Local/embedded OLTP | Distributed real-time OLAP | Large-scale cloud analytics |
| Installation difficulty | A single pip install | Bundled with the OS | Server installation required | No installation (SaaS) |
| Concurrent users | Single to a few | Single to a few | Hundreds to thousands | Thousands+ |
| Maximum data scale | Tens to hundreds of GB (single node) | A few GB | TB to PB (cluster) | PB+ |
| Write concurrency | Single Writer | Single Writer (Readers concurrent in WAL mode) | Multiple Writers | Multiple Writers |
| Native Parquet support | Built in | Not supported | Possible via MATERIALIZED VIEW | Native |
| Direct S3 querying | httpfs extension | Not supported | s3 table function | Native (External Table) |
| Cost model | Free (open source, MIT) | Free (public domain) | Free (open source) / paid in the cloud | Charged per query ($5/TB scanned) |
| Python integration | SQL directly over DataFrames | DB-API basics | clickhouse-driver | google-cloud-bigquery |
| Real-time ingestion | Unsuitable | Unsuitable | Optimized (MergeTree) | Streaming Insert |
| Transactions | ACID (single Writer) | ACID | Limited ACID | Full ACID |
Recommendations by Use Case
- Analyzing Parquet/CSV on a laptop → DuckDB
- Local data storage in a mobile app → SQLite
- A real-time dashboard over billions of rows → ClickHouse
- A data warehouse shared by a whole team → BigQuery
- Fast prototyping in a development/test environment → DuckDB
- Data quality checks in a CI/CD pipeline → DuckDB
9. Making Use of Extensions
DuckDB follows a design that keeps the core light and lets you add what you need selectively through extensions.
The Main Extensions
-- Check the installed extensions
SELECT * FROM duckdb_extensions() WHERE installed = true;
-- Install the core extensions
INSTALL httpfs; -- S3/HTTP remote file access
INSTALL json; -- JSON parsing (auto-loaded)
INSTALL parquet; -- Parquet support (auto-loaded)
INSTALL spatial; -- geospatial data processing (ST_* functions)
INSTALL icu; -- internationalized collation/comparison
INSTALL fts; -- Full-Text Search
INSTALL delta; -- reading Delta Lake tables
INSTALL excel; -- reading/writing Excel files
INSTALL sqlite; -- reading SQLite databases directly
INSTALL postgres; -- querying PostgreSQL directly (FDW)
INSTALL mysql; -- querying MySQL directly
-- Load the extensions
LOAD httpfs;
LOAD spatial;
A Practical Spatial Extension Example
INSTALL spatial;
LOAD spatial;
-- Analyze orders within a 1km radius of Seoul-area stores
SELECT
store_name,
COUNT(*) AS nearby_orders,
SUM(order_amount) AS nearby_revenue
FROM orders o
JOIN stores s ON o.store_id = s.id
WHERE ST_DWithin(
ST_Point(o.longitude, o.latitude),
ST_Point(s.longitude, s.latitude),
0.01 -- about 1km (WGS84 approximation)
)
GROUP BY store_name
ORDER BY nearby_revenue DESC;
Querying PostgreSQL Directly (the postgres extension)
INSTALL postgres;
LOAD postgres;
-- Connect directly to PostgreSQL and query
ATTACH 'dbname=mydb user=analyst host=pg-server.internal' AS pg (TYPE POSTGRES);
-- Join a PostgreSQL table with a local Parquet file
SELECT
p.customer_id,
p.customer_name,
l.total_purchases,
l.last_purchase_date
FROM pg.public.customers p
JOIN (
SELECT
customer_id,
COUNT(*) AS total_purchases,
MAX(purchase_date) AS last_purchase_date
FROM read_parquet('s3://data-lake/purchases/**/*.parquet')
GROUP BY customer_id
) l ON p.customer_id = l.customer_id
WHERE l.total_purchases > 100
ORDER BY l.total_purchases DESC;
10. MotherDuck: DuckDB in the Cloud
MotherDuck is a serverless cloud analytics service run by a company founded by one of DuckDB's co-creators. It keeps DuckDB's strengths while adding the scalability and collaboration features of the cloud.
Key Characteristics
- Hybrid query execution: part of a query runs locally (on the client) and part in the cloud, minimizing network transfer
- Serverless infrastructure: pick an instance size called a Duckling (pulse, standard, jumbo, mega, giga) to match the workload
- Data sharing: share databases between team members by URL
- Persistent storage: data is stored safely in the cloud and optimized automatically
- MCP server integration: integrates with a range of development tools such as Warp and JetBrains IDEs
Connecting to MotherDuck
import duckdb
# Connect with a MotherDuck token
con = duckdb.connect('md:my_database?motherduck_token=eyJhb...')
# Hybrid query over local and cloud data
con.execute("""
SELECT
cloud_table.user_id,
local_file.purchase_amount
FROM my_database.users AS cloud_table
JOIN read_parquet('local_data/purchases.parquet') AS local_file
ON cloud_table.user_id = local_file.user_id
WHERE cloud_table.signup_date >= '2025-01-01'
""")
result = con.fetchdf()
print(result)
11. Performance Optimization Tips
DuckDB performs well even with default settings, but tuning it to the characteristics of your workload can gain you additional performance.
Memory and Thread Settings
-- Check the current settings
SELECT name, value, description
FROM duckdb_settings()
WHERE name IN ('memory_limit', 'threads', 'temp_directory');
-- Set the memory limit (default: about 80% of system RAM)
SET memory_limit = '16GB';
-- Adjust the thread count (default: number of CPU cores)
-- Lower it when running alongside other work
SET threads = 4;
-- Set the disk spill directory (used when memory is exceeded)
SET temp_directory = '/fast-ssd/duckdb_temp';
Query Optimization Principles
-- 1. Check the query plan with EXPLAIN
EXPLAIN
SELECT region, SUM(amount) AS total
FROM read_parquet('large_sales.parquet')
WHERE sale_date >= '2025-01-01'
GROUP BY region;
-- 2. Profile the actual execution with EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT region, SUM(amount) AS total
FROM read_parquet('large_sales.parquet')
WHERE sale_date >= '2025-01-01'
GROUP BY region;
-- 3. Take advantage of Parquet filter pushdown
-- the WHERE condition is matched against the Row Group statistics
-- of the Parquet file, so unnecessary data is never read
SELECT COUNT(*)
FROM read_parquet('partitioned_data/**/*.parquet',
hive_partitioning=true)
WHERE year = 2025 AND month = 12;
-- → files where year != 2025 are never even opened (partition pruning)
-- 4. Use appropriate data types
-- use ENUM instead of VARCHAR to optimize low-cardinality columns
CREATE TYPE status_enum AS ENUM ('active', 'inactive', 'pending');
-- 5. Reuse connections
-- frequent connect/disconnect causes cache loss
-- keep a single connection open and run queries on it
Optimizing Parquet Files
-- Produce an optimized Parquet file
-- ZSTD compression + an appropriate Row Group size
COPY (
SELECT * FROM large_table
ORDER BY partition_key, timestamp -- sort to maximize the compression ratio
) TO 'optimized_output.parquet'
(FORMAT PARQUET,
COMPRESSION ZSTD,
ROW_GROUP_SIZE 122880); -- about 120K rows per group
-- Export partitioned Parquet
COPY (SELECT * FROM events)
TO 'output_dir' (FORMAT PARQUET,
PARTITION_BY (year, month),
COMPRESSION ZSTD);
12. Troubleshooting
Common Errors and How to Fix Them
Out of Memory errors are the problem that comes up most often.
-- Symptom: "Out of Memory Error: could not allocate block of size ..."
-- Fix 1: raise the memory limit
SET memory_limit = '32GB';
-- Fix 2: enable disk spill
SET temp_directory = '/tmp/duckdb_spill';
-- Fix 3: split the query into chunks
-- process partition by partition instead of aggregating the whole table
CREATE TABLE monthly_agg AS
SELECT * FROM (
SELECT month, SUM(amount) AS total
FROM read_parquet('data/year=2025/month=01/*.parquet')
GROUP BY month
UNION ALL
SELECT month, SUM(amount) AS total
FROM read_parquet('data/year=2025/month=02/*.parquet')
GROUP BY month
-- ... and so on
);
S3 connection errors also come up frequently.
-- Symptom: "HTTP Error: Unable to connect to URL"
-- Fix 1: check the credentials
SELECT * FROM duckdb_secrets();
-- Fix 2: state the region explicitly
CREATE OR REPLACE SECRET (
TYPE S3,
REGION 'ap-northeast-2',
KEY_ID 'AKIA...',
SECRET '...'
);
-- Fix 3: custom endpoint (MinIO, R2, and so on)
CREATE SECRET (
TYPE S3,
KEY_ID 'minioadmin',
SECRET 'minioadmin',
ENDPOINT 'localhost:9000',
USE_SSL false,
URL_STYLE 'path'
);
File lock conflicts occur in multi-process environments.
-- Symptom: "Could not set lock on file ... database is locked"
-- Cause: another process has the same DB file open
-- DuckDB allows only a single Writer
-- Fix 1: set access_mode to read_only (when you only need to read)
-- in Python:
-- con = duckdb.connect('my.db', read_only=True)
-- Fix 2: query Parquet directly in in-memory mode
-- this avoids the DB file lock entirely
-- con = duckdb.connect() -- in-memory
-- con.sql("SELECT * FROM read_parquet('data.parquet')")
13. Production Operating Patterns
These are patterns for using DuckDB safely in production.
Pattern 1: The Transformation Layer of an ETL Pipeline
import duckdb
from datetime import datetime
def daily_aggregation_job():
"""Use DuckDB as the T (Transform) stage of ETL"""
con = duckdb.connect()
today = datetime.now().strftime('%Y-%m-%d')
# Read the source data from S3 → transform → save the result to S3
con.execute(f"""
INSTALL httpfs; LOAD httpfs;
CREATE SECRET (TYPE S3, REGION 'ap-northeast-2');
COPY (
SELECT
date_trunc('hour', event_ts) AS hour,
event_type,
COUNT(*) AS cnt,
COUNT(DISTINCT user_id) AS unique_users,
APPROX_COUNT_DISTINCT(session_id) AS approx_sessions,
PERCENTILE_CONT(0.50) WITHIN GROUP
(ORDER BY duration_ms) AS p50_duration,
PERCENTILE_CONT(0.99) WITHIN GROUP
(ORDER BY duration_ms) AS p99_duration
FROM read_parquet(
's3://raw-data/events/dt={today}/**/*.parquet'
)
GROUP BY hour, event_type
)
TO 's3://processed-data/hourly_agg/dt={today}/result.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);
""")
con.close()
print(f"Daily aggregation completed for {today}")
Pattern 2: CI/CD Data Quality Checks
import duckdb
import sys
def validate_data_quality(parquet_path: str) -> bool:
"""Verify data quality automatically in a CI/CD pipeline"""
con = duckdb.connect()
checks_passed = True
# Check 1: minimum row count
row_count = con.execute(f"""
SELECT COUNT(*) FROM read_parquet('{parquet_path}')
""").fetchone()[0]
if row_count < 1000:
print(f"FAIL: Row count {row_count} below minimum 1000")
checks_passed = False
# Check 2: NULL ratio
null_check = con.execute(f"""
SELECT
ROUND(COUNT(*) FILTER (WHERE user_id IS NULL)
* 100.0 / COUNT(*), 2) AS null_pct
FROM read_parquet('{parquet_path}')
""").fetchone()[0]
if null_check > 5.0:
print(f"FAIL: user_id NULL ratio {null_check}% exceeds 5%")
checks_passed = False
# Check 3: value range
range_check = con.execute(f"""
SELECT
MIN(amount) AS min_amt,
MAX(amount) AS max_amt
FROM read_parquet('{parquet_path}')
""").fetchone()
if range_check[0] < 0:
print(f"FAIL: Negative amount detected: {range_check[0]}")
checks_passed = False
con.close()
return checks_passed
if __name__ == '__main__':
path = sys.argv[1]
if not validate_data_quality(path):
sys.exit(1)
print("All data quality checks passed")
Pattern 3: Embedded Analytics Inside a Microservice
By embedding DuckDB inside an API server, you can return fast aggregate responses without calling out to an external analytics service.
from fastapi import FastAPI
import duckdb
app = FastAPI()
# Connect only once at application startup (connection reuse)
analytics_con = duckdb.connect('analytics_cache.db', read_only=True)
@app.get("/api/dashboard/summary")
async def dashboard_summary(date_from: str, date_to: str):
result = analytics_con.execute("""
SELECT
product_category,
COUNT(*) AS orders,
ROUND(SUM(revenue), 2) AS total_revenue,
ROUND(AVG(revenue), 2) AS avg_order_value
FROM sales
WHERE order_date BETWEEN ? AND ?
GROUP BY product_category
ORDER BY total_revenue DESC
""", [date_from, date_to]).df()
return result.to_dict(orient='records')
14. Failure Cases and Responses
Here are the failure cases teams commonly hit when adopting DuckDB in practice, and how to respond to them.
Failure 1: Applying DuckDB to an OLTP Workload
Symptom: DuckDB was applied to a service backend with frequent row-level reads and writes, such as user authentication and order processing, and single-row lookups turned out abnormally slow while lock conflicts happened often on concurrent writes.
Cause: DuckDB is a columnar OLAP engine. For single-row point queries, the overhead of columnar storage works against you. On top of that, the single-Writer constraint makes concurrent writes fundamentally impossible.
Response: use PostgreSQL or MySQL for OLTP workloads and keep DuckDB as a separate analytics-only layer. Adopt a two-track structure in which the OLTP database's data is exported to Parquet periodically and analyzed with DuckDB.
Failure 2: Handling a Dataset Larger Than Memory Unguarded
Symptom: running a GROUP BY aggregation over a 100GB Parquet file on a 32GB RAM server got the process killed by OOM.
Cause: DuckDB does provide spill to disk, but if temp_directory is not configured or there is not enough temporary disk space, it tries to do the work in memory alone.
Response: temp_directory must be set to an SSD path with enough space. Also set memory_limit lower than system RAM (around 70~80%) to protect the memory of the OS and other processes.
Failure 3: Sharing a Single DB File Across Concurrent Services
Symptom: when several microservices accessed the same DuckDB file at once, "database is locked" errors happened constantly.
Cause: only one process can access DuckDB as a Writer. When several processes try to write at the same time, lock conflicts occur.
Response: restrict write work to a single process and use read_only mode for read-only access. Alternatively, switch to a structure where each service queries Parquet files directly in in-memory mode. If concurrent multi-user access is a hard requirement, consider moving to MotherDuck or ClickHouse.
Failure 4: Using It for Real-Time Streaming Ingestion
Symptom: INSERTing tens of thousands of events per second from Kafka into DuckDB in real time made write performance degrade sharply.
Cause: DuckDB is optimized for analytical queries (reads) and is not suited to row-level real-time INSERTs. Columnar storage is efficient for batch writes but carries heavy overhead for single-row INSERTs.
Response: handle real-time ingestion with Kafka + ClickHouse or Kafka + S3 (Parquet), and use DuckDB solely to analyze the Parquet files once they have landed.
15. When Not to Use DuckDB
Recognizing clearly where DuckDB is a poor fit is the first step to choosing the right tool.
- High-concurrency OLTP: a service where hundreds or more concurrent users read and write row by row → PostgreSQL, MySQL
- Real-time streaming ingestion: tens of thousands to hundreds of thousands of real-time INSERTs per second → ClickHouse, Apache Kafka + Flink
- Petabyte-scale distributed processing: a scale a single node cannot handle → Spark, Trino, BigQuery
- Multiple Writers required: several processes must update data at the same time → PostgreSQL
- Payment/financial systems where ACID transactions are central: → PostgreSQL, CockroachDB
- A cache layer needing sub-millisecond point queries: → Redis, Memcached
16. References
- DuckDB Official Documentation - every official guide, covering installation, the API, and the SQL reference
- DuckDB In-Depth: How It Works and What Makes It Fast (endjin) - an in-depth analysis of the vectorized execution engine and the architecture
- MotherDuck: Architecture and Capabilities - the MotherDuck hybrid query architecture
- DuckDB S3 Parquet Import Official Guide - S3 integration setup and usage
- DuckDB Extensions Official Documentation - a guide to installing and managing extensions
- ClickHouse vs DuckDB 2026 Comparison (Tasrie IT) - an OLAP database benchmark comparison
- DuckDB Python API Official Documentation - a detailed guide to the Python client
- DuckDB Performance Guide - the official performance optimization guide
- 15+ Companies Using DuckDB in Production (MotherDuck) - a collection of production case studies
- DuckDB: An Architectural Deep Dive (ThinhDA) - an architectural analysis of the in-process OLAP engine