Someone hands you a folder of Parquet files, or a 2 GB CSV export, and asks a simple question about it. The traditional answers are all a bit heavy: load it into a database first, write a pandas script, or open it in a spreadsheet that gives up at a million rows. DuckDB offers a lighter answer. It is an in-process analytical database that can run SQL directly against files on disk, with no server and no import step.
This post covers the DuckDB SQL you need for querying Parquet and CSV files, and how to do it from a desktop GUI rather than the command line, so you get a proper editor, a result grid you can scroll and export, and completion for the views you build along the way.
Setting up DuckDB in SQLly
SQLly connects to DuckDB natively. The DuckDB
engine is compiled into the app, together with its Parquet and JSON extensions, so
there is nothing to install and file queries work offline. A DuckDB connection is
just a path, like SQLite: point it at a .duckdb file (it is created if
it does not exist) or use :memory:. You can also drag a
.duckdb or .ddb file onto SQLly to open it.
Which one should you pick? :memory: is fine for a quick look, but it is
one shared scratch database for as long as SQLly runs, and everything you create
in it disappears when you quit. For anything you will come back to, make a small
analysis.duckdb file next to your data. The file holds your views and
any tables you materialize; the Parquet and CSV files stay where they are.
Querying a Parquet file
The shortest possible query names the file as if it were a table. DuckDB recognizes the extension and reads it:
SELECT *
FROM '/data/trips/2026-08.parquet'
LIMIT 100;
The explicit form is read_parquet, which also accepts a glob or a list
of files, and options for the cases where files disagree:
-- every month in the folder, treated as one table
SELECT pickup_zone, count(*) AS trips, avg(fare) AS avg_fare
FROM read_parquet('/data/trips/*.parquet', union_by_name = true)
GROUP BY pickup_zone
ORDER BY trips DESC;
-- Hive-style folders (year=2026/month=08/...) become columns
SELECT year, month, count(*)
FROM read_parquet('/data/events/**/*.parquet', hive_partitioning = true)
GROUP BY ALL
ORDER BY ALL;
union_by_name matches columns by name rather than position, which saves
you when a later file added a column. hive_partitioning turns folder
names like year=2026 into real columns, and DuckDB can skip whole
folders when you filter on them. GROUP BY ALL and
ORDER BY ALL are DuckDB conveniences that group or sort by every
non-aggregated column, which is exactly what you want for quick exploration.
Look before you query
Parquet files carry their own schema, and DuckDB will show it to you without reading the data:
-- column names and types, as DuckDB will see them
DESCRIBE SELECT * FROM '/data/trips/2026-08.parquet';
-- min, max, null percentage and approximate distinct count per column
SUMMARIZE SELECT * FROM '/data/trips/2026-08.parquet';
-- the file's own metadata: row groups, sizes, compression
SELECT * FROM parquet_metadata('/data/trips/2026-08.parquet');
SUMMARIZE is the one to remember. It answers "what is actually in
this file?" in one statement, and the result is an ordinary grid you can sort and
copy from.
Querying CSV files
CSV works the same way, with more options because CSV carries no schema. By
default read_csv sniffs the delimiter, header, quoting and column types
from a sample of the file:
SELECT *
FROM read_csv('/data/exports/customers.csv')
LIMIT 20;
When the sniffer guesses wrong, usually on dates or on a column that looks numeric until row 400,000, tell it what you know:
SELECT *
FROM read_csv(
'/data/exports/customers.csv',
delim = ';',
header = true,
dateformat = '%d/%m/%Y',
types = {'postal_code': 'VARCHAR'}
);
Forcing identifier-like columns such as postal codes or account numbers to
VARCHAR is a good habit: it keeps leading zeros intact. If a file is
messy enough that you just want to see it, all_varchar = true reads
every column as text so nothing fails to parse.
Joining files to each other
Because every file is just a table expression, you can join a CSV to a Parquet folder in one statement. This is the part that is genuinely awkward in most other tools:
SELECT c.segment,
count(*) AS orders,
sum(o.amount) AS revenue
FROM read_parquet('/data/orders/*.parquet') AS o
JOIN read_csv('/data/exports/customers.csv') AS c
ON c.customer_id = o.customer_id
WHERE o.order_date >= DATE '2026-01-01'
GROUP BY c.segment
ORDER BY revenue DESC;
Turn files into views you can browse
Typing file paths gets old quickly. Create views in your .duckdb file
once, and from then on the files behave like tables:
CREATE OR REPLACE VIEW orders AS
SELECT * FROM read_parquet('/data/orders/*.parquet');
CREATE OR REPLACE VIEW customers AS
SELECT * FROM read_csv('/data/exports/customers.csv');
SQLly reads DuckDB's own catalog, so those views show up in the explorer, and
IntelliSense completes their columns
like any other table. A view re-reads the files each time you query it, so new
files that match the glob are picked up automatically. If a CSV is slow to parse
and you query it constantly, materialize it instead with
CREATE TABLE customers AS SELECT * FROM read_csv(...); DuckDB stores it
in its own columnar format inside the .duckdb file.
One more small comfort: every connection in SQLly has a Startup SQL
field in the connection editor, which runs each time the connection opens. It is
a handy place for session settings or for ATTACHing a second DuckDB
file you always want alongside the first.
Use absolute paths
One practical gotcha. In the DuckDB command line, relative paths resolve against
the directory you launched it from. A desktop app has no meaningful "directory you
launched it from", so a relative path like 'data/orders.parquet' may
not point where you expect. Use absolute paths in anything you save, and especially
in view definitions.
Big results, nested types and exporting
Scanning files is fast; displaying 40 million rows is a different problem. SQLly
streams results into the grid
and, past a threshold, spills them to a temporary file the grid pages from, so an
accidental SELECT * does not eat your memory. The trade-off is honest:
sort and filter in the grid are disabled for a spilled set, so aggregate in SQL
first.
Parquet files often hold nested data. DuckDB's LIST,
STRUCT and MAP values show up in the grid as JSON text with
field order preserved, so a struct column reads as
{"city": "Leeds", "zip": "LS1"} rather than an opaque blob. To flatten
them in SQL, use unnest for lists and dot notation for struct fields.
When you have the answer, you can export the grid to CSV, JSON and other formats, or let DuckDB write a file directly, which is the better choice for large outputs:
COPY (
SELECT pickup_zone, date_trunc('day', pickup_at) AS trip_day, count(*) AS trips
FROM read_parquet('/data/trips/*.parquet')
GROUP BY ALL
) TO '/data/out/daily_trips.parquet' (FORMAT parquet);
Checking what DuckDB did
If a file query is slower than you expect, ask for the plan. SQLly captures
DuckDB's EXPLAIN and EXPLAIN ANALYZE output in the same
plan views it uses for other engines, so you
can see whether a filter was pushed down into the Parquet scan or whether the whole
folder was read. Filters on Hive partition columns and on columns with useful
Parquet statistics are the ones DuckDB can use to skip work.
What this setup is not
- Remote object storage is not covered here. The Parquet and JSON extensions are built in; reading straight from S3 or HTTP URLs relies on DuckDB's separate
httpfsextension, which SQLly does not bundle. Everything in this post uses local files. - DuckDB is single-writer. A
.duckdbfile that another process has open for writing cannot also be opened for writing by SQLly. Point SQLly at its own analysis file and read the shared data files from there. - Not every SQLly tool has a DuckDB counterpart. Things like server activity and security management do not exist in an embedded database, and SQLly says so rather than showing an empty pane.
If you have a folder of files and a question, this is about the quickest way to an
answer: download SQLly, add a DuckDB connection pointing at
a new .duckdb file, and start with SUMMARIZE.
Keep reading
- Azure Data Studio is retired: where SQL Server users go next
- Reading query plans in PostgreSQL, SQL Server, MySQL, SQLite
- Using a GUI with Turso and libSQL: URLs, tokens, transactions
- Working with ClickHouse from a desktop SQL client
- Catch UPDATE/DELETE without WHERE before it runs
- Streaming huge result sets without eating your RAM
- Keep pivots, charts and formatting in your SQL
- IntelliSense that writes JOIN ON from foreign keys