Query Parquet and CSV files with DuckDB in a desktop GUI

Run SQL straight against Parquet and CSV files with DuckDB: read_parquet, read_csv, globs, SUMMARIZE, views over files, and exporting results from a GUI.

By

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 httpfs extension, which SQLly does not bundle. Everything in this post uses local files.
  • DuckDB is single-writer. A .duckdb file 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.