Skip to content

SQL Query Explainer

Read a SQL query clause by clause and see what it actually does — including the outer join your WHERE clause quietly turned into an inner one.

Query

PostgreSQL

SELECT

Reads from users, orders, then filters them, groups them, computes COUNT, sorts the result, returns one page.

Tables

  • users as uvia FROM
  • orders as ovia LEFT OUTER JOIN

Step by step

8 clauses
  1. SELECT

    Returns 3 columns: u.id, u.email, plus COUNT of o.id.

    u.id, u.email, COUNT(o.id) AS orders

  2. FROM

    Reads from users, referred to as u.

    users AS u

  3. LEFT OUTER JOIN

    Joins orders (o) on u.id = o.user_id — keeping every row from the left side, with NULLs where there is no match.

    orders AS o ON u.id = o.user_id

  4. WHERE

    Keeps only the rows where u.country = 'SE' AND o.status = 'paid'.

    u.country = 'SE' AND o.status = 'paid'

  5. GROUP BY

    Collapses the rows into one group per distinct u.id + u.email, so the aggregates are computed per group.

    u.id, u.email

  6. HAVING

    Discards whole groups unless COUNT(o.id) > 3. This runs after grouping, which is why it can test an aggregate and WHERE cannot.

    COUNT(o.id) > 3

  7. ORDER BY

    Sorts the result by orders descending.

    orders DESC

  8. LIMIT

    Returns at most 10 rows.

    10

Probably not what you meant

1
  • WHERE filters o, which came from a LEFT OUTER JOIN. That turns the outer join back into an inner one.

    Unmatched rows have NULL in every column of the joined table, and NULL fails every comparison — so the rows the outer join added are removed again. Move the condition into the ON clause to keep them.

Worth knowing

1
  • COUNT(o.id) counts rows where that column is not NULL.

    COUNT(*) counts every row. The two differ exactly by the number of NULLs, which is easy to mistake for a bug elsewhere.

Parsed in your browser — no query is uploaded and no database is contacted. This describes what the query does, not how it will be executed: for an execution plan, run EXPLAIN ANALYZE against the real database.

About the SQL Query Explainer

Paste a query and this SQL query explainer breaks it into clauses and says what each one does — which tables are read, what each join keeps, what the filter removes, what the grouping collapses. It answers the question "what does this SQL query do" for a query you inherited, reviewed, or wrote six months ago.

It is not an execution plan. EXPLAIN in Postgres or MySQL tells you how the database intends to run a query — which index, which join strategy, how many rows it expects — and that depends on a live database with its own statistics. This tool describes the query's logic instead, which is the part that is the same everywhere and the part that is usually being misread.

The explanation is generated by rules over a real SQL tokenizer, not by a language model. That matters: the same query always produces the same explanation, and nothing is ever invented. An explanation that is confidently wrong about your query would be worse than none, because you would have no way to know which one you had been given.

The findings are the reason to run a query you already understand through it. A LEFT JOIN filtered in WHERE, a comparison against NULL with =, NOT IN over a nullable subquery, an implicit cross join, LIMIT with no ORDER BY — every one of those is valid SQL that does something other than what it looks like, and each finding comes with the reason so you can disagree with it.

  • Clause-by-clause breakdown, each with the original SQL beside the explanation
  • Join types explained by what they keep, not by their name
  • The table list, showing how each one entered the query and on what condition
  • Findings for the classic traps, each with its reasoning rather than a bare warning
  • Multiple statements in one paste, explained separately
  • PostgreSQL, MySQL and SQL Server tokenizing, so quoted identifiers and parameters are read correctly
  • Deterministic output — no model, no network, no variation between runs

How to use it

  1. Paste a query. A whole script works; each statement is explained on its own.
  2. Pick the dialect if you use dialect-specific quoting — it affects how identifiers and parameters are read.
  3. Read the summary first, then the step-by-step list to see where each clause fits.
  4. Read the findings. Everything there is valid SQL, which is exactly why it is worth reading.
  5. For performance questions, take the query to your database and run EXPLAIN ANALYZE. This tool cannot answer those and does not try.

Real-world use cases

Backend & full-stack developers

Read what a query inherited from a previous engineer, an ORM-generated statement, or a six-month-old file actually does before changing it, instead of reverse-engineering it clause by clause by eye.

Code reviewers

Paste a query from a PR to see its logic broken down and its findings surfaced before approving it — catching a filtered outer join or an ungrouped aggregate that reads fine at a glance.

QA & test engineers

Check what a query under test is supposed to keep and exclude before writing assertions against it, particularly for a LEFT JOIN or a NOT IN whose edge-case behaviour is easy to get wrong.

Junior developers & SQL learners

See a plain-English account of what each clause does next to the SQL that produced it, and read the reasoning behind classic traps like NULL failing every comparison in a WHERE clause.

Data analysts

Confirm what a long, joined analytics query is actually keeping and grouping before trusting its output in a report, without asking the person who wrote it to walk through it out loud.

Examples

A join explained by what it keeps

Input
LEFT JOIN orders AS o ON u.id = o.user_id
Output
Joins orders (o) on u.id = o.user_id — keeping every row from the left side, with NULLs where there is no match.

The name of a join says nothing about its behaviour. What it keeps is the only thing worth knowing.

The outer join that is secretly an inner join

Input
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid'
Output
WHERE filters o, which came from a LEFT OUTER JOIN.
That turns the outer join back into an inner one.

Unmatched rows have NULL in every column of the joined
table, and NULL fails every comparison — so the rows the
outer join added are removed again. Move the condition
into the ON clause to keep them.

The most common reason a query returns fewer rows than expected. Nothing in the SQL looks wrong.

NOT IN over a subquery

Input
WHERE id NOT IN (SELECT a_id FROM b)
Output
NOT IN is used with a subquery.

If the subquery returns a single NULL, the whole NOT IN
evaluates to NULL for every row and the query returns
nothing. NOT EXISTS does not have this behaviour.

This one fails completely rather than partially: not a few missing rows, but an empty result from a query that looks correct.

COUNT of a column, not of rows

Input
SELECT COUNT(email) FROM users
Output
COUNT(email) counts rows where that column is not NULL.

COUNT(*) counts every row. The two differ exactly by the
number of NULLs.

A note rather than a warning — often deliberate. It is worth stating because the gap between the two numbers is easy to attribute to a bug somewhere else.

Common errors

MessageCauseFix
The query returns fewer rows than expectedMost often a LEFT JOIN whose right-hand table is filtered in WHERE, which reduces it to an inner join.Move the condition into the ON clause. This tool reports the case by name, including which alias triggered it.
The query returns nothing at allFrequently NOT IN over a subquery that contains a NULL, or a comparison written as = NULL.Use NOT EXISTS instead of NOT IN, and IS NULL instead of = NULL. Both are reported here.
column must appear in the GROUP BY clause or be used in an aggregate functionA plain column sits next to an aggregate without being grouped.Group it or aggregate it. MySQL accepts the query and returns an arbitrary row per group, so the warning appears whatever dialect you select.
The result is enormous and the query never finishesAn implicit cross join — several tables listed in FROM with no condition joining them.Add the join condition, or write the join out explicitly. Row counts multiply rather than add, so three tables of 10,000 rows is 10^12 rows.
Pagination shows the same row on two different pagesLIMIT and OFFSET with no ORDER BY, or an ORDER BY that is not unique.Sort by something unique — usually add the primary key as the last sort key. Without a total order, the database is free to return rows in any order and will change its mind as data changes.
This tool says my query is unrecognisedThe statement is a DDL or vendor-specific form the clause splitter does not model — CREATE, ALTER, MERGE, PL/pgSQL blocks.The tool covers SELECT, INSERT, UPDATE and DELETE, including CTEs and set operators. Anything else is echoed rather than explained, which is deliberate: inventing an explanation would be worse.

Frequently asked questions

Is my query sent anywhere?

No. Tokenizing and explaining both happen in your browser, so a query naming your real tables and columns never leaves the machine. There is no model call behind this, which is also why it works offline.

Is this the same as EXPLAIN?

No, and the distinction matters. EXPLAIN (or EXPLAIN ANALYZE) asks the database how it plans to execute a query: which indexes, which join algorithm, how many rows it expects, and with ANALYZE how long each step actually took. That answer depends on the database, its version, its statistics and its data — none of which exist here. This tool explains the query's meaning, which is stable across all of those. Use both: this one to check the query says what you meant, EXPLAIN to find out why it is slow.

Why not use an AI to explain the query?

Because a wrong explanation is worse than no explanation. A model produces fluent prose about a query whether or not it has understood it, and you cannot tell the difference from the output. Every sentence here is generated by a rule over the parsed token stream, so it is deterministic and it is limited to claims the parser can actually support — which is why the tool says nothing at all about statements it does not model, instead of guessing.

Does it handle CTEs, window functions and subqueries?

CTEs and subqueries are recognised and described at the top level; a subquery in FROM is labelled as one rather than being given an invented name. Window functions are shown in the select list and a WINDOW clause is described, but the tool does not currently break down a window frame. Clause detection ignores anything inside parentheses, so a nested query never confuses the outer breakdown.

The findings warned about something I did on purpose. Is it wrong?

No — every finding is legal SQL, and several have legitimate uses. A CROSS JOIN is right when you want combinations. Filtering a LEFT JOINed table with IS NULL is the standard anti-join, which is why that specific case is excluded from the warning. Each finding states its reasoning precisely so you can decide it does not apply; the point is to make the behaviour explicit, not to grade your query.

Which dialects does it understand?

PostgreSQL, MySQL and SQL Server, sharing the tokenizer used by the SQL Formatter and SQL Minifier. The dialect affects identifier quoting and parameter syntax — double quotes, backticks or brackets — so selecting the right one keeps a quoted identifier from being misread. The clause logic is standard SQL and is the same for all three.

Can it tell me whether my query is slow?

Only indirectly. It flags shapes that are usually expensive — an implicit cross join, UNION where UNION ALL would do, a filter that defeats an outer join — but it has no access to your indexes, row counts or hardware, so it cannot tell you what a query will cost. Anything claiming otherwise from the query text alone is guessing.

How is this different from the SQL Query Builder?

They run in opposite directions. The builder starts from a table, columns and conditions typed into a form and produces SQL; this tool starts from SQL you already have — pasted, inherited or generated elsewhere — and produces a plain-English breakdown of it. If you are starting from scratch, use the builder; if you are trying to understand a statement that already exists, paste it here.

Last updated