SQL & data validation
Chapter 3 · Phase 2 · practice in Week 4 and Week 5
1. SQL as source of truth
LLMs are fluent writers but unreliable accountants. When a model states "revenue grew 23% in Q3," that number came from training data pattern-matching — not from running a query on your database. It may be a hallucination, outdated, or from a different company entirely.
The correct mental model: LLM writes the query → you run it → database returns the number. The LLM is a translator, not an oracle.
"What's our MAU this month?"
generates SQL
returns real number
2. Text-to-SQL — how it works
Text-to-SQL converts a natural language question into a SQL query. The model needs two things in the prompt: the question and enough schema context to write a valid query.
Minimal text-to-SQL prompt
You are a SQL expert. Convert the question to a valid SQLite query.
Return ONLY the SQL. No explanation.
Schema:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
created_at TIMESTAMP,
plan TEXT, -- 'free' | 'pro' | 'enterprise'
last_active TIMESTAMP
);
CREATE TABLE events (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
event_type TEXT,
created_at TIMESTAMP
);
Question: How many Pro plan users were active in the last 30 days?
Expected output
SELECT COUNT(DISTINCT u.id)
FROM users u
JOIN events e ON u.id = e.user_id
WHERE u.plan = 'pro'
AND e.created_at >= datetime('now', '-30 days');
What the model needs to produce good SQL
- Full schema — table names, column names, data types, foreign keys
- Example data (optional but helpful) — sample rows clarify ambiguous column semantics
- Glossary — business term definitions ("active user = logged in within 30 days")
- Dialect — SQLite, PostgreSQL, BigQuery, Snowflake all have different functions
3. Where text-to-SQL breaks
| Failure type | Example | Detection |
|---|---|---|
| Wrong JOIN | LEFT JOIN where INNER is needed — inflates count | Cross-check total against known baseline |
| Date arithmetic error | Counts today's events in "last 7 days" query inclusively vs exclusively | Run with known fixed date range; verify manually |
| Ambiguous column | Two tables have a created_at — model picks the wrong one | Alias columns explicitly in schema description |
| Missing NULL handling | AVG ignores NULLs silently — result looks valid but is wrong | Add COUNT(*) - COUNT(column) NULL check |
| Case sensitivity | Filters on 'Pro' when data stores 'pro' | Always include LOWER() or canonicalize inputs |
| Wrong aggregation level | Aggregates at user level when you want at account level | Add GROUP BY assertion in the prompt |
4. Threat model — SQL injection via prompt
When users can influence the question that becomes a query, there is a prompt-injection risk where a malicious user crafts a natural language question that tricks the LLM into generating a destructive or data-exfiltrating query.
An unprotected system might generate:
SELECT * FROM users; DROP TABLE users;
Mitigations
Read-only database user
The database user the app connects with should have SELECT only. No DROP, DELETE, INSERT, or UPDATE. This is the single most important mitigation.
SQL validator before execution
Parse the generated SQL with a library before running it. Reject any query that contains DML statements (DROP, DELETE, INSERT, UPDATE, TRUNCATE).
Schema allowlist
Only expose tables the user is authorized to query. Remove sensitive tables from the schema context entirely — the model cannot query what it does not know exists.
Row-level security
Even for SELECT, apply WHERE filters that scope results to the authenticated user's org. Pass user ID and org ID as parameters, never as values the LLM can influence.
5. Validation patterns for LLM-generated SQL
Pattern A — Sanity check with known values
-- After running generated query:
-- 1. Check result is in expected range
SELECT COUNT(*) FROM users; -- should be ~50,000 for our DB
-- LLM-generated MAU query returning 2M users is clearly wrong
-- 2. Check denominator separately
SELECT COUNT(DISTINCT user_id) FROM events
WHERE created_at >= '2024-01-01';
-- Compare to LLM's numerator to validate fraction
Pattern B — Dry-run with EXPLAIN
-- Before running an unfamiliar generated query:
EXPLAIN QUERY PLAN
SELECT ... -- paste LLM-generated query here
-- Review the query plan for:
-- - Unexpected full table scans on large tables
-- - Missing index usage
-- - Surprising join order
Pattern C — Result count heuristic
-- Wrap any generated query in a count first
SELECT COUNT(*) FROM (
-- LLM-generated query here
) t;
-- If count is 0 or unexpectedly large, investigate before returning to user
2. Write a schema summary with business glossary.
3. Create 20 test questions with known correct answers.
4. Measure query accuracy (does the SQL run AND produce the right number?).
5. Ship only after achieving ≥ 90% accuracy on test set.
6. Log every generated query for audit and debugging.
Test your understanding
Further reading & watching
- BlogChip Huyen — Task Composability & Talk-to-your-data (Parts 2 & 3)Part 2 explains how to chain LLM tasks (natural language to SQL to response). Part 3 covers talk-to-your-data product patterns.
- BlogDefog.ai — How good is GPT-4 at SQL?Benchmark comparison of LLMs on text-to-SQL across difficulty levels. Good calibration data for PMs.
- PaperDIN-SQL — Decomposed Prompting for Complex SQLResearch paper on decomposing SQL generation into sub-problems.
- TutorialMode Analytics — SQL TutorialSolid SQL fundamentals and analytical query patterns used in PM workflows.
- BlogRetool — Building AI-powered SQL toolsReal-world text-to-SQL product patterns, risks, and implementation choices.