SQL query explainer
Reads a query the way a reviewer should: what rows come out, which rows silently disappear, and where the time goes.
When to use
- On any query in a PR, a dashboard, or a ticket ("why is this slow", "why is this number wrong").
- With
EXPLAIN (ANALYZE)output when available — the explanation gets sharper.
Input
The query, the dialect (PostgreSQL, MySQL, SQLite, BigQuery…), table definitions if available, and optionally the plan.
Explanation, in this order
- One sentence: what the result set is, in business terms ("one row per customer with their last paid invoice").
- Grain. What one output row represents. If a join can multiply rows (one-to-many without aggregation), say so — this is the most common wrong-number bug.
- Filters that drop rows silently:
INNER JOINon optional relations,WHERE col = xon a nullable column (NULLs vanish),NOT INwith NULLs (returns nothing),DISTINCThiding a join fan-out. - NULL semantics wherever they change the answer.
- Where the time goes. From the plan if given; otherwise from the shape: sequential scans on filtered columns, functions on indexed columns (
WHERE date(created_at) = …),ORDER BYwithout a supporting index,ORacross columns. - The index it wishes existed — as a concrete statement with the predicate it serves. Say when an index would not help (low selectivity, tiny table).
Rules
- Never rewrite the query unless asked; explain it. If a rewrite is the fix, show the smallest one and state that the result set is identical (or how it differs).
- Dialect matters: say when a claim is dialect-specific.
- If tables are unknown, say which assumption the explanation rests on.
Output format
**Result:** one row per customer who has at least one invoice, with the total of *paid* invoices.
**Grain:** customer. Safe — `SUM` aggregates the invoices join.
**Silent drops:** `INNER JOIN invoices` removes customers with no invoices; `WHERE status = 'paid'` moves into the join filter — customers with only unpaid invoices also vanish. If you want them at 0, use `LEFT JOIN … AND status = 'paid'`.
**Time:** `WHERE date(created_at) = current_date` prevents the index on `created_at`. Use a range: `created_at >= current_date AND created_at < current_date + 1`.
**Index:** `CREATE INDEX invoices_customer_status ON invoices (customer_id, status) WHERE status = 'paid';` — serves the join + filter. (PostgreSQL partial index.)
License
MIT