DuckDB 2.0 Tutorial: Fast SQL on CSV, Parquet and JSON in Python and Node.js

DuckDB 2.0 tutorial banner: CSV, Parquet and JSON files flowing into an in-process SQL engine
v2.0 · LTS Databases · DevDojo DuckDB 2.0 Tutorial Fast SQL on CSV, Parquet and JSON Python and Node.js · No server · Runs on your laptop sales.csv .parquet events.json SQL engineinside your app Results in milliseconds GROUP BY JOIN window devdojo.co.in · Beginner friendly · Tested with DuckDB 2.0.0, Python 3.13 and Node.js 22

DuckDB 2.0 was released on 21 October and it is one of the most talked-about tools in data engineering right now. In this DuckDB tutorial you will learn what DuckDB is, why developers call it “SQLite for analytics”, and how to query CSV, Parquet and JSON files with plain SQL from Python and Node.js – no database server, no Docker, no import step. Every example below was actually run on DuckDB 2.0.0, and the terminal output you see is real.

Quick Summary
  • DuckDB is a free, open-source analytics database that runs inside your program (like SQLite), but is built for fast reports, aggregations and big files.
  • You can run SQL directly on files: SELECT * FROM 'sales.csv'.
  • DuckDB 2.0 (“Cinnamon Teal”) is a long-term support release (supported until at least October 2027) with a new SQL parser, a VARIANT type, triggers, an agent mode for the CLI and faster file reading.
  • Upgrading from 1.x? Python now needs 3.11+, relations lost fetchone(), the old x -> x + 1 lambda syntax is off by default, and 1.x cannot open new 2.0 database files.

What is DuckDB? (DuckDB tutorial basics)

DuckDB is an in-process analytical database. Let’s break that sentence into simple words:

  • In-process – there is no separate server to install or start. You install a library with pip or npm, and the database runs inside your Python or Node.js program.
  • Analytical – it is made for questions like “total revenue per city per month” over millions of rows, not for saving one user’s profile thousands of times per second.
  • Database – it speaks full SQL: JOIN, GROUP BY, window functions, CTEs and more. You can keep data in memory or in a single .duckdb file.
Client-server (PostgreSQL, MySQL) In-process (DuckDB, SQLite) Your app DB server install, run, manage network + login data must be imported Your app (Python / Node.js) DuckDB library (pip / npm) CSV Parquet JSON reads files directly – no server, no import step
Figure 1: A normal database is a separate server. DuckDB is a library that runs inside your program, like SQLite – but built for analytics.

The second big idea is column storage. A normal app database (PostgreSQL, MySQL, SQLite) stores data row by row. DuckDB stores and processes it column by column, in batches. For reports that read only a few columns from a big table, this is much faster.

Row storage (good for apps) Column storage (DuckDB, Parquet) idcityproductprice1DelhiPhone180002PuneLaptop550003DelhiMonitor120004MumbaiPhone18000id1234cityDelhiPuneDelhiMumbaiproductPhoneLaptopMonitorPhoneprice18000550001200018000 Reads whole rows, even unused columns Reads only city + price for SUM(price) BY city Less data to read + compressed columns + vectorized processing = fast analytics
Figure 2: For a “revenue by city” report, DuckDB only touches the two columns it needs. That is a big reason it is so fast on large files.
Zero setupOne pip install. No server, no users, no ports.
Query files directlyCSV, Parquet, JSON – even files on S3 or a URL (with the httpfs extension).
Very fastColumnar, vectorized engine that uses all your CPU cores.
Plays well with othersReads and returns pandas, Polars and Arrow data.

Typical uses: data analysis in Jupyter, cleaning and converting files (CSV to Parquet), small data pipelines, dashboards and APIs that show reports, testing SQL locally, and even giving AI agents a fast SQL tool.

What’s new in DuckDB 2.0

DuckDB 2.0 (code name “Cinnamon Teal”) is built from close to 20,000 commits since version 1.5. The team calls it “above all a feature release”. The highlights, from the official DuckDB 2.0 announcement:

FeatureWhat it means for you
Long-term support2.0 is supported until at least October 2027 – a safe choice for production.
New SQL parser (PEG)Replaces the old PostgreSQL-based parser. Error messages now point to the exact place in your query.
VARIANT typeStore mixed, schema-less data (like JSON) in a fast binary form.
TriggersCREATE TRIGGER with BEFORE/AFTER, row and statement level.
Asynchronous I/OFaster reading of Parquet, CSV and DuckDB files – especially from S3.
Agent mode in the CLIDetects AI coding agents and prints compact, agent-friendly output.
Server mode (Quack) and CONNECTShare one DuckDB over the network. Still new – treat it as early.
Friendlier SQL$variable shorthand, lambda x: ... syntax, JSON edit functions like json_set, nested schemas and more.
Version check: at the time of writing, pip install duckdb gives 2.0.0 and npm install @duckdb/node-api gives 2.0.0-r.1. DuckDB 1.4 is supported until 17 November and 1.5 until March next year.

Install DuckDB

Pick your language. You do not need both – the SQL is exactly the same.

Terminal — install DuckDB
# Python (DuckDB 2.0 needs Python 3.11 or newer)
$ python3 --version
$ pip install duckdb
$ python3 -c "import duckdb; print(duckdb.__version__)"
2.0.0

# Node.js (the modern "Node Neo" client)
$ npm install @duckdb/node-api

# Optional: the command-line shell (macOS / Linux)
$ curl https://install.duckdb.org | sh
On Python 3.10 or older? pip will quietly install an old 1.x version. Upgrade Python to 3.11+ (or use uv / pyenv) to get DuckDB 2.0.

Your first query on a CSV file

We will use a small sales.csv file with 1,000 orders from five Indian cities (columns: order_id, order_date, city, product, qty, price). Create any similar CSV, or generate one with a few lines of Python. Now ask a real business question: which city brings the most revenue?

quickstart.py — duckdb-demo — Visual Studio Code
duckdb-demo › quickstart.py
import duckdb

print("DuckDB version:", duckdb.__version__)

# Query a CSV file directly - no import step, no server
result = duckdb.sql("""
    SELECT city,
           count(*)              AS orders,
           sum(qty * price)      AS revenue
    FROM 'sales.csv'
    GROUP BY city
    ORDER BY revenue DESC
""")
result.show()
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, Linux)
$ python3 quickstart.py
DuckDB version: 2.0.0
┌───────────┬────────┬─────────┐
│   city    │ orders │ revenue │
│  varchar  │ int64  │ int128  │
├───────────┼────────┼─────────┤
│ Bengaluru │    208 │ 9048500 │
│ Mumbai    │    215 │ 8241000 │
│ Pune      │    194 │ 7765000 │
│ Chennai   │    179 │ 7343500 │
│ Delhi     │    204 │ 6381500 │
└───────────┴────────┴─────────┘
⎇ mainLn 14, Col 1Spaces: 4UTF-8LFPython
duckdb-demo › quickstart.mjs
import { DuckDBInstance } from '@duckdb/node-api';

// In-memory database (pass a file name to save to disk)
const instance = await DuckDBInstance.create(':memory:');
const connection = await instance.connect();

const reader = await connection.runAndReadAll(`
  SELECT city,
         count(*)         AS orders,
         sum(qty * price) AS revenue
  FROM 'sales.csv'
  GROUP BY city
  ORDER BY revenue DESC
`);
console.table(reader.getRowObjectsJson());
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, Node.js 22)
$ node quickstart.mjs
┌─────────┬─────────────┬────────┬───────────┐
│ (index) │ city        │ orders │ revenue   │
├─────────┼─────────────┼────────┼───────────┤
│ 0       │ 'Bengaluru' │ '208'  │ '9048500' │
│ 1       │ 'Mumbai'    │ '215'  │ '8241000' │
│ 2       │ 'Pune'      │ '194'  │ '7765000' │
│ 3       │ 'Chennai'   │ '179'  │ '7343500' │
│ 4       │ 'Delhi'     │ '204'  │ '6381500' │
└─────────┴─────────────┴────────┴───────────┘
⎇ mainLn 15, Col 1Spaces: 2UTF-8LFJavaScript
duckdb-demo › sales.csv
order_id,order_date,city,product,qty,price
1,2026-07-13,Delhi,Headphones,1,2500
2,2026-07-12,Chennai,Monitor,1,12000
3,2026-07-02,Delhi,Monitor,2,12000
4,2026-07-08,Delhi,Keyboard,3,1500
...
⎇ mainLn 6, Col 1Spaces: 4UTF-8LFCSV

Click the tabs to switch between Python, Node.js and the data file, then click ▶ Run to see the real output.

Notice what we did not do: no CREATE TABLE, no import, no column types. DuckDB looked at the file, detected the columns and types, and ran the query. A few things to note:

  • duckdb.sql() uses a default in-memory database. .show() prints a nice table.
  • The revenue column is int128 (DuckDB’s HUGEINT) because summing many BIGINT values could overflow.
  • In Node.js, getRowObjectsJson() turns big integers into strings so they are safe to send as JSON from an API.
1. Raw filesCSV / JSON / Parquet 2. Query with SQLduckdb.sql(…) 3. Saveshop.duckdb / .parquet 4. Use the resultpandas / API / chart Everything happens inside one Python or Node.js process
Figure 3: The workflow you will build in this tutorial.

Save to a database file and Parquet

In-memory is great for quick analysis. For real projects you usually want to keep the data. Pass a file name to duckdb.connect() and everything you create is stored in that single file. We will also export the table to Parquet – a compressed, column-based file format used by Spark, pandas, BigQuery and most data tools.

save_db.py — duckdb-demo — Visual Studio Code
duckdb-demo › save_db.py
import duckdb

# Open (or create) a database file on disk
con = duckdb.connect("shop.duckdb")

# Load the CSV into a real table (types are detected automatically)
# Tip: use execute() for statements that change data (DDL / DML)
con.execute("CREATE OR REPLACE TABLE sales AS FROM 'sales.csv'")
con.sql("SELECT column_name, column_type FROM (DESCRIBE sales)").show()

# Export to Parquet - a compressed, column-based file format
con.execute("COPY sales TO 'sales.parquet' (FORMAT parquet)")

# Read the Parquet file back and get monthly revenue
con.sql("""
    SELECT strftime(order_date, '%Y-%m') AS month,
           sum(qty * price)              AS revenue
    FROM 'sales.parquet'
    GROUP BY ALL
    ORDER BY month
""").show()
con.close()
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, Linux)
$ python3 save_db.py
┌─────────────┬─────────────┐
│ column_name │ column_type │
│   varchar   │   varchar   │
├─────────────┼─────────────┤
│ order_id    │ BIGINT      │
│ order_date  │ DATE        │
│ city        │ VARCHAR     │
│ product     │ VARCHAR     │
│ qty         │ BIGINT      │
│ price       │ BIGINT      │
└─────────────┴─────────────┘

┌─────────┬──────────┐
│  month  │ revenue  │
│ varchar │  int128  │
├─────────┼──────────┤
│ 2026-07 │ 13404500 │
│ 2026-08 │ 13935000 │
│ 2026-09 │ 11440000 │
└─────────┴──────────┘
⎇ mainLn 22, Col 1Spaces: 4UTF-8LFPython

Click ▶ Run to see the detected column types and the monthly revenue.

  • CREATE TABLE sales AS FROM 'sales.csv' uses DuckDB’s “friendly SQL”: you can skip SELECT *.
  • GROUP BY ALL groups by every column that is not an aggregate – no need to repeat month.
  • On our machine the 38,890-byte CSV became an 8,199-byte Parquet file – about 4.7 times smaller.
  • We used con.execute() for CREATE and COPY. That is the recommended style in DuckDB 2.0 for statements that change data.

DuckDB with pandas

This is the feature that makes data scientists fall in love with DuckDB: it can query a pandas DataFrame by its variable name and join it with files on disk. The result comes back as a DataFrame with .df().

pandas_demo.py — duckdb-demo — Visual Studio Code
duckdb-demo › pandas_demo.py
import duckdb
import pandas as pd

# A normal pandas DataFrame in memory
targets = pd.DataFrame({
    "city":   ["Delhi", "Mumbai", "Bengaluru", "Pune", "Chennai"],
    "target": [7_000_000, 8_000_000, 9_000_000, 7_500_000, 7_000_000],
})

# DuckDB can query the DataFrame by its variable name
# and JOIN it with a file on disk
report = duckdb.sql("""
    SELECT t.city,
           sum(s.qty * s.price)              AS revenue,
           t.target,
           revenue >= t.target               AS hit_target
    FROM 'sales.csv' AS s
    JOIN targets   AS t USING (city)
    GROUP BY t.city, t.target
    ORDER BY revenue DESC
""").df()          # back to a pandas DataFrame

print(report)
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, pandas 3.0)
$ python3 pandas_demo.py
        city    revenue   target  hit_target
0  Bengaluru  9048500.0  9000000        True
1     Mumbai  8241000.0  8000000        True
2       Pune  7765000.0  7500000        True
3    Chennai  7343500.0  7000000        True
4      Delhi  6381500.0  7000000       False
⎇ mainLn 23, Col 1Spaces: 4UTF-8LFPython

Click ▶ Run to see which cities hit their sales target.

Tip: You can also return Polars (.pl()) or Arrow (.to_arrow_table()). Mixing pandas for small tables and DuckDB for heavy SQL is a very common, very productive pattern.

How fast is it?

Let’s create 5 million rows inside DuckDB and run an aggregation. range(5_000_000) is a table function that generates numbers, so we don’t need any file.

big_data.py — duckdb-demo — Visual Studio Code
duckdb-demo › big_data.py
import time
import duckdb

con = duckdb.connect()

# Generate 5 million fake page-view rows inside DuckDB
con.execute("""
    CREATE TABLE views AS
    SELECT range                         AS id,
           range % 1000                  AS page_id,
           (random() * 300)::INT         AS seconds_on_page
    FROM range(5_000_000)
""")

start = time.perf_counter()
top = con.execute("""
    SELECT page_id, avg(seconds_on_page) AS avg_seconds
    FROM views
    GROUP BY page_id
    ORDER BY avg_seconds DESC
    LIMIT 3
""").fetchall()
elapsed = time.perf_counter() - start

print("rows:", con.execute("SELECT count(*) FROM views").fetchone()[0])
print("top pages fetched:", len(top))
print(f"query time: {elapsed:.3f} s")
TERMINALOUTPUTPROBLEMSReal output – 2-core cloud Linux VM; your time will differ
$ python3 big_data.py
rows: 5000000
top pages fetched: 3
query time: 0.025 s
⎇ mainLn 27, Col 1Spaces: 4UTF-8LFPython

Click ▶ Run to see the real timing from our small 2-core test machine.

Grouping 5 million rows took about 25 milliseconds on a small 2-core cloud machine. Your number will be different, but the point is clear: for analytics on a laptop, DuckDB is often faster than setting up a “big data” cluster. For files bigger than your RAM, DuckDB can spill to disk, so it does not simply crash.

Try the new DuckDB 2.0 features

Now let’s try four changes that are new in 2.0: the VARIANT type, the new lambda syntax, the $variable shorthand and stricter division by zero.

new_in_v2.py — duckdb-demo — Visual Studio Code
duckdb-demo › new_in_v2.py
import duckdb

con = duckdb.connect()

# 1) VARIANT: store mixed, schema-less data (like JSON, but faster)
con.execute("CREATE TABLE events (payload VARIANT)")
con.execute("""
    INSERT INTO events VALUES
      ('{"user": {"id": 42, "tags": ["a", "b"]}}'::JSON::VARIANT),
      ('{"event": "click", "x": 10, "y": 20}'::JSON::VARIANT)
""")
con.sql("""SELECT variant_type(payload) AS type,
                  variant_keys(payload) AS keys
           FROM events""").show()

# 2) New lambda syntax:  lambda x: ...   (old  x -> ...  is off by default)
tens = con.execute("SELECT list_transform([1, 2, 3], lambda x: x * 10)")
print(tens.fetchone())

# 3) $variable shorthand instead of getvariable('min_qty')
con.execute("SET VARIABLE min_qty = 3")
big = con.execute("SELECT count(*) FROM 'sales.csv' WHERE qty >= $min_qty")
print(big.fetchone())

# 4) Integer division by zero is now an error (TRY() gives NULL instead)
print(con.execute("SELECT TRY(10 // 0)").fetchone())
try:
    con.execute("SELECT 10 // 0").fetchone()
except duckdb.Error as e:
    print("Error:", type(e).__name__)
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, Linux)
$ python3 new_in_v2.py
┌─────────┬───────────────┐
│  type   │     keys      │
│ varchar │   varchar[]   │
├─────────┼───────────────┤
│ OBJECT  │ [user]        │
│ OBJECT  │ [event, x, y] │
└─────────┴───────────────┘

([10, 20, 30],)
(343,)
(None,)
Error: InvalidInputException
⎇ mainLn 30, Col 1Spaces: 4UTF-8LFPython

Click ▶ Run to see the output of all four mini-demos.

  1. VARIANT stores different shapes of data in one column. variant_type() and variant_keys() let you inspect each value. Think of it as “JSON on steroids” – it is stored in a binary form, and common structure is split out automatically so queries are faster.
  2. Lambda syntax: use lambda x: x * 10 (Python style). The old arrow form x -> x * 10 now raises an error unless you turn it back on.
  3. $min_qty is a short way to write getvariable('min_qty').
  4. 10 // 0 (integer division) and % 0 now raise an error instead of quietly returning NULL. Wrap with TRY() if you want NULL. Note that 10 / 0 (normal division) still returns inf.

Triggers: an automatic audit log

Triggers run SQL automatically when data changes. This is a classic use case: keep a history of price changes during a festival sale.

audit.sql — duckdb-demo — Visual Studio Code
duckdb-demo › audit.sql
-- audit.sql: keep a history of price changes with a trigger
CREATE TABLE products (id INTEGER PRIMARY KEY, name VARCHAR, price INTEGER);
CREATE TABLE price_log (id INTEGER, old_price INTEGER, new_price INTEGER);

CREATE TRIGGER log_price AFTER UPDATE ON products
REFERENCING OLD TABLE AS o NEW TABLE AS n
FOR EACH STATEMENT
INSERT INTO price_log
    SELECT n.id, o.price, n.price FROM o JOIN n ON o.id = n.id;

INSERT INTO products VALUES (1, 'Laptop', 55000), (2, 'Phone', 18000);

-- Festival sale: 10% off everything
UPDATE products SET price = price * 0.9;

FROM price_log;
⎇ mainLn 16, Col 1Spaces: 4UTF-8LFSQL
duckdb-demo › run_sql.py
import sys
import duckdb

# Run every statement in a .sql file and show the last result
script = open(sys.argv[1]).read()
duckdb.connect().sql(script).show()
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, Linux)
$ python3 run_sql.py audit.sql
┌───────┬───────────┬───────────┐
│  id   │ old_price │ new_price │
│ int32 │   int32   │   int32   │
├───────┼───────────┼───────────┤
│     1 │     55000 │     49500 │
│     2 │     18000 │     16200 │
└───────┴───────────┴───────────┘
⎇ mainLn 6, Col 1Spaces: 4UTF-8LFPython

The first tab is the SQL file. Switch to run_sql.py and click ▶ Run to execute it.

REFERENCING OLD TABLE AS o NEW TABLE AS n gives you two temporary tables: the rows before and after the UPDATE. Because the trigger is FOR EACH STATEMENT, it runs once per UPDATE, not once per row – efficient for analytics. Some combinations are not supported yet, so check the CREATE TRIGGER docs before using them in production.

The DuckDB command-line shell also got useful upgrades:

Terminal — DuckDB CLI tips
# Open (or create) a database file in the CLI
$ duckdb shop.duckdb
# Run one query and exit
$ duckdb -c "SELECT city, count(*) FROM 'sales.csv' GROUP BY city"
# New in 2.0: agent mode (compact output for AI coding agents)
$ duckdb -agent shop.duckdb
# Keep a new file readable by DuckDB 1.5
$ duckdb old_friendly.duckdb -storage-version v1.5.6

Python API changes in DuckDB 2.0

If you have DuckDB code from older tutorials (or from ChatGPT answers trained on them), some of it will break. Click each tab to see the change and the fix:

Before: con.sql(q).fetchone() and fetchmany(n) worked on a relation.

Now: a relation is just a saved query – it runs again on every read, so fetchone(), fetchmany(), execute() and fetch_df_chunk() were removed from it. Use con.execute(q).fetchone(), or read a relation with fetchall(), df(), pl(), iter_rows() or to_arrow_reader(). Use rel.materialize() to compute the rows once and keep them.

Before: you could run new queries while an execute() result was only half read.

Now: that raises ResourceInUseException. Read the result to the end, or call con.complete() (or con.abort()). A new execute() simply replaces the open result.

Before: threads sharing one connection waited for each other.

Now: using one connection from two threads at the same time raises ResourceInUseException. Give each thread its own con.cursor(). In a web API (for example FastAPI), create a cursor per request.

Python version: DuckDB 2.0 needs Python 3.11 or newer (free-threaded builds are not supported).

Database files: DuckDB 2.0 can open your old files, but DuckDB 1.x cannot open files that 2.0 creates. To stay compatible, connect with config={"storage_compatibility_version": "v1.5.0"}.

Also changed: con.sql("CREATE ...") now returns a relation instead of None, a failed statement inside a transaction aborts the whole transaction, and write_csv(..., overwrite=False) now raises an error if the file exists.

Here is the same small script written the old way and the 2.0 way:

old_1x.py — duckdb-upgrade — Visual Studio Code
duckdb-upgrade › old_1x.py
# Works on DuckDB 1.x - FAILS on DuckDB 2.0
import duckdb

con = duckdb.connect()
rel = con.sql("SELECT 42 AS answer")

print(rel.fetchone())                # AttributeError on 2.0
print(con.sql("SELECT list_transform([1, 2], x -> x + 1)").fetchall())
TERMINALOUTPUTPROBLEMSReal error on DuckDB 2.0.0 (traceback shortened)
$ python3 old_1x.py
Traceback (most recent call last):
  ...
AttributeError: Relations no longer have fetchone(): a relation keeps
no result between calls, so every call runs it again.
Use rel.limit(1).fetchall() instead.
⎇ mainLn 8, Col 1Spaces: 4UTF-8LFPython
duckdb-upgrade › new_2x.py
# The DuckDB 2.0 way
import duckdb

con = duckdb.connect()

# fetchone(): use execute() - it returns a result you can read row by row
print(con.execute("SELECT 42 AS answer").fetchone())
con.complete()   # finish the open result before running the next query

# Relations (con.sql): read them with fetchall(), df(), pl() or iter_rows()
print(con.sql("SELECT list_transform([1, 2], lambda x: x + 1)").fetchall())

# Threads: give every thread its own cursor
cur = con.cursor()
print(cur.execute("SELECT 'one cursor per thread'").fetchall())
TERMINALOUTPUTPROBLEMSReal output (DuckDB 2.0.0, Linux)
$ python3 new_2x.py
(42,)
[([2, 3],)]
[('one cursor per thread',)]
⎇ mainLn 15, Col 1Spaces: 4UTF-8LFPython

Run old_1x.py to see the real error, then switch to new_2x.py and run the fixed version.

Upgrade from DuckDB 1.x: checklist

  1. Test on a copy first. The DuckDB team advises a shadow system, and for mission-critical work, waiting for the first patch release planned for November.
  2. Back up your .duckdb files, or keep them in the old format with storage_compatibility_version if other tools still use DuckDB 1.x.
  3. Search your SQL for -> lambdas and change them to lambda x: .... (Temporary escape hatch: SET lambda_syntax = 'enable_single_arrow'; – it goes away in 2.1.)
  4. Check regex and maths: ~ now does partial matching ('abc' ~ 'b' is true), 2 * 3! is now 12, and // or % by zero raise errors.
  5. Check MERGE INTO: if several source rows match one target row, it now errors instead of picking one.
  6. Update Python code as shown above, and read the official DuckDB 2.0 migration guide.

DuckDB vs SQLite vs PostgreSQL

DuckDBSQLitePostgreSQL
Runs asLibrary inside your appLibrary inside your appSeparate server
Best forAnalytics, reports, data filesSmall app storage, mobileWeb apps with many users
StorageColumn by columnRow by rowRow by row
Many writers at onceNo (one writer process)LimitedYes
Query CSV/Parquet directlyYes, built inNoNeeds extensions

They work great together: PostgreSQL for your app (see our PostgreSQL 19 guide), DuckDB for reports on top of exported data. DuckDB can even attach a PostgreSQL database and query it directly with its postgres extension.

Common errors and fixes

Error message (shortened)Why it happensFix
Relations no longer have fetchone()2.0 removed row-by-row reading from con.sql() relations.Use con.execute(q).fetchone() or rel.fetchall().
ResourceInUseException: connection has an unfinished result from execute()You started a new query before finishing the previous execute() result.Fetch all rows, or call con.complete(); use con.cursor() per thread.
Binder Error: Deprecated lambda arrow (->) detectedOld x -> x + 1 lambda syntax.Write lambda x: x + 1.
Invalid Input Error: Division by zero in expression (10 // 0)Integer division by zero now raises an error.TRY(10 // 0) or NULLIF(divisor, 0).
IO Error: No files found that match the pattern "missing.csv"Wrong path – paths are relative to where you run the script.Use an absolute path or cd into the data folder.
Old tool cannot open a new .duckdb fileFiles created by 2.0 use a new storage format.Upgrade the tool, or create files with storage_compatibility_version.
“Could not set lock on file”Two processes open the same file for writing.Use one writer process, or open others with read_only=True.

Best practices

  • Convert big CSVs to Parquet once, then query the Parquet. It is smaller and much faster to read.
  • Select only the columns you need – column storage rewards you for it.
  • Use execute() for writes and parameters: con.execute("SELECT * FROM sales WHERE city = ?", ["Pune"]). Never build SQL by joining user input strings.
  • One connection, many cursors in multi-threaded apps and web servers.
  • Pin the version in requirements.txt / package.json (for example duckdb==2.0.0) so upgrades are deliberate.
  • Do not use DuckDB as your main web app database with many concurrent writers – use PostgreSQL there and DuckDB for analytics. To run both side by side locally, see our Docker Compose tutorial.

Practice task

Use the same sales.csv and complete the TODOs. It takes about 15 minutes and uses everything from this post.

practice.py — duckdb-practice — Visual Studio Code
duckdb-practice › practice.py
import duckdb

con = duckdb.connect("practice.duckdb")

# TODO 1: create a table "sales" from sales.csv
# TODO 2: find the best-selling PRODUCT in each CITY (by revenue)
#         hint: use a window function -> row_number() OVER (PARTITION BY city ...)
# TODO 3: save the answer to best_products.parquet
# TODO 4: read best_products.parquet back and print it with .show()
⎇ mainLn 9, Col 1Spaces: 4UTF-8LFPython

Stuck on TODO 2? Look up QUALIFY in the DuckDB docs – it filters window function results without a subquery.

FAQ

Is DuckDB free for commercial use?

Yes. DuckDB is open source under the MIT license, so you can use it in personal and commercial projects.

Is DuckDB a replacement for PostgreSQL or MySQL?

Not for typical web apps. DuckDB is built for analytics with one writer process. Use PostgreSQL or MySQL for app data with many users, and DuckDB for reports, data files and pipelines.

Can DuckDB handle data bigger than my RAM?

Yes, in many cases. DuckDB streams data from files and can spill to disk for big joins and aggregations, so it can process data sets larger than memory – just slower than fully in-memory work.

Should I upgrade to DuckDB 2.0 now?

For new projects, yes – it is a long-term support release. For important existing systems, test on a copy first and read the migration guide; the DuckDB team suggests waiting for the November patch release for mission-critical workloads.

Does DuckDB work in Node.js and the browser?

Yes. Use @duckdb/node-api in Node.js. There is also DuckDB-Wasm for running DuckDB inside a web page. New to Node.js? Start with our Node.js Part 1 guide.

Conclusion

DuckDB turns your laptop into a fast analytics machine: install one package, point SQL at a CSV or Parquet file, and get answers in milliseconds. Version 2.0 makes it a stable, long-term choice and adds VARIANT, triggers and a friendlier SQL dialect – just remember the Python API changes when you upgrade old code. Start with the practice task, then try DuckDB on a real data file from your job or college project.

New posts every day on DevDojo

Practical tutorials on AI, frontend, backend, databases and DevOps – with runnable code. Bookmark devdojo.co.in and share this DuckDB tutorial with a friend who still waits for big CSV files to load in Excel.

Share