Language lab · Beginner → Data
🗄️ SQL
Concept → trace → write code → run tests → explain why → improve → mini-project. Start in the browser if you like; set up your own computer when you are ready.
📖 Topics with real code (15)
Every verified output below was produced by running the code with SQLite 3.45.1 on 5 Nov 2026. Open a topic to edit the code and run it right here in your browser.
🌱 Beginner (4)
Create a table and insert rows
CREATE TABLE defines columns and their types (INTEGER, TEXT, REAL). PRIMARY KEY makes each row unique. INSERT adds rows.
CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT NOT NULL, class INTEGER, city TEXT);
INSERT INTO students (name, class, city) VALUES ('Asha', 8, 'Hyderabad'), ('Ravi', 9, 'Vijayawada'), ('Meena', 8, 'Warangal'), ('Imran', 10, 'Hyderabad');
SELECT * FROM students;
✅ Verified output
id | name | class | city 1 | Asha | 8 | Hyderabad 2 | Ravi | 9 | Vijayawada 3 | Meena | 8 | Warangal 4 | Imran | 10 | Hyderabad
SELECT, WHERE and boolean logic
WHERE keeps only matching rows. Combine conditions with AND, OR and NOT; IN tests a list, BETWEEN a range, LIKE a text pattern.
CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT, class INTEGER, city TEXT, marks INTEGER);
INSERT INTO students (name, class, city, marks) VALUES ('Asha', 8, 'Hyderabad', 88), ('Ravi', 9, 'Vijayawada', 95), ('Meena', 8, 'Warangal', 72), ('Imran', 10, 'Hyderabad', 64), ('Priya', 9, 'Guntur', 81);
SELECT name, marks FROM students WHERE class = 8 AND marks >= 80;
SELECT name FROM students WHERE city IN ('Hyderabad', 'Guntur') OR marks > 90;
SELECT name, marks FROM students WHERE marks BETWEEN 70 AND 90 AND name LIKE '%a%';
✅ Verified output
name | marks Asha | 88 name Asha Ravi Imran Priya name | marks Asha | 88 Meena | 72 Priya | 81
ORDER BY, LIMIT and calculated columns
ORDER BY sorts (ASC or DESC); LIMIT keeps the first rows. You can calculate new columns and name them with AS.
CREATE TABLE students (name TEXT, marks INTEGER, max_marks INTEGER);
INSERT INTO students VALUES ('Asha', 88, 100), ('Ravi', 95, 100), ('Meena', 72, 100), ('Imran', 64, 100);
SELECT name, marks, ROUND(100.0 * marks / max_marks, 1) AS percent, marks >= 75 AS distinction FROM students ORDER BY marks DESC LIMIT 3;
✅ Verified output
name | marks | percent | distinction Ravi | 95 | 95.0 | 1 Asha | 88 | 88.0 | 1 Meena | 72 | 72.0 | 0
Aggregates: COUNT, SUM, AVG, MIN, MAX
Aggregate functions turn many rows into one value. COUNT(*) counts rows; COUNT(col) skips NULLs.
CREATE TABLE results (name TEXT, subject TEXT, marks INTEGER);
INSERT INTO results VALUES ('Asha', 'Maths', 88), ('Asha', 'Science', 92), ('Ravi', 'Maths', 95), ('Ravi', 'Science', NULL), ('Meena', 'Maths', 72);
SELECT COUNT(*) AS rows_, COUNT(marks) AS with_marks, SUM(marks) AS total, ROUND(AVG(marks), 2) AS average, MIN(marks) AS lowest, MAX(marks) AS highest FROM results;
✅ Verified output
rows_ | with_marks | total | average | lowest | highest 5 | 4 | 347 | 86.75 | 72 | 95
🌿 Intermediate (5)
GROUP BY and HAVING
GROUP BY makes one row per group; HAVING filters groups (WHERE filters rows before grouping).
CREATE TABLE results (name TEXT, subject TEXT, marks INTEGER);
INSERT INTO results VALUES ('Asha', 'Maths', 88), ('Asha', 'Science', 92), ('Ravi', 'Maths', 95), ('Ravi', 'Science', 70), ('Meena', 'Maths', 72), ('Meena', 'Science', 66);
SELECT subject, COUNT(*) AS students, ROUND(AVG(marks), 1) AS average FROM results GROUP BY subject;
SELECT name, SUM(marks) AS total FROM results GROUP BY name HAVING SUM(marks) > 150 ORDER BY total DESC;
✅ Verified output
subject | students | average Maths | 3 | 85.0 Science | 3 | 76.0 name | total Asha | 180 Ravi | 165
INNER JOIN and LEFT JOIN
JOIN combines tables on matching keys. INNER JOIN keeps only matches; LEFT JOIN keeps every row from the left table and fills NULL when there is no match.
CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT); CREATE TABLE loans (book_id INTEGER, member_id INTEGER, returned TEXT); INSERT INTO members VALUES (1, 'Asha'), (2, 'Ravi'), (3, 'Meena'); INSERT INTO books VALUES (1, 'Wings of Fire'), (2, 'Malgudi Days'), (3, 'Gitanjali'); INSERT INTO loans VALUES (1, 1, '2026-10-10'), (2, 1, NULL), (3, 2, NULL); SELECT m.name, b.title, COALESCE(l.returned, 'not returned') AS status FROM loans l JOIN members m ON m.id = l.member_id JOIN books b ON b.id = l.book_id ORDER BY m.name; SELECT m.name, COUNT(l.book_id) AS loans FROM members m LEFT JOIN loans l ON l.member_id = m.id GROUP BY m.name ORDER BY m.name;
✅ Verified output
name | title | status Asha | Wings of Fire | 2026-10-10 Asha | Malgudi Days | not returned Ravi | Gitanjali | not returned name | loans Asha | 2 Meena | 0 Ravi | 1
Subqueries
A query inside another query. Use it in WHERE (compare with a calculated value), with IN, or with EXISTS.
CREATE TABLE students (name TEXT, class INTEGER, marks INTEGER);
INSERT INTO students VALUES ('Asha', 8, 88), ('Ravi', 9, 95), ('Meena', 8, 72), ('Imran', 9, 64), ('Priya', 8, 81);
SELECT name, marks FROM students WHERE marks > (SELECT AVG(marks) FROM students);
SELECT name, class, marks FROM students s WHERE marks = (SELECT MAX(marks) FROM students t WHERE t.class = s.class) ORDER BY class;
✅ Verified output
name | marks Asha | 88 Ravi | 95 Priya | 81 name | class | marks Asha | 8 | 88 Ravi | 9 | 95
UPDATE, DELETE and constraints
UPDATE changes rows, DELETE removes them: always use WHERE. NOT NULL, UNIQUE and CHECK constraints reject bad data.
CREATE TABLE wallets (owner TEXT UNIQUE NOT NULL, paise INTEGER CHECK (paise >= 0));
INSERT INTO wallets VALUES ('Asha', 50000), ('Ravi', 20000), ('Old', 0);
UPDATE wallets SET paise = paise + 5000 WHERE owner = 'Ravi';
DELETE FROM wallets WHERE paise = 0;
SELECT * FROM wallets;
SELECT changes() AS rows_changed_by_last_statement;
✅ Verified output
owner | paise Asha | 50000 Ravi | 25000 rows_changed_by_last_statement 1
CASE expressions
CASE adds if/else logic inside a query, for example to turn marks into grades or to count conditionally.
CREATE TABLE students (name TEXT, marks INTEGER);
INSERT INTO students VALUES ('Asha', 88), ('Ravi', 95), ('Meena', 72), ('Imran', 30);
SELECT name, marks, CASE WHEN marks >= 90 THEN 'A+' WHEN marks >= 75 THEN 'A' WHEN marks >= 35 THEN 'Pass' ELSE 'Fail' END AS grade FROM students;
SELECT SUM(CASE WHEN marks >= 35 THEN 1 ELSE 0 END) AS passed, SUM(CASE WHEN marks < 35 THEN 1 ELSE 0 END) AS failed FROM students;
✅ Verified output
name | marks | grade Asha | 88 | A Ravi | 95 | A+ Meena | 72 | Pass Imran | 30 | Fail passed | failed 3 | 1
🌳 Advanced (6)
Common table expressions (WITH) and recursion
WITH names a sub-result so a query reads top to bottom. WITH RECURSIVE builds rows step by step: here, numbers 1 to 10 and a Fibonacci table.
WITH class_avg AS (SELECT 8 AS class, 80.3 AS avg UNION ALL SELECT 9, 79.5) SELECT class, avg, CASE WHEN avg > 80 THEN 'above 80' ELSE 'below 80' END AS band FROM class_avg; WITH RECURSIVE fib(n, a, b) AS (SELECT 1, 0, 1 UNION ALL SELECT n + 1, b, a + b FROM fib WHERE n < 10) SELECT n, a AS fibonacci FROM fib;
✅ Verified output
class | avg | band 8 | 80.3 | above 80 9 | 79.5 | below 80 n | fibonacci 1 | 0 2 | 1 3 | 1 4 | 2 5 | 3 6 | 5 7 | 8 8 | 13 9 | 21 10 | 34
Window functions
Window functions calculate across related rows without collapsing them: RANK within each class, a running total, the difference from the class average.
CREATE TABLE students (name TEXT, class INTEGER, marks INTEGER);
INSERT INTO students VALUES ('Asha', 8, 88), ('Ravi', 9, 95), ('Meena', 8, 72), ('Imran', 9, 64), ('Priya', 8, 81);
SELECT name, class, marks,
RANK() OVER (PARTITION BY class ORDER BY marks DESC) AS rank_in_class,
SUM(marks) OVER (ORDER BY name) AS running_total,
ROUND(marks - AVG(marks) OVER (PARTITION BY class), 1) AS vs_class_avg
FROM students ORDER BY class, rank_in_class;
✅ Verified output
name | class | marks | rank_in_class | running_total | vs_class_avg Asha | 8 | 88 | 1 | 88 | 7.7 Priya | 8 | 81 | 2 | 305 | 0.7 Meena | 8 | 72 | 3 | 224 | -8.3 Ravi | 9 | 95 | 1 | 400 | 15.5 Imran | 9 | 64 | 2 | 152 | -15.5
Transactions
BEGIN starts a transaction; COMMIT saves every change together; ROLLBACK undoes them all. Money moves either fully or not at all.
CREATE TABLE wallets (owner TEXT PRIMARY KEY, paise INTEGER CHECK (paise >= 0));
INSERT INTO wallets VALUES ('Asha', 50000), ('Ravi', 20000);
BEGIN;
UPDATE wallets SET paise = paise - 10000 WHERE owner = 'Asha';
UPDATE wallets SET paise = paise + 10000 WHERE owner = 'Ravi';
COMMIT;
BEGIN;
UPDATE wallets SET paise = paise + 99999 WHERE owner = 'Asha';
ROLLBACK;
SELECT owner, paise, (SELECT SUM(paise) FROM wallets) AS total FROM wallets;
✅ Verified output
owner | paise | total Asha | 40000 | 70000 Ravi | 30000 | 70000
Indexes and query plans
An index lets the database find rows without scanning the whole table. EXPLAIN QUERY PLAN shows SCAN (every row) before and SEARCH ... USING INDEX after.
CREATE TABLE results (id INTEGER PRIMARY KEY, roll TEXT, marks INTEGER); WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 2000) INSERT INTO results (roll, marks) SELECT 'R' || i, i % 101 FROM n; EXPLAIN QUERY PLAN SELECT * FROM results WHERE roll = 'R42'; CREATE INDEX idx_results_roll ON results(roll); EXPLAIN QUERY PLAN SELECT * FROM results WHERE roll = 'R42'; SELECT * FROM results WHERE roll = 'R42';
✅ Verified output
id | parent | notused | detail 2 | 0 | 0 | SCAN results id | parent | notused | detail 3 | 0 | 0 | SEARCH results USING INDEX idx_results_roll (roll=?) id | roll | marks 42 | R42 | 42
Views and normalisation
Normalised tables avoid repeating data; a VIEW saves a query so it can be used like a table.
CREATE TABLE classes (id INTEGER PRIMARY KEY, name TEXT, teacher TEXT);
CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT, class_id INTEGER REFERENCES classes(id), marks INTEGER);
INSERT INTO classes VALUES (1, '8A', 'Mrs Rao'), (2, '9B', 'Mr Khan');
INSERT INTO students (name, class_id, marks) VALUES ('Asha', 1, 88), ('Ravi', 2, 95), ('Meena', 1, 72);
CREATE VIEW report AS SELECT c.name AS class, c.teacher, s.name AS student, s.marks FROM students s JOIN classes c ON c.id = s.class_id;
SELECT * FROM report ORDER BY class, marks DESC;
SELECT class, teacher, COUNT(*) AS size FROM report GROUP BY class;
✅ Verified output
class | teacher | student | marks 8A | Mrs Rao | Asha | 88 8A | Mrs Rao | Meena | 72 9B | Mr Khan | Ravi | 95 class | teacher | size 8A | Mrs Rao | 2 9B | Mr Khan | 1
JSON functions
Modern SQL engines store and query JSON. json_extract reads a field; json_each turns a JSON array into rows.
CREATE TABLE events (id INTEGER PRIMARY KEY, payload TEXT);
INSERT INTO events (payload) VALUES ('{"user":"Asha","action":"login","tags":["web","mobile"]}'), ('{"user":"Ravi","action":"quiz","score":9}');
SELECT id, json_extract(payload, '$.user') AS user, json_extract(payload, '$.action') AS action, json_extract(payload, '$.score') AS score FROM events;
SELECT e.id, t.value AS tag FROM events e, json_each(json_extract(e.payload, '$.tags')) t;
✅ Verified output
id | user | action | score 1 | Asha | login | NULL 2 | Ravi | quiz | 9 id | tag 1 | web 1 | mobile
🧪 Browser workspace
Runs in a sandbox inside your browser with a time limit: no install needed, and your code never reaches our server.
Loading the workspace…
🛠️ Set up and verify
- Install
winget install SQLite.SQLite
brew install sqlite
sudo apt install sqlite3
- Editor: Visual Studio Code with SQLTools (+ SQLite driver).
- Create
hello.sqlSELECT 'Hello, GayaGnan!' AS greeting;
- Run it and check the version
sqlite3 :memory: < hello.sql sqlite3 --version
- Health check: paste what your terminal printed.
Install one language at a time. These commands only install or print versions; nothing here deletes files. Versions change: check the official page and note the version you installed.
🧭 The 14-lab path
- 00 · Setup and verify — install the runtime and editor, run hello world, print the version, fix PATH
- 01 · Syntax and trace — follow each line and the value of every variable
- 02 · Logic — conditionals and decision trees
- 03 · Loops — trace tables and loop challenges
- 04 · Functions — break problems down and test each piece
- 05 · Data structures — lists, maps, stacks, queues and trees you can see 🧪 SQL Joins Detective
- 06 · Debugging — bug cases with a hint ladder
- 07 · Algorithms — search, sort and Big-O comparison 🧪 Index Tuning Game
- 08 · Files and data — read and write fixture files
- 09 · API or database — talk to a mock API or a sandbox database 🧪 Transactions & Consistency
- 10 · Testing — write tests and read failing assertions
- 11 · Security — secure coding and dependency hygiene for this language
- 12 · Project — one portfolio mini-project
- 13 · Assessment — quiz, coding test, rubric and an explain-your-answer prompt
📚 Modules
- SELECT
- filters
- ORDER BY
- aggregates
- GROUP BY and HAVING
- joins
- subqueries
- CTEs
- window functions
- insert, update, delete (sandbox)
- constraints
- normalisation
- indexes
- transactions
- EXPLAIN plans
- data quality
🏗️ Mini-projects
- School records schema
- Synthetic bank transaction report
- Claims status analytics
- Star schema and dashboard query
Projects use synthetic data only.