JSON SQL Analyzer

Desktop Required

JSON SQL Analyzer is optimized for desktop use. Please open this tool on a larger screen.

One dropped file becomes a table before you write a single query

Dropping a .json, .ndjson, .jsonl, .csv, .tsv, or .parquet file registers its raw bytes into a DuckDB instance running as WebAssembly inside a dedicated Web Worker, then runs one statement — CREATE TABLE data AS SELECT * FROM read_json_auto('yourfile') (or read_csv_auto, or read_parquet, chosen from the file's extension) — and that table, named data, is what every query runs against. A .tsv file runs through the same CSV reader with the separator forced to a tab.

The reader infers column names and types from a sample of the file in that same statement, and the file's bytes stay inside the browser tab's own memory the entire time. The DuckDB WASM engine is fetched once from a CDN on first load — that is the query engine, not your data; your uploaded file is never sent anywhere over the network.

Schema inference, and the editor built for when it guesses wrong

DESCRIBE data, run automatically after ingestion, populates the Schema panel: nested values come back as STRUCT, MAP, LIST, or UNION, and a list of structs reports as LIST rather than STRUCT so it is not mistaken for a plain nested object.

When inference calls a column the wrong type — a numeric-looking field that is actually mixed, or a date format DuckDB does not recognize — ingestion fails, and this tool parses the raw DuckDB error to extract the offending column and opens the Schema Editor with that field highlighted. From there you can override individual columns' types, adjust the sample size DuckDB reads before guessing, turn on union_by_name to merge column sets across an NDJSON file with inconsistent line shapes, or raise the maximum nesting depth — options scoped to JSON only, so the editor offers only what your file's format supports. Applying an override re-reads the whole file with the corrected schema in the same CREATE TABLE statement, not a cast layered on afterward.

Querying nested JSON: dot notation, list indexing, and when neither works

A plain STRUCT column reads with ordinary dot notation — address.city — because DuckDB treats a nested object as a first-class row type. A list of structs is different: it is a LIST, not a STRUCT, so dot notation fails, and the fix is indexing first — orders[1].total reads the first element's total (DuckDB lists are 1-indexed, not 0-indexed). A column left as raw JSON rather than exploded into STRUCT or LIST needs json_extract(column, '$.field') or json_extract_string(column, '$.field') instead.

Getting this wrong produces a real DuckDB error — "Cannot extract field," "not a struct," and similar — and this tool watches for exactly those shapes, plus a bare "column not found" case, and prints the matching tip under the query editor rather than leaving you to work out which access pattern applies from the raw error alone.

The data profile: a full-table SUMMARIZE you can cancel mid-scan

The Profile view runs DuckDB's own SUMMARIZE over the active table in one pass: per-column min, max, an approximate distinct-value count (a HyperLogLog estimate, not exact, so it stays cheap on a huge column), mean, standard deviation, the three quartiles, and a null percentage, each shown as a colored density bar so a fully-null or single-valued column stands out at a glance.

SUMMARIZE can refuse a nested STRUCT, MAP, LIST, or UNION column on some DuckDB builds; when that happens this tool retries with only the scalar columns projected in and reports which columns it skipped, rather than dropping them silently or failing the whole profile. It runs through the same cancellable path as a query, so a profile started on a huge table by mistake can be stopped mid-scan.

Focus Datasets: materializing a result and freeing the original file's memory

Extract turns whatever query produced the rows on screen into a brand-new named table — a genuine CREATE TABLE ... AS (your query), not a saved filter — so a working subset of a huge file becomes its own lightweight table. Extractions chain: extracting from a subset creates a second subset built on the first, and every subset from the session is dropped together the moment you load a new file.

Because WASM memory can only grow, extracting a subset optionally offers to release the original file's table and its registered bytes from memory — worth doing once you have narrowed a multi-hundred-megabyte file down to what you need, since the original's memory otherwise sits held for the rest of the session.

A worked query, and the real size limits behind "massive"

Say the file is NDJSON where each line looks like {"user": {"name": "Ana", "tier": "pro"}, "amount": 42.5}. DESCRIBE data reports user as a STRUCT and amount as a DOUBLE. SELECT user.name, amount FROM data WHERE user.tier = 'pro' AND amount > 20 ORDER BY amount DESC runs the dot-notation path directly, no json_extract needed, because the reader already exploded that object into a real STRUCT column.

Two hard limits define "massive" here: a file over 1GB is rejected by a size check before it reaches DuckDB, with a message to split or pre-filter it; and 50,000+ result rows trigger a dialog to render everything or cap the grid at 10,000, since rendering — not the SQL engine — is what strains the tab. Sharing a query shares a URL, not the data: the SQL text is compressed into the query string, so a shared link parses to an empty result until you drop the same file back in yourself.

Common use cases

Querying a multi-hundred-megabyte NDJSON log dump

Drop a log export straight in and run a WHERE/GROUP BY over it with real SQL, instead of writing a one-off script or opening it in an editor that cannot handle the file size.

Fixing a column DuckDB inferred wrong

When ingestion fails, follow the auto-opened Schema Editor to the flagged column, override its type by hand, and re-read the file with the correction applied.

Narrowing a huge file to a working subset

Extract the rows a query returns into their own table, then release the original file's memory once you no longer need the full dataset loaded.

Spotting bad or empty columns before writing a report

Run the data profile to see which columns are entirely null or hold only one repeated value before building a query or export around them.

Frequently asked questions

Does DuckDB actually run inside my browser, or is my file sent to a server to be queried?

It runs entirely client-side — DuckDB compiled to WebAssembly, executing inside a dedicated Web Worker in your browser tab. The only thing fetched over the network is the DuckDB engine bundle itself, from a CDN, the first time the page loads; your uploaded file's bytes are registered directly into that worker's own memory and never leave your device.

What file formats and how large a file can I actually load?

JSON, NDJSON/JSONL, CSV, TSV, and Parquet, detected from the file extension. There is a hard 1GB size limit enforced before the file is even handed to DuckDB — a larger file is rejected outright with a message to split or pre-filter it first.

Why did loading my file fail with a type error, and what do I do about it?

DuckDB inferred a column's type from a sample of the file and got it wrong for a value later in the data. The Schema Editor opens automatically with the offending column highlighted where it can be extracted from the error — override its type there and re-read the file, or adjust the sample size DuckDB scans before guessing.

My query says a column "is not a struct" — what does that mean?

The column is a LIST of structs, not a plain STRUCT, so dot notation alone will not reach into it. Index the list first, like orders[1].total, then dot notation into the element from there. This tool's own error hint shows the same guidance whenever it sees this specific failure.

How do I read a value out of a column that is stored as raw JSON rather than a nested object?

Use json_extract(column, '$.field') to get a JSON value back, or json_extract_string(column, '$.field') to get it as plain text. DuckDB only exposes STRUCT and LIST dot-and-index access for columns it exploded during ingestion — a column left as JSON needs these functions instead.

What does the data profile actually compute, and can it be wrong about how many distinct values a column has?

It runs DuckDB's SUMMARIZE over the whole table in one pass: min, max, an approximate distinct count, mean, standard deviation, quartiles, and null percentage per column. The distinct count is a HyperLogLog estimate, not an exact count, so treat it as approximate on any large column — that approximation is why the profile stays fast on tables with millions of rows.

What happens to my original file after I extract a subset from it?

Nothing, unless you say so — extracting offers an option to release the original file's table and registered bytes from memory. Since a WASM heap can only grow, doing this after narrowing a huge file down to what you need frees memory that would otherwise sit unused for the rest of the session.

My query returned way more rows than I expected and the tab is sluggish — what happened?

Any result of 50,000 rows or more triggers a dialog asking whether to render everything or cap the grid at the first 10,000 — rendering that many rows, not the SQL execution itself, is what strains the browser tab. Choose the cap, or add a LIMIT to your query before running it again.

You might also like