Why Bother
Where SQL shows up in AI work
Feeding your models
Training sets, RAG corpora, and fine-tuning data all start as SQL queries against someone's database.
Analysing evals & logs
"Which model scored best last week?" is a GROUP BY, not a dashboard you wait three days for.
Vector databases speak SQL
pgvector turns Postgres into a vector store — your RAG retrieval can literally be a SELECT.
Interviews
Almost every AI/data role screens SQL. It's the easiest interview section to score full marks on.
The Mental Model
A query is a question about tables
A database is a set of tables (like spreadsheets): rows are records, columns are fields.
All examples below use two tables you might have in a real AI project:
evals(id, model, question, score, latency_ms, created_at) and
models(name, provider, cost_per_1k).
The Crash Course
Eight topics = 90% of daily SQL
1 · Reading rows — SELECT, WHERE, ORDER BY, LIMIT
Pick columns, filter rows, sort, and cap the result.
-- The 10 worst-scoring answers from claude models this week SELECT question, score, latency_ms FROM evals WHERE model LIKE 'claude%' AND created_at >= '2026-07-28' ORDER BY score ASC LIMIT 10;
Operators: = != < > BETWEEN IN (…) LIKE (with % wildcard) IS NULL. Combine with AND / OR / NOT.
2 · Summarising — COUNT, AVG, GROUP BY, HAVING
Collapse many rows into stats per group. This is the query you'll write most.
-- Average score and volume per model, best first — models with 50+ runs only
SELECT model,
COUNT(*) AS runs,
ROUND(AVG(score), 2) AS avg_score
FROM evals
GROUP BY model
HAVING COUNT(*) >= 50
ORDER BY avg_score DESC;
WHERE filters rows before grouping; HAVING filters groups after. Aggregates: COUNT SUM AVG MIN MAX.
3 · Combining tables — JOIN
Match rows across tables on a shared key.
-- Add provider and cost info to every eval row SELECT e.model, m.provider, e.score, m.cost_per_1k FROM evals e JOIN models m ON e.model = m.name; -- LEFT JOIN: keep ALL evals, even if the model isn't in the models table SELECT e.model, m.provider FROM evals e LEFT JOIN models m ON e.model = m.name WHERE m.name IS NULL; -- finds evals with no matching model — a classic data check
JOIN (inner) keeps only matches. LEFT JOIN keeps every left row, filling the right side with NULLs. Those two cover ~95% of real joins.
4 · Readable pipelines — CTEs (WITH)
Name intermediate results instead of nesting subqueries five levels deep.
-- Which models beat the overall average? WITH per_model AS ( SELECT model, AVG(score) AS avg_score FROM evals GROUP BY model ), overall AS ( SELECT AVG(score) AS avg_all FROM evals ) SELECT p.model, p.avg_score FROM per_model p, overall o WHERE p.avg_score > o.avg_all;
CTEs run top to bottom and read like steps in a pipeline — reviewers (and future you) will thank you.
5 · Ranking & trends — window functions
Compute per-row stats without collapsing rows — rank, running totals, previous value.
-- Best answer per model (rank within each model's rows)
SELECT model, question, score,
ROW_NUMBER() OVER (PARTITION BY model ORDER BY score DESC) AS rank
FROM evals;
-- wrap it in a CTE and keep WHERE rank = 1 → "top answer per model"
-- Did latency improve vs the previous run?
SELECT created_at, latency_ms,
latency_ms - LAG(latency_ms) OVER (ORDER BY created_at) AS delta
FROM evals WHERE model = 'claude-sonnet-5';
Pattern: fn() OVER (PARTITION BY … ORDER BY …). Learn ROW_NUMBER, RANK, LAG/LEAD, and windowed SUM/AVG — interviewers love these.
6 · Writing data — INSERT, UPDATE, DELETE, CREATE
The other half of CRUD. Handle with care.
CREATE TABLE evals (
id SERIAL PRIMARY KEY,
model TEXT NOT NULL,
question TEXT,
score REAL,
latency_ms INTEGER,
created_at TIMESTAMP DEFAULT now()
);
INSERT INTO evals (model, question, score, latency_ms)
VALUES ('claude-sonnet-5', 'What is RAG?', 0.92, 840);
UPDATE evals SET score = 0.95 WHERE id = 42;
DELETE FROM evals WHERE created_at < '2025-01-01';
Never run UPDATE or DELETE without a WHERE clause. Habit: write the same condition as a SELECT first, check what comes back, then swap the verb.
7 · Making it fast — indexes & EXPLAIN
Enough performance knowledge to not be dangerous.
-- Speed up the columns you filter/join on constantly CREATE INDEX idx_evals_model_time ON evals (model, created_at); -- Ask the database how it will run your query EXPLAIN ANALYZE SELECT * FROM evals WHERE model = 'claude-sonnet-5';
Rules of thumb: index your WHERE/JOIN columns, avoid SELECT * in production code, and if a query is slow, read EXPLAIN before guessing.
8 · SQL × AI — vector search with pgvector
This is where SQL meets your roadmap's Phase 3: RAG retrieval as a query.
-- Postgres + the pgvector extension = a vector database CREATE EXTENSION vector; CREATE TABLE chunks ( id SERIAL PRIMARY KEY, doc TEXT, content TEXT, embedding vector(768) -- one embedding per chunk ); -- Top-5 most similar chunks to a query embedding (cosine distance: <=>) SELECT content, 1 - (embedding <=> $query_embedding) AS similarity FROM chunks ORDER BY embedding <=> $query_embedding LIMIT 5;
JSON is the other AI-adjacent power tool: Postgres JSONB columns store raw LLM outputs and let you query into them with ->>.
Quick Reference
The cheat sheet
| Clause / keyword | What it does | Example |
|---|---|---|
| SELECT … FROM | Choose columns from a table | SELECT model, score FROM evals |
| WHERE | Filter rows (before grouping) | WHERE score > 0.8 AND model LIKE 'claude%' |
| ORDER BY … LIMIT | Sort and cap results | ORDER BY score DESC LIMIT 10 |
| GROUP BY | One row per group + aggregates | GROUP BY model |
| HAVING | Filter groups (after grouping) | HAVING COUNT(*) >= 50 |
| JOIN … ON | Match rows across tables | JOIN models m ON e.model = m.name |
| LEFT JOIN | Keep all left rows, NULL-fill right | LEFT JOIN models m ON … |
| WITH (CTE) | Name a sub-result, build pipelines | WITH top AS (SELECT …) SELECT * FROM top |
| OVER (PARTITION BY) | Window: per-row stats without collapsing | ROW_NUMBER() OVER (PARTITION BY model ORDER BY score DESC) |
| CASE WHEN | If/else inside a query | CASE WHEN score > 0.9 THEN 'pass' ELSE 'fail' END |
| DISTINCT | Drop duplicate rows | SELECT DISTINCT model FROM evals |
| INSERT / UPDATE / DELETE | Write data (always with WHERE!) | UPDATE evals SET score = 0.95 WHERE id = 42 |
| CREATE INDEX | Speed up filtered/joined columns | CREATE INDEX ON evals (model) |
| EXPLAIN | Show the query plan | EXPLAIN ANALYZE SELECT … |
Reference Guide
Practice resources — verified & free
Same rules as the Resources page: every link checked live, each with a job to do.
SQLBolt
The best first contact with SQL: bite-size interactive lessons that run entirely in the browser.
Best for: your first 2–3 hours of SQL
SQLZoo
Step-by-step tutorials with live exercises on real datasets — a natural follow-up to SQLBolt.
Best for: drilling SELECT/JOIN basics
PostgreSQL Exercises
One realistic dataset, ~80 graded problems from basic to window functions — with explanations.
Best for: going from "knows syntax" to "fluent"
DataLemur
Real SQL interview questions from FAANG-style screens, with a free tier and instant feedback.
Best for: the week before interviews
Kaggle — Intro to SQL
Query real public datasets (BigQuery) in hosted notebooks; pairs with their Advanced SQL follow-up.
Best for: SQL on big, real data
SQL Tutorial (formerly Mode)
The much-loved Mode analytics tutorial, now hosted by ThoughtSpot — strong on aggregation and window functions.
Best for: analytics-style SQL (evals, metrics)
PostgreSQL Tutorial
The former postgresqltutorial.com — every Postgres feature explained with runnable examples.
Best for: looking things up while building
W3Schools SQL
Fast syntax lookups with a try-it-yourself editor. Not deep, but always the quickest answer.
Best for: "what was the syntax again?"