Local SQL workbench
Data conversion
Loading
Loading tool
The tool is loaded only when you open it.
All processing for this tool happens in your browser. Your input is not sent to a server.
About this tool
Load a CSV or flat JSON array as a named temporary table, then use real SQLite SQL to filter, join, group and sort the data. CSV cells remain text, including leading zeros; JSON primitives retain supported numeric, text and NULL values. A Web Worker runs the sql.js engine locally. The tool does not upload inputs, fetch URLs from your data or save input history. The same-origin bundled SQLite WASM asset is 658,410 bytes (about 643 KiB), plus worker JavaScript, and is fetched only after an explicit load action. Tables exist only in this page’s temporary session. This workbench accepts imported CSV/JSON tables and read queries; opening SQLite database files or Parquet files is outside its scope.
Common uses
- Join an order CSV to a customer JSON lookup and keep unmatched orders visible with LEFT JOIN.
- Filter an export, explicitly cast numeric text, group by department or region, and download the reviewed result.
- Check missing JSON values, compare string identifiers and build a readable multi-step query with a WITH common table expression.
How to use it
- 1.Choose CSV or JSON, enter a unique table name, and paste text or select a local file and choose UTF-8, UTF-16LE or UTF-16BE. For CSV, choose comma, semicolon, tab or pipe and whether a header is present. Add up to 8 source drafts, load the batch, and review each table’s columns and row count. Any invalid source blocks the whole batch; nothing is silently skipped. JSON must be an array of flat objects; convert nested data separately first.
- 2.Write one SELECT query, optionally introduced by WITH, using the displayed table and column names. Double-quote identifiers containing spaces or punctuation. CSV numbers are text: use CAST only after checking that values are valid numbers. Add ORDER BY for a reproducible order and WHERE or LIMIT to keep results within the declared bounds, then run the query.
- 3.Review the result count and paged preview before downloading CSV. Formula protection is enabled by default; explicitly choose raw CSV only after reviewing its warning. Export includes the complete successful bounded result, not just the visible page. Cancel or Reset releases the worker and discards every imported table, so reimport the sources to continue.
Executable CSV and JSON SQL examples
Filter numeric CSV text without losing identifiers
{
"sources": [
{
"name": "stock",
"format": "csv",
"text": "id,units\n001,12\n002,3\n010,20",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT id, units FROM stock WHERE CAST(units AS INTEGER) >= 10 ORDER BY CAST(units AS INTEGER) DESC",
"safe": true
}{
"columns": [
"id",
"units"
],
"rows": [
[
"010",
"20"
],
[
"001",
"12"
]
]
}CAST changes the comparison, not the selected id or units strings. The returned identifiers remain 010 and 001; the explicit ORDER BY places 20 before 12.
Import JSON booleans, numbers and missing keys
{
"sources": [
{
"name": "tickets",
"format": "json",
"text": "[{\"id\":\"T1\",\"open\":true,\"score\":4.5},{\"id\":\"T2\",\"open\":false,\"score\":null},{\"id\":\"T3\",\"open\":true}]",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT id, open, score FROM tickets ORDER BY id",
"safe": true
}{
"columns": [
"id",
"open",
"score"
],
"rows": [
[
"T1",
1,
4.5
],
[
"T2",
0,
null
],
[
"T3",
1,
null
]
]
}JSON true/false become 1/0, 4.5 remains numeric, and both explicit null and the missing score key produce SQL NULL. ID strings stay unchanged.
LEFT JOIN a CSV to a JSON lookup
{
"sources": [
{
"name": "orders",
"format": "csv",
"text": "order_id,customer_id,total\nA100,001,12.50\nA101,002,0\nA102,009,7",
"delimiter": ",",
"header": true
},
{
"name": "customers",
"format": "json",
"text": "[{\"id\":\"001\",\"name\":\"Alice\"},{\"id\":\"002\",\"name\":\"ボブ\"}]",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT o.order_id, c.name, o.total FROM orders AS o LEFT JOIN customers AS c ON o.customer_id = c.id ORDER BY o.order_id",
"safe": true
}{
"columns": [
"order_id",
"name",
"total"
],
"rows": [
[
"A100",
"Alice",
"12.50"
],
[
"A101",
"ボブ",
"0"
],
[
"A102",
null,
"7"
]
]
}Customer IDs are strings in both sources. The unmatched order A102 remains in the result with a NULL customer name; its original total is still text.
Group and sum validated integer amounts
{
"sources": [
{
"name": "sales",
"format": "csv",
"text": "region,amount\nEast,10\nWest,7\nEast,20",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT region, SUM(CAST(amount AS INTEGER)) AS total, COUNT(*) AS records FROM sales GROUP BY region ORDER BY region",
"safe": true
}{
"columns": [
"region",
"total",
"records"
],
"rows": [
[
"East",
30,
2
],
[
"West",
7,
1
]
]
}The amount column is explicitly cast before SUM. East has two source records totaling 30, while West has one totaling 7. This example uses integers rather than claiming exact decimal arithmetic.
Filter an aggregate with a WITH query
{
"sources": [
{
"name": "sales",
"format": "csv",
"text": "region,amount\nEast,10\nWest,7\nEast,20",
"delimiter": ",",
"header": true
}
],
"sql": "WITH totals AS (SELECT region, SUM(CAST(amount AS INTEGER)) AS total FROM sales GROUP BY region) SELECT region, total FROM totals WHERE total >= 10 ORDER BY region",
"safe": true
}{
"columns": [
"region",
"total"
],
"rows": [
[
"East",
30
]
]
}The common table expression computes regional totals, and the outer SELECT keeps totals of at least 10. WITH is part of the single read statement.
Sort numeric values while retaining their original spelling
{
"sources": [
{
"name": "items",
"format": "csv",
"text": "id,quantity\nA,10\nB,2\nC,001",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT id, quantity, CAST(quantity AS INTEGER) AS numeric_quantity FROM items ORDER BY CAST(quantity AS INTEGER), id",
"safe": true
}{
"columns": [
"id",
"quantity",
"numeric_quantity"
],
"rows": [
[
"C",
"001",
1
],
[
"B",
"2",
2
],
[
"A",
"10",
10
]
]
}CAST provides numeric ordering and a numeric output column. The source quantity 001 remains 001 alongside its numeric interpretation 1.
Quote identifiers with spaces and Unicode
{
"sources": [
{
"name": "商品",
"format": "csv",
"text": "名称,unit price\n猫,12.50",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT \"名称\", \"unit price\" AS price FROM \"商品\"",
"safe": true
}{
"columns": [
"名称",
"price"
],
"rows": [
[
"猫",
"12.50"
]
]
}Double quotes identify the table 商品 and the column unit price. The value 猫 and price string 12.50 are returned unchanged; single quotes would instead create a string literal.
Query quoted multiline CSV fields
{
"sources": [
{
"name": "notes",
"format": "csv",
"text": "id,note\n001,\"first\nsecond\"\n002,\"comma, quote \"\"ok\"\"\"",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT id, note FROM notes ORDER BY id",
"safe": true
}{
"columns": [
"id",
"note"
],
"rows": [
[
"001",
"first\nsecond"
],
[
"002",
"comma, quote \"ok\""
]
]
}Parsing respects CSV record boundaries: the newline inside the first note belongs to one value, and doubled quotation marks decode to literal quotes.
Count NULL separately from an empty string
{
"sources": [
{
"name": "entries",
"format": "json",
"text": "[{\"id\":\"A\",\"value\":\"\"},{\"id\":\"B\",\"value\":null},{\"id\":\"C\"}]",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT COUNT(*) AS records, COUNT(value) AS non_null, SUM(value IS NULL) AS nulls FROM entries",
"safe": true
}{
"columns": [
"records",
"non_null",
"nulls"
],
"rows": [
[
3,
1,
2
]
]
}There are three rows but only one non-NULL value, the empty string. Both an explicit JSON null and an absent key satisfy IS NULL.
Use generated names for headerless CSV
{
"sources": [
{
"name": "people",
"format": "csv",
"text": "001;Paris\n002;東京",
"delimiter": ";",
"header": false
}
],
"sql": "SELECT column1 AS id, column2 AS city FROM people ORDER BY column1",
"safe": true
}{
"columns": [
"id",
"city"
],
"rows": [
[
"001",
"Paris"
],
[
"002",
"東京"
]
]
}With the header option off and semicolon selected, both lines are data. The importer names columns column1 and column2; SELECT aliases give the result meaningful headers.
Protect formula-like headers and values by default
{
"sources": [
{
"name": "sheet",
"format": "csv",
"text": "note,total\n=1+1,-42\n@SUM(A1),0",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT note AS \"=header\", total FROM sheet ORDER BY total",
"safe": true
}{
"columns": [
"=header",
"total"
],
"rows": [
[
"=1+1",
"-42"
],
[
"@SUM(A1)",
"0"
]
],
"csvRecords": [
[
"'=header",
"total"
],
[
"'=1+1",
"'-42"
],
[
"'@SUM(A1)",
"0"
]
]
}Query rows remain unchanged. The csvRecords field shows how CSV protection adds apostrophes to the =header column name, formula-like strings and -42. Ordinary CSV quotes alone do not stop formulas.
Choose raw CSV explicitly for a trusted destination
{
"sources": [
{
"name": "sheet",
"format": "csv",
"text": "note,total\n=1+1,-42",
"delimiter": ",",
"header": true
}
],
"sql": "SELECT note AS \"=header\", total FROM sheet",
"safe": false
}{
"columns": [
"=header",
"total"
],
"rows": [
[
"=1+1",
"-42"
]
],
"csvRecords": [
[
"=header",
"total"
],
[
"=1+1",
"-42"
]
]
}With safe false, the csvRecords values match the query result and header exactly. Such output can execute formulas in a spreadsheet; the raw choice requires deliberate review.
Common local SQL mistakes
- Comparing or sorting CSV numeric text as though it were already a numeric column.
- Using CAST as a validator and unintentionally converting bad numeric text to zero.
- Using = NULL instead of IS NULL, or treating COUNT(column) as a count of all rows.
- Joining non-unique keys and unexpectedly multiplying result rows.
- Using single quotes for a column name instead of double-quoted SQL identifiers.
- Importing nested JSON, duplicate keys or an unquoted integer larger than JavaScript can safely represent.
- Expecting a cancelled worker to keep its imported tables or a failed oversized query to offer a partial export.
- Opening untrusted raw CSV in a spreadsheet or assuming CSV quoting stops formulas.
Limits and notes
- Each source is limited to 2 MiB of raw file bytes and 2 MiB of decoded UTF-8 text, 10,000 data rows, 128 columns and 200,000 data cells. A session holds at most 8 tables, 8 MiB of raw source bytes and 8 MiB of decoded UTF-8 text, 50,000 rows and 500,000 cells. Table and column names must be nonblank and at most 128 UTF-8 bytes; ASCII case variants collide, and table names beginning sqlite_ are reserved. SQLite native allocations are capped at 64 MiB and database pages at 32 MiB. Parsed inputs, JavaScript copies and worker messages use additional memory; these are not limits on the browser’s total heap.
- File input uses explicitly chosen UTF-8, UTF-16LE or UTF-16BE encoding; malformed bytes and conflicting BOMs are rejected. CSV uses an explicit delimiter. Unequal record widths, malformed quoting and blank or duplicate headers fail import; there is no delimiter or legacy-encoding detection. Headerless columns are named column1, column2 and so on. CSV values remain TEXT, including empty strings and numeric-looking IDs. Imported NUL characters and unpaired UTF-16 surrogates are rejected to prevent silent value changes. JSON accepts flat object arrays, unions keys in first-seen order and maps missing values and JSON null to SQL NULL. Booleans become 1/0. Nested values, duplicate object keys, unsafe integers and unsupported precision-losing numeric tokens are rejected; quote precise IDs and decimals as JSON strings.
- Queries use SQLite syntax and must be a single SELECT or WITH…SELECT of at most 20,000 UTF-8 bytes. Writes, PRAGMA, ATTACH, SQL extensions, file/network access, bound parameters and multiple statements are unavailable. Query results are limited to 1,000 rows, 128 columns, 50,000 cells, 2 MiB total and 256 KiB per cell. Exceeding any result limit fails the query and leaves no partial result or export; narrow WHERE, select fewer columns or add LIMIT. The visible preview shows 25 rows per page, at most 12 columns and 500 characters per cell; a successful export retains the complete bounded result.
- SQLite casts and comparisons follow SQLite rules. CAST of invalid numeric text can become zero, and INTEGER casts beyond the signed 64-bit range saturate to an endpoint, so validate values before calculating. Decimal arithmetic uses binary floating point, not exact decimal accounting. Unsafe 64-bit integer query results are represented as exact decimal strings; BLOB results are displayed and exported as uppercase X'hex' text rather than raw bytes. Without ORDER BY, row order is not guaranteed. Repeated join keys can multiply rows; LIMIT restricts returned rows but does not guarantee a cheap query.
- CSV output is capped at 6 MiB, uses UTF-8, comma delimiters, quoted fields and CRLF records, and downloads as sql-result.csv. SQL NULL becomes an empty field, losing the distinction from an empty string. Formula protection prefixes risky headers and values with an apostrophe, including negative numbers, and intentionally changes those exported values. It cannot prevent every spreadsheet interpretation; raw CSV may activate formulas or coerce identifiers. Each load, query, page or export has a 10-second deadline. Cancellation, timeout, worker failure, source edits, Reset and leaving the tool discard all in-memory tables and results; downloads remain on your device and may contain private information. No database file, external extension, remote table or saved query history is created by this tool.
Frequently asked questions
Why does ORDER BY put 10 before 2 in my CSV?
CSV columns are imported as text so that identifiers such as 001 retain their original spelling. Text order compares characters. For validated numeric data, use ORDER BY CAST(quantity AS INTEGER) or CAST(amount AS REAL). Keep the original column in your SELECT when its exact string matters. CAST does not validate input: invalid numeric text can silently become zero.
What is the difference between an empty CSV field and JSON null?
An empty CSV field is an empty text string. JSON null and an absent JSON object key both become SQL NULL; true and false become 1 and 0. Use IS NULL rather than = NULL. COUNT(column) skips NULL, while COUNT(*) counts rows. CSV export writes both NULL and an empty string as empty fields, so that distinction is lost in the downloaded CSV.
Can I change a table, open a database file or use SQL parameters?
The import controls create temporary local tables; submitted SQL is restricted to one read query. SELECT, joins, aggregates and WITH…SELECT are supported within the resource limits. INSERT, UPDATE, DELETE, CREATE, DROP, PRAGMA, ATTACH, bound parameters and extension loading are rejected. This tool does not open SQLite database files or Parquet files, and SQLite SQL is not interchangeable with every other database dialect.
What happens when I cancel or my query is too large?
Cancel terminates the worker, releasing the temporary database and every imported table, not just the running query. Reset or leaving the tool also discards the session; reimport to continue. A result over its row, column, cell or byte cap fails rather than offering a partial download. Use filters, fewer selected columns or an explicit LIMIT. Preview clipping alone does not shorten a successful export.
- SQLite: SELECT, joins and result ordering
- SQLite: expressions, CAST and NULL
- sql.js: in-memory SQLite for the web
- OWASP: CSV formula injection