sql + llm workbench
Learn by querying real data

SQL, and how an LLM writes it for you.

A real SQLite database that lives in your browser. Start from SELECT *, work up to joins — then watch a model turn plain English into the same queries, and learn why the prompt makes or breaks it.

Open the workbench → Architecture & code map no backend · no install · AI runs on your local Ollama
00

What's in the box

Everything ships as static files. The whole application — database engine, data, UI, and AI wiring — is a single HTML file.

FileWhat it is
sql-workbench.htmlThe app — database, data, UI, AI, all in one file.
help.htmlArchitecture, workflow, and a line-by-line code map.
index.htmlThis page — the tutorial.
Script_Create_Tables.sql.txtThe 5-table schema.
*.csvThe raw HR dataset (headerless), embedded into the page.

The dataset — 5 tables, 36 rows

TableRowsKey columns
EMPLOYEES10EMP_ID, F_NAME, L_NAME, SALARY, JOB_ID, DEP_ID
JOBS10JOB_IDENT, JOB_TITLE, MIN_SALARY, MAX_SALARY
JOB_HISTORY10EMPL_ID, START_DATE, JOBS_ID, DEPT_ID
DEPARTMENTS3DEPT_ID_DEP, DEP_NAME, MANAGER_ID, LOC_ID
LOCATIONS3LOCT_ID, DEP_ID_LOC
01

Quickstart

Just want SQL? Open sql-workbench.html in a browser — the editor and database work immediately, offline. Want the AI too? Point it at your local Ollama:

# Option A — allow the browser to reach Ollama, then open the file directly
OLLAMA_ORIGINS='*' ollama serve

# Option B — serve the page, then browse to it
python3 -m http.server 8000
# → http://localhost:8000/sql-workbench.html

# check the model name matches one you have installed:
ollama list

Set the model in the top bar (default deepseek-v4.1-flash:cloud).

02

Part 1 — SQL, from zero

Run each of these in the Write SQL tab. Type it, press Ctrl+Enter, read the table that comes back. Every result below is real output from the dataset.

1 · Read a table
SELECT * FROM EMPLOYEES;

SELECT picks columns, * means all of them, FROM names the table. 10 rows come back.

2 · Pick and rename columns
SELECT F_NAME, L_NAME, SALARY FROM EMPLOYEES;
3 · Filter rows with WHERE
SELECT F_NAME, L_NAME, SALARY
FROM EMPLOYEES
WHERE SALARY > 70000;

Comparisons: =, >, <, >=, <=, <>. Combine with AND / OR.

4 · Sort and limit
SELECT F_NAME || ' ' || L_NAME AS name, SALARY
FROM EMPLOYEES
ORDER BY SALARY DESC
LIMIT 5;

|| joins text in SQLite; AS names the result column; ORDER BY ... DESC sorts high→low.

Result
nameSALARY
John Thomas100000
Nancy Allen90000
Alice James80000
Ahmed Hussain70000
Andrea Jones70000
5 · Aggregate — count, average, min, max
SELECT COUNT(*) AS employees,
       ROUND(AVG(SALARY), 0) AS avg_salary,
       MIN(SALARY) AS lowest,
       MAX(SALARY) AS highest
FROM EMPLOYEES;

An aggregate collapses many rows into one number.

6 · Group — one row per category
SELECT SEX, COUNT(*) AS n
FROM EMPLOYEES
GROUP BY SEX;
Result
SEXn
F5
M5

GROUP BY splits rows into buckets; the aggregate runs per bucket.

7 · Join — combine two tables
SELECT d.DEP_NAME,
       COUNT(*) AS people,
       ROUND(AVG(e.SALARY), 0) AS avg_salary
FROM EMPLOYEES e
JOIN DEPARTMENTS d ON e.DEP_ID = d.DEPT_ID_DEP
GROUP BY d.DEP_NAME
ORDER BY avg_salary DESC;

The heart of relational SQL. e and d are aliases; JOIN ... ON states the matching rule.

Result
DEP_NAMEpeopleavg_salary
Architect Group386667.0
Design Team366667.0
Software Group465000.0
8 · Join three tables
SELECT e.F_NAME || ' ' || e.L_NAME AS name, j.JOB_TITLE, e.SALARY
FROM EMPLOYEES e
JOIN JOBS j ON e.JOB_ID = j.JOB_IDENT
WHERE e.SALARY > 70000
ORDER BY e.SALARY DESC;
Result
nameJOB_TITLESALARY
John ThomasSr. Architect100000
Nancy AllenLead Architect90000
Alice JamesSr. Software Developer80000

The join map

EMPLOYEES.DEP_ID    ──┐
                     ├─► DEPARTMENTS.DEPT_ID_DEP
JOB_HISTORY.DEPT_ID ─┘

EMPLOYEES.JOB_ID    ──► JOBS.JOB_IDENT
JOB_HISTORY.JOBS_ID ──► JOBS.JOB_IDENT
JOB_HISTORY.EMPL_ID ──► EMPLOYEES.EMP_ID
DEPARTMENTS.LOC_ID  ──► LOCATIONS.LOCT_ID
03

Part 2 — How an LLM writes SQL

The Ask tab takes “who earns the most, with their department?” and produces the query you'd have written by hand. The mechanism is simpler than it looks.

The pipeline

▸your question
↓build a prompt
▸schema + glossary + rules + examples + question
↓call the model · temperature 0
▸extract the SQL — strip prose, keep the SELECT
↓read-only guard
▸run it in SQLite · show the SQL + the result

The four ingredients of a good NL→SQL prompt

  1. Schema — the actual CREATE TABLE statements. The model must see real names; guessing produces wrong identifiers.
  2. Glossary — the semantics the schema can't express. “Full name is F_NAME || ' ' || L_NAME”, “join DEP_ID to DEPT_ID_DEP”. This is what makes joins work.
  3. Rules — constrain output: “Output ONLY the SQL. One SELECT. No DDL/DML.”
  4. Examples — two or three question→SQL pairs. Few-shot examples teach shape better than instructions alone.
Grounding beats cleverness
A model already knows SQL. What it doesn't know is your schema. Almost every bad generated query is a naming failure — an invented column, a wrong join key. Feed it the exact DDL and the join map and the error rate collapses. That's the single highest-leverage thing you can do.

Guardrails

Generated SQL is untrusted input. The app accepts only statements starting with SELECT / WITH / EXPLAIN and containing no INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, REPLACE, TRUNCATE, or ATTACH. Anything else is shown but not run. And the database is in-memory — reloading resets it — so a mistake can't touch your files.

Failure modes and fixes

SymptomCauseFix
no such columnModel guessed a nameTighten the schema block; list columns explicitly
wrong / missing joinJoin keys not statedAdd the join to the glossary
prose not SQLPrompt not strict enough“Output ONLY the SQL” + few-shot
two statementsAmbiguous questionAsk narrower; keep one SELECT
odd resultModel hallucinated rowsSet temperature: 0

Try these in the Ask tab

Watch the generated SQL appear above each answer — that's the model showing its work.

04

Part 3 — Exercises

Try each in the Write SQL tab. Every answer is a variation of Part 1.

  1. List every employee's full name and salary, cheapest first.
  2. How many people work in the Software Group?
  3. Show the highest-paid person in each department.
  4. Which job titles pay more than 90,000 at the top?
  5. Find employees hired before 2001 (hint: JOB_HISTORY.START_DATE is YYYY-MM-DD text — string comparison sorts correctly).
  6. Count how many distinct departments appear in JOB_HISTORY.
05

Architecture

UI layer
tabs · chat · editor · results
→
sql.js
SQLite compiled to WASM
↑
embedded data
DDL + 5 CSVs as strings
CDN · jsdelivr
sql-wasm.js + .wasm
→
loads the engine
Ollama :11434
/v1/chat/completions
⇄Ask mode only
returns one SELECT

Two paths, one engine. Direct SQL reaches SQLite straight. Ask goes through the model first, then the same engine. If Ollama is down, Direct mode still works. Full flow diagrams and a line-by-line code map are in help.html.

06

Deploy

Static site — GitHub Pages serves it with no build step. Settings → Pages → Source: Deploy from a branch → main / / (root).

The AI can't run on the hosted site
A page on github.io can't reach your local Ollama. Pages hosts the tutorial and the SQL editor; open the file locally for the full English→SQL experience.