Query Advisor
A language model is given a slow query and asked to make it faster. Then the database decides whether it did.
- .NET 10
- Microsoft.Extensions.AI
- MCP
- PostgreSQL
- Evals
The model proposes, the database disposes
Five questions, each answered by a statement that is correct and slow. The model gets the question, the slow statement, and three tools: the schema, the planner, and a read-only query runner. It proposes a rewrite and explains itself.
Then the part that is not a language model runs. Both statements execute against
1,000,000 orders and 1,500,001 order lines. Every cell of both result sets is
hashed and compared. Both plans are captured with EXPLAIN (ANALYZE, BUFFERS).
The verdict comes out of that comparison and nothing else.
Nothing here asks a second model to grade the first.
Why that matters more than it sounds
Nine in ten teams running agents in production have observability on them. About half have evaluations. The gap is not laziness — it is that agent output is usually prose, and the only judge available for prose is another model, so the measurement inherits the thing it was supposed to check.
SQL does not have that problem. Two statements either return the same rows or they do not, and one of them is either faster or it is not. This demo takes the one agent task where a deterministic oracle exists and builds the eval nobody can argue with.
There are four verdicts, and three of them are failures with different causes. Upheld is same rows, measurably faster. Wrong rows is a wrong answer. No faster is an honest rewrite that bought nothing. Invalid never ran. Collapsing those into a boolean would throw away the more interesting half of what was measured — a fast wrong answer and a correct pointless one are not the same finding.
What it measured
| Haiku 4.5 | Opus 5 | |
|---|---|---|
| Revenue by status for a month | upheld, 12.3× | upheld, 8.1× |
| One customer’s order lines | no faster, 1.0× | upheld, 62.2× |
| Top ten customers | no faster, 0.6× | no faster, 0.8× |
| The value of twenty orders | upheld, 8.1× | upheld, 2,982× |
| Page 25,001 of the order list | upheld, 6,458× | upheld, 3,893× |
| 3 of 5 · $0.17 | 4 of 5 · $0.62 |
The last question was written to be the hard one: a keyset rewrite needs a cursor, and the cursor for that page is a row the model has to go and find. Both models found it. The hard one turned out to be the third, which is the sort of thing you only learn by running the suite rather than reasoning about it.
The question they both got wrong
Ten customers, ranked by order count, from a correlated count(*) evaluated once
per candidate row. The textbook fix is to replace the correlated subquery with a
grouped aggregate, and that is exactly what both models did.
Both got slower. 213ms became 329ms.
The baseline only ever counted the customers that matched the filter, using the
index on customer_id. The rewrite grouped the entire million-row orders table
first and filtered afterwards — strictly more work, arrived at by applying a
rule that is usually right.
There is a rewrite that wins: filter the customers before aggregating, and it runs in 146ms. Neither model found it. And no amount of reading their explanations would have told you that, because both explanations are correct about why the original was slow. Only running both statements says which one is faster.
That question stays in the set exactly as it is. A golden set whose answer is always the one everybody already knows measures recall, not judgement.
The bug that mattered more than the scores
The first run reported both models as answering one question wrongly. They had not. The baseline for that question exceeded the reader role’s five-second statement budget, so it never ran — and the comparison, which checked the proposal and never the baseline, matched a real hash against an empty one and blamed the model.
A harness that blames the model for its own failure is worse than no harness, because it produces numbers that look like findings. The fix is a verdict of its own — the baseline failed — checked before anything is asked of the answer, and a question rewritten so its slow version is still ruinous and still finishes.
It took a query run by hand against the database to notice. That is the argument for running an eval against real data before believing what it prints, and it is why the scores above are worth reading: they survived the harness being wrong once.
Two roles that cannot do each other’s job
The model’s tools run as a role holding SELECT on five tables, read-only at
the transaction level, with a five-second statement budget. The budget meter
runs as a different role that can append to the spend ledger and cannot see the
data at all.
A model that can reach the table recording what it costs is not being metered by
it. CI asserts all four privileges rather than trusting the file that granted
them — one GRANT is all it would take for this paragraph to become fiction.
The budget, and what happens when it runs out
This is a public endpoint that spends money when someone clicks it. Rate limiting caps how fast that happens, not how much of it there is, so there is a hard ceiling per day.
When the ceiling is reached the service refuses and says so. It does not serve a stored answer, for the same reason the SQL lab does not: the moment a visitor cannot tell a measurement from a recording, every number on the page stops being worth anything.
live p95 -uptime -