Module 1: Why SQL and LLMs — the Text-to-SQL Problem, Introduced

Why SQL is still the interface

Description

This lesson closes out the landscape with a foundational idea that's easy to overlook and dangerous to ignore: the language model doesn't replace the database or the query language; it only translates the question toward them. SQL isn't an awkward intermediate step that will someday disappear; it's the right interface, and the model is the new way to reach it —not a replacement for it—.

You'll understand why the temptation to "skip SQL" —handing the raw data to the model and asking it to compute the answer itself— is a bad idea, with three reasons run for real: the database is exact and deterministic, the generated SQL is auditable, and the query scales to volumes where stuffing the data into the prompt is impossible. By the end, you'll be clear on why the whole guide is called "SQL for LLMs" and not "LLMs instead of SQL."

Connection to the module

The previous lessons built the assistant and its dangers. This one explains why that assistant translates to SQL instead of answering directly with the model. It's the conceptual justification for the entire architecture, and the natural close before the mini-project, where you'll assemble your first round trip knowing why each piece is where it is.


Analogy: the interpreter doesn't keep the contract

Go back to the translator from lesson 02, but now notice a detail. When the translator translates a contract from plain language into the lawyer's language, the contract doesn't stop existing. The translator doesn't say "you no longer need the document, I remember what it said"; they produce a precise version of the contract in the other language, which gets written down, can be read, can be filed, and can be checked again word for word.

Imagine, instead, an interpreter who offered to replace the contract: "don't write it down, just ask me what each clause said." Nobody would trust that. Memory fails, it changes from one telling to the next, and there's nothing to audit.

SQL is the written contract. The model is the translator that produces it from your question. The database is the lawyer who executes it to the letter. Removing SQL —asking the model to "remember" the data and compute the answer itself— means being left without a contract: no exactness, no record, no audit trail. The interface is still SQL; the model just helps us write it.


The temptation to skip SQL

When someone first sees how well a model understands questions, a seductive idea comes up: "what if I just hand the data straight to the model and ask for the answer, without generating SQL?". Instead of translating into a query, you'd dump the rows into the prompt —"here are all the bookings: (1, 6000), (2, 8000)..."— and ask "how much do the confirmed ones add up to?"

It's tempting because it seems more direct. And it's a bad idea for three reasons, each fatal on its own. Let's see them run for real.


Reason 1: the database is exact and deterministic

When the database engine sums a column, it gives the exact number, always the same one. A language model "mentally adding up" the data in the prompt is probabilistic: it can get it right, it can be off by a cent, and it can give different answers to the same question.

Let's confirm the database is deterministic by running the same sum three times:

import sqlite3

con = sqlite3.connect("reservo.db")

sql = "SELECT SUM(price_cents) FROM bookings WHERE status = 'confirmed'"
print("run 1:", con.execute(sql).fetchone()[0])
print("run 2:", con.execute(sql).fetchone()[0])
print("run 3:", con.execute(sql).fetchone()[0])
con.close()

What to expect:

run 1: 211900
run 2: 211900
run 3: 211900

Three runs, one single number: 211900 cents, exact, unchanging. And since money is stored in integer cents, there isn't even a shadow of a floating-point error: 211900 / 100 = 2119.00 dollars, exact.

Compare that to the alternative of "let the model add it up." You'd pass it twenty numbers in the prompt and ask for the total. Sometimes it would get it right. But a language model doesn't execute arithmetic like a calculator: it predicts it. With twenty numbers it might get it right; with two hundred, or with carrying sums, it starts to slip up —and without warning—. The database, on the other hand, computes, it doesn't predict. For an answer that's going to drive a business decision, you want the computation, not the prediction.

That's why the correct architecture is: the model writes the SQL, the database runs it. The model does what it's good at —understanding the question and translating it—; the database does what it's good at —computing exactly—. Each in its own role.


Reason 2: the generated SQL is auditable

When the model produces SQL, it produces an artifact you can read. Before trusting the answer, you can look at the query and judge whether it does the right thing:

SELECT SUM(price_cents) FROM bookings WHERE status = 'confirmed';

Just by reading it, you verify everything: it sums price_cents (good), from bookings (good), only the confirmed ones (good). If the model had omitted WHERE status = 'confirmed' —the silent error from lesson 03— you'd see it in the SQL, before running it. The SQL is the evidence of what was done to get the number.

If instead the model answered directly "the total is 2119 dollars," you'd have nothing to audit. Did it sum the cancelled ones? Did it skip a row? Did it make up a plausible-looking number? Impossible to know: there's no artifact, only a claim. The answer would be a black box.

This auditability isn't a luxury. In a business, when a number is wrong, someone has to be able to trace why. With SQL, the answer is in the query: you read it, find the missing filter, fix it. Without SQL, the wrong number is indefensible and untraceable. SQL is the interface because it's the one that leaves a record.


Reason 3: the query scales; the prompt doesn't

The third reason is about size. For the model to "add up the data itself," you'd have to stuff all the data into the prompt. With Reservo's 20 confirmed bookings, it fits. But look at how many rows the engine aggregated to produce that number:

con = sqlite3.connect("reservo.db")
n = con.execute("SELECT COUNT(*) FROM bookings WHERE status = 'confirmed'").fetchone()[0]
print("rows the engine summed:", n)
con.close()

What to expect:

rows the engine summed: 20

Twenty rows today. But toy-sized Reservo has 23 bookings; a real Reservo would have hundreds of thousands, and an enterprise database, hundreds of millions. You cannot stuff a million rows into a model's prompt —they don't fit, it would cost a fortune in tokens, and even if they fit, the model wouldn't sum them correctly—.

The database, on the other hand, was designed exactly for this: scanning millions of rows and returning one number —the total—. The query travels to the data, not the data to the model. That's the entire point of a database: doing the heavy lifting over huge volumes and returning just the answer. Text-to-SQL leverages that design; "let the model compute it" throws it away.

With SQL, the question "how much revenue did we bring in?" costs the same —one query— whether there are 20 bookings or 20 million. The model generates a short query; the database does the work. That's the only architecture that scales.


The division of labor, in one sentence

Put the three reasons together and you have the guide's thesis:

The model translates the question into SQL. The database runs the SQL. Neither replaces the other.

The model brings what the database doesn't have: understanding natural language. The database brings what the model doesn't have: exact computation, an auditable artifact, and scale. A text-to-SQL assistant combines them, each in its own strength. Removing SQL would mean asking the model to do the database's job —and it would do it badly, with no record and no scale—.

That's why this guide is called SQL for LLMs and Agents, not "LLMs instead of SQL." SQL isn't a necessary evil on the road to a database-free future; it's the right interface, and language models are, finally, a natural way to reach it. All the engineering in the modules that follow —giving it the schema, prompting, validating, hardening, orchestrating, evaluating— exists to make that translation reliable. The interface was never in question; what we're building is a good translator toward it.


Common mistakes

  1. Believing LLMs will make databases obsolete. The opposite is true: they make databases more accessible, generating the SQL that used to have to be written by hand. The database is still the exact, auditable, scalable engine. The model is the new front door, not a replacement for the building.

  2. Asking the model to compute over data in the prompt. Except for tiny volumes with no accuracy requirement, this is an anti-pattern: probabilistic, expensive, doesn't scale, and leaves no artifact. If there's a database behind it, generate SQL.

  3. Treating the generated SQL as disposable. The SQL is the auditable record of how the answer was obtained. Saving it, showing it, reviewing it: that's what makes the assistant trustworthy. Discarding it right after running it wastes its greatest value.

  4. Forgetting that accuracy comes from the database, not the model. The correct number comes from the SQL engine. The model only has to produce the correct query. If you confuse where the accuracy comes from, you'll put your effort in the wrong place.


Exercises

Exercise 1: Determinism (Easy)

Run SELECT COUNT(*) FROM payments twice and confirm the result is identical. Explain why that property matters for a business assistant.

See solution
import sqlite3
con = sqlite3.connect("reservo.db")
sql = "SELECT COUNT(*) FROM payments"
print(con.execute(sql).fetchone()[0])
print(con.execute(sql).fetchone()[0])
con.close()

Expected output:

26
26

Explanation: The engine gives 26 both times, with no variation. For a business assistant, this means the same question always produces the same answer —two people asking it get the same thing, and today's number is the same as tomorrow's if the data didn't change—. A model "counting" the rows in the prompt offers no such guarantee. Determinism is what lets you trust a number to make decisions.

Exercise 2: Audit a SQL statement (Medium)

A user asks "how much did we refund in total?" and the model returns SELECT SUM(amount_cents) FROM payments. Without running it, audit the SQL: does it answer the question? What filter is missing? Then fix it and run both versions.

See solution

Audit: The SQL sums all payments —charges and refunds together—, but the question was only about refunds. It's missing WHERE kind = 'refund'. This is exactly the value of having the SQL as an artifact: you can catch the error by reading it, before trusting the number.

import sqlite3
con = sqlite3.connect("reservo.db")
def one(sql): return con.execute(sql).fetchone()[0]
print("all payments:", one("SELECT SUM(amount_cents) FROM payments"))
print("refunds only:", one("SELECT SUM(amount_cents) FROM payments WHERE kind='refund'"))
con.close()

Expected output:

all payments: 262900
refunds only: 21500

Explanation: The model's SQL would give 262900 (all payments), but the correct answer to "how much did we refund" is 21500 (the three refunds: 7500 + 8000 + 6000). The error is visible in the SQL —the missing WHERE kind='refund'— before running it. Without the SQL artifact, 262900 would have passed as the answer, indefensible and untraceable. Auditability turned a silent error into a visible one.

Exercise 3: The scale argument (Hard)

Estimate (by hand, no network code) why "stuffing the data into the prompt" doesn't scale. If each bookings row took about 20 tokens in the prompt, how many tokens would a million rows take? Compare it to the size of the equivalent SQL query and explain the architectural conclusion.

See solution

Calculation: 1,000,000 rows × 20 tokens/row = 20,000,000 tokens just to dump the data into the prompt. No model accepts a prompt of twenty million tokens, and even if it did, it would cost a fortune and the model wouldn't sum that many numbers correctly.

The equivalent SQL query takes a handful of tokens, regardless of the number of rows:

SELECT SUM(price_cents) FROM bookings WHERE status = 'confirmed';

Architectural conclusion: The cost of the SQL query is constant in prompt size —a short query— while the cost of "stuffing in the data" grows with every row. The database does the heavy lifting over the million rows and returns one number; the model only generates the query. This is the only architecture that works at real scale, and it's the underlying reason SQL remains the interface: no matter how much data there is, the question travels to the data, not the data to the question.


Summary and next step

  • The model doesn't replace the database or the query language; it translates into them. SQL is the right interface; the model is the new way to reach it.
  • Skipping SQL —asking the model to compute over raw data— is an anti-pattern for three reasons: the database is exact and deterministic (211900, identical on every run), the generated SQL is auditable (you read the query and spot the missing filter), and the query scales (a short query aggregates 20 or 20 million rows).
  • The division of labor: the model brings language understanding; the database brings exact computation, an auditable artifact, and scale. Each in its own strength.
  • That's why the guide is "SQL for LLMs," not "LLMs instead of SQL": all the engineering that follows exists to make the translation toward that interface reliable.

Next lesson: Mini-project — Your first round trip. On your own, you'll assemble the complete flow: populate Reservo, take a question, an example "generated" SQL, run it, and return the answer; and try one with a hallucinated column to see the failure.


Additional resources

  1. SQLite — Datatypes In SQLite — Why storing money as INTEGER (cents) gives exact calculations, with no floating-point errors.
  2. SQLite — Query Optimizer Overview — How the engine aggregates millions of rows efficiently, something the prompt can't do.
  3. Spider / BIRD: why evaluation runs the SQL — Why the community measures text-to-SQL by running the query against the database, not by asking the model for the number.
  4. Claude — Messages API (context size) — The prompt's token limit, which makes "stuffing in all the data" impossible at scale.