Engineering
Your AI Can Write SQL. That Doesn't Mean It's Good SQL.
AI can generate SQL that is valid, returns the right rows, passes review and passes every test. None of that says what the query will cost in production, because nothing in the pipeline ever looked at the PostgreSQL planner.

AI can generate SQL that is syntactically valid, returns exactly the rows you asked for, sails through code review, and passes every test in your suite. None of that tells you what the query will cost when it runs against production data. And the reason is uncomfortable once you say it plainly: nothing in the typical pipeline, not the AI that wrote the query, not your tests, not your staging database, ever looked at the one component that actually decides how the query runs.
That component is the PostgreSQL planner. It’s a black box to the model that generated your SQL (and to most people), a black box to your test suite, and effectively invisible in staging. As AI makes query generation nearly free, more SQL is entering production faster than ever and each of those queries carries a planner decision that no one examined. The faster you generate, the more of those unexamined decisions you ship.
This is why validation of AI-generated SQL isn’t a nice-to-have that “matters a bit more now.” Validation is the only place anyone ever opens the black box.
The planner is the real author of performance
SQL is declarative. It describes the result you want, not the procedure for producing it. SELECT ... JOIN ... WHERE says nothing about whether Postgres should scan a table sequentially or seek through an index, which table to drive the join from, or whether to build a hash table in memory. Those are decisions, and you didn’t make them. The planner did.
So whoever writes the SQL, whether it’s a senior engineer, a junior engineer, or a coding assistant, is not the one who decides how it runs. They hand a description to the planner, and the planner writes the procedure. The performance of your query is authored at planning time, inside the database, by a component the author of the SQL never sees.
It’s like handing over a recipe to another chef. The less accurate the recipe, or the less you know about how the other chef cooks, the more likely you are to have a bad dinner. And as it turns out, there are more and more chefs in the kitchen.
What the planner actually does and what it can’t see
For any nontrivial query there are many valid ways to produce the same rows. The planner weighs sequential scans against index scans, index-only scans, and bitmap scans. It considers join orders and join methods, nested loops, hash join, merge join, and so on, along with sort and aggregation strategies and whether parallel execution is worth the overhead.
To choose among them, PostgreSQL uses cost-based selection. Every candidate plan is assigned an estimated cost, and the lowest-cost plan wins. Cost here is an internal unit for ranking plans against each other, not a prediction of wall-clock runtime.
At the center of the whole process sits cardinality estimation: the planner’s guess at how many rows each step will produce. Those estimates drive everything downstream, join order, scan type, whether an index earns its keep, how intermediate results are handled. When the estimates are close, the plan is usually good. When a join blows up the search space beyond what it can explore exhaustively, PostgreSQL hands off to its Genetic Query Optimizer, which searches join orders heuristically. Even the planner’s own search is bounded.
Two facts about this process matter more than any other.
The planner chooses from statistics, not from your data. It never looks at the actual rows to pick a plan. It looks at the statistics PostgreSQL maintains about each table, row counts, value distributions, most-common values, null fractions, plus the schema, the indexes, and the configuration. Change nothing but the statistics and the planner will choose a different plan against the identical data.
The planner never rewrites your query. It picks the best plan available for the SQL it was handed. It will not (cannot) restructure a correlated subquery into a join, unwrap a function around an indexed column, or trim a bloated select list for you. If the query is shaped inefficiently, the planner faithfully finds the best of a bad set of options.
What this means (especially for folks vibe coding) is that the two things developers have come to depend on for checking their queries suffer from a couple of pretty serious blind spots.
Blind spot #1: the AI is blind to the planner
Most AI assistants have no connection to a live database, let alone your production one. They generate from schema fragments, ORM models, and whatever is open in the editor. That already produces the obvious failures we’ve all seen, references to columns that don’t exist, a reinvented version of a table the schema already has, an assumed index nobody ever created.
But set those aside and assume the AI produced a functionally perfect query. It still generated that query with no statistics, no cost model, and no idea of your production cardinality. It cannot know what the planner will do, because it can’t see what the planner sees.
So if a human is dependent on AI-generated SQL, even if that SQL is “cleaner,” it’s still missing the core understanding of the planner.
Worse yet, the SQL the AI chooses sets a ceiling. A correlated subquery where a join would do, a WHERE clause that wraps an indexed column in a function and defeats the index, a SELECT * that blocks an index-only scan. Each of these caps the best plan the planner can possibly produce. The AI, fluent and confident, quietly welds a lid onto performance and moves on.
Beyond all of the above, 9 times out of 10 AI is missing some REALLY important context that typically only humans know: that this query serves a feature launching to a handful of beta users next week and 100x that within the quarter. It optimizes, at best, for the world as described in the prompt. Production is a different world.
Blind spot #2: your tests are blind to the planner
Every gate a query normally passes through is built to test correctness. Functional tests confirm it runs and returns the right rows. Code review confirms it reads sensibly. Staging confirms it doesn’t break the app. All valuable. None of them looks at the plan, and virtually no one is looking at the time to complete queries. The reason is because staging is not production.
No standard test compares the planner’s estimated rows against actual rows. None inspects which indexes were used and which were ignored, detects a plan regression, or measures resource consumption under production-scale conditions. A staging database with ten thousand rows will happily bless a query that collapses at fifty million, not because of hardware or bad luck, but because the planner plans for the world its statistics describe, and staging’s statistics describe a smaller, gentler world. The query passes every gate designed to test whether it’s correct while sailing straight past the question of whether its plan is survivable.
Even a correct plan isn’t a guarantee
Give the planner perfect information and it still only chooses the best plan for the query it was handed, and two things can go wrong even then.
The estimate can be right for the data as described and wrong for the data as it arrives. Cardinality estimation is a prediction, and predictions miss on skew and correlation. The canonical case: the planner estimates a step will return a handful of rows and picks a nested loop join, perfect for small inputs. At production scale that step returns two hundred thousand rows, and the nested loop that scored cheapest becomes the most expensive line in the plan.
And the planner never escapes the shape of the query it was given. It won’t restructure a correlated subquery into a join or lift a function off an indexed column; it finds the best plan available, which may be nowhere near the best plan possible. That gap, between the best available plan and the best approach to the problem, is invisible to the planner by definition, and closing it is the work no automatic step performs.
The plan that was fine yesterday
There’s a second way a good plan goes bad: it stops being replanned.
Postgres re-plans ad-hoc queries on every execution, so a plain query always sees current statistics. But prepared statements, which most ORMs and drivers use for parameterized queries, cache their plan per database connection. The documented behavior is specific: the first five executions run as custom plans, built for the actual parameter values you pass. Then Postgres builds a single generic plan that ignores those values, and if its estimated cost isn’t much worse than the custom average, it locks onto the generic plan and reuses it.
That generic plan is chosen for the average case, which is exactly when it’s dangerous. It can be quietly catastrophic for a skewed value, the customer with two million rows instead of twenty, because the plan that was cheap for typical inputs was never built for that one. And with connection pooling, that cached plan can ride a long-lived connection across millions of requests before anything forces a rethink.
This isn’t a staleness problem. Postgres invalidates cached plans when statistics or the schema change. It’s a parameter-blindness problem, and it means a query you validated once, against the parameters you happened to test, is not necessarily the query that runs in production a thousand executions later.
Why AI raises the stakes
The problem was never that AI-generated SQL is worse than human SQL. Syntactically it’s often better. The problem is arithmetic.
AI didn’t change the rules of query planning or the physics of a bad join at scale. It changed the rate. Generating a plausible query used to take a developer real effort; now it’s nearly free, and the volume of SQL entering the workflow has climbed accordingly. Meanwhile the layer that actually inspects planner decisions, a senior engineer reading EXPLAIN ANALYZE, comparing estimates to reality, reasoning about the production workload, is expensive, human, and flat. It does not scale just because generation did.
So queries pile up faster than anyone can examine them, and the probability that an expensive plan reaches production climbs with the volume. Unless something is added specifically to inspect plans at the pace queries are now produced, the blind spots simply process more traffic.
Validation is the gate that opens the black box
Query optimization and validation is the layer that doesn’t happen automatically, and usually doesn’t happen at all. It’s a deliberate practice, and it starts with evidence: reading EXPLAIN and EXPLAIN ANALYZE, comparing estimated rows against actual rows, checking which indexes were used and which were ignored, and finding the node where the cost concentrates.
Then it becomes a set of experiments. Sometimes the fix is in the query: restructuring a join, removing a non-sargable predicate, trimming the select list so an index-only scan becomes possible. Sometimes it’s in the environment: adding an index, refreshing statistics with ANALYZE, raising the statistics target on a skewed column, tuning configuration. Then you measure again, because an optimization you didn’t measure is a guess.
This is skilled, context-heavy work. It requires knowing the production data volume, its distribution, the workload patterns, and the concurrency the query will actually face. That context lives in the production database and in the heads of the people who run it, which is exactly the context the AI generating the query never had. Validation is the only step in the entire pipeline that looks directly at what the planner decided and asks whether it’s good enough to ship.
How DBGorilla closes the gap
This is the layer DBGorilla is built for. It analyzes a query’s execution behavior against the real database (its actual statistics, indexes, and configuration), identifies the likely cause of an inefficient plan, and proposes a targeted change to the query, the indexes, the schema, the statistics, or the configuration.
The proposal isn’t the product. The proof is. And proving a change is where the “just test it somewhere else” instinct usually falls apart, because any other database that isn’t production is, as we’ve seen, a different world to the planner. DBGorilla’s job is to reproduce the planner’s view of production without ever testing against production itself.
It does that in a few reinforcing ways. It reads production’s own optimizer statistics directly, so the analysis is grounded in the distributions the production planner is actually working from, not a fresh, misleading sample from a small clone. It ranks the query by call volume and share of production load, using pg_stat_statements, so a result is judged in context rather than in isolation. And where the PostgreSQL version allows carrying optimizer statistics into a clone (which DBGorilla supports today across Postgres 13 through 18, by vendoring Postgres 18’s statistics-restore functions and compiling them for the earlier versions), the clone’s planner can be made to see exactly what production’s planner sees, closing the loop cleanly. Additionally we use a sampling-and-comparison approach that keeps the evidence honest rather than letting a stats-blind clone mislead you the way staging does. We continually collect how queries behave in production and run scheduled analysis comparing that behavior across time periods.
The result is before-and-after evidence that means something: plan shape, row estimates, and index usage as production would actually produce them, with a human reviewing and approving the change before it ships. Production is never the test environment and always tracked.
The plan still has to be proven
AI can generate the SQL. The PostgreSQL planner can select the cheapest plan available for it. Neither step proves the query will behave in your production environment, because neither step can see what the other is doing, and the whole point of the planner is that it decides in a place the author never looks.
Generation produces a candidate. Optimization produces a better candidate. Validation produces evidence. Teams adopting AI-generated SQL have massively scaled the first step and left the other two flat. As generation gets faster, validation has to scale with it, because the alternative is finding out about the plan the way teams always have: in production, from a pager or an angry customer.