Nothing here connects to a database
The analyser reads the text sitting in the editor. It opens no connection, sends the statement nowhere, and has never seen your schema. Every line of output comes from pattern matching over the SQL you typed, which is why a query against a table that does not exist still returns a clean result, and a misspelled column name goes unmentioned.
That trade buys three things a live connection would not. The query stays inside your browser, so a statement pulled from a client database is safe to paste. A DELETE with no WHERE gets flagged instead of run. And you get an answer on a machine with no database installed at all.
| Control | What it does to your query |
|---|---|
| Analyze query | Runs every check the current settings allow. With Analyze as I type ticked it also fires on its own a moment after you stop typing, so the button is there for when you switch that off. |
| Dialect | Selects which engine-specific rules apply. The tag above the editor mirrors the choice so a stale setting stays visible. |
| Expected statement | Set it to SELECT, UPDATE, or any other verb and the analyser reports a mismatch. Handy against a template that assembles the statement in pieces, where an UPDATE arriving instead of a SELECT means a bug upstream. Leave it on Any to skip the check. |
| Checks | Three levels. Syntax only runs the shared structural rules, syntax and dialect adds the engine rules, and the third level adds the performance pass. |
| Explain | Swaps the panel for a plain reading of the statement: its verb, its effect on stored data, the tables and joins found. No execution plan is involved, since that comes from the database engine. |
| Format | Rebuilds the statement with one clause per line. Read the caution further down before pasting the result back into code. |
| Copy and Clear | Copy puts the editor contents on your clipboard. Clear empties the editor and resets the four counters. |
The four sample queries below the panels load in one click. The last two are worth a look before you paste anything of your own, since they show what a flagged query and a broken query each read like.
What the syntax check catches, and what slips past
Four checks run on every statement, whatever the dialect:
- Bracket balance. Parentheses are counted left to right. A closing bracket with no opener is reported with the character position it sits at, and any openers left over at the end are counted separately. This is the check that earns its place, because a subquery missing its closing bracket is close to invisible in a forty line statement.
- Unclosed string literals. An odd number of single quotes means one of them is holding the rest of the query hostage. Doubled quotes inside a literal, the standard way of escaping one, stay balanced and pass.
- Row clauses with no
FROM. ASELECTcarrying aWHERE,JOIN,GROUP BY, orHAVINGand noFROMgets flagged. A bareSELECT NOW()is left alone, since selecting a constant needs no table. - Statement mismatch. Only when Expected statement is set to something other than Any. It compares the opening keyword against your choice, treating a
WITHclause as aSELECT.
Plenty gets through. A missing comma between two column names, a keyword spelled wrong, a join condition pointing at the wrong key, an aggregate with no matching GROUP BY entry: none of those trip a rule here. Treat a clean result as evidence the shape of the statement holds together, not as proof the database will accept it.
Dialect rules are narrow, and deliberately so
Picking a dialect adds a small set of extra checks on top of the shared ones, from the second Checks level upward. The list is short because each rule has to hold without a schema in front of it.
| Dialect | Extra rule applied |
|---|---|
| MySQL | A LIMIT and OFFSET pair that does not read as LIMIT 10 OFFSET 20, which catches the split forms borrowed from other engines. |
| PostgreSQL | Backtick identifiers, a MySQL habit that is a syntax error in Postgres. Also IFNULL, which Postgres spells COALESCE. |
| SQLite | AUTO_INCREMENT, since SQLite writes AUTOINCREMENT with no underscore. Also RIGHT JOIN, which needs SQLite 3.39 or newer. |
| SQL Server | LIMIT, which does not exist there. The equivalents are TOP n and OFFSET n ROWS FETCH NEXT m ROWS ONLY. |
| Oracle | LIMIT again, replaced by FETCH FIRST n ROWS ONLY from 12c onward. Lower case inside a double quoted identifier is flagged too, since Oracle folds unquoted names to upper case and quoted ones stay exact. |
| Standard SQL | Skips the dialect pass entirely. Pick this one when the query has to stay portable across engines. |
One quirk worth knowing: these scans read the whole statement as flat text. A backtick living inside a quoted string still trips the PostgreSQL note, and AUTO_INCREMENT written inside a comment still trips the SQLite one. False positives are the price of a checker with no parse tree.
How the complexity score is built
The Complexity counter is a weighted count of constructs found in the text, and the panel prints the raw score next to the label. Knowing the weights makes both readable rather than mysterious.
| Construct | Points each | Reasoning |
|---|---|---|
| Subquery, window function | 3 | Each one adds a result set the engine builds before the outer query proceeds. |
| Join, aggregate, CTE | 2 | Joins multiply the row space. Aggregates and CTEs add a grouping or materialisation step. |
AND or OR in a condition | 1 | A rough stand-in for how much filtering logic the planner has to reason about. |
The total maps to four labels: Low up to 5, Medium up to 15, High up to 30, and Very High above that. The query loaded on arrival scores 11 and lands in Medium, from two joins, three aggregate calls, and one condition. Load the CTE sample to watch a window function and a WITH clause push the same statement higher.
The number describes the statement, never the data underneath it. A three-way join across tables of four hundred rows scores the same as one across four hundred million. One unindexed join beats ten indexed ones for real cost every time. Read the score as a measure of how hard the query is for a person to review, and take timing from the database itself.
The performance warnings and the reasoning behind each
Set Checks to Add performance pass to switch this on. The two lower levels stay with syntax and dialect, which is why a query holding an obvious SELECT * reports clean on the default setting. Results split into two groups: warnings for the two patterns that carry real risk, and a worth timing group for the rest.
SELECT *pulls every column across the wire, including the text blob nobody on the page renders. It also defeats covering indexes, and it breaks silently the day somebody adds a column.UPDATEorDELETEwith noWHERErewrites the whole table. Each statement is checked on its own, and the check looks for the keyword anywhere inside it, so aWHEREbelonging to a subquery quiets the warning while the outer statement stays unfiltered. Read this one yourself before trusting it.- A function wrapped around a filtered column, as in
WHERE LOWER(email) = 'a@b.com', hides the raw value from a plain index and forces a scan. Store the lowercased value in its own column, or build a functional index on the expression. ORbetween conditions often pushes the planner into a wider read than two separate lookups would need. Rewriting as aUNION ALLof two indexed halves is worth timing when the table is large.LIKE '%term'starts with a wildcard, so a B-tree index has no prefix to seek on and the engine reads every row. Full text search or a trigram index is the fix, not a rewrite of theLIKE.INwith a subquery is worth comparing against the same logic written asEXISTSor as a join. Which one wins depends on the engine, its version, and how many rows the subquery returns.ORDER BYwith no row limit sorts the whole result set before the first row comes back. On a paged screen that sort is repeated on every page view.
Every one of these is a heuristic. SELECT * in a one-row lookup by primary key costs nothing worth measuring, and a leading wildcard over a two hundred row lookup table is fine. The warnings mark places to think, not defects to fix on sight.
Format moves line breaks, it does not parse. It collapses the statement to a single line, then breaks before each clause keyword and each AND or OR. Column lists stay intact, which the older version of this page did not manage. What it still gets wrong is text inside quotes: a string literal holding the word AND, or a multi-line literal you wanted left alone, comes back split and upper cased. Use it on a wall of text you are reading. For a query going back into a codebase, keep the original or run it through the dedicated SQL formatter.
The query is valid and still slow. What next
This page reaches its limit once the syntax holds and the warnings are addressed. Real cost lives in the execution plan, which only the engine holding your data produces. Run EXPLAIN in MySQL or EXPLAIN ANALYZE in PostgreSQL against the query and read three things: whether each table is seeked or scanned, the gap between estimated and actual row counts, and which step holds the largest share of total time. A planner estimating 40 rows where 900,000 arrive points at stale statistics, and no amount of query rewriting fixes that.
For tracking timings across a batch of statements rather than checking one, the database query profiler is built for that shape of work. Building the statement from scratch instead of debugging one belongs in the SQL query builder.
