Skip to main content
← All Articles Β· Β· 12 min read

DuckDB vs PostgreSQL for Analytics: When to Choose Each 2026

DuckDB vs PostgreSQL 2026: OLAP vs OLTP, query performance on large datasets, Parquet/CSV support, and when to choose DuckDB for analytics workloads.

duckdbpostgresqlanalyticsolapdata-engineeringsqldata-science

DuckDB emerged from the research community and, in a few years, became one of the most beloved tools in the data engineering ecosystem. Its pitch is straightforward: run analytical SQL queries on your laptop, reading Parquet files directly, with no server to manage and performance that rivals cloud data warehouses. PostgreSQL, meanwhile, has been the workhorse of application databases for decades and has extensive analytical capabilities of its own.

The question is not which is β€œbetter” β€” they are built for different problems. Understanding where each excels will help you stop reaching for the wrong tool.

Architecture: The Fundamental Difference

The most important architectural distinction is in-process vs client-server.

DuckDB is an in-process OLAP database. Like SQLite, it runs inside your application process β€” no server to start, no port to configure, no authentication to manage. Import it as a Python library, and queries run directly in your process. This means zero network overhead, trivial deployment, and instant startup.

PostgreSQL is a client-server database. A dedicated server process manages connections, transactions, concurrent writes, and durability. Clients (your application, psql, BI tools) connect over TCP. This architecture enables concurrent multi-user access, ACID transactions across multiple clients, and long-running server-side processes β€” but it requires operational overhead.

DuckDB (in-process)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Python Process                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  import duckdb            β”‚   β”‚
β”‚  β”‚  conn = duckdb.connect()  β”‚   β”‚
β”‚  β”‚  conn.execute(sql)        β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚  No network, no server, instant β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

PostgreSQL (client-server)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    TCP    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Your App     β”‚ ◄──────► β”‚  postgres server  β”‚
β”‚  (psycopg2)   β”‚    5432   β”‚  (manages state,  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚   concurrency,    β”‚
                            β”‚   WAL, vacuuming) β”‚
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DuckDB: What Makes It Fast for Analytics

DuckDB is a columnar, vectorized execution engine. It stores data column-by-column and processes chunks (vectors) of values at a time using SIMD CPU instructions. For analytical queries that aggregate millions of rows but touch only a few columns, this is dramatically more efficient than row-based storage.

PostgreSQL is row-based (heap storage) β€” each row is stored together. For SELECT email FROM users WHERE id = 42, this is optimal (one disk read gets all columns for that row). For SELECT SUM(revenue) FROM orders, PostgreSQL must read the entire row for every row just to get the revenue column. DuckDB only reads the revenue column.

Performance Benchmark: Analytical Query

Let’s look at a real aggregation query on a 10 million row orders table:

import duckdb
import time

# DuckDB reading from Parquet (100MB file, 10M rows)
conn = duckdb.connect()

start = time.time()
result = conn.execute("""
    SELECT
        date_trunc('month', order_date) as month,
        status,
        COUNT(*) as order_count,
        SUM(amount_usd) as total_revenue,
        AVG(amount_usd) as avg_order_value,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount_usd) as median_amount
    FROM read_parquet('orders/*.parquet')
    WHERE order_date >= '2025-01-01'
    GROUP BY 1, 2
    ORDER BY 1, 2
""").fetchdf()
elapsed = time.time() - start
print(f"DuckDB: {elapsed:.2f}s β€” {len(result)} rows")
# Typical result: DuckDB: 0.43s β€” 36 rows

The same query against PostgreSQL on equivalent hardware with proper indexes would typically take 8-30 seconds on 10M rows, depending on whether the table fits in buffer pool and index coverage.

Approximate Benchmarks (10M rows, analytical aggregations)

DatabaseQuery TypeTypical TimeNotes
DuckDB (Parquet)GROUP BY + SUM0.2 – 0.8sIn-memory columnar
DuckDB (DuckDB file)GROUP BY + SUM0.1 – 0.5sOptimized columnar file
PostgreSQL (no index)GROUP BY + SUM8 – 25sRow scan, buffer-dependent
PostgreSQL (partial index)GROUP BY + SUM2 – 8sWith covering index
BigQueryGROUP BY + SUM1 – 5sCloud columnar, network overhead
Snowflake (XS)GROUP BY + SUM1 – 4sCloud columnar

For analytical queries, DuckDB running on a laptop beats PostgreSQL running on a powerful server.

DuckDB’s Native File Format Support

This is where DuckDB shines brightest for data engineers. It reads popular data formats directly without an import step:

import duckdb

conn = duckdb.connect()

# Read Parquet directly β€” no import needed
conn.execute("""
    SELECT year, SUM(revenue)
    FROM read_parquet('s3://my-bucket/data/year=*/revenue.parquet')
    GROUP BY year
""")

# Read CSV with automatic schema inference
conn.execute("""
    SELECT *
    FROM read_csv_auto('data/*.csv', header=True, ignore_errors=True)
    LIMIT 10
""")

# Read JSON (including NDJSON)
conn.execute("""
    SELECT json_extract(data, '$.user.id') as user_id
    FROM read_json('events.ndjson')
""")

# Read from a Pandas DataFrame directly
import pandas as pd
df = pd.read_csv('orders.csv')
conn.execute("SELECT * FROM df WHERE amount > 100")  # df is the variable!

# Read from Polars DataFrame
import polars as pl
pl_df = pl.read_csv('orders.csv')
conn.execute("SELECT COUNT(*) FROM pl_df")

# Query multiple sources in one SQL statement
conn.execute("""
    SELECT o.order_id, c.customer_name, o.amount
    FROM read_parquet('orders.parquet') o
    JOIN read_csv_auto('customers.csv') c ON o.customer_id = c.id
    WHERE o.amount > 1000
""")

No other database makes ad-hoc multi-format querying this simple. This capability alone makes DuckDB the go-to tool for exploratory data analysis on heterogeneous data.

PostgreSQL’s Analytical Strengths

PostgreSQL is not helpless at analytics. It has accumulated powerful OLAP features over the years:

-- Window functions β€” PostgreSQL pioneered SQL window functions
SELECT
    customer_id,
    order_date,
    amount,
    SUM(amount) OVER (
        PARTITION BY customer_id
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) as running_total,
    RANK() OVER (PARTITION BY customer_id ORDER BY amount DESC) as amount_rank
FROM orders;

-- ROLLUP for hierarchical aggregations
SELECT
    EXTRACT(year FROM order_date) as year,
    EXTRACT(month FROM order_date) as month,
    category,
    SUM(amount) as revenue
FROM orders
GROUP BY ROLLUP(
    EXTRACT(year FROM order_date),
    EXTRACT(month FROM order_date),
    category
);

-- Materialized views for pre-computed aggregations
CREATE MATERIALIZED VIEW mv_monthly_revenue AS
SELECT
    date_trunc('month', order_date) as month,
    SUM(amount) as revenue,
    COUNT(*) as order_count
FROM orders
GROUP BY 1;

-- Refresh concurrently (non-blocking)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_revenue;

-- TimescaleDB extension: columnar compression + time-series optimization
-- (makes PostgreSQL genuinely competitive for time-series analytics)
SELECT add_compression_policy('orders', INTERVAL '7 days');

With TimescaleDB, cstore_fdw (columnar), or pg_analytics (DuckDB execution engine for PostgreSQL), PostgreSQL can close the performance gap significantly.

Python and R Integration

DuckDB’s Python and R integrations are class-leading:

import duckdb
import pandas as pd
import polars as pl

conn = duckdb.connect()

# DuckDB ↔ Pandas: zero-copy via Apache Arrow
df = conn.execute("SELECT * FROM 'data.parquet' WHERE amount > 100").fetchdf()
# Returns a Pandas DataFrame β€” extremely fast, minimal memory overhead

# Query a Pandas DataFrame as a SQL table
orders_df = pd.read_parquet('orders.parquet')
result = conn.execute("""
    SELECT customer_id, SUM(amount) as total
    FROM orders_df
    GROUP BY customer_id
    HAVING SUM(amount) > 10000
""").pl()  # Return as Polars DataFrame

# Register a DataFrame as a named view
conn.register('orders', orders_df)
conn.execute("CREATE VIEW big_orders AS SELECT * FROM orders WHERE amount > 1000")

# DuckDB + Jupyter = fast local analytics notebook
# No Spark, no Databricks, no cloud needed for <50GB datasets

This integration is why DuckDB has become the default analytical engine for data scientists doing local work. You get SQL on any file format, with results directly as Pandas/Polars DataFrames, with near-cloud-warehouse performance.

Comparison Table: DuckDB vs PostgreSQL vs BigQuery

DimensionDuckDBPostgreSQLBigQuery
ArchitectureIn-process, embeddedClient-serverCloud serverless
Storage modelColumnarRow-basedColumnar
Best forOLAP, analyticsOLTP + moderate OLAPOLAP at cloud scale
Concurrent writesNo (single writer)Yes (MVCC)Yes (streaming insert)
Parquet/CSV supportNative, direct readVia extensionsVia external tables
Max practical size~100GB on laptopUnlimited (server)Petabytes
Network requiredNoYes (TCP)Yes (HTTPS)
Python integrationFirst-classpsycopg2/asyncpggoogle-cloud-bigquery
Setup timepip install duckdbInstall + configure serverGCP account setup
CostFreeFree (hosting costs)Per TB scanned
ACID transactionsLimitedFullLimited
Streaming insertsNoYesYes
SQL standardExcellentExcellentGood (dialect quirks)

When DuckDB Is the Right Tool

Local analytical workloads: If you have a CSV or Parquet file and want to run SQL on it, DuckDB is the fastest path from data to answer. No server, no setup, just pip install duckdb.

Data science and ML preprocessing: Replace Pandas for aggregation-heavy preprocessing on datasets that fit on disk (up to ~100GB). DuckDB is 10-100x faster than Pandas for GROUP BY, window functions, and joins.

ETL and data pipeline scripts: Query source files directly, transform in SQL, write to destination β€” all in one script without moving data through multiple formats.

BI tools and embedded analytics: Tools like Evidence, Rill, and Observable use DuckDB as an embedded query engine for fast browser-side analytics.

Testing dbt models locally: Run your dbt project against DuckDB instead of Snowflake/BigQuery during development. Free, instant, no cloud credentials needed.

# dbt profiles.yml
my_project:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: 'dev.duckdb'   # Local DuckDB file
      threads: 4
    prod:
      type: snowflake       # Production uses Snowflake
      # ...

When DuckDB Is the Wrong Tool

Multi-user concurrent writes: DuckDB has a single-writer model. If multiple application instances write to the same DuckDB file concurrently, you will get errors. PostgreSQL handles this correctly; DuckDB does not.

Operational application databases: User authentication systems, session management, shopping carts β€” these are OLTP workloads with point lookups and frequent small writes. PostgreSQL is built for this; DuckDB is not.

Datasets larger than your machine’s disk: DuckDB scales to hundreds of gigabytes on a single machine, but for terabyte-scale analytics you need a distributed system (BigQuery, Snowflake, Spark).

When you need server features: Connection pooling, role-based access control, SSL client certificates, pg_stat_statements monitoring β€” all PostgreSQL server features that simply do not exist in DuckDB.

The Practical Conclusion

DuckDB and PostgreSQL are not competitors for the same use case. Use PostgreSQL (or any operational database) when you have a transactional application with concurrent reads and writes. Use DuckDB when you are doing analytics: querying files, transforming data, running aggregations, or building data pipelines.

The exciting development in 2026 is that DuckDB’s performance makes local analytics practical at a scale that previously required cloud infrastructure. A data analyst with a MacBook Pro and DuckDB can now analyze datasets that would have required a Redshift cluster three years ago. That genuinely changes what is possible without a data engineering team standing behind you.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.