How to keep an LLM from inventing a column name (four layers, zero model upgrades)
The 60-second version
- Invented columns are a retrieval failure, not a reasoning failure — the model was asked to write SQL against a schema it couldn't see.
- Four layers: retrieve the real schema, constrain it to what's relevant, dry-run before executing, repair from the literal error.
- Valid isn't right. None of this catches a real column used wrongly — column descriptions and curated views do.
fix what the model sees, not the model
Ask a language model for "average order value by city last quarter" without showing it your schema, and it will write something confident:
SELECT city, AVG(order_value) AS aov
FROM orders
WHERE order_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 MONTH)
GROUP BY city;Your table is actually shop_orders. There's no order_value — the field is net_amount. City isn't on the order at all; it's billing_city on customers. And order_date doesn't exist — there's a placed_at timestamp.
That isn't a bad model. It's a model doing exactly what it was asked with information it never had. The fix is four layers of plain engineering, and "upgrade the model" isn't one of them.
Why it happens
A language model writes the most plausible continuation of its context. It has seen a huge amount of public SQL, so its idea of an orders table is order_id, customer_id, order_date, order_value, status — what most tutorial schemas look like.
If your schema isn't in the context window, the model has no signal that it's guessing. "I know this column exists" and "this is the likeliest name for a column like this" feel the same from the inside, and produce the same confident output.
The four layers
- Retrieve — put the real schema in contextPull actual table and column names, types and descriptions from the warehouse. The model can't invent what it's been handed.
- Constrain — only what's relevantTen relevant tables beat two hundred. Too much schema hurts as much as too little, just differently.
- Validate — dry-run before executingA dry run checks the SQL against the real schema without running it, fails on any bad column, and costs nothing.
- Repair — feed the error back, retry, stopA validation error names the exact bad identifier. One or two retries fix most of what's left; after that, ask the person.
Layer 1: Retrieve
Get the schema from the warehouse itself. In BigQuery, INFORMATION_SCHEMA has everything you need — including the column descriptions that carry real business knowledge:
SELECT
c.table_name,
c.column_name,
c.data_type,
c.is_nullable,
f.description, -- where the business knowledge lives
c.is_partitioning_column -- tells the model which date to filter on
FROM `project.shop.INFORMATION_SCHEMA.COLUMNS` c
LEFT JOIN `project.shop.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS` f
ON f.table_name = c.table_name
AND f.column_name = c.column_name
ORDER BY c.table_name, c.ordinal_position;Layer 2: Constrain
Sending all 200 tables creates three problems: the right one competes for attention with 199 others, every question costs more tokens, and long contexts make instructions easier to ignore. Ways to narrow it, from least to most effort:
| Approach | How it works | Good for |
|---|---|---|
| Curated allowlist | Only expose clean, documented views | Almost everyone — and it doubles as a semantic layer |
| Keyword matching | Question mentions "campaign" → include campaign tables | Small schemas |
| Embedding search | Retrieve the tables whose descriptions best match the question | Hundreds of tables |
| Two passes | A cheap call picks tables by name; a second gets full schemas for those only | 50+ tables — usually the best trade-off |
Too much schema fails more subtly than too little. The query is valid and runs — against orders_old instead of shop_orders, because both were offered and nothing said which was current. A validator can't catch that. Only constraint can.
Layer 3: Validate
This is the layer that makes a probabilistic system safe, and it's almost free. A BigQuery dry run parses and plans the query without executing it:
POST /bigquery/v2/projects/{projectId}/queries
{ "query": "<generated sql>", "dryRun": true, "useLegacySql": false }You get back one of two useful things:
- A precise error —
Unrecognized name: order_value at [1:18]. Nothing ran, nothing was billed. - The bytes it would scan — so you can refuse anything over a cost ceiling before it runs. For extra safety, set
maximumBytesBilledon the real run so it fails rather than overspends.
Layer 4: Repair
A validation error is an unusually good correction signal because it's specific — it names the identifier and its position, and BigQuery often suggests the nearest real column.
- The dry run fails with a named column"Unrecognized name: order_value. Did you mean net_amount?"
- Send the literal error plus the relevant schema backDon't paraphrase it — the exact text is the useful part.
- Regenerate and validate againSame gate, no special cases.
- Stop after two tries and askIf it still fails, the question is usually ambiguous, and a third guess won't fix that. "I couldn't find a column for X — did you mean one of these?" is the honest answer.
What this doesn't catch
All four layers make sure the SQL is valid. None of them make sure it's right.
Two things address this: column descriptions that say which column not to use, and curated views that expose net_revenue and simply don't expose order_total. And the SQL has to be visible, so a person can catch what's left.
Quick gut-check
The dry run fails with "Unrecognized name: order_value". What's the right move?
Send the exact error plus the schema back and retry — twice at most
Exactly. A named error is a precise correction signal; after two misses, the question itself is ambiguous.
Fine-tune the model on your schema
No. Schemas change; a fine-tune goes stale immediately. Retrieval is always current.
Send all 200 tables so it can find the right one
No. That invites wrong-table picks — a quieter and worse failure.
Questions people ask
Does a bigger model fix this?
It lowers the rate without changing the shape. A big model without your schema still guesses; a smaller one with the schema, a dry run and a repair loop is more reliable.
What if my database has no dry run?
Use EXPLAIN (Postgres and others) or a LIMIT 0 run. At minimum, parse the SQL and check every identifier against your cached schema before executing.
Does this slow things down?
Barely. A dry run is sub-second and the repair loop only fires on failure — when it turns an error the user would have seen into a fixed query they never notice.
Takeaways
- Invented columns are a retrieval failure — change what the model sees.
- Retrieve, constrain, dry-run, repair from the literal error.
- Cap retries at two, then ask the person.
- Valid isn't right: descriptions and curated views handle real-but-wrong columns.