How SQLCopilot works
A plain-English guide to what we analyze, how the score is calculated, and what each part of the result means.
In one line
Paste a query β we parse it and run 29 checks for known performance & correctness anti-patterns β you get a list of findings, fixes, and a health score (lower is better; 0 = nothing flagged). We never connect to or run against your database.
Want table-aware feedback? Optionally paste an EXPLAIN (FORMAT JSON) plan and weβll factor in your real tables, indexes, and row estimates.
What happens when you click Analyze
- 1Parse. Your SQL text is parsed into a structured tree (AST), aware of whether you picked Postgres or MySQL.
- 2Scan. 29 deterministic rules inspect that tree for anti-patterns β no AI, no guessing, same input β same output.
- 3Score. Each finding contributes points by severity; we sum them into a 0β100 health score.
- 4Recommend. Every finding maps to a concrete fix (a rewrite or a
CREATE INDEX), and you can optionally ask AI to explain why it matters.
Itβs static analysis of the query text β like a spell-checker for SQL. It does not execute your query or read your data.
How the score is calculated
The score is a problem score from 0 to 100 β lower is better. A clean query scores 0. Itβs not a grade where higher means better.
Each finding adds points based on its severity, and we add them all up (capped at 100):
| Severity | Points each |
|---|---|
| critical | 30 |
| high | 15 |
| medium | 7 |
| low | 3 |
| info | 1 |
Worked example
SELECT * FROM users WHERE email LIKE '%@gmail.com' ORDER BY created_at;
- LEADING_WILDCARD_LIKE β high β 15
- SELECT_STAR β medium β 7
- UNINDEXED_ORDER_BY β low β 3
- MISSING_LIMIT β info β 1
- Total score = 26
Rough reading: 0 clean Β· 1β29 minor Β· 30β59 worth fixing Β· 60+ serious. A single critical issue alone is 30 points.
What we check β all 29 rules
Rule set version 2026.29.0. Each finding tells you the rule, a plain-English message, and a fix.
| Severity | Rule | What it catches |
|---|---|---|
| critical | CARTESIAN_JOIN | Tables joined with no join condition β every-row Γ every-row explosion. |
| critical | DELETE_WITHOUT_WHERE | DELETE with no WHERE β wipes the whole table. |
| critical | UPDATE_WITHOUT_WHERE | UPDATE with no WHERE β rewrites every row. |
| high | LEADING_WILDCARD_LIKE | LIKE '%x' can't use a B-tree index β full scan. |
| high | NOT_IN_NULL | NOT IN (subquery) returns wrong results if the subquery yields NULLs. |
| high | OFFSET_PAGINATION | Large OFFSET scans and discards all skipped rows. |
| high | ORDER_BY_RANDOM | ORDER BY RANDOM() sorts the entire table to pick a few rows. |
| medium | FUNCTION_IN_WHERE | A function on an indexed column in WHERE prevents index use. |
| medium | HAVING_WITHOUT_GROUP_BY | HAVING used as a WHERE β filter earlier instead. |
| medium | NULL_EQUALITY_COMPARISON | = NULL / <> NULL never matches; use IS NULL. |
| medium | SELECT_STAR | SELECT * fetches every column β more IO/network than needed. |
| medium | IMPLICIT_GROUP_BY_COLUMN | A selected column is neither aggregated nor in GROUP BY (PG errors; MySQL is arbitrary). |
| low | DISTINCT_WITH_GROUP_BY | DISTINCT on top of GROUP BY is usually redundant. |
| low | IMPLICIT_CONVERSION | Comparing mismatched types forces a cast that can skip an index. |
| low | INSERT_WITHOUT_COLUMN_LIST | INSERT without explicit columns is fragile to schema changes. |
| low | LIMIT_WITHOUT_ORDER_BY | LIMIT with no ORDER BY returns arbitrary rows. |
| low | ORDER_BY_EXPRESSION | Ordering by a computed expression usually canβt use an index. |
| low | OR_INSTEAD_OF_UNION | OR chains across columns can defeat indexes; UNION may be faster. |
| low | ORDER_BY_ORDINAL | ORDER BY a column number is fragile β a SELECT-list edit silently changes the sort. |
| low | GROUP_BY_ORDINAL | GROUP BY a column number is fragile β a SELECT-list edit silently regroups data. |
| low | LIKE_WITHOUT_WILDCARD | LIKE with no % or _ behaves like = β likely a forgotten wildcard. |
| low | SELECT_DISTINCT | DISTINCT can mask a join that produces duplicates. |
| low | SUBQUERY_IN_SELECT_LIST | A correlated subquery in SELECT runs per row. |
| low | UNINDEXED_ORDER_BY | ORDER BY without LIMIT sorts every row. |
| info | IMPLICIT_JOIN_SYNTAX | Comma joins are easy to turn into accidental cartesian joins. |
| info | INEQUALITY_OPERATOR | != / <> on a column rarely uses an index. |
| info | MISSING_LIMIT | No LIMIT β result size is unbounded. |
| info | MULTIPLE_STATEMENTS | Multiple statements in one string β review separately. |
| info | WHERE_TAUTOLOGY | A constant always-true condition (e.g. 1=1) filters nothing. |
Do I need to give you my table schema?
No. Just paste the query and pick the dialect β thatβs it. SQLCopilot analyzes the query text, so it needs no schema, no table definitions, no data, and no database connection. If you want analysis based on your real tables (sizes, indexes, row estimates), you can optionally paste an EXPLAIN plan β see below.
Going deeper with EXPLAIN (optional)
On the Analyze page, expand βAdd EXPLAIN JSONβ and paste an EXPLAIN (FORMAT JSON) plan. We read the real plan tree, flag things like sequential scans and bad row estimates, and suggest concrete indexes β based on your actual data, not just the query text.
How much detail you get depends on what you paste
- 1. Just the query β quick checks for common anti-patterns (no schema needed).
- 2. Query +
EXPLAIN (FORMAT JSON)β analysis aware of your real tables, indexes, and row estimates. - 3. Query +
EXPLAIN ANALYZEJSON β all of the above, plus real execution timings.
How to generate it
PostgreSQL β run this and copy the JSON output:
EXPLAIN (FORMAT JSON) <your query>; -- with real timings (runs the query β avoid on writes): EXPLAIN (ANALYZE, FORMAT JSON) <your query>;
MySQL (8.0+) β run this and copy the JSON output:
EXPLAIN FORMAT=JSON <your query>; -- with real execution stats (MySQL 8.0.18+): EXPLAIN ANALYZE FORMAT=JSON <your query>;
Paste the full JSON into the EXPLAIN box. Tip: plain EXPLAIN only estimates the plan; ANALYZE actually executes the query for real numbers, so donβt use it on INSERT/UPDATE/DELETE in production.
What it can and canβt tell you
- β It reliably flags known anti-patterns in your SQL and explains the fix.
- β οΈ It analyzes the query text, so it doesnβt know your real table sizes, indexes, or data distribution. A flagged βfull scanβ might be fine on a tiny table.
- β οΈ Treat findings as informed guidance, not guarantees β verify big changes with
EXPLAINon your own database. - π Your queries are encrypted at rest and PII is masked. We never store database credentials.