Train an SLM for Text-to-SQL
Text-to-SQL is a question-answering task where the input is a schema plus a question and the output is one executable query. Small models learn SQL syntax quickly; what they get wrong is your schema — inventing column names, missing joins, and dropping aggregates.
The schema is the hard part, not the SQL
The failure modes of an untrained small model on this task are almost all grounding failures, not grammar failures. Our Text2SQirreL work catalogued them on a base Qwen3-4B: invalid syntax, wrong column or table names, missing WHERE clauses or join conditions, and explanatory prose wrapped around the query.
Two of those are instructive. In one case the base model wrote SELECT team, (base_salary + bonus) ... GROUP BY team and omitted the SUM() — syntactically valid, silently wrong, and it returns an arbitrary row per group. In another it emitted END. instead of END inside a CASE expression, which at least fails loudly.
Silent wrongness is the risk that shapes this recipe. A query that errors is caught by your database; a query that runs and returns the wrong number is caught by nobody.
Step 1: Decide how the schema reaches the model
The schema must be in the prompt. There is no version of this task where a small model reliably remembers your column names, and pretending otherwise produces exactly the hallucinations above.
A CREATE TABLE block plus the question is a good input format because it is unambiguous and the model has seen millions of them:
Schema:
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
salary INTEGER
);
Question: How many employees earn more than 50000?
For a wide database, select the relevant tables before you build the prompt rather than pasting the whole catalogue. Include the join keys of any table you include, or you will train the model to guess them.
Step 2: Seed across the difficulty range, not the frequency range
Fifty to a hundred seed examples is the working range, and their distribution should reflect what is hard, not what is common. Simple single-table selects are what a base model already does; they will not move the needle.
| Query class | Why it belongs in the seed set |
|---|---|
| Single-table filter | Establishes the output contract — bare SQL, no prose |
Aggregation with GROUP BY |
The dropped-SUM() failure lives here |
Multi-table JOIN |
Requires the model to use your keys, not plausible ones |
Subquery / CASE expression |
Where syntax errors concentrate |
| Questions with no valid answer | Teaches the model to fail explicitly rather than invent a column |
The last row is often skipped and matters most in production, where users ask about data that is not in the schema.
Step 3: Configure and run the training
distil model create text2sql
# Output: Model created with ID: <model-id>
distil model upload-data <model-id> --data ./data
distil model upload-status <model-id>
distil model run-teacher-evaluation <model-id>
distil model teacher-evaluation <model-id>
distil model run-training <model-id>
distil model training <model-id>
base:
task: question-answering
student_model_name: Qwen3-4B-Instruct-2507
teacher_model_name: zai.glm-5
tuning:
num_train_epochs: 4
synthgen:
teacher_temperature: 0.6
mutation_topics:
- "aggregations over grouped rows"
- "joins across two or three tables"
- "filters combining several conditions"
- "questions the schema cannot answer"
Synthetic expansion matters more here than in most tasks because it varies the schema, not just the question. The published run expanded roughly 50 seed examples into about 10,000 training examples with diverse schemas across e-commerce, HR, healthcare, and other domains — which is what stops the model overfitting to your column names rather than learning to read a schema. The config reference documents these fields.
Step 4: Evaluate on execution, not on string equality
Two correct queries rarely match character for character, so exact match will understate a good model badly. Run all three metrics from the metrics guide and read them together:
| Model | LLM-as-a-judge | Exact match | ROUGE |
|---|---|---|---|
| DeepSeek-V3 685B (teacher) | 80% | 48% | 87.6% |
| Qwen3-4B tuned | 80% | 60% | 89.5% |
| Qwen3-0.6B tuned | 74% | 40% | 88.5% |
| Qwen3-4B base | 62% | 16% | 84.2% |
| Qwen3-0.6B base | 36% | 24% | 69.3% |
Measured on 50 held-out queries; that run used DeepSeek-V3 as the teacher. Note that the tuned 4B matches the 685B teacher on the judge metric and beats it on exact match, and that base Qwen3-0.6B scores higher on exact match than base Qwen3-4B while scoring far lower on the judge — a good reminder that one metric alone will mislead you.
Add the check the platform does not: execute the generated SQL against a copy of the database and compare result sets. Execution accuracy is the only metric that catches a query that is syntactically fine, semantically wrong, and lexically similar to the reference.
Step 5: Deploy against your own database
distil model deploy local <model-id>
distil model invoke <model-id>
Serve the model inside your network, generate the query, and show it to the user before running it. The reference implementation loads CSVs into an in-memory SQLite database and exposes a --show-sql flag for exactly this reason. Read-only credentials for the query path are not optional.
What text-to-SQL models still get wrong
Roughly one query in five, at the accuracy levels above. That is a usable assistant and an unusable autopilot, and the difference is whether a human sees the SQL.
Three specific limits. Dialect: a model trained on SQLite-flavoured output will not produce correct Postgres window functions without training on them. Business semantics: “active customer” is a definition, not a schema fact, so it belongs in task_description or in a view. And schema drift: a renamed column is a retraining trigger, because the model was trained to read the schema you gave it but its priors were shaped by the ones it saw. For the task type see what is question answering as a training task, and for sizing the student, what size model do you need.