Reviewing AI-Written SQL, and What Appreciates in the AI Era
A generated query can parse perfectly and still be wrong in three ways at once, so the closing skill of the course is reviewing one against its contract and proving the divergence in SQL.
Correct syntax, wrong meaning
Omni's April 2026 analysis of text-to-SQL failures put 81.2% of them in the schema and semantic bucket, not the syntax bucket. That statistic describes a specific and uncomfortable experience: the query runs, the numbers look like money, the chart renders, and every figure is wrong.
Here is the query an assistant produced when asked for "monthly revenue" against the schema from the previous lesson.
-- What the assistant wrote. Every line parses. Every number is wrong.
SELECT strftime('%Y-%m', o.order_dt) AS month,
SUM(o.gross_usd) AS revenue
FROM orders o
JOIN order_items i ON i.order_id = o.order_id
GROUP BY month
ORDER BY month;
Four defects, none of them a syntax error.
- The join fans out.
order_itemshas one row per line item, so joining it multiplies each order row by its item count. An order of three items contributes its gross three times. This is the single most common way a generated aggregate is silently inflated, and it is invisible unless you know the child table's cardinality. - The same join silently drops orders. It is an inner join, so any order with no line item at all vanishes from the result. Fan-out inflates the months full of multi-item orders and the dropped parents deflate the months full of item-less ones, which is why you cannot assume a fanned-out number is simply too big. It can land on either side of the truth, and in this data one month lands on each side.
- The required filter is missing. The contract says
status = 'complete'. Cancelled and pending orders are being counted as revenue. - It reports gross, not net. The contract defines
net_revenueasSUM(gross_usd - refund_usd). Refunds never leave the number.
Defects 1 and 2 are the reason a review is a query rather than an opinion. Two errors pointing in opposite directions can cancel to something that looks plausible, so the only way to know the size of the damage is to compute both numbers and subtract.
Notice that the join was not even needed. Nothing in the request required a line item. The generator reached for a related table because the schema said the tables were related, which is exactly the guess a semantic model's predefined join paths exist to remove.
The four checks
Reviewing generated SQL is a repeatable procedure, and it is the durable half of this skill because it works on a colleague's query, on your own at 6pm, and on anything a model writes next year.
- Grainone row per what?
- Filtersis every required predicate present?
- Fan-outdoes a join multiply the measure?
- Contractdoes it match the metric definition?
The fan-out check has a fast field test: count the rows before and after the join. If the row count grew, any SUM of a parent-table column grew with it. If it shrank, parents without a matching child were dropped and the same SUM is now too small. When you genuinely need both the parent measure and the child detail, aggregate the child first in a CTE and join one row to one row, or use a correlated subquery. Do not aggregate across a fanned-out result and hope.
The strongest form of a review is not an opinion, it is a query. If you can produce the months where the suspect number and the contract number disagree, and the size of each gap, the conversation is over in one message. That is the closing exercise of this course.
What commoditizes, and what appreciates
Writing SQL is being automated in public. dbt Copilot reached GA in March 2025; Databricks shipped Assistant, Genie Code, and Lakeflow through spring 2026, with a cited benchmark moving coding-agent success from 32.1% to 77.1% once Genie Code was in the loop. Treating "I can write a window function" as your differentiator is a losing bet.
What appreciates is everything the generator cannot do because it does not know your business: modeling and grain decisions, data quality, cost governance, and review judgment. The adoption data says the market agrees. Only about 5.2% of organizations run AI agents in production. 72% want AI-assisted coding, but only 24% trust AI to manage pipelines. Gartner expects 60% of agentic analytics projects that rely on MCP alone without a semantic layer to fail by 2028.
Read the gap between 72 and 24 carefully, because it is a job description. Closing it requires reasoning traces, auditable logs, human-in-the-loop checkpoints, and eval harnesses. That is more engineering rigor, not less, and it is rigor of a kind this course has been teaching since the first idempotent pipeline.
One honest closing note about this whole level. None of the AI-era material is intern-loop core yet. A 2,817-report corpus of mainstream data-engineering interviews contains zero RAG or vector questions, and fundamentals still gate the hire. But 2026 job descriptions ask for it, AI-native startups test it, and it all compounds on the fundamentals rather than replacing them.
Common mistake: trusting a query because it executed and returned plausible numbers. Execution proves the syntax, nothing more. The three defects above all survive execution, and two of them survive a code review that only reads the query without knowing the contract.
Interview nuance: "here is a query our AI assistant wrote, review it" is entering loops at AI-forward companies, and the fan-out plus missing-filter combination is the archetypal planted bug. Say the checks out loud as you read: grain, filters, fan-out, contract. Then offer to prove it with a diff query rather than asserting it, because that is what a senior reviewer actually does.
On a real platform this differs. Here you diff two CTEs in SQLite. In production the same reasoning is a reconciliation test in dbt or a data-diff job comparing a candidate model against the governed metric, wired into CI so a query that disagrees with the contract fails the pull request instead of reaching a dashboard. The query you are about to write is that test, minus the scheduler.
CREATE TABLE orders (
order_id TEXT,
customer_id TEXT,
order_dt TEXT, -- ISO timestamp; the month grain comes from strftime over this
status TEXT, -- complete | cancelled | pending
gross_usd REAL,
refund_usd REAL -- refunded back to the customer; 0.00 when nothing was returned
);
INSERT INTO orders (order_id, customer_id, order_dt, status, gross_usd, refund_usd) VALUES
('o-1001', 'c-101', '2026-01-05 09:14:00', 'complete', 1200.00, 0.00),
('o-1002', 'c-102', '2026-01-11 16:02:00', 'complete', 840.50, 150.50),
('o-1003', 'c-101', '2026-01-19 11:38:00', 'cancelled', 500.00, 500.00),
('o-1004', 'c-103', '2026-01-27 08:55:00', 'pending', 640.25, 0.00),
('o-1005', 'c-102', '2026-02-03 13:21:00', 'complete', 950.00, 0.00),
('o-1006', 'c-104', '2026-02-09 10:07:00', 'complete', 1500.00, 300.00),
('o-1007', 'c-103', '2026-02-17 15:44:00', 'complete', 700.00, 75.00),
('o-1008', 'c-105', '2026-02-24 09:30:00', 'cancelled', 1100.00, 1100.00),
('o-1015', 'c-105', '2026-02-27 10:05:00', 'cancelled', 400.00, 400.00),
('o-1009', 'c-101', '2026-03-04 12:12:00', 'complete', 2000.00, 0.00),
('o-1010', 'c-105', '2026-03-12 17:49:00', 'complete', 460.00, 60.00),
('o-1011', 'c-102', '2026-03-20 07:26:00', 'pending', 880.00, 0.00),
('o-1012', 'c-101', '2026-03-28 14:03:00', 'complete', 1300.00, 0.00),
('o-1013', 'c-103', '2026-04-06 11:11:00', 'complete', 900.00, 0.00),
('o-1014', 'c-106', '2026-04-15 18:37:00', 'cancelled', 300.00, 180.00);
CREATE TABLE semantic_metrics (
metric_name TEXT,
grain TEXT, -- the time grain the metric is defined at
definition TEXT, -- the exact expression the contract compiles to
required_filter TEXT -- the predicate every implementation must apply
);
INSERT INTO semantic_metrics (metric_name, grain, definition, required_filter) VALUES
('net_revenue', 'monthly', 'SUM(gross_usd - refund_usd)', 'status = ''complete'''),
('gross_revenue', 'monthly', 'SUM(gross_usd)', 'status = ''complete'''),
('active_customers', 'monthly', 'COUNT(DISTINCT customer_id)', 'status = ''complete'''),
('refund_exposure', 'monthly', 'SUM(refund_usd)', 'status = ''cancelled''');
CREATE TABLE order_items (
order_id TEXT,
item_id TEXT,
item_usd REAL -- the line's own price; the lines do not always sum to the order's gross
);
INSERT INTO order_items (order_id, item_id, item_usd) VALUES
('o-1001', 'it-9001', 400.00),
('o-1001', 'it-9002', 400.00),
('o-1001', 'it-9003', 400.00),
('o-1003', 'it-9004', 250.00),
('o-1003', 'it-9005', 250.00),
('o-1006', 'it-9006', 900.00),
('o-1009', 'it-9008', 1200.00),
('o-1009', 'it-9009', 800.00),
('o-1013', 'it-9010', 940.00);-- Fan-out, made visible: joining order_items repeats each order once per line item,
-- so a SUM of the ORDER's gross counts that order once per item.
SELECT o.order_id, o.status, o.gross_usd,
COUNT(i.item_id) AS joined_rows,
ROUND(SUM(o.gross_usd), 2) AS gross_after_join
FROM orders o
JOIN order_items i ON i.order_id = o.order_id
GROUP BY o.order_id, o.status, o.gross_usd
ORDER BY gross_after_join DESC, o.order_id;Apply
Your turn
The task this lesson builds to.
Write the corrected query the assistant should have written: monthly net_revenue per the semantic contract, as (month, net_revenue), earliest month first, over orders(order_id, customer_id, order_dt, status, gross_usd, refund_usd) and order_items(order_id, item_id, item_usd).
The contract has not changed: grain monthly, definition SUM(gross_usd - refund_usd), required filter status = 'complete'. Your numbers must be identical to the contract's own implementation no matter what order_items contains. Alias the columns exactly month and net_revenue, and round the revenue to 2 decimals.
3 hints and 1 automated check are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, plus 1 bonus drill.
Write a query that proves the assistant's query is wrong: return every month where its number disagrees with the contract's net_revenue, as (month, ai_revenue, contract_revenue, diff_usd), ordered by diff_usd from highest to lowest, over orders and order_items.
The assistant's query was SELECT strftime('%Y-%m', o.order_dt), SUM(o.gross_usd) FROM orders o JOIN order_items i ON i.order_id = o.order_id GROUP BY 1. Reproduce that number as ai_revenue, put the contract's net_revenue beside it as contract_revenue, and report diff_usd as ai_revenue - contract_revenue. Keep only the months where the two disagree, in either direction, and round every money column to 2 decimals.
This is the closing exercise of the course. Reconstructing someone else's wrong query in order to disprove it is the entire skill, so nothing here is scaffolded for you.
1 automated check is waiting in the workspace.