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

  1. 1Parse. Your SQL text is parsed into a structured tree (AST), aware of whether you picked Postgres or MySQL.
  2. 2Scan. 29 deterministic rules inspect that tree for anti-patterns β€” no AI, no guessing, same input β†’ same output.
  3. 3Score. Each finding contributes points by severity; we sum them into a 0–100 health score.
  4. 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):

SeverityPoints each
critical30
high15
medium7
low3
info1

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.

SeverityRuleWhat it catches
criticalCARTESIAN_JOINTables joined with no join condition β†’ every-row Γ— every-row explosion.
criticalDELETE_WITHOUT_WHEREDELETE with no WHERE β€” wipes the whole table.
criticalUPDATE_WITHOUT_WHEREUPDATE with no WHERE β€” rewrites every row.
highLEADING_WILDCARD_LIKELIKE '%x' can't use a B-tree index β†’ full scan.
highNOT_IN_NULLNOT IN (subquery) returns wrong results if the subquery yields NULLs.
highOFFSET_PAGINATIONLarge OFFSET scans and discards all skipped rows.
highORDER_BY_RANDOMORDER BY RANDOM() sorts the entire table to pick a few rows.
mediumFUNCTION_IN_WHEREA function on an indexed column in WHERE prevents index use.
mediumHAVING_WITHOUT_GROUP_BYHAVING used as a WHERE β€” filter earlier instead.
mediumNULL_EQUALITY_COMPARISON= NULL / <> NULL never matches; use IS NULL.
mediumSELECT_STARSELECT * fetches every column β€” more IO/network than needed.
mediumIMPLICIT_GROUP_BY_COLUMNA selected column is neither aggregated nor in GROUP BY (PG errors; MySQL is arbitrary).
lowDISTINCT_WITH_GROUP_BYDISTINCT on top of GROUP BY is usually redundant.
lowIMPLICIT_CONVERSIONComparing mismatched types forces a cast that can skip an index.
lowINSERT_WITHOUT_COLUMN_LISTINSERT without explicit columns is fragile to schema changes.
lowLIMIT_WITHOUT_ORDER_BYLIMIT with no ORDER BY returns arbitrary rows.
lowORDER_BY_EXPRESSIONOrdering by a computed expression usually can’t use an index.
lowOR_INSTEAD_OF_UNIONOR chains across columns can defeat indexes; UNION may be faster.
lowORDER_BY_ORDINALORDER BY a column number is fragile β€” a SELECT-list edit silently changes the sort.
lowGROUP_BY_ORDINALGROUP BY a column number is fragile β€” a SELECT-list edit silently regroups data.
lowLIKE_WITHOUT_WILDCARDLIKE with no % or _ behaves like = β€” likely a forgotten wildcard.
lowSELECT_DISTINCTDISTINCT can mask a join that produces duplicates.
lowSUBQUERY_IN_SELECT_LISTA correlated subquery in SELECT runs per row.
lowUNINDEXED_ORDER_BYORDER BY without LIMIT sorts every row.
infoIMPLICIT_JOIN_SYNTAXComma joins are easy to turn into accidental cartesian joins.
infoINEQUALITY_OPERATOR!= / <> on a column rarely uses an index.
infoMISSING_LIMITNo LIMIT β€” result size is unbounded.
infoMULTIPLE_STATEMENTSMultiple statements in one string β€” review separately.
infoWHERE_TAUTOLOGYA 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. 1. Just the query β†’ quick checks for common anti-patterns (no schema needed).
  2. 2. Query + EXPLAIN (FORMAT JSON) β†’ analysis aware of your real tables, indexes, and row estimates.
  3. 3. Query + EXPLAIN ANALYZE JSON β†’ 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 EXPLAIN on your own database.
  • πŸ”’ Your queries are encrypted at rest and PII is masked. We never store database credentials.