The query someone left behind

Every team has one. It lives in a scheduled report, it is 47 lines long, it was written by an engineer who left in 2023, and this morning someone asked you to change one number in it. Reading it top to bottom is the natural instinct, and it is also the wrong one — SQL's written order is a lie about its execution order, the SELECT list you read first is actually computed almost last, and by the time you have mentally simulated the GROUP BY, the coffee is cold.

This page takes that query — paste it whole, comments and all — and hands back two things: a plain-English walkthrough of every clause in the order the database actually processes them, and a visual syntax tree showing the query's structure. It is deterministic parsing and narration, not a language model improvising; the same query always produces the same explanation, and every claim in the explanation is traceable to a node in the tree.

The audience is broader than "junior developers learning SQL," though it is happily that too. It is anyone who needs to answer, quickly and with confidence, the question what does this query actually do — reviewers checking a pull request, analysts deciphering a dashboard's source query, support engineers staring at something a ticket pasted in at 4:55 p.m.

From query text to an explanation you can check

The pipeline has three stages and no magic. First, the query is tokenized and parsed into an abstract syntax tree — the same kind of structured representation a compiler builds, adapted to SQL's grammar. Second, the tree is walked in a defined order and each node is rendered as an English sentence: what the JOIN connects and on which keys, what each WHERE conjunct filters, what the GROUP BY collapses, what HAVING keeps. Third, the walk is annotated with execution-order context, so the narration explicitly says things like *this filter runs before grouping* rather than leaving you to infer the sequencing.

Nothing in that pipeline consults a database or sends your text anywhere. The explainer's knowledge is entirely structural — it knows what a LEFT JOIN means semantically, what a correlated subquery implies about row-by-row evaluation, what a window function computes. It has no opinions about your data because it cannot see your data, and that boundary is what makes its output trustworthy: no hallucinated tables, no confident claims about rows it never counted.

Where the query contains constructs with famous gotchas — an alias used in WHERE, an aggregate in WHERE instead of HAVING, an implicit cross join from a comma-separated FROM list — the narration flags them inline. The flags are pattern matches against the tree, deterministic and cheap, which is why they can appear instantly alongside the explanation instead of arriving later as a production incident.

Explaining a query in four moves

  1. Paste the query

    Any SELECT, plus INSERT/UPDATE/DELETE if that is what you were handed. Comments are preserved in the output so you can map clauses back to the original line numbers.

  2. Pick or confirm the dialect

    The parser auto-detects among the supported dialects; override it if the query mixes conventions and the guess lands wrong.

  3. Read the walkthrough

    Clauses appear in execution order, each with a plain-English sentence, the columns and tables it touches, and any gotcha flags the tree walk produced.

  4. Inspect the syntax tree

    The collapsible tree sits beside the narration. Every explanation sentence links to its node, so you can verify the story against the structure — the habit that turns explanation into learning.

For a 50-line query the whole cycle is under a second. The bottleneck is human: reading a correct explanation of a five-table join still takes the time it takes.

Parsing: how text becomes a tree

The tool uses node-sql-parser, a battle-tested JavaScript parser that covers the major dialects and emits a JSON abstract syntax tree. Parsing proper is two phases. Lexing splits the text into tokens — keywords, identifiers, string literals, numbers, operators, comments — discarding whitespace and normalizing nothing. Parsing then consumes the token stream against the SQL grammar, building nodes as it goes: a SELECT statement node with children for the column list, the FROM clause, the WHERE predicate tree, and so on, each expression becoming a subtree whose shape encodes precedence.

A WHERE clause makes the tree idea concrete. `a = 1 AND (b = 2 OR c = 3)` is not a flat string to a parser; it is an AND node whose left child is `a = 1` and whose right child is an OR node with two children of its own. When the explainer narrates that clause, it walks this tree and renders each branch — which is also why it can spot that a rewrite would change semantics: structure, not character order, is what carries meaning.

{
  "type": "select",
  "from": [
    { "table": "customers", "as": "c" },
    { "join": "INNER JOIN", "table": "orders", "as": "o",
      "on": { "left": "o.customer_id", "operator": "=", "right": "c.id" } }
  ],
  "where": { "operator": "AND", "left": { ... }, "right": { ... } },
  "groupby": ["c.id", "c.full_name"],
  "having": { "count": { "column": "o.id" }, "operator": ">=", "value": 3 },
  "orderby": [{ "column": "revenue_cents", "direction": "DESC" }],
  "limit": 5
}

That snippet is the top level of our running example's tree, abbreviated. The visual version on the page is the same JSON rendered as boxes and lines — expandable, and clickable in sync with the narration. The value is not decorative: once you can read the tree, you can see SQL's deep regularity, that a 200-line analytics query is the same dozen node types arranged at scale.

The order SQL actually runs in

The single most useful fact this tool teaches is the gap between written order and execution order. SQL is written SELECT-first and executed SELECT-almost-last. The logical pipeline, the one every mainstream engine follows when computing results:

Runs atClauseWritten at positionWhat happens
1FROM / JOIN3rd–4thTables are combined into the working row set
2WHERE5thRows failing the predicate are dropped, one row at a time
3GROUP BY6thSurviving rows collapse into groups per distinct key
4HAVING7thGroups failing the aggregate predicate are dropped
5SELECT1stExpressions and aggregates are computed — per group if grouped
6DISTINCTafter SELECT listDuplicate result rows are removed
7ORDER BY8thResult rows (or groups) are sorted
8LIMIT / OFFSETlastThe sorted result is truncated
Logical processing order versus the order clauses appear in the text. Physical plans differ; this is the semantics every plan must honor.

The pipeline explains the classic alias trap with no memorization required. `WHERE revenue_cents > 1000` fails or behaves oddly because WHERE runs at step 2 while the alias `revenue_cents` is invented at step 5 — the name does not exist yet, like referring to a variable before its line of code executes. `ORDER BY revenue_cents` works because sorting happens at step 7, comfortably after the alias was defined. Engines add their own wrinkles — some let HAVING see aliases, some ORDER BY corner cases differ — which is exactly why the explainer states the logical order and notes dialect exceptions when the dialect is known.

It also explains why aggregates cannot live in WHERE. `WHERE COUNT(*) > 3` is a category error: COUNT needs groups to count, groups do not exist until step 3, and WHERE is stuck back at step 2 with individual rows. The correct home for that condition is HAVING, one pipeline stage later. The narrator translates such a query in those terms, which is more useful than the error message most engines produce.

A full query, narrated end to end

Here is a revenue-reporting query in the shape you would actually meet in a codebase — two tables, two filters, grouping, a group filter, sort, and truncation. Paste it in and the walkthrough reads:

SELECT c.full_name,
       COUNT(o.id) AS order_count,
       SUM(o.total_cents) AS revenue_cents
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.country = 'US'
  AND o.placed_at >= '2026-01-01'
GROUP BY c.id, c.full_name
HAVING COUNT(o.id) >= 3
ORDER BY revenue_cents DESC
LIMIT 5;
FROM customers (alias c)
  -> the driving table; 1,200 rows in this mental model
JOIN orders o ON o.customer_id = c.id
  -> inner join: keep only orders with a matching customer row
     (and only customers with at least one order)
WHERE c.country = 'US'
  -> filter rows BEFORE grouping; drops non-US customers
WHERE o.placed_at >= '2026-01-01'
  -> rows outside the date range leave now, not later
GROUP BY c.id, c.full_name
  -> collapse surviving rows into one group per customer
HAVING COUNT(o.id) >= 3
  -> group filter AFTER collapsing; keeps 3+ order customers only
SELECT c.full_name, COUNT(o.id), SUM(o.total_cents)
  -> computed per group, not per row
ORDER BY revenue_cents DESC
  -> sorts the ~87 surviving groups, highest revenue first
LIMIT 5
  -> last word, last effect: keep the top 5 groups

Now the same narration with numbers, because row counts are where execution order stops being academic. Suppose customers holds 1,200 rows and orders holds 14,800:

StageRows inRows outWhy
FROM + JOIN1,200 + 14,80014,800Inner join keeps one row per order that matches a customer
WHERE (country + date)14,8004,300Both predicates applied per row, before any grouping
GROUP BY customer4,300610 groupsOne group per customer who survived the filters
HAVING count >= 361087Groups with 1–2 orders are dropped here, not earlier
SELECT + ORDER BY + LIMIT875Aggregates computed per group, sorted, truncated
The same query with plausible cardinalities at each stage — the mental model the narration builds.
What changes if HAVING moves to WHERE

Input

HAVING COUNT(o.id) >= 3  (aggregate filter, runs on groups)

Output

WHERE COUNT(o.id) >= 3 → rejected by every engine: no groups exist yet at that stage
What changes if the date filter moves into the JOIN's ON clause

Input

JOIN orders o ON o.customer_id = c.id AND o.placed_at >= '2026-01-01' with an INNER JOIN

Output

Same result as WHERE for inner joins; with LEFT JOIN it would change meaning — unmatched customers survive with NULL aggregates, a classic silent bug the narrator flags

Those two examples compress most of what goes wrong in real reporting queries. Filters migrate between stages because the text order misleads the author; conditions drift between ON and WHERE because for inner joins the difference is invisible right up until someone changes the join type. The narrator's stage annotations exist to make the migration visible before the migration costs you.

What the narrator knows how to say

The clause vocabulary the walkthrough covers is the working set of everyday SQL:

  • All join types — INNER, LEFT, RIGHT, FULL, CROSS — with the semantics of each stated in one sentence and mixed-type comma joins flagged as implicit cross joins.
  • Subqueries — scalar, IN/EXISTS correlated and uncorrelated, with the row-by-row re-evaluation implication of correlation called out.
  • CTEs and window functions — WITH blocks narrated as named intermediate results, OVER partitions described as per-group computation that runs after grouping without collapsing rows.
  • Set operations — UNION versus UNION ALL with the deduplication cost difference noted.
  • Expressions and CASE — rendered as decision trees, with type-mismatch warnings where literals disagree.
  • DML — INSERT ... SELECT, UPDATE with FROM, DELETE with joins, each narrated as the read query it secretly is plus the write it performs.

Each item maps to a node type in the parser's grammar, which is the point: the narrator never improvises. If a construct is in the grammar, there is a sentence written for it; if it is not, you get a parse error with a character position instead of a paragraph of confident fiction.

What it cannot tell you

An honest explainer states its blind spots. This one has three:

  • No schema knowledge. It narrates `JOIN orders o ON o.customer_id = c.id` precisely, but whether that join produces 1 row or 14,800 depends on cardinalities it cannot see. It will tell you the join is structurally sound and say nothing about whether `customer_id` is indexed.
  • No cost model. Nothing here approximates runtime. A function wrapped around an indexed column gets flagged as a pattern that often disables index use, but whether it does in your database is EXPLAIN's business.
  • No semantics beyond the standard. Views, stored procedures, and application-side logic are invisible. A query against a view called `active_users` is narrated as a plain table reference; the view's own definition would need its own pasting.

The boundary is also the privacy story, stated technically: because the tool works purely on text, there is nothing it needs from your infrastructure, and nothing your infrastructure needs to expose to it.

Who pastes queries here

Juniors, first — the explainer is effectively a tutor that never tires of the FROM-before-WHERE conversation. Students preparing for interviews use the execution-order walkthrough the way they use flashcards, because the eight-stage pipeline is exactly the kind of thing an interviewer asks and exactly the kind of thing people half-remember.

Then the reviewers. A pull request with a 30-line SQL change gets the same treatment as a 30-line code change: read it before approving it. Pasting the query and reading a structured narration catches the silent-bug class — an ON-clause filter on a LEFT JOIN, an alias in WHERE that some dialects accept and others reject — faster than eyeballing raw SQL, and the reviewer's comment can link the exact stage where the problem lives.

And the maintainers: the people who inherit reports, un-break them, and add the one column the business asked for. For them the payoff is confidence — a verified mental model of a query they did not write, built in a minute instead of reconstructed from git blame and folklore.

Neighboring tools cover the rest of the loop: SQL on CSV lets you actually run a query against flat data with no database, SQL CREATE TABLE Generator builds the schema the query will target, SQL Syntax Validator checks a query for dialect errors when that is the only question, and the Excel Formula Explainer applies the same explain-don't-execute philosophy to spreadsheets.

Explainer versus EXPLAIN versus a chatbot

ApproachUnderstands structureKnows your dataPrivacyWhat it is for
This explainerYes — full parse and narrationNoQuery never leaves the tabLearning what a query does
Database EXPLAINYes — planner's actual physical planYes — statistics, indexes, row estimatesNeeds a live connectionTuning how a query runs
AI chat assistantsOften yes, sometimes inventedOnly what you pasteQuery and schema details go to a serverConversational debugging, at a price
Reading the docsYou build the model yourself—TotalThe deep path; slowest, most durable
Four ways to answer "what does this query do" — different tools for different questions.

EXPLAIN and this tool are complements, not competitors — the classic pairing is explain-for-meaning first, EXPLAIN-for-cost second, because tuning a query you misread is optimizing the wrong thing. The chatbot comparison is fairer than it sounds: conversational explainers are genuinely flexible, but they send your production SQL to someone else's server and occasionally describe a join that is not in your query with total confidence. A deterministic narrator that cannot hallucinate is the right default when the query is proprietary or the reader is still learning to trust their own reading.

Reading the docs remains the undefeated long game. This page exists to make that path shorter, not to replace it — the narration gives you the vocabulary, and the manual gives you the fine print.

Queries stay off the network

Production SQL is sensitive in ways people forget: table names describe the business, literal values in WHERE clauses are sometimes customer IDs or emails, and comment blocks have a long tradition of containing things HR should never see. The explainer runs its parser and narrator entirely in the page, so the query text moves from your clipboard to your browser and stops there.

Deterministic parsing is what makes that architecture possible at zero cost — no model weights to host, no inference to bill, just a grammar walk that a 2015 laptop finishes between keystrokes. The page loads once from the CDN and then never needs the network again, which conveniently also describes its behavior on a train, a plane, or a locked-down corporate laptop.

If the query came from a system with a compliance story attached, the same property applies with emphasis: there is no retention question to answer, because there is no transmission to retain.

Questions about the SQL Query Explainer

Which SQL dialects does the explainer understand?

The parser handles standard SQL plus the big dialects: MySQL (including backticked identifiers), PostgreSQL, SQLite, T-SQL basics, and most Hive/Spark flavors. If a query is syntactically valid in one of those, it parses; if it is dialect-broken, you get a parse error with a position, which is itself diagnostic information. Non-SELECT statements — INSERT, UPDATE, DELETE, CTE-heavy scripts — are also fair game.

Does it need a database connection or my data?

No. The explainer is purely syntactic: it reads the query text, parses it into a syntax tree, and narrates the structure. It never sees your tables, your row counts, or your indexes. Everything it says is derived from the query alone, which is exactly why it can run with zero setup.

Can it tell me why my query is slow?

It can tell you what the query does, and it will point out patterns that are notorious performance smells — a function wrapped around an indexed column in WHERE, SELECT * feeding a GROUP BY, correlated subqueries inside SELECT. But actual speed depends on your data volume, statistics, and indexes, which requires EXPLAIN against the live database. Use the explainer to understand, use EXPLAIN to tune.

What is the syntax tree actually for?

It is the intermediate representation the narration is generated from — every clause becomes a node, every expression a subtree. Showing it alongside the plain-English output lets you verify the narration against structure instead of taking it on faith, and it teaches the shape of SQL in a way prose cannot. Reading a tree is a skill that transfers to compilers, linters, and every serious tool that manipulates code.

Why can I use a SELECT alias in ORDER BY but not in WHERE?

Because of logical execution order. WHERE runs before SELECT, so the alias does not exist yet when the filter is evaluated; ORDER BY runs after SELECT, so the alias is already defined. The explainer walks clauses in execution order and calls out this exact trap when it spots an alias misuse.

Does it store or transmit my queries?

No. Parsing and narration happen entirely in your browser. Production queries embed table names, business logic, and occasionally literal customer values, so this tool was deliberately built with no network hop — the text you paste stays in the tab.

Will it rewrite or optimize my query for me?

It explains; it does not edit. Suggestions appear as annotations — an implied cross join, a HAVING condition that could move to WHERE, a missing GROUP BY column — but the query text is never modified. You decide what to change, which is how it should be when the tool has no idea what correct means for your data.

Where to head next

Related stops on the same road: SQL on CSV for running queries without a database, SQL CREATE TABLE Generator for designing the tables those queries will read, and SQL Syntax Validator for a fast dialect check on the query you just understood.

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.