Catches the missing index and the full table scan before production does.
git clone --depth 1 https://github.com/botaoishere/sql-sanity .claude/skills/sql-sanityRestart Claude Code, then say "review this query" and paste the SQL.
Slow queries rarely announce themselves. A WHERE DATE(created_at) = ... looks perfectly reasonable, runs in 4ms on your 200 row dev database, and turns into a sequential scan over 8 million rows in production. This skill applies the same ten point checklist every time, so the scan gets caught in review instead of in a pager alert.
Paste a query and ask for a review.
> review this against orders (12M rows) and customers (400k)
>
> SELECT * FROM orders o
> JOIN customers c ON c.id = o.customer_id
> WHERE DATE(o.created_at) = '2026-07-01'
> AND c.tenant_id = '9f3a'
> ORDER BY o.created_at DESC
> LIMIT 20 OFFSET 40000;
Output:
| Severity | Line | Issue | Fix |
| -------- | ---- | -------------------------------------------- | ------------------------------------- |
| High | 4 | DATE(o.created_at) blocks any index on the | half open range on created_at |
| | | column, forcing a full scan of orders | |
| High | 7 | OFFSET 40000 reads and throws away 40k rows | keyset pagination on (created_at, id) |
| Medium | 1 | SELECT * pulls 31 columns, 4 are used | name the columns |
| Low | 6 | ORDER BY created_at is not unique | add id as a tiebreaker |
Rewritten:
SELECT o.id, o.total_cents, o.created_at, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-07-01' AND o.created_at < '2026-07-02'
AND c.tenant_id = '9f3a'
AND (o.created_at, o.id) < ($last_created_at, $last_id)
ORDER BY o.created_at DESC, o.id DESC
LIMIT 20;
Indexes:
CREATE INDEX CONCURRENTLY idx_orders_created_id
ON orders (created_at DESC, id DESC);
CREATE INDEX CONCURRENTLY idx_customers_tenant
ON customers (tenant_id);
Confirm with: EXPLAIN (ANALYZE, BUFFERS) <query>;
Before and after on the plan for that query:
before: Seq Scan on orders (cost=0.00..412883.00 rows=61 width=214)
(actual time=0.281..3894.117 rows=1204 loops=1)
Filter: (date(created_at) = '2026-07-01'::date)
Rows Removed by Filter: 11998796
after: Index Scan using idx_orders_created_id on orders
(cost=0.43..38.19 rows=1180 width=36)
(actual time=0.019..0.612 rows=1204 loops=1)
Index Cond: ((created_at >= ...) AND (created_at < ...))
SELECT *, missing or unselective WHERE, functions wrapped around indexed columns, implicit type casts, OR chains that want to be a UNION, N+1 patterns in the surrounding ORM code, LIMIT without a deterministic ORDER BY, unbounded IN lists, composite index column order, and deep OFFSET pagination.
Engine aware for Postgres, MySQL and SQLite, including the right EXPLAIN invocation for each.
MIT. See LICENSE.
