SQL on CSV

Run a SQL query against a CSV file without importing it into a database first. Drop the file below and it becomes a table you can select from immediately, with columns typed by the engine rather than treated as text. GROUP BY, HAVING, subqueries, window functions and joins all work. The file stays in your browser, and the result downloads as CSV or Excel.

Two files to join? Use the join workbench, which opens with both slots ready.

Why SQL beats a spreadsheet formula here

The usual route to answering a question about a CSV is to open it in a spreadsheet and build a pivot table, and for a one-off that is fine. It stops being fine the third time you do it. A pivot table is a set of clicks that lives in a file; a SQL query is a sentence you can paste into a message, keep in a note, put in a pull request, and run again next month against a fresh export in about two seconds. The reasoning is visible instead of being buried in a dialog box.

SQL is also the only sensible way to ask certain questions at all. "Which order IDs appear more than once" is a GROUP BY with a HAVING. "What is each region's share of the total" is a window function. "Which customers are in this file but not in that one" is an anti join. Each of those is one line of SQL and a genuine chore in a spreadsheet.

The traditional cost of SQL was the setup: install a database, write a schema, work out the import command, discover that one column has a stray quote in row 40,000. That cost is what this page removes. The database is already running, in the tab, and the schema is inferred from the file.

A worked example

Press Try with sample data and the page loads sales-10k.csv: 10,000 rows of order_id, order_date, region, channel, units, amount and refunded. It becomes the table sales_10k, also reachable as t. A starter query is written and run before you do anything, so the first thing on screen is a result.

Now ask a real question. Which regions are carrying the refund problem, and how bad is it?

SELECT
  region,
  COUNT(*) AS orders,
  COUNT(*) FILTER (WHERE refunded) AS refunds,
  ROUND(100.0 * COUNT(*) FILTER (WHERE refunded) / COUNT(*), 2) AS refund_pct,
  ROUND(SUM(amount) FILTER (WHERE NOT refunded), 2) AS net_revenue
FROM sales_10k
GROUP BY 1
HAVING COUNT(*) > 100
ORDER BY refund_pct DESC

That is five aggregates, two of them conditional, a HAVING filter on the group, and a sort on a computed column. It returns in a handful of milliseconds. Try expressing it in a pivot table.

A second one, this time using a window function to rank inside each group, which is the query people most often discover they cannot write in a spreadsheet:

SELECT * FROM (
  SELECT
    region,
    channel,
    SUM(amount) AS revenue,
    RANK() OVER (PARTITION BY region ORDER BY SUM(amount) DESC) AS rank_in_region
  FROM sales_10k
  GROUP BY region, channel
)
WHERE rank_in_region <= 2
ORDER BY region, rank_in_region

Click a column header to sort the result on screen without touching the query. Press ★ Save query to keep either of these under a name, and it will be waiting for you next month, on this page and inside the full editor. Copy as Markdown table puts the result straight into a ticket or a pull request comment with the alignment already correct.

Queries worth keeping for any CSV

See what you have
SUMMARIZE t

One row per column with the type, the null count, the distinct count, the minimum, the maximum and the quartiles. Better than a first look at the grid, and it is a single word.

Find duplicate keys
SELECT order_id, COUNT(*) AS n
FROM t GROUP BY 1 HAVING COUNT(*) > 1 ORDER BY n DESC

The first thing to run on any export somebody else produced.

Find the empty cells
SELECT COUNT(*) - COUNT(region) AS missing_region,
       COUNT(*) - COUNT(amount) AS missing_amount
FROM t

COUNT of a column skips nulls, so the difference from COUNT(*) is the gap.

Bucket a number
SELECT CASE WHEN amount < 100 THEN 'small'
            WHEN amount < 1000 THEN 'medium'
            ELSE 'large' END AS band,
       COUNT(*) AS n
FROM t GROUP BY 1 ORDER BY n DESC

A histogram without a chart, and the band definition is right there to argue with.

Group by month
SELECT strftime(order_date, '%Y-%m') AS month, SUM(amount) AS revenue
FROM t GROUP BY 1 ORDER BY 1

Works because the date column was read as a DATE, not as text that happens to look like one.

The engine, named

This page runs DuckDB compiled to WebAssembly. Not a subset, not a pattern matcher over five keywords, and not a reduced mode with a "full SQL" checkbox somewhere that downloads a second engine on demand. There is one engine here, it is the real one, and it is live from the first query. Every clause on this page was run against the sample file before the page was written.

It is read-only by design. Statements have to start with SELECT, WITH, FROM, DESCRIBE, SUMMARIZE or EXPLAIN, only one runs at a time, and anything that writes, attaches another database or installs an extension is refused in the page before the engine sees it. Read-only table functions such as read_csv_auto stay available on the file you loaded, which is how you override a sniffed delimiter or force a column to text.

Frequently asked questions

How do I run a SQL query on a CSV file without a database?

Drop the file on this page. It is registered with an analytical database that runs inside your browser tab, so the CSV becomes a table without an import step, a schema you have to write, or a server anywhere. The table is named after the file, and it also answers to the short alias t, so SELECT * FROM t LIMIT 10 works whatever the file is called.

What is my CSV's table called in the query?

The file name, lowercased, with everything outside letters, digits and underscores turned into an underscore. So quarterly-sales (final).csv becomes quarterly_sales_final. A name that starts with a digit or collides with a SQL keyword is prefixed with t_. Every table also answers to a short alias: t for the first file, t2 for the second. The Tables panel above the editor lists both names.

Does it handle semicolon-delimited or tab-separated files?

Yes. The delimiter is sniffed from the file, so European exports that use semicolons and TSV files both open without you telling the page anything. If the sniffer gets it wrong on an unusual file you can override it in the query itself with read_csv_auto and explicit options, which is allowed here.

Are CSV columns typed, or is everything text?

They are typed. The engine reads the file and infers BIGINT, DOUBLE, DATE, TIMESTAMP, BOOLEAN or VARCHAR per column, which is why SUM and AVG work on a number column without a cast and why date functions work on a date column. The Tables panel shows the type it settled on for each column. A column it read as text can always be cast in the query with CAST or TRY_CAST.

How do I count rows matching a condition?

SELECT COUNT(*) FROM t WHERE region = 'North'. For several counts at once, COUNT(*) FILTER (WHERE region = 'North') AS north alongside other FILTER clauses gives you a one-row summary. Both are ordinary SQL and both run here, because the engine is a real database rather than a pattern matcher over a few keywords.

Can I find duplicate rows with SQL?

Yes, and it is a good first query on any export you did not produce yourself. SELECT order_id, COUNT(*) AS n FROM t GROUP BY 1 HAVING COUNT(*) > 1 ORDER BY n DESC lists every repeated key with its count. HAVING is exactly the sort of clause a hand-written browser SQL parser does not support, and it works here.

What happens to my file?

Nothing leaves the tab. There is no upload endpoint on this page. JavaScript reads the bytes, hands them to the in-page engine as a virtual file, and the query runs against that. Close the tab and it is gone. The only thing kept between visits is your query history and any query you deliberately saved with a name, both in your own browser.

My CSV is 400 MB. What then?

This page warns you above 100 MB and offers to hand the same file to the full editor at /app, which streams it rather than holding all of it in memory and handles files up to a gigabyte. The SQL you write there is the same SQL, in a step that chains with 39 other operations and rebuilds when the source file changes.

Query your CSV

No database to install, no schema to write, no upload. Drop the file and select from it.

Back to the workbench