tutorials/duckdb/query-files

DuckDB Query CSV, Parquet, JSON, Excel and S3 Files

DuckDB reads files where they sit, so there is nothing to import first. Open a Parquet, CSV or JSON file on S3 or HTTPS as a table in 1bench, or put the file path straight into a SQL query.

Updated Nov 2, 2026Tested against DuckDB 1.4.5, 1.5.6 and 2.0.0

Quick answer

In 1bench
Add a DuckDB connection, choose Remote URL, paste an s3:// or https:// link to a Parquet, CSV or JSON file, and it opens as a table in the sidebar.
In SQL
sql
SELECT channel, count(*) FROM 'sales.csv' GROUP BY channel;

A quoted file path works anywhere a table name does. Switch to read_csv, read_parquet or read_json when you need options.

1bench
No SQL needed

Query files visually in 1bench

1bench opens a single Parquet, CSV or JSON file on S3, MinIO, R2 or HTTPS as a DuckDB connection, with the file shown as a table you can browse. Local files and anything more involved go through the SQL editor in the Query panel.

Prerequisite: download 1bench. Free 7-day trial, no sign-up.

  1. 1

    Add a DuckDB connection

    Click the + button (Add connection) at the top of the sidebar, or New connection on the start screen, and pick DuckDB.

  2. 2

    Choose Remote URL

    Under Connection mode, click Remote URL and paste a link such as s3://shop-data/sales.parquet or https://host/data.csv. Table name (optional) sets the name shown in the sidebar; by default it comes from the file name.

  3. 3

    Add S3 credentials if the bucket is private

    Expand S3 credentials (optional) and fill in Access key ID, Secret access key and Region. For MinIO, R2 or another S3-compatible store, also fill in Endpoint (MinIO / R2 / custom), for example localhost:9000.

  4. 4

    Test and save

    Test Connection reads zero rows from the file to check the link and the keys. Save Connection connects, and the file appears in the sidebar as a view you can open and browse like any table.

  5. 5

    Write SQL for everything else

    For local files, globs, Excel or joins, right-click any DuckDB connection (In-memory works well for scratch work), choose New Query and type the SQL from the sections below. Click Run or press Cmd+Enter (Ctrl+Enter on Windows and Linux).

Good to know
  • Nothing is copied. A Remote URL connection is an in-memory DuckDB with a view over read_parquet, read_csv_auto or read_json_auto, so each read goes back to the file.
  • The format comes from the extension: .parquet and .pq, .csv, .csv.gz and .tsv, or .json, .ndjson, .jsonl and .json.gz. Any other link is read as Parquet, and a failure then ends with "Supported files: Parquet, CSV, JSON and NDJSON." Excel files need read_xlsx in the Query panel.
  • An Endpoint without a scheme and with a port other than 443 is reached over plain HTTP, and 1bench always uses path-style bucket URLs when an Endpoint is set. Write https:// in front to force TLS.
  • The keys are set on the connection itself, so New Query on a Remote URL connection can read other files in the same bucket without a CREATE SECRET.
  • The Query panel adds a LIMIT of one page (100 rows by default) to a SELECT that has none. DuckDB's FROM-first shorthand does not get one, so add your own LIMIT when browsing a large file that way.
The manual way

Query files with SQL

DuckDB treats a file path as a table. Put the path in quotes after , and DuckDB works out the format from the extension and the column names and types from the data. Nothing is loaded into the database, so the file can sit on your disk, on S3 or behind an HTTPS link.

sql
SELECT channel, count(*) AS orders, sum(gross_cents) / 100 AS revenue
FROM 'sales.csv'
GROUP BY channel
ORDER BY revenue DESC;

The examples use the 1bench demo shop: , and , exported to files. Relative paths resolve from the folder DuckDB was started in. Every statement below gives the same results on DuckDB 1.4, 1.5 and 2.0.

Query a CSV file

sniffs the delimiter, quoting, header and column types from the first 20,480 rows. Call it directly when you need to override any of that:

OptionWhat it doesExample
First row holds column names (auto-detected)
Column separator (auto-detected)
Column names for a file without a header
Force a type for some columns
Read every column as text
Parse dates in a given format
Rows used to guess types; -1 reads them all
Skip rows that fail to parse
Add a column with the source file path

Example: read a semicolon-separated export with no header row.

sql
SELECT *
FROM read_csv('customers_noheader.txt', delim = ';', header = false,
              names = ['id', 'name', 'email', 'country', 'plan', 'signup_date', 'ltv'])
LIMIT 5;

Type detection only looks at the sample. If the first 30,000 rows of are numbers and row 30,001 says , the query fails partway through with "Conversion Error: CSV Error on Line: 30002 ... Could not convert string "n/a" to 'BIGINT'". Fix it with , (the column then reads as VARCHAR) or (the bad row is dropped). Compressed files such as are read directly, and reads a CSV piped into the shell.

Query a Parquet file

Parquet stores column types and per-column statistics, so there is nothing to sniff and DuckDB reads only the columns a query uses.

sql
SELECT product_id, sum(quantity) AS units
FROM 'sales.parquet'
WHERE occurred_at >= '2025-10-01'
GROUP BY product_id
ORDER BY units DESC
LIMIT 10;

shows the row groups and the min and max of each column, which is what DuckDB uses to skip data, and lists the stored types.

sql
SELECT path_in_schema, type, stats_min, stats_max
FROM parquet_metadata('sales.parquet');

Query files on S3

The httpfs extension handles , and paths, and DuckDB installs and loads it the first time you use one. Store the keys once as a secret; it lasts for the session, or use to keep it.

sql
CREATE SECRET (TYPE s3, KEY_ID 'AKIA...', SECRET '...', REGION 'us-east-1');

SELECT count(*) FROM 's3://shop-data/sales.parquet';

Without a secret, a private bucket answers "HTTP 403 Forbidden ... No credentials are provided". Wrong keys give the same 403 with "Credentials are provided, but they did not work". picks up your AWS CLI profile or environment variables instead of keys typed into SQL.

For MinIO, Cloudflare R2 or another S3-compatible store, add the endpoint and path-style URLs. Leave out on a plain HTTP server and you get "SSL connect error".

sql
CREATE SECRET minio (
  TYPE s3, KEY_ID 'minio', SECRET 'minio12345',
  ENDPOINT 'localhost:9000', URL_STYLE 'path', USE_SSL false,
  SCOPE 's3://shop-data'
);

ties a secret to one bucket or prefix, so you can keep separate keys for several buckets in one session. lists them. Public files need no secret at all:

sql
SELECT count(*) FROM 'https://shell.duckdb.org/data/tpch/0_01/parquet/lineitem.parquet';

Query JSON files

accepts both newline-delimited JSON (one object per line) and a single top-level array, and turns nested objects into STRUCT columns and nested arrays into lists.

sql
SELECT name, country FROM read_json('customers.json');

Example: reach into nested fields with dots, and turn an array of line items into rows with .

sql
SELECT id, customer.name, customer.country FROM 'orders.json';

SELECT id, unnest(items, recursive := true) FROM 'orders.json';

Query Excel files

comes from the excel extension, which also loads itself on first use. It reads files, one sheet at a time.

sql
SELECT name, country, lifetime_value_cents
FROM read_xlsx('customers.xlsx', sheet = 'customers');

SELECT * FROM read_xlsx('customers.xlsx', range = 'A1:C4');

Excel keeps every number as floating point, so and come back as DOUBLE (). Cast them, for example , before joining on them.

Query multiple files with a glob

A glob or a list of paths reads many files as one table. matches inside one folder and matches any depth. Add to see which file each row came from.

sql
SELECT filename, count(*)
FROM read_csv('exports/*.csv', filename = true)
GROUP BY filename;

When the files do not share the same columns, use . DuckDB then lines columns up by name and fills the gaps with NULL.

sql
SELECT channel, count(*)
FROM read_csv('exports/*.csv', union_by_name = true)
GROUP BY channel;

Without it, DuckDB takes the column list from the first file it reads. If that file has fewer columns, the extra columns in later files are silently dropped: here has no column, so disappears from the whole result and fails with "Referenced column "channel" not found". If the first file has more columns, the query stops with "Schema mismatch between globbed files ... Column with name: "channel" is missing". and take the same and options.

Query hive-partitioned folders

Folders named , such as , are the Hive layout that Spark, Athena and DuckDB's own write. turns the folder names into columns.

sql
SELECT channel, count(*)
FROM read_parquet('lake/*/*.parquet', hive_partitioning = true)
GROUP BY channel;

A filter on a partition column skips whole folders. shows it: with the Read Parquet step lists the filter under File Filters and reports "Scanning Files: 1/4". DuckDB also detects folders when you leave the option out, but setting it keeps the query explicit. The same paths work on S3, for example .

Join files with each other and with tables

Files, S3 objects and tables mix freely in one query, so you can join a Parquet file to a CSV, or a file to a table in an open database.

sql
SELECT c.country, count(*) AS orders, sum(s.gross_cents) / 100 AS revenue
FROM 'sales.parquet' s
JOIN 'customers.csv' c ON c.id = s.customer_id
GROUP BY c.country
ORDER BY revenue DESC
LIMIT 5;

Save a file as a view or a table

A view keeps the path and reads the file on every query, so it always shows the current contents. copies the rows into the database once, which is faster for repeated queries on a CSV.

sql
CREATE VIEW sales_files AS FROM 's3://shop-data/sales.parquet';

CREATE TABLE sales_2025 AS FROM read_csv('sales.csv');

To add a file to a table that already exists, use . lines columns up by position, so name them on both sides when the file's order differs from the table's. does the same in one statement.

From the CLI and Python

The shell opens an in-memory database when you give it no file, which is all you need to query files from your own shell.

bash
duckdb -c "SELECT channel, count(*) FROM 'sales.parquet' GROUP BY ALL"

In Python, runs on a default in-memory connection and returns a relation. Call to print it, for a pandas DataFrame or for tuples.

python
import duckdb

top = duckdb.sql("""
    SELECT channel, count(*) AS orders
    FROM 'sales.parquet'
    GROUP BY channel
    ORDER BY orders DESC
""")
top.show()
df = top.df()

DuckDB 2.0 removed from relations, failing with "Relations no longer have fetchone()", because a relation reruns its query on every call. Use there, or call on a connection's result as before.

Work with DuckDB in one window

1bench is a desktop GUI for DuckDB. Browse tables, filter rows, run SQL and export what you see, with the SQL still there when you want it.

Open DuckDB in 1bench

Frequently asked questions

Can DuckDB query a CSV or Parquet file without importing it?
Yes. DuckDB reads the file each time the query runs and keeps nothing afterwards. Parquet is cheap to re-read because DuckDB fetches only the columns the query uses and skips row groups whose min and max values rule them out, while a CSV is parsed in full every time. If you query the same CSV over and over, CREATE TABLE ... AS FROM read_csv(...) once and query the table.
How do I see the columns and types DuckDB detected in a file?
Run DESCRIBE FROM read_csv('sales.csv'), which works for any reader. For CSV, FROM sniff_csv('sales.csv') also shows the detected delimiter, quote and header, plus a Prompt column holding the full read_csv call with every option spelled out, ready to copy and adjust. For Parquet, parquet_schema('sales.parquet') lists the physical types stored in the file.
How do I query MinIO or Cloudflare R2 from DuckDB?
Create an S3 secret with ENDPOINT set to the server and URL_STYLE 'path'. For MinIO over plain HTTP, add USE_SSL false, or DuckDB tries TLS and stops with "SSL connect error". R2 has its own secret type: CREATE SECRET (TYPE r2, KEY_ID '...', SECRET '...', ACCOUNT_ID '...') and then read r2://bucket/file.parquet paths.
Can DuckDB query a pandas DataFrame and a file in the same query?
Yes. In Python, a DataFrame in scope can be used by its variable name, so duckdb.sql("SELECT * FROM targets JOIN 'sales.parquet' USING (channel)") joins the DataFrame targets to the file with no copy step. Call .df() on the result to get pandas back, or .fetchall() for plain tuples.