developer · Browser tool
SQL Query Explainer
Paste a SELECT statement to see what one row of the result represents, then a read of every join, filter, group and window in the order the engine applies them.
The query
The reading
What this does and what it cannot do
The statement is read into tokens first — quoted strings, quoted identifiers, comments, numbers, words and operators — and only then split into clauses. That ordering matters: a word like order inside 'order from the old queue' is a string, not a clause keyword, and a reader that works by search and replace gets it wrong. The dialect selector changes how the tokeniser treats backticks, square brackets, backslashes and dollar quoting.
Nothing is executed. The page has no connection to a database, so it cannot see your schema, your indexes, your statistics or how many rows any table holds. Everything under Things to check is a pattern spotted in the text. A pattern is a reason to look, not a verdict: SELECT * on a four-column lookup table costs nothing, and a filter that looks selective may match every row. The plan your server would actually choose comes from EXPLAIN, or EXPLAIN ANALYZE if you want measured timings rather than estimates.
SQL is written SELECT-first and evaluated FROM-first
The clause order you type is not the order the engine reasons about. Logically it starts with FROM and the joins, then WHERE, then GROUP BY, then the aggregates, then HAVING, then the window functions, then SELECT itself, then DISTINCT, ORDER BY and finally LIMIT. That sequence explains several things that otherwise look arbitrary. A column alias defined in the SELECT list cannot be used in WHERE, because WHERE has already run. A window function cannot be filtered in the same WHERE clause, because the window has not been computed yet. HAVING can test an aggregate and WHERE cannot, for the same reason. The step-by-step output follows this order deliberately.
Row grain, and the joins that quietly change it
Before the SELECT list means anything, settle what one row of the driving table represents: an order, a customer, a line item, a daily snapshot. Then walk each join and ask whether it can match more than one row. A join to a child table with several matches fans the result out, and a SUM further down then adds the same parent value once per child row. The total is wrong by a factor nobody notices until someone reconciles it by hand. A DISTINCT bolted on afterwards hides the duplicate rows without fixing the arithmetic.
A filter in WHERE is not the same as a filter in ON
With an inner join the two are interchangeable. With a LEFT JOIN they are not, and this is the most common silent bug in inherited SQL. A condition in ON is applied while matching; rows on the left that find no match still come through with nulls attached. The same condition in WHERE runs after the join, and because a null fails almost every comparison, it removes exactly the rows the outer join was there to keep. The outer join has become an inner join written the long way. The one deliberate exception is WHERE right_table.id IS NULL, which turns the outer join into a search for rows with no match at all — an anti-join. This page flags both cases so you can tell which one you meant.
What the performance notes look for
A function wrapped around a column inside a filter, because an index stores the column value and not the result of a function. A LIKE pattern beginning with a wildcard, because a B-tree is ordered from the start of the string. Tables joined by comma, or a JOIN with no ON clause, both of which produce a cross join. An OR that spans different columns, which one index rarely covers. A subquery in the SELECT list that refers to the outer query and may therefore run once per output row. NOT IN against a subquery that can return null, which returns nothing at all rather than what you expected. These are textual patterns with no knowledge of your data, and the note says what to check rather than what to change.