SQL Playground: Query Files in Your Browser
After reading this you will know how to load CSV, Excel and Parquet files as SQL tables, write joins and aggregations that run entirely in your browser, and read the results without being tricked by type inference or memory limits.
What it is, with one query
The SQL Playground turns files into tables and runs real SQL against them. Drop a file named sales.csv and you get a table called sales. From there you write standard SQL. The engine is DuckDB compiled to WebAssembly, so everything happens inside the browser tab. No data is uploaded anywhere.
Here is the hook. Suppose sales.csv has 1,000 rows with columns region, product and amount. This query answers "how much did each region sell?" in one pass:
SELECT region, SUM(amount) AS total FROM sales GROUP BY region ORDER BY total DESC
That is not a screenshot of a database server. It runs on the file you just dropped, using about 35 MB of engine code that downloads once and then stays cached.
When to use it, and when not
Use the Playground when you have tabular files and a question that SQL expresses cleanly: filtering, grouping, joining two files on a shared key, ranking within groups, or computing running totals. SQL is compact for these. A three-table join with a filter and a sort is four lines here and a fiddly afternoon in a spreadsheet.
Do not reach for it when a simpler tool already fits. If you only want to look at a CSV, sort it and delete a column, the CSV Viewer & Editor is faster. If you want a quick cross-tab without writing SQL, the Pivot Table Maker does grouping through menus. If you just need to see inside a Parquet file, the Parquet & Feather Preview shows the schema and a sample without a query.
Files load fully into RAM. A 200 MB CSV can expand to several times that in memory once parsed into typed columns. A desktop database would spill to disk; this cannot. If a query fails on a multi-gigabyte file, that is the memory ceiling, not a bug.
How files become tables
Each file is registered under a table name derived from its filename, with the extension stripped. orders_2024.csv becomes orders_2024. A leading digit or a space forces you to quote the name: write SELECT * FROM "2024 orders".
Column types are inferred, not declared. DuckDB reads a sample of rows and guesses. This is where surprises begin. A column of postal codes like 01234 may be read as the integer 1234, dropping the leading zero. A column mixing 12 and N/A becomes text, so SUM on it will error or return zero.
You can override inference by calling the reader function directly instead of relying on the auto-registered table:
SELECT * FROM read_csv_auto('sales.csv', types={'zip': 'VARCHAR'})
Here read_csv_auto is the CSV reader, 'sales.csv' is the file, and types pins the zip column to text so leading zeros survive.
Grouping and aggregation, the core idea
Most useful queries collapse many rows into few. GROUP BY region takes 1,000 sales rows and produces one row per distinct region. The aggregate functions decide what each group becomes:
Here g is one group, i ranges over the rows in that group, x_i is the value of the aggregated column, and n_g is the number of rows in the group. Count ignores the values and just tallies rows. Sum adds them. Average divides sum by count.
The mental model: SQL sorts rows into buckets by the GROUP BY key, then computes one number per bucket. A row with a NULL amount is skipped by SUM and AVG but still counted by COUNT(*). That mismatch explains many "the numbers do not add up" moments.
Reproducing the demo dataset
The sample dataset ships with the tool, so you can run this without uploading anything. Assume the demo sales table holds these four rows for two regions:
| region | product | amount |
|---|---|---|
| North | Widget | 120 |
| North | Gadget | 80 |
| South | Widget | 200 |
| South | Gadget | 50 |
- Group by
region. North gets rows 1 and 2; South gets rows 3 and 4. - Sum
amountper group. North is120 + 80 = 200. South is200 + 50 = 250. - Order by that sum descending. South (250) comes first, North (200) second.
- Average per group for a sanity check. North is 200 / 2 = 100. South is 250 / 2 = 125.
The query SELECT region, SUM(amount) AS total, AVG(amount) AS mean FROM sales GROUP BY region ORDER BY total DESC returns two rows: South with 250 and 125, then North with 200 and 100.
Window functions, the part spreadsheets cannot do easily
A window function computes across a set of rows without collapsing them. You keep all four sales rows but attach a per-region rank or running total to each. The clause OVER (PARTITION BY region ORDER BY amount DESC) defines the window.
RANK() OVER (PARTITION BY region ORDER BY amount DESC)
PARTITION BY region restarts the ranking for each region. ORDER BY amount DESC ranks largest first. In North, Widget (120) gets rank 1 and Gadget (80) rank 2. In South, Widget (200) gets rank 1 and Gadget (50) rank 2. All four rows survive; each just gains a rank column.
To keep only the top row per region, wrap it in QUALIFY rank = 1. That is DuckDB shorthand that filters on a window result without a subquery.
Joining across files
The Playground shines when two files share a key. Drop sales.csv and regions.csv where regions maps each region to a manager. Join them:
SELECT s.region, r.manager, SUM(s.amount) FROM sales s JOIN regions r ON s.region = r.region GROUP BY 1, 2
The join key is region. An inner JOIN keeps only rows where a match exists in both files. If regions is missing the South entry, South sales vanish silently from the result. Use LEFT JOIN to keep every sales row and get NULL for the missing manager, which makes the gap visible instead of hidden.
Common mistakes
- Selecting a non-grouped column
- Writing
SELECT region, product, SUM(amount) ... GROUP BY regionfails or returns an arbitraryproduct, because SQL cannot pick one product for a group of many. Either group by it too or aggregate it. - Trusting inferred types
- A version column like
1.10read as a number becomes1.1, and IDs with leading zeros lose them. Pin the type with the reader function when the column is really text. - Counting rows instead of values
COUNT(*)counts all rows including NULLs;COUNT(amount)counts only non-NULL amounts. If 50 of 1,000 rows have a NULL amount, these differ by 50.- Filtering after grouping by mistake
WHEREfilters rows before grouping;HAVINGfilters groups after. To keep regions whose total exceeds 200, useHAVING SUM(amount) > 200, notWHERE.
Related tools
Once you have an answer, other tools finish the job. Export the result and chart it in the Data Visualizer, or turn a CSV into JSON with the CSV ↔ JSON Converter. If your source is an Excel workbook, the Excel → CSV Converter gives you one clean CSV per sheet to drop in here. To view JSON as a table before querying, use the JSON Table Viewer. To mask sensitive columns before sharing a result, use the Data Anonymizer. To format a small result for a document, paste it into the Markdown Table Generator.
Frequently asked questions
Does my data get uploaded to a server?
No. The engine runs in your browser as WebAssembly. Files are read from your device into the tab's memory and never sent anywhere. Only the 35 MB engine downloads once from a CDN, then caches.
What SQL dialect does it accept?
DuckDB's dialect: standard SQL plus analytics extras like window functions, QUALIFY, PIVOT, list and struct types, and reader functions such as read_csv_auto(). Most PostgreSQL-style queries run unchanged.
Why did my big file fail to load?
Everything lives in RAM. A file that expands beyond the memory the browser tab can allocate will fail. There is no disk spill. Reduce columns, filter rows first, or convert to Parquet, which loads more compactly than CSV.
How do I query a file whose name starts with a number?
Quote the table name with double quotes: SELECT * FROM "2024_sales". The table name is the filename minus its extension.
Can I join three or more files at once?
Yes. Drop all the files, then chain JOIN clauses on their shared keys. There is no fixed limit beyond the memory needed to hold every file at once.