A database, minus the database

The question arrives as a spreadsheet problem and leaves as an identity problem. "Which region bought the most this quarter?" — except the customer list is in one CSV and the orders are in another, forty thousand rows each, and the answer needs a join followed by a group. The traditional menu: import both into a database you spin up somewhere, or reach for a Python environment and hope pandas is installed and current, or open Excel and start building VLOOKUP chains with the quiet despair of a person doing relational algebra by hand.

All of that is ceremony disproportionate to the question. The data is already on your machine, structured, and flat. What's missing is a query engine — and as of the WebAssembly era, a query engine is just a file the browser can run. This page loads DuckDB, the analytical database that has spent the last few years teaching analysts they can SQL files directly, compiled for the browser. Drop your CSVs in, type SELECT, get answers. No install, no server, no upload.

If you have ever typed a question like this into a search box, you already know the incumbent answers are scattered: a hosted notebook, a signup-gated demo, an import wizard. The point of this tool is that the middle step disappears. File in, SQL out, result downloaded, tab closed.

What SQL-on-CSV means on this page

Concretely: an editor, a file panel, and a result grid. Load one or more CSV files and each is registered as a table named after its file. The editor accepts any statement the engine understands — queries, joins, aggregations, window functions, CTEs, even CREATE TABLE for staging intermediate results. Results render in a sortable grid, and every result exports to CSV or JSON.

The engine behind it is duckdb-wasm, the official WebAssembly build of DuckDB, running inside your tab. This is not a JavaScript reimplementation of SQL, and it is not a fuzzy approximation that pattern-matches your WHERE clause. It is the real database, byte-for-byte the same query processor as the desktop version, executing locally on your data with full ANSI-grade grammar. When a join plan works here, it works the same way in the desktop CLI, in a Python notebook, or in a server deployment — the semantics transfer because the software is identical.

Why DuckDB and not one of the lighter options? Because the design goal was analytical queries over flat files — columnar scans, vectorized aggregation, aggressive filtering — which is precisely the shape of "question about a big CSV." DuckDB has been called the SQLite of analytics since its 2019 debut out of Amsterdam's CWI research institute, and the nickname stuck because it earned it: embedded, dependency-free, and startlingly fast on exactly this workload.

From file to result set

  1. Load your CSVs

    Drop one file or several. Each gets sniffed for delimiter, header row, quoting, and column types, then registered as a table you can reference by name in SQL.

  2. Check the schema

    The file panel shows every detected column and its inferred type — integer, double, varchar, date. If a column got the wrong type (a numeric column with one stray text value), override it before querying rather than debugging surprises later.

  3. Write the query

    Standard SQL in the editor: SELECT with WHERE, GROUP BY, HAVING, ORDER BY, LIMIT — or joins across your loaded files, or window functions, or a CTE chain. Run it with Ctrl+Enter.

  4. Read the grid

    Results appear in a sortable grid with a row count and elapsed time. Wide results scroll horizontally; NULLs display explicitly as NULL so empty cells are unambiguous.

  5. Export or iterate

    Download the result as CSV or JSON, or refine the query and run again — the loaded files stay in memory, so iteration costs nothing beyond the query itself.

A note on table names: files register under their name, so orders.csv becomes orders. If a filename contains spaces or punctuation, the engine quotes it — the panel shows the exact name to use, which saves the classic first-query failure of guessing wrong about capitalization.

DuckDB in a browser tab

How a database fits in a tab: WebAssembly. duckdb-wasm compiles the C++ engine to a WASM binary that the browser executes at near-native speed inside the page's sandbox, with memory the tab allocates itself. The binary loads on demand — only when you open this tool — so the rest of the site doesn't carry the weight. Once loaded, it speaks the same SQL and runs the same plans as any other DuckDB build.

The part that makes file loading feel magical is the CSV sniffer. Before your first query runs, the engine samples the file and decides: this delimiter (comma, semicolon, tab, pipe — it counts candidates and picks the most consistent), this header behavior, this quote character, these column types. It reads a prefix of the file rather than all of it, which is why schema detection is instant on files that take a second to fully load.

Then execution is columnar. A row-store database reads whole records to answer a question about one column; a columnar one reads just the columns your query touches. Summing a single revenue field over a million rows scans roughly that field's bytes and skips the other ninety percent of the file. That's the technology bet behind DuckDB — and the reason GROUP BY over exports here feels instant where a spreadsheet's pivot engine needs a progress bar. It is the same trade-off warehouse builders make when they choose columnar storage, miniaturized into a browser tab.

-- Everything below runs in this tool, against loaded CSVs:

SELECT region, COUNT(*) AS orders, ROUND(SUM(amount), 2) AS revenue
FROM orders.csv o
JOIN customers.csv c ON o.customer_id = c.id
WHERE o.status = 'shipped'
GROUP BY region
HAVING COUNT(*) >= 2
ORDER BY revenue DESC
LIMIT 10;

The SQL that pays rent on this page

Five clause families cover almost every CSV question that brings someone here. WHERE filters rows before grouping; GROUP BY collapses them per key with COUNT, SUM, AVG, MIN, MAX; HAVING filters the groups themselves — the distinction that confuses everyone exactly once; ORDER BY and LIMIT shape the output. Joins connect files: INNER keeps matched rows, LEFT keeps every left-side row padded with NULLs where no match exists, FULL keeps both sides. And window functions — OVER (PARTITION BY ... ORDER BY ...) — compute running totals and per-group rankings without collapsing rows the way GROUP BY does.

ClauseRunsWhat it does to your rows
FROM / JOINFirstChooses the tables and wires them together
WHERESecondDrops rows that fail the condition
GROUP BYThirdCollapses rows into one per group key
HAVINGFourthDrops groups that fail the condition
SELECTFifthComputes the output columns, including aggregates
ORDER BY / LIMITLastSorts and truncates the final result
Logical evaluation order — the reason SELECT aliases work in ORDER BY but not in WHERE.

That ordering table is not trivia; it explains real errors. "Can't use an alias in WHERE" happens because WHERE runs before SELECT ever computes the alias; using the alias in ORDER BY works because sorting comes last. It also explains why COUNT(*) in HAVING counts group members — by the time HAVING runs, the group already exists to be counted. The SQL Query Explainer on this site walks any specific query through this order clause by clause, and people who internalize it stop fighting SQL and start aiming it.

Two queries against two exports, verified by hand

Here is the standard demo data, small enough to check with a calculator. Two files: a customer list and an order log. Both are exactly what falls out of a typical business system's export button.

customers.csv                orders.csv
id,name,region               order_id,customer_id,amount,status
101,Meridian Foods,West      5001,101,1240.00,shipped
102,Halcyon Print,East       5002,101,830.50,shipped
103,Juniper Labs,West        5003,102,419.25,open
104,Apex Supply,South        5004,103,1240.00,open
105,Terrace Co,North         5005,101,96.75,open
                             5006,104,610.00,shipped
Worked example — revenue by customer, joined and ranked

Input

SELECT c.name, COUNT(*) AS orders,
       ROUND(SUM(o.amount), 2) AS total
FROM customers.csv c
JOIN orders.csv o ON o.customer_id = c.id
GROUP BY c.name
ORDER BY total DESC;

Output

name            orders  total
Meridian Foods      3   2167.25   (1240.00 + 830.50 + 96.75)
Juniper Labs        1   1240.00
Apex Supply         1    610.00
Halcyon Print       1    419.25

Terrace Co is absent: the INNER JOIN drops customers with no orders. 5 rows in, 4 rows out, 0.004 seconds.
Worked example — who bought nothing

Input

SELECT c.name, COUNT(o.order_id) AS orders
FROM customers.csv c
LEFT JOIN orders.csv o ON o.customer_id = c.id
GROUP BY c.name
ORDER BY orders DESC, c.name;

Output

name            orders
Meridian Foods      3
Apex Supply         1
Halcyon Print       1
Juniper Labs        1
Terrace Co          0   ← the row the INNER JOIN hid

Note COUNT(o.order_id), not COUNT(*): with a LEFT JOIN, Terrace Co's padded row exists, so COUNT(*) would report 1 — a fake order. Counting a column from the right table counts only real matches.
Worked example — the HAVING filter on aggregate values

Input

SELECT status, COUNT(*) AS n,
       ROUND(AVG(amount), 2) AS avg_amount
FROM orders.csv
GROUP BY status
HAVING COUNT(*) >= 2
ORDER BY n DESC;

Output

status    n  avg_amount
shipped   3      893.50   (1240.00 + 830.50 + 610.00) / 3
open      3      585.33   (419.25 + 1240.00 + 96.75) / 3 = 585.333...

One status with a single row would have been filtered out by HAVING. AVG ignores NULL amounts; COUNT(*) counts rows regardless — a difference that matters the day your export has blanks.

Where a CSV database bites back

  • Type inference is a guess with excellent manners. A column of integers with one blank trailing field can arrive as VARCHAR, and then SUM() fails with a binder error instead of quietly skipping the blank. The schema panel exists so you catch this before the query — override the column type there and the problem is gone.
  • NULLs travel under many names. Depending on the exporting system, missing values arrive as empty fields, the literal string NULL, N/A, or a lone hyphen. The sniffer treats some of these as NULL and others as text, and AVG over a text-ish column is a different number than you expect. Normalize early: SELECT ... WHERE col IS NOT NULL, or use NULLIF.
  • Leading zeros evaporate on export, not here. If the source system wrote customer 0071 as 71, no query engine can restore what the export dropped. Compare exports with an Excel round trip carefully — the zeros die in Excel's CSV parsing, before SQL ever sees the file. Our XLS to CSV Converter keeps stored text intact on the way out.
  • Very wide files waste memory. The engine loads whole columns; a 200-column export where you query three of them still pays for 200. Pre-trimming with CSV Filter Rows or a column extractor keeps the tab's footprint honest.
  • Joins on floating-point keys are a quiet disaster. Two exports of the same IDs, one written as 101 and one as 101.0, may join fine or may not, depending on inferred types. Cast explicitly — ON o.customer_id = CAST(c.id AS INTEGER) — when the files come from different systems.
  • Duplicated rows in the fact table double-count joins. If orders.csv contains the same export twice (it happens more than anyone admits), every joined aggregate inflates by exactly 2x. A quick SELECT order_id, COUNT(*) ... GROUP BY order_id HAVING COUNT(*) > 1 pays for itself the first time it screams at you.
  • Memory is finite and the tab will tell you so. A WASM engine lives inside browser tab memory limits; push a multi-gigabyte export and you'll get an allocation failure, not a slow query. That failure is honest — it's the signal to graduate to desktop DuckDB, where the same SQL runs against the same file with disk-backed memory.

The people who skip the import step

The heaviest users are analysts between systems. The data lives in exports — from the ERP, the CRM, the ad platform, whatever combination the week involves — and the question is ad hoc enough that building a recurring pipeline would be theater. Load two files, write one query, download the answer, close the tab. For recurring work the pipeline eventually deserves to exist; for the Tuesday-afternoon question, it doesn't.

The second group is engineers and data people testing hypotheses before committing: does this join even produce sane cardinality, what does the distribution of this column actually look like, is the dedup key really unique. Ten minutes of local SQL against the raw file settles questions that would otherwise become assumptions baked into the real pipeline. The zero-setup part matters in exactly these moments — friction is the enemy of checking.

Then there's everyone learning SQL. A queryable CSV with instant feedback is a better classroom than any toy northwind database, because the data is yours — your sales, your tickets, your game stats. People who learn WHERE and GROUP BY on data they actually care about retain it; people who learn it on a sample schema of fake employees forget it by spring. Teachers have started assigning this page directly, which we consider the highest compliment a SQL tool can receive.

DuckDB here versus SQLite versus a pivot table

Every alternative has a constituency, so here is the comparison without cheerleading:

ApproachSetup costJOIN across filesScales toBest at
This tool (duckdb-wasm)Zero — open the pageFirst-class, typedHundreds of MBAd hoc questions, learning, private data
Desktop DuckDB CLIOne binary, ~100 MBFirst-classLarger than RAM, disk-backedRecurring local analytics
SQLite + .importInstall, CLI ceremonyManual staging tablesMillions of rows, but row-store slow on scansTransactional work, embedded apps
Excel / Sheets pivotAlready installed, probablyPainful (data model or VLOOKUP chains)~1M rows, badlyOne-table interactive slicing
Python + pandasEnvironment, dependenciesmerge(), well-troddenRAM-bound, like thisProgrammatic multi-step pipelines
Postgres COPY importServer, schema, rightsFull relational powerWarehouse scaleShared, persistent, production data
The honest decision table for querying CSV files.

The pattern to notice: every row below "this tool" buys capability with ceremony. A pivot table answers slice-and-dice questions about one table beautifully — genuinely better than SQL for exploring a single cube of numbers interactively. But the moment the question spans two files or needs a HAVING, the pivot path becomes data-model plumbing, and SQL becomes the short path. This tool is the short path with no setup tax, which is a combination that only recently became possible in a browser at all.

When your queries outgrow the tab — recurring schedules, gigabyte files, results others need to query too — that is the correct graduation moment, and nothing here fights it. The SQL you wrote transfers to desktop DuckDB unchanged. The SQL Validator and SQL Query Explainer on this site keep helping along the way.

Your data never crosses the network

Say it plainly, because it is the property that decides whether a tool like this can exist at all: the database is on your machine, and the data never moves. Files load through the browser's File API into tab-local memory; the query engine is a WebAssembly binary executing in that same tab; results export through a local download. There is no API call carrying your sales export anywhere, no session on somebody's server, no retention policy to read and choose to believe.

This matters more for SQL than for almost any other tool category, because the files people query are the files with the answers in them — revenue by customer, headcount by team, order history by account. An upload-based SQL demo asks you to hand exactly that to a vendor for the privilege of a GROUP BY. The browser build asks you for nothing. Load the page, watch the network panel stay silent, and query your exports with the calm of a person whose data is still, verifiably, their data.

SQL-on-CSV questions

What SQL dialect does this tool actually run?

DuckDB's — an analytical engine whose dialect sits close to PostgreSQL, with extensions for file scanning. SELECT, JOIN, GROUP BY, HAVING, window functions, CTEs, and the standard function library all behave the way a Postgres user expects, minus the server administration. If you can write it against Postgres, it will almost certainly run here.

Can I join two or more CSV files together?

Yes — that is the flagship feature. Load several files, and each becomes a queryable table you can INNER, LEFT, RIGHT, or FULL JOIN on any shared column. Joining an orders export to a customers export on customer_id, or prices to skus, is exactly the workload this exists for.

Does querying modify my original files?

No. Files are read once into an in-memory database, and every query, join, and aggregation happens on that copy. The CSVs on disk are never opened for writing, and closing the tab discards the in-memory database entirely.

How large a CSV can I query here?

The practical ceiling is browser memory, since the engine holds the loaded data in the tab. Files in the tens to low hundreds of megabytes work comfortably on an ordinary laptop; multi-gigabyte exports are where you should graduate to desktop DuckDB or a server. Aggregations are columnar and fast — it is loading, not grouping, that sets the limit.

What about messy CSVs — semicolons, quotes, missing values?

The reader sniffs each file's structure before importing: delimiter, header row, quoting style, and types are all detected automatically, with manual overrides available when the sniff guesses wrong. Semicolon-delimited European exports, quoted fields containing commas, and ragged rows are all handled.

Can I export the query results?

Yes. Any result set can be downloaded as CSV for the next step of your pipeline, or as JSON for scripts and APIs. The export is generated from the in-memory result — it never re-reads or rewrites your source files.

Can I create tables and indexes, or only SELECT?

You can run the full statement set the engine supports — CREATE TABLE, INSERT, UPDATE on the in-memory database, for shaping intermediate results mid-session. Just know that everything lives in the tab's memory: close the page and the schema is gone, by design.

Is my data uploaded to a server?

No. The SQL engine itself is WebAssembly running in your browser, and the files are read with the local File API. Your CSVs and your queries stay on your machine — which is what makes this safe for the exports people actually want to query: sales, payroll, customer lists.

Follow-on tools

Once the querying habit forms, the surrounding steps have tools too. Filter and shape the raw file first with CSV Filter Rows; move verified rows into a real database with CSV to SQL Insert; design the table they're landing in with the SQL CREATE TABLE Generator. And when a query comes back from a colleague with no comments and three nested subqueries, the SQL Query Explainer is the reading aid.

Sources behind the engine

Sources and further reading

We cite primary sources (standards bodies, government agencies, official documentation) wherever they exist so you can verify every number on this page yourself.