Data Analysis with Python (pandas, polars, DuckDB) Module 13 – DuckDB: SQL on DataFrames and files
DuckDB: query CSV, Parquet and JSON files
Query CSV, Parquet and JSON files in place with DuckDB, see what its CSV sniffer guessed and correct it, and compare the three formats for analysis.
What you will learn
- Query CSV, Parquet and JSON files in place with read_csv, read_parquet and read_json
- Diagnose CSV detection with sniff_csv and override delimiters, types and date formats
- Capture bad rows with store_rejects instead of failing or silently skipping them
- Read many files at once with globs, file names and Hive partitions
Before you start
What you’ll learn · Before you start
What you will learn
- Query CSV, Parquet and JSON files in place with read_csv, read_parquet and read_json
- Diagnose CSV detection with sniff_csv and override delimiters, types and date formats
- Capture bad rows with store_rejects instead of failing or silently skipping them
- Read many files at once with globs, file names and Hive partitions
Before you start
On this page
For three invoice sources, DuckDB queries data files where they are. SELECT * FROM 'invoices.csv' reads a CSV file as if it were a table, and
the same works for Parquet and JSON files, with no import step and no DataFrame in between. Each query reads the file
afresh and uses what the file says about itself: a Parquet file stores its column names and types, JSON records are
parsed into columns, and for a CSV file a CSV sniffer: A reader process that infers a text table's dialect, column types and header from sampled input. guesses the delimiter, the header and the column types from a
sample of rows. When a guess is wrong, read_csv, read_parquet and read_json take options that state the answer
instead. This is the quickest way to look at an export before deciding how to load it, to join a file with a table or
a DataFrame, and to summarise files larger than the computer’s memory, because DuckDB streams through them.
One query joins three original file formats
A small book distributor keeps its invoice lines in CSV, its catalogue in Parquet and its returns as JSON records, one per line, each with a nested list of returned items:
books/invoices.csv
invoice,sold_on,shop,city,sku,qty,amount
INV-101,2026-07-05,Lakshmi Books,Pune,BK-101,4,1800.00
INV-101,2026-07-05,Lakshmi Books,Pune,BK-102,2,598.00
INV-102,2026-07-12,Page Turners,Delhi,BK-103,3,1950.00
INV-103,2026-07-19,Readers Corner,Chennai,BK-101,10,4500.00
INV-104,2026-07-31,Lakshmi Books,Pune,BK-103,1,650.00
INV-105,2026-08-03,Book Nook,Kolkata,BK-102,6,1794.00
INV-105,2026-08-03,Book Nook,Kolkata,BK-104,2,700.00 books/returns.json
{"invoice": "INV-101", "reason": "damaged", "items": [{"sku": "BK-101", "qty": 1}, {"sku": "BK-102", "qty": 1}]}
{"invoice": "INV-105", "reason": "wrong title", "items": [{"sku": "BK-104", "qty": 2}]} # Three files, three formats, no loading step: each query reads the files where they are.
import duckdb
print(duckdb.sql("SELECT count(*) AS lines, min(sold_on) AS first, max(sold_on) AS last FROM 'invoices.csv'"))
print(duckdb.sql("SELECT sku, title, publisher, price FROM 'catalogue.parquet' ORDER BY sku"))
print(duckdb.sql("SELECT invoice, reason, len(items) AS skus FROM 'returns.json' ORDER BY invoice"))
# One question across all three: revenue per publisher, less the value of returned copies.
print(duckdb.sql("""
WITH sold AS (
SELECT c.publisher, sum(i.amount) AS sold
FROM 'invoices.csv' AS i JOIN 'catalogue.parquet' AS c USING (sku)
GROUP BY c.publisher
), returned AS (
SELECT c.publisher, sum(r.item.qty * c.price) AS returned
FROM (SELECT unnest(items) AS item FROM 'returns.json') AS r
JOIN 'catalogue.parquet' AS c ON c.sku = r.item.sku
GROUP BY c.publisher
)
SELECT publisher, sold, coalesce(returned, 0) AS returned, sold - coalesce(returned, 0) AS net
FROM sold LEFT JOIN returned USING (publisher)
ORDER BY publisher
""")) Output
┌───────┬────────────┬────────────┐ │ lines │ first │ last │ │ int64 │ date │ date │ ├───────┼────────────┼────────────┤ │ 7 │ 2026-07-05 │ 2026-08-03 │ └───────┴────────────┴────────────┘ ┌─────────┬────────────────────┬─────────────────┬──────────────┐ │ sku │ title │ publisher │ price │ │ varchar │ varchar │ varchar │ decimal(8,2) │ ├─────────┼────────────────────┼─────────────────┼──────────────┤ │ BK-101 │ Monsoon Maths │ Kavya Press │ 450.00 │ │ BK-102 │ River Stories │ Kavya Press │ 299.00 │ │ BK-103 │ Data at Dawn │ Northwind Books │ 650.00 │ │ BK-104 │ Hill Station Poems │ Northwind Books │ 350.00 │ └─────────┴────────────────────┴─────────────────┴──────────────┘ ┌─────────┬─────────────┬───────┐ │ invoice │ reason │ skus │ │ varchar │ varchar │ int64 │ ├─────────┼─────────────┼───────┤ │ INV-101 │ damaged │ 2 │ │ INV-105 │ wrong title │ 1 │ └─────────┴─────────────┴───────┘ ┌─────────────────┬────────┬───────────────┬────────┐ │ publisher │ sold │ returned │ net │ │ varchar │ double │ decimal(38,2) │ double │ ├─────────────────┼────────┼───────────────┼────────┤ │ Kavya Press │ 8692.0 │ 749.00 │ 7943.0 │ │ Northwind Books │ 3300.0 │ 700.00 │ 2600.0 │ └─────────────────┴────────┴───────────────┴────────┘
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_files.py
The file names stand in FROM like table names, and DuckDB chooses the reader from the extension. The CSV dates came
out as DATE because the sniffer recognised them, the Parquet prices kept the DECIMAL(8,2) type stored in the file,
and unnest(items) turned each returned item of the JSON records into a row of its own. The JSON reader also worked
out by itself that the file holds one record per line rather than one big list. The report then joins all three:
Kavya Press sold 8,692 worth and took back 749, so it nets 7,943. Nothing was copied into a database on the way, so
running the script again tomorrow reads tomorrow’s files.
Join the invoice, catalogue and return inputs; reconcile 8,692 sold with 749 returned.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Invoice lines, the typed catalogue and nested return records remain in their original files.
- Each reader exposes the needed rows and types to one SQL report; the JSON item list expands into return lines.
- The recorded publisher amount subtracts 749 returned rupees from 8,692 sold rupees, producing net 7,943.
Checkpoint
Check: One query joins three original file formats
The sniffer separates dialect, type and header
A Parquet file describes itself; a CSV file does not, so read_csv works out its layout before reading it:
How read_csv decides how to read a CSV file
Text description of the diagram
The diagram is a flow read from top to bottom, with one side box.
- It starts with a CSV file.
- DuckDB samples up to 20,480 rows from across the file.
- It detects the dialect: the delimiter, quote and escape characters that give every sampled row the same, largest number of columns.
- It detects each column's type: the first type in the order BOOLEAN, TIME, DATE, TIMESTAMP, BIGINT, DOUBLE that can hold every sampled value, or VARCHAR when none can.
- It decides whether the first row is a header: it is one if its values' types differ from the types of the rows below.
- It reads the whole file with these settings.
- While reading, a value that does not fit its column's type stops the query with an error by default, is skipped with ignore_errors, or is kept in the reject_errors table with store_rejects.
A dashed side box says that options you pass, such as delim, quote, header, types, dateformat, thousands and decimal_separator, replace the matching guess.
The order is the documented one: dialect first, then types, then the header, all from a sample of 20,480 rows by
default (CSV auto detection). sniff_csv reports the sampled detection decisions without returning the complete data table:
# What DuckDB decided about a CSV file, and the read_csv call that reproduces it.
import textwrap
import duckdb
found = duckdb.sql("SELECT Delimiter, Quote, HasHeader, Columns, DateFormat FROM sniff_csv('invoices.csv')").fetchone()
delimiter, quote, has_header, columns, date_format = found
print("delimiter:", repr(delimiter), "| quote:", repr(quote), "| header:", has_header, "| date format:", date_format)
for col in columns:
print(f" {col['name']:<8} {col['type']}")
# Prompt holds the full call with every setting spelled out: paste it into a script to stop guessing.
prompt = duckdb.sql("SELECT Prompt FROM sniff_csv('invoices.csv')").fetchone()[0]
print(textwrap.fill(prompt, 100, subsequent_indent=" ")) Output
delimiter: ',' | quote: '(empty)' | header: True | date format: %Y-%m-%d
invoice VARCHAR
sold_on DATE
shop VARCHAR
city VARCHAR
sku VARCHAR
qty BIGINT
amount DOUBLE
FROM read_csv('invoices.csv', auto_detect=false, delim=',', quote='', escape='', new_line='\n',
skip=0, comment='', header=true, columns={'invoice': 'VARCHAR', 'sold_on': 'DATE', 'shop':
'VARCHAR', 'city': 'VARCHAR', 'sku': 'VARCHAR', 'qty': 'BIGINT', 'amount': 'DOUBLE'},
dateformat='%Y-%m-%d');
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sniff.py
The Prompt column is the most useful part: a read_csv call with every setting spelled out and detection switched
off. Pasted into a pipeline, it makes the next run read the file exactly as this one did, instead of guessing again
from data that may have changed.
Walk from sampled fields through dialect and types to the reproducible reader call.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- A sample supplies evidence for the delimiter, quote rules and column types; it is not a guarantee about every later value.
- The documented detection order establishes field boundaries before judging types and the header.
- The Prompt field gives an explicit read_csv call that can repeat the detected settings with guessing switched off.
Checkpoint
Check: The sniffer separates dialect, type and header
Parquet separates columns and stores statistics
| Criterion | CSV | Parquet | JSON |
|---|---|---|---|
| Where the columns and types come from | Guessed from a sample of rows | Stored in the file | Inferred from the records |
| Reads only the columns a query needs | No: every line is parsed | Yes: each column is stored apart | No: every record is parsed |
| Can skip data a filter excludes | No | Yes, using per-group minimum and maximum values | No |
| Nested lists and objects | Not supported | Supported | Supported, and common |
| When to choose | Hand-offs, exports and spreadsheets | Storing data for analysis | Data from APIs, logs and event streams |
CSV is a common hand-off format, but parsing and type inference add work and can misread source conventions. Its cost relative to other formats depends on the data, query and physical layout. Parquet is the format to keep data in for analysis: it stores types exactly, compresses well, and lets DuckDB read only the columns and row groups a query needs. JSON sits between them: it keeps nesting that CSV cannot express, but every record has to be parsed.
Compare record parsing with column chunks and ask which ranges the predicate can exclude.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- CSV and JSON need the record structure read to find the requested fields; JSON can additionally represent nesting.
- Parquet stores typed column data separately, enabling a projection to avoid unneeded chunks.
- A predicate can reject a group whose stored bounds exclude the requested value; actual selectivity and layout determine what is skipped.
Checkpoint
Check: Parquet separates columns and stores statistics
Explicit reader options state the source contract
Real exports break the guesses in a handful of ways, and each has an option or a function that settles it:
delim,headerandtypesstate the delimiter, whether there is a header row, and the type of any column.dateformatandtimestampformatsay how dates are written, such as%d/%m/%Yfor day-first dates.thousandsanddecimal_separatorread numbers such as1,234,567.50or1.234,50as numbers.- reject record: An audit entry identifying a row whose attempted conversion failed, with its location and error. sets each line that fails aside in a table, with its line number and error, while the rest load.
ignore_errorsskips failing lines silently, which is why a report should preferstore_rejects.sample_sizesets how many rows the sniffer samples;-1samples the whole file.- A glob such as
'monthly/*.csv'reads many files as one table, and schema union by name: A combined table schema that aligns fields by their names and fills absent fields with missing values. lines their columns up by name. - Hive partition: A dataset folder whose key=value path segments identify partition values used while reading its files. turns folder names such as
city=Puneinto columns that filters can use to skip files. encodingreads Latin-1 and UTF-16 files; UTF-8 is the default.parquet_metadata()lists a Parquet file’s row groups and each column’s minimum and maximum, without reading rows.read_jsonturns records into rows and nested lists into list columns, whichunnestexpands into rows.
Choose the reader setting that states each known source convention.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- The source convention determines its delimiter, date format, numeric grouping and required types.
- A scheduled load can stop on malformed data or retain reject details; the choice must be explicit.
- Named schema alignment and partition values make a folder query's structure inspectable instead of assuming every export matches.
Checkpoint
Check: Explicit reader options state the source contract
Messy CSV files: overrides, regional number formats and rejected lines Pro: opens with a Pro, Premium or Ultimate pass
Exports from tills, banks and older systems break the sniffer’s guesses in predictable ways. This part reads such a file correctly, reproduces the traps of dates, sampling and rejects, and ends with an exercise that loads a folder of branch exports with a report of every line set aside.
In this part:
- Types and numeric separators recover four valid rows
- A successful parse can use the wrong day convention
- Projection can avoid converting the bad date
- One dialect per branch yields eight audited invoices
4 diagrams · 6 runnable examples (CSV, Python) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Many files in one query: globs, schema drift and Hive partitions Pro: opens with a Pro, Premium or Ultimate pass
A folder of exports is one table to an analyst and many files to DuckDB. This part shows which columns a glob produces when the files disagree, and how Hive-style folder names let a filter skip files without opening them.
In this part:
- Name alignment preserves added and renamed fields
- Path values let a filter skip seven files
2 diagrams · 3 runnable examples (Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Parquet and JSON in depth: row groups, measured speed and nested records Pro: opens with a Pro, Premium or Ultimate pass
Parquet is fast for reasons you can inspect, and JSON is flexible in ways that break strict schemas. This part reads a Parquet file’s statistics, measures CSV against Parquet on a million rows, and flattens JSON whose shape varies from record to record.
In this part:
- Sorted groups make one range relevant
- Measured: the same million rows as CSV and as Parquet
- A mixed JSON item shape needs a stated normalization
2 diagrams · 3 runnable examples (Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise the stated query contracts
Practice that adapts to you
Practise this lesson
9 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
What does DuckDB's CSV sniffer decide about a file, and when should you stop relying on it?
From a sample of up to 20,480 rows, the sniffer decides the dialect (the delimiter, quote and escape characters), the type of each column, and whether the first row is a header. sniff_csv shows those decisions, and its Prompt column is the equivalent read_csv call with detection switched off. Rely on it to look at a file you have never seen. Stop relying on it once the file feeds a scheduled job or a report: a value outside the sample can change a column's type or fail the read, and dates that fit both day-first and month-first are read day-first. From then on, state the delimiter, the header, the date format and the types, for example by pasting the Prompt into the pipeline.
-
Fresher to mid
A vendor's monthly CSV has loaded cleanly for six months. In month seven, the revenue query fails because amount is suddenly VARCHAR, though nothing in your code changed. How do you find the cause, and how do you make the load robust?
-
Mid to senior
Every morning a job reads 900 daily CSV files, about 40 GB, with one read_csv glob, and keeps only the last seven days for a dashboard. It now takes 25 minutes. What do you check first, and how do you make it fast?
-
Mid to senior
A partner's JSON export has an items field that is a list of objects in most records, a single object in some, and missing or null in a few. Design a DuckDB load that gives one row per item, and say how you would notice a new shape next month.
Key takeaways
FROM 'file.csv','file.parquet'and'file.json'read files in place;read_csv,read_parquetandread_jsontake options when the defaults are wrong.- The CSV sniffer detects the dialect, the types and the header from a sample.
sniff_csvshows its decisions, and itsPromptis aread_csvcall that repeats them without guessing. - Parquet stores types and statistics and lets DuckDB read only the columns a query needs; CSV is text to be parsed and guessed; JSON keeps nesting.
store_rejectssets failing lines aside in a table with their line numbers, whereignore_errorshides them.
Terms in this lesson
- CSV sniffer
- A reader process that infers a text table's dialect, column types and header from sampled input.
- Hive partition
- A dataset folder whose key=value path segments identify partition values used while reading its files.
- Parquet row group
- A horizontal group of rows stored as separate column chunks, usually with statistics for each chunk.
- reject record
- An audit entry identifying a row whose attempted conversion failed, with its location and error.
- schema union by name
- A combined table schema that aligns fields by their names and fills absent fields with missing values.
References
- CSV import (DuckDB) (DuckDB Foundation)
- CSV auto detection (DuckDB) (DuckDB Foundation)
- Reading faulty CSV files (DuckDB) (DuckDB Foundation)
- Reading and writing Parquet files (DuckDB) (DuckDB Foundation)
- Querying Parquet metadata (DuckDB) (DuckDB Foundation)
- Parquet tips (DuckDB) (DuckDB Foundation)
- JSON overview (DuckDB) (DuckDB Foundation)
- Loading JSON (DuckDB) (DuckDB Foundation)
- Reading multiple files (DuckDB) (DuckDB Foundation)
- Combining schemas (DuckDB) (DuckDB Foundation)
- Hive partitioning (DuckDB) (DuckDB Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress