Large language models can draft SQL in seconds. That’s useful, and dangerous, if you treat the first answer like a senior engineer signed off on it. Here’s a plain checklist before anything hits a dashboard or a stakeholder email. A few small examples show what “looks fine” AI SQL often hides.
1. Confirm the grain
What is one row supposed to mean? One order? One user per day? If the model guessed wrong, every total will be wrong with confidence.
AI often returns order-level rows when you needed one row per customer per day. Compare these two patterns:
-- Risky: looks like "daily revenue" but is still order grain
SELECT
order_date,
customer_id,
SUM(amount) AS revenue
FROM orders
GROUP BY order_date, customer_id;-- Clear daily grain: one row per day
SELECT
order_date,
SUM(amount) AS revenue,
COUNT(DISTINCT customer_id) AS customers
FROM orders
GROUP BY order_date
ORDER BY order_date;Before you ship, say the grain out loud: “This query returns one row per ___.” If you can’t finish that sentence, rewrite.
2. Check joins and filters
Read every JOIN and WHERE like you wrote them carefully. Wrong join type, missing tenant filter, or open-ended dates are classic failure modes, and models love sounding sure while inventing a table name.
A confident INNER JOIN can drop rows you still need. Prefer LEFT JOIN when the right-side table is optional, and always pin the tenant and date window:
-- Common AI slip: INNER JOIN drops customers with no orders
SELECT c.customer_id, c.name, SUM(o.amount) AS lifetime_value
FROM customers c
INNER JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_date >= '2024-01-01'
GROUP BY c.customer_id, c.name;-- Safer: keep customers, filter dates on the join, scope tenant
SELECT
c.customer_id,
c.name,
COALESCE(SUM(o.amount), 0) AS lifetime_value
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.order_date >= DATE '2024-01-01'
AND o.order_date < DATE '2025-01-01'
WHERE c.tenant_id = :tenant_id
GROUP BY c.customer_id, c.name;Also scan for invented columns (o.revenue when the column is amount) and missing predicates on multi-tenant tables.
3. Run it small, then compare
Limit to a day or a sample, compare counts to a known report, and only then scale up. AI is a drafting partner. You still own the number.
Start with a tight window and a row limit while you validate:
-- Draft check: one day + hard limit
SELECT
order_id,
customer_id,
amount,
order_date
FROM orders
WHERE order_date = DATE '2024-06-15'
AND tenant_id = :tenant_id
ORDER BY order_id
LIMIT 100;Then reconcile totals against a trusted source (warehouse metric, last week’s export, or a known dashboard):
-- Spot-check aggregate for the same day
SELECT
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS customers,
ROUND(SUM(amount), 2) AS revenue
FROM orders
WHERE order_date = DATE '2024-06-15'
AND tenant_id = :tenant_id;If order_count or revenue disagrees with a report you trust, stop and fix the grain/joins before removing the limit.
Quick pre-ship checklist
Before the query leaves your notebook:
- Grain stated in one sentence
- Every JOIN type justified; no accidental fan-out
- Tenant / partition / date filters present
- Small-window totals match a known number
- Column and table names verified in the schema (not just the model’s memory)
More vocabulary on the Key Terms page. Start with hallucination, prompt, and eval.
