Query CSV and TSV Files with SQL in Your Browser
When CSV exports land on your desk, they often look too simple to query. Every spreadsheet tool, database, and API can produce them, yet those plain text files still carry enough structure for serious analysis.1 The SQL Workbench reads both CSV and TSV with DuckDB, automatically detecting delimiters and inferring column types, so you can run GROUP BY, JOIN, and window function queries without writing any Python.
Why SQL on CSV beats a spreadsheet for data work
Spreadsheets handle a few thousand rows comfortably. Beyond that, scrolling and manual filtering become impractical, pivot tables slow down, and formula errors multiply. SQL does not share those constraints. DuckDB reads CSV files row by row without loading the entire file into memory at once, making it practical to query files with millions of rows in a browser tab. Furthermore, SQL is reproducible: a query you save and re-run tomorrow produces the same result, whereas a series of spreadsheet filter clicks does not. For tasks like aggregating log exports, summarising survey data, or cross-tabulating report outputs, SQL on CSV is faster to write and easier to verify than equivalent spreadsheet work.2
How the workbench loads CSV and TSV files
Drop a .csv file and DuckDB sniffs the delimiter, quoting character, and column types automatically. Tab-separated files use an explicit tab delimiter, so .tsv files load correctly without any configuration. The view name comes from the file stem: sales_2026_q1.csv becomes the view sales_2026_q1. For files with non-standard headers or leading whitespace, the sniffer still identifies the column boundary positions correctly. This automatic detection process is what makes the workbench practical for CSV exploration, because you can run meaningful queries on an unfamiliar file without first inspecting its contents in a text editor.3
Type inference and schema preview
DuckDB inspects the first rows to infer types: integers become BIGINT, decimals become DOUBLE, ISO date strings become DATE or TIMESTAMP, and everything else becomes VARCHAR. The workbench shows a schema preview and runs SELECT * FROM table LIMIT 10 on its own before you write anything. If DuckDB infers a type incorrectly (a ZIP code column read as INTEGER, for example), you can cast it in your query: SELECT zip_code::VARCHAR FROM addresses. The preview also reveals whether the header row was parsed correctly, so you can spot offset columns before writing aggregation queries.
Because the sniffer reads only a sample of leading rows, a column that looks numeric at the top can turn out to contain text further down, and DuckDB still declares it VARCHAR to stay safe. When you know a column is numeric throughout, an explicit cast keeps your aggregations from silently returning zero rows. Reviewing the preview before writing GROUP BY or JOIN logic is the fastest way to avoid a query that runs but reports the wrong totals.
Practical query patterns for CSV data
Start with DESCRIBE to confirm column names and inferred types, then run a LIMIT query to scan a few rows. From there, GROUP BY aggregations identify the shape of the data: SELECT category, COUNT(*) FROM products GROUP BY category ORDER BY COUNT(*) DESC. Filtering with WHERE, sorting with ORDER BY, and computing derived columns with expressions all work exactly as in any SQL database. Window functions such as ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date DESC) rank rows while preserving each row's identity.4
Because the workbench loads one file at a time, JOINs across two separate CSV files are not directly possible. Within a single file, self-joins and subqueries work normally. For a quick cross-check on data quality, SELECT COUNT(*) AS total, COUNT(DISTINCT id) AS unique_ids FROM table reveals duplicate key problems instantly.
Handling encoding and delimiter detection problems
Handling a CSV that fails to load correctly usually comes down to two issues: wrong encoding or wrong delimiter. DuckDB's CSV sniffer handles comma, semicolon, pipe, and tab delimiters automatically, but it may guess incorrectly for unusual files.5 When the sniffer picks the wrong delimiter, your data loads as one large VARCHAR column per row rather than individual typed columns. The symptom is DESCRIBE returning a single column named something like column0 with all values being long strings.
Overriding the delimiter and encoding
If automatic detection fails, override it with a custom read_csv query: SELECT * FROM read_csv('myfile.csv', delim=';', header=true). Replace ; with the correct delimiter for your file. For TSV files with a .csv extension, use delim='\t'. Encoding issues appear as replacement characters or garbled accented text in query results, and they are especially common when a file uses Windows-1252 or Latin-1 rather than UTF-8. Convert such files to UTF-8 first using most text editors or the iconv command-line tool, which handle this conversion reliably.
Multi-file analysis when the workbench loads one CSV at a time
When you need to compare or join data from two separate CSV files, you have two practical options within the one-file-per-session constraint. The first option is to concatenate the rows before loading: combine both CSVs into a single file using a command-line tool, then load the combined file and use a source discriminator column to distinguish which file each row came from.
Comparing files without direct multi-file joins
The second option applies when the files have different schemas and you need to join them on a key column. Export one file to SQLite using a tool like csvkit's csvsql insert command, load the SQLite database in the workbench, then query across both tables from the single database file.6 For repeated multi-file workflows, loading the data into a local SQLite database once and querying the .db file is more efficient than preprocessing on each workbench session. Pasting either the concatenated file or the SQLite export into the workbench gives you two CSV files joined without a server.
When to use this
Use this when you have a CSV export from a CRM, a database dump, a spreadsheet export, or an API response saved to disk and want to run SQL against it without installing anything.
Examples
Count rows and check for duplicates
SELECT COUNT(*) AS total,
COUNT(DISTINCT id) AS unique_ids
FROM customers; If total and unique_ids differ, you have duplicate id values in the CSV.
Aggregate by a category column
SELECT category, SUM(revenue) AS total_revenue FROM sales GROUP BY category ORDER BY total_revenue DESC;
DuckDB infers numeric types from the first rows — confirm with DESCRIBE sales before aggregating.
Cast an incorrectly inferred column type
SELECT zip_code::VARCHAR AS zip,
city,
state
FROM addresses
WHERE state = 'CA'; DuckDB may infer a ZIP code column as BIGINT. Cast it to VARCHAR to avoid losing leading zeros.
Filter rows and export a subset
SELECT * FROM orders WHERE order_date >= '2026-01-01' AND status = 'fulfilled';
Use the Export button after running this query to download the filtered subset as CSV, JSON, Excel, or Parquet.
- 1.
Y. Shafranovich, "Common Format and MIME Type for Comma-Separated Values (CSV) Files," RFC 4180, IETF, October 2005. https://www.rfc-editor.org/info/rfc4180/
- 2.
DuckDB Foundation, "CSV Import," duckdb.org, accessed June 2026. https://duckdb.org/docs/current/data/csv/overview
- 3.
DuckDB Foundation, "CSV Auto Detection," duckdb.org, accessed June 2026. https://duckdb.org/docs/current/data/csv/auto_detection
- 4.
PostgreSQL Global Development Group, "Window Functions," postgresql.org, June 2026. https://www.postgresql.org/docs/current/tutorial-window.html
- 5.
DuckDB, "dialect_detection.cpp," github.com, accessed June 2026. https://github.com/duckdb/duckdb/blob/main/src/execution/operator/csv_scanner/sniffer/dialect_detection.cpp
- 6.
wireservice, "csvsql," github.com, accessed June 2026. https://github.com/wireservice/csvkit/blob/master/docs/scripts/csvsql.rst