Skip to content
All posts

Why EXPLAIN Has to Review AI-Written SQL

July 20, 2026·Read on Medium·

Correct syntax is easy. The expensive mistake is the query plan nobody checked.

The scary part about AI-written SQL is not that it fails loudly. Most of the time it does the opposite. It returns the right rows in local development, passes code review, survives QA and then shows up in production as a slow request that nobody can explain from the diff alone.

That is the trap. Once a model can produce a plausible query in seconds, the bottleneck stops being query authoring and becomes query verification. You are no longer asking, “Can this assistant write SQL?” You are asking, “What plan will my database execute when this SQL meets a real table, a real index and a real amount of data?”

This is where most teams are still reviewing the wrong thing.

The model output gets inspected at the syntax layer. People look for missing bindings, obvious injection risks, maybe an accidental SELECT *. What they often do not inspect is the thing MySQL will actually do with the statement: range scan, ref lookup, temporary table, full scan, filesort, row estimate drift. The query can be semantically correct and still be operationally wrong.

If AI is going to draft more of the SQL, EXPLAIN has to become part of the review process, not a rescue step after Grafana gets ugly.

The failure is usually in the plan, not the text

AI systems are good at generating the shape of a query. They are bad at understanding the physical cost of that query in your database.

That difference matters more than people think.

A human reviewer sees:

  • the right table names
  • the right filters
  • the right join keys
  • a result set that looks correct in staging

MySQL sees something else:

  • whether a useful index is even eligible
  • whether the predicate forces a conversion
  • whether a leading wildcard destroys the index path
  • whether a composite index is being used from the leftmost column forward, or not at all

That is why “it worked on my laptop” is such a weak defense for database code. On a tiny dataset, a full scan still feels fast. On a production table, the same query becomes rent.

MySQL’s own documentation makes this plain. EXPLAIN ANALYZE runs the statement and shows the optimizer's estimate next to actual rows and timing. That is exactly the view you need when a generated query looks harmless in the code diff. You are not guessing anymore. You are checking whether the chosen plan matches reality.

And this is not some exotic database tuning ritual for companies with a dedicated data team. Small teams need it more. They have fewer people watching slow drifts in query behavior, fewer chances to catch them in review and less appetite for incidents caused by “technically correct” code.

What EXPLAIN sees that code review misses

If you only remember three parts of EXPLAIN, make them these:

1. Access type

This tells you how the engine plans to find the rows. ref, range and const usually mean the optimizer found a selective path, while ALL means full table scan, which is occasionally acceptable and often the whole story.

2. Key and possible keys

This is the difference between “we created an index” and “the optimizer can use it here.” Those are not the same statement.

3. Row estimate versus actual work

EXPLAIN ANALYZE matters because it does not stop at the optimizer's guess. It shows actual rows and actual timing. When those numbers drift far apart, your query is more fragile than it looked in review.

Here is the first habit worth building: stop treating SQL as reviewed when the text looks clean. It is reviewed when the plan looks intentional.

MySQL’s reference examples are a good reminder of how quickly the plan changes. A prefix search like Code LIKE 'A%' can use an index range scan. A pattern like name1 LIKE '%e%' falls to access_type: ALL. Same SQL family. Very different cost profile.

/* Prefix search: the optimizer can keep the index in play */
EXPLAIN SELECT Name
FROM country
WHERE Code LIKE 'A%';

/* Leading wildcard: the engine falls back to a full scan */
EXPLAIN FORMAT=JSON DELETE FROM a
WHERE name1 LIKE '%e%';

That is the part a coding assistant will not infer from your schema comments. It can produce a valid filter. It cannot see your future incident graph.

Why generated SQL drifts toward bad plans

This is not because the model is stupid. It is because the incentives are different.

A coding model is trained to continue a pattern that looks plausible in context. It is rewarded for producing something syntactically valid and semantically aligned with the prompt. It is not rewarded for understanding your cardinality, your hot partitions, your composite index order or the part of your data that exploded last quarter.

So the common failures are predictable.

It preserves intent, not selectivity

If you ask for “case-insensitive email lookup,” the model may wrap the indexed column in LOWER(email). The business intent is right. The physical path may be wrong if that expression bypasses the index strategy you actually rely on.

It generalizes to broad patterns

If you ask for “contains” search, it often reaches for %term%. That makes sense from a plain-language perspective. It is a very expensive default if you expected the engine to stay on a B-tree index.

It does not know which column type mismatch matters

MySQL’s own documentation warns that comparisons between dissimilar column types may prevent index use when conversion is required. A model does not know whether your user id arrived as a string, whether that mattered in this query or whether the conversion pushed the engine off the path you wanted.

It writes the first complete answer

Humans do this too, to be fair. But the model does it faster and with more confidence. It gives you a finished query earlier than your instincts are ready to distrust it.

That last point is the dangerous one.

Fast output changes team behavior. People stop exploring alternatives because the first draft arrived before the harder question did: “What will the optimizer do with this?”

Make plan review part of the Laravel loop

If your team works in Laravel, the lowest-friction fix is not a new database abstraction. It is a new review habit.

First, capture the exact SQL your application is emitting. Laravel’s database layer already gives you the hook you need.

<?php

namespace App\Providers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
DB::listen(function (QueryExecuted $query) {
logger()->debug('sql', [
'sql' => $query->toRawSql(),
'time_ms' => $query->time,
]);
});
}
}

That snippet does two useful things.

  • Kill the “the ORM probably did something else” excuse.
  • You also get the exact SQL string you can paste into EXPLAIN or EXPLAIN ANALYZE.

Second, review generated queries in the same order every time:

  1. Ask the model for the query.
  2. Run the query against realistic data.
  3. Run EXPLAIN on the exact emitted SQL.
  4. If the query is performance-sensitive, run EXPLAIN ANALYZE.
  5. Compare the chosen path with the index you expected.

That order matters. If you skip straight from generated code to “looks fine,” you are reviewing prose, not execution.

For higher-risk code paths, I like one extra rule: the reviewer must say which index they expect the optimizer to use before looking at the output. That sounds pedantic. It is not. It forces the team to reason about the physical plan instead of passively accepting whatever the database decides.

The checklist I would actually hand a team

This is the part worth saving because it turns “be careful with AI SQL” into a real operating rule.

Before an AI-written query ships, check these five things:

1. Can you name the intended access path?

If the reviewer cannot say “this should be a range scan on idx_orders_created_at_status" or something equally concrete, the query is not reviewed yet.

2. Does the predicate preserve index use?

Watch for leading wildcards, function-wrapped columns, implicit conversions and broad OR conditions. These are not always wrong. They are just too expensive to approve casually.

3. Does the composite index start where the query starts?

MySQL can use the leftmost prefix of a multi-column index. If your filter starts from column three, the existence of the index is not the same as usefulness.

4. Do estimated rows and actual rows tell the same story?

This is where EXPLAIN ANALYZE earns its keep. If the optimizer expects a tiny result set and reality is much larger, you found a query that may age badly.

5. Is this query on a hot path or a cold path?

A slow monthly admin export is annoying. A slow request inside login, checkout or a queue worker fan-out is operational debt, so context changes the review bar fast. Not every rule in a checklist needs the same amount of ceremony, and that is exactly the point.

That last distinction is the one teams often miss. AI generation increases the number of “good enough” queries in the codebase. The risk is not that every one of them is terrible. The risk is that the dangerous ones stop feeling unusual.

Where this review pattern pays for itself fastest

You do not need to run this process for every tiny back-office query. Start where plan mistakes get expensive quickly.

Search endpoints

These attract %term% queries, optional filters and sorting logic that looks innocent until the table grows.

Background jobs

A generated query inside a queue worker can be more dangerous than the same query in a controller because it runs over and over. One slow plan becomes steady pressure instead of one bad request.

Admin dashboards

These are magnets for “one query that does everything.” Models are especially willing to write giant joins because they look complete. Complete is not the same as cheap.

Migration cleanup code

Teams relax here because the script feels temporary. Temporary SQL still runs on real tables. It can still take locks, force scans and ruin an otherwise calm deploy window.

If you want one simple rule, use this one: the more often the query runs, the less you should trust text-only review.

The real shift is cultural, not mechanical

The interesting part of AI-assisted engineering is not that code shows up faster. It is that the definition of senior review changes.

A few years ago, strong SQL review mostly meant spotting correctness bugs and obvious schema mismatches. Now it increasingly means asking the next question after the model is done. Not “does this compile?” Not even “does this return the right rows?” The real question is “what work did we just ask the database to do?”

Recent software engineering research is already describing this shift in broader terms. As coding agents take on more implementation work, verification, validation and judgment become more important, not less. Databases make that shift painfully concrete because query plans do not care how elegant the prompt was.

That is why I do not think the right posture is “never trust AI-generated SQL.” That posture is lazy and not very useful. The useful posture is stricter: trust it at the text layer, then verify it at the execution layer.

The model can draft the query. Your database still writes the bill.

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium
Why EXPLAIN Has to Review AI-Written SQL — Hafiq Iqmal — Hafiq Iqmal