Module 1

Overview — the big picture

Why SQL exists, how to think in it, and how databases actually work — before you write a single real query.

Concept

Why SQL exists

When does Excel break?

Drag to grow your dataset. See what survives.

1001k10k100k1M10M100M
Excel
Fine
Manageable in one sheet.
SQL database
Fine
Indexed lookup stays fast.

Why exactly does Excel die?

  1. No real indexing — every filter scans the whole sheet.
  2. No safe concurrency — two people editing the same cell can corrupt data.
  3. No enforced rules — "fees must be > 0" is just a hope.
  4. Complex questions get painful — try "students who attended >80% but paid late" across 6 months.
  5. Weak audit and recovery — Excel assumes things go right. Databases assume things go wrong.
Excel isn't bad. It's just not designed for what databases are designed for.
Mindset

The mental shift: HOW vs WHAT

Same goal, two mindsets

Find all active students.

Python — say HOW
active = []
for s in students:
    if s.is_active:
        active.append(s)
return active
SQL — say WHAT
SELECT *
FROM students
WHERE is_active = 1;

No loops. No counters. You describe the result; the engine picks the strategy — which index, what order, whether to parallelize.

When you write a Python loop, you're locked into your strategy. If the data grows 1000×, your code still does the same dumb scan.

In SQL, the same query runs on 100 rows or 100 million rows. The engine picks a different plan each time — index scan, hash join, parallel workers. You didn't write any of that. You just described what you wanted.

That separation between "what" and "how" is the whole point.

The restaurant analogy

The cleanest way to feel the difference.

🍳
Cooking at home
Imperative · Python, Java
  • Get the pan
  • Turn on the gas
  • Add oil
  • Chop onions, stir
  • Watch the heat

You control every step.

vs
🍕
Ordering at a restaurant
Declarative · SQL
  • "I want a pepperoni pizza."

You describe the result. The kitchen handles the rest.

History

SEQUEL → SQL: the name story

1
1974
IBM builds SEQUEL
Researchers at IBM create a language called SEQUEL — Structured English Query Language — for their experimental System R database.
2
~1977
Trademark conflict
An aircraft company already owned the trademark "Sequel". IBM had to drop the name to avoid legal trouble.
3
Late 1970s
Renamed to SQL
IBM shortened it to SQL — Structured Query Language. Same language. But the old pronunciation stuck.
4
Today
Both pronunciations win
Engineers at Google, Amazon, every major tech company still say "sequel". Some say S-Q-L. Both are correct.
Internals · Game

Inside a database: pages, indexes, search

The four pieces

📄
Pages
Rows live in fixed-size pages (~8 KB). The database reads and writes one page at a time, not one row.
⚡
Buffer cache
Hot pages live in RAM. Disk reads are slow. Cache avoids them when you re-ask the same question.
🌳
B-tree index
Like a book's index. Jump straight to the right page in a few hops. Most common database index.
🎯
Hash lookup
When you know the exact ID, hashing finds it in one step. Even faster than a B-tree.

🎮 Find Aisha Khan

She's in row 37 of this 50-row table. Pick a search strategy and watch the difference.

Steps
0
Time
0ms
Strategy
—
Concept

Tables, rows, and columns

Anatomy of a table

Hover the row or column highlights to see what each piece is called.

customer_id name email signup_date is_active
1Aisha Khanaisha@example.com2024-01-101
2Raj Patelraj.patel@sample.org2024-02-150
3Meera Joshimeera.j@example.com2024-03-011
Hover a header or a row.
Choices · Mini-game

Relational vs NoSQL

Relational
Strict tables, rows, columns, relationships
structure & correctness
  • Tables connected via shared identifiers (keys)
  • Strict schema — every row must fit
  • Strong ACID guarantees
  • Engines: Postgres, MySQL, Oracle, SQL Server
  • Best for: banking, accounting, anywhere data must be 100% accurate
NoSQL
JSON docs, key-value pairs, graphs
speed & flexibility
  • No strict schema — flexible structure
  • Related data often stored together (less joining)
  • Optimized for fast reads at scale
  • Engines: MongoDB, Cassandra, Redis, Neo4j
  • Best for: social feeds, real-time chat, caches, big analytics

🎮 Pick the right tool

For each use case — which type fits better? Tap an option, see why.

Concept

OLTP vs OLAP

OLTP
Online Transaction Processing
day-to-day operations
  • Many users at once
  • Tiny, frequent queries
  • Fast response, strict consistency
  • Example: mark attendance, book a cab, place an order
  • Engines: Postgres, MySQL, Oracle
OLAP
Online Analytical Processing
analysis & decision-making
  • Few users (analysts, dashboards)
  • Huge scans across millions of rows
  • Optimized for aggregation, not consistency
  • Example: "average attendance by city over 5 years"
  • Engines: Snowflake, BigQuery, Redshift

Side by side

AspectOLTP — operationsOLAP — analysis
Primary goalRun the business minute-by-minuteUnderstand and grow the business
Data ageCurrent, liveHistorical (months, years)
Query typeSimple insert / selectComplex aggregations & sums
User baseEvery customer & employeeAnalysts & executives
Concept · Game

Transactions — pay fees demo

Run the transaction

One user action, three database steps. Run the happy path, then crash it on purpose.

1
Deduct ₹5,000 from wallet
2
Insert payment record
3
Mark student "Fees paid"
Idle. Wallet ₹10,000, status: Not paid.

Before changing real data, the database writes its intent to a log on disk:

LOG: "About to change wallet 10000 → 5000"
LOG: "About to insert payment row id=789"
LOG: "About to update student status to 'paid'"
LOG: COMMIT

On crash + restart, the database scans the log:

  • If it sees COMMIT → reapply any unwritten changes.
  • If it doesn't see COMMIT → undo the partial changes.

That's the whole magic.

Concept

ACID — the four promises

Every serious database is designed around these four guarantees. Click any card for the deep version.

Click any letter.
Vocabulary

DBMS vs RDBMS

The hierarchy

Click any node to see what it actually means.

DBMS
Database Management System
Document
NoSQL · JSON
Key-Value
NoSQL · cache
Graph
NoSQL · networks
Click any node above.

The rule to remember

✓ All RDBMS are DBMS.
✗ Not all DBMS are RDBMS.

DBMS = the broad category of database engines. RDBMS = the relational kind specifically.

Module 2

Installation & tools

SQL is learned by doing: write → run → observe → fix → repeat. First, build a setup where practicing is effortless.

Setup · Mini-demo

Where do you actually run SQL?

The three options

⌨️
Terminal
Native MySQL CLI
  • Hard to visualize tables
  • Errors are painful to debug
  • More setup-fighting than learning
🌐
Online editor
Browser playgrounds
  • Network latency every query
  • Depends on someone else's server
  • Random downtime, slow days
💻
Local + UI tool
MySQL + Beekeeper, your machine
  • Instant — zero latency
  • Real, beginner-friendly UI
  • Full control, no surprises

🎮 Feel the latency

Hit each "send" button. The difference is the whole reason.

Online editor
💻
☁️
—
Local MySQL
💻
⚡
—
Setup · Game

Set up MySQL with Docker

Why Docker beats native install

Native install
  • Different installer per OS
  • Default paths differ
  • Port 3306 conflicts
  • Version mismatches
  • Permission issues
→
Docker
  • Same image for everyone
  • Same container internals
  • Steps identical on Mac, Win, Linux
  • No path issues
  • Clean and isolated

Image vs container — click to compare

Click either card above.

🎮 Configure your container

Fill the form like you would in Docker Desktop. Hit "Start" — wrong or missing values show what would break.

Fill the form and start.

Connect with Beekeeper Studio

Beekeeper is the UI that talks to your container. Cleaner than MySQL Workbench, beginner-friendly.

MySQL
localhost
3306
root
••••••
tuf_sql
💡
Quick tip: after running CREATE DATABASE tuf_sql_3;, the new database may not appear in Beekeeper's left sidebar immediately. Hit the refresh button on the sidebar panel — it's not a failure, just a UI caching thing.
Module 3

Database & table basics

Now you actually build things: command families, data types, tables, keys, constraints, and how a query runs under the hood.

Vocabulary · Game

The 5 SQL command families

Every SQL command belongs to one of five families. Click a card to see what it does.

Click a family above.

🎮 Which family does each command belong to?

Tap the family for each command. Wrong picks turn red.

💡
Case note: SQL keywords are case-insensitive (SELECT = select), but object names like database/table names can be case-sensitive depending on settings and OS.

DDL vs DML — the house analogy

The two families you'll use most. One shapes the house; the other lives in it.

🏗️
DDL = architect & builder
structure · CREATE, ALTER, DROP
  • Build a room → CREATE
  • Change the plan → ALTER
  • Knock it down → DROP
vs
🛋️
DML = the resident
contents · INSERT, UPDATE, DELETE
  • Add a sofa → INSERT
  • Swap the TV → UPDATE
  • Throw out trash → DELETE
⚠️
DELETE vs TRUNCATE vs DROP: DELETE is DML — removes rows one by one, can be rolled back in a transaction. TRUNCATE is DDL — wipes the whole table instantly by resetting it, usually can't be undone. DROP removes the table itself, structure and all.
Reference

Database setup commands

The essentials

-- create a database
CREATE DATABASE tuf_sql_2;

-- list all databases
SHOW DATABASES;

-- switch into one (all later queries run inside it)
USE tuf_sql;

-- delete a database (irreversible!)
DROP DATABASE tuf_sql_2;

-- safe create: no error if it already exists
CREATE DATABASE IF NOT EXISTS tuf_sql;
DROP DATABASE is irreversible in most setups — it removes the whole database. Handle with care.
💡
Common mistakes: it's SHOW DATABASES (plural) but CREATE DATABASE name (singular). Mental model: a database is the whole filing cabinet, a table is a drawer, a row is a file. And there's no recycle bin — DROP DATABASE is gone for good, which is why pros often have DROP permissions removed.
Reference · Game

Common data types

What each column can hold

A data type defines what kind of value a column stores.

TypeStoresExample
INTWhole numbers — ids, counts, agesage INT
BIGINTVery large whole numbersviews BIGINT
VARCHAR(n)Variable text up to n charsemail VARCHAR(50)
TEXTLong text — blogs, reviewsdescription TEXT
DATEDate onlydob DATE
DATETIMEDate + timecreated_at DATETIME
BOOLEANtrue/false (1/0)is_active BOOLEAN
DECIMAL(p,s)Exact decimals — moneybalance DECIMAL(10,2)

🎮 Pick the right type

For each thing you need to store, tap the best data type.

⚠️
Common mistakes: never use FLOAT for money — rounding errors creep in; use DECIMAL. Don't store numbers or dates as VARCHAR (you lose sorting and math). And don't oversize: a 3-letter country code is VARCHAR(3), not VARCHAR(255).
Reference

Create tables, insert, select

Create & drop a table

CREATE TABLE IF NOT EXISTS users (
  user_id          INT,
  name             VARCHAR(30),
  email            VARCHAR(50),
  is_active        BOOLEAN,
  account_balance  DECIMAL(10,2),
  signup_datetime  DATETIME
);

SHOW TABLES;          -- list tables in the current database
DROP TABLE IF EXISTS users;   -- IF EXISTS avoids an error if missing

Insert rows

-- single row
INSERT INTO users (user_id, name, email)
VALUES (1, 'Raj', 'raj@gmail.com');

-- multiple rows at once
INSERT INTO users (user_id, name, email)
VALUES
  (2, 'Mark',   'mark@gmail.com'),
  (3, 'Albert', 'albert@gmail.com'),
  (4, 'Peter',  'peter@gmail.com');

Read rows + alias

SELECT * FROM users;          -- everything (avoid on huge tables)

SELECT
  name,
  email AS mail_id            -- AS renames the column in output
FROM users;
Avoid SELECT * on big tables in real systems. Ask only for the columns you need.
Practice · Game

Build the query

Drag the right chip into each slot. Wrong piece bounces back. 5 puzzles, real SQL.

Puzzle 1 / 5 Score 0
Goal Loading…
Drag from here ↓
Concept

Keys: primary, foreign, composite

Keys are how rows stay unique and how tables connect. Click a card.

Click a key type above.

See the relationship

A foreign key in orders points at the primary key of users.

users
user_id PK
name
email
orders
order_id PK
user_id FK
amount
CREATE TABLE orders (
  order_id   INT PRIMARY KEY,
  user_id    INT,
  amount     DECIMAL(10,2),
  FOREIGN KEY (user_id) REFERENCES users(user_id)
);
If a table is referenced by a foreign key, drop the child table first (drop orders before users).

Primary key vs UNIQUE

Easy to confuse — here's the clean split.

FeaturePrimary keyUNIQUE
How many per tableExactly oneMany allowed
NULLsNever allowedUsually allowed
PurposeIdentifies the whole rowPrevents duplicates in a column
Typical useuser_idemail, phone_number

🎮 Will the foreign key let it in?

The users table has user_id 1 and 2. Predict PASS or FAIL for each order insert — the FK rejects "ghost" users.

Concept · Game

Keys & relationships

A relationship links two tables via a primary key + foreign key. This is why we call them relational databases. Click a type.

Click a relationship type above.

Many-to-Many needs a junction table

A student joins many batches; a batch has many students. A third table in the middle holds the pairs.

students
student_id PK
name
enrollments
student_id FK
batch_id FK
batches
batch_id PK
batch_name
SELECT students.name, batches.batch_name
FROM enrollments
JOIN students ON enrollments.student_id = students.student_id
JOIN batches  ON enrollments.batch_id   = batches.batch_id;

Normalization = splitting data into related tables so nothing repeats. Store Raj's email once in students; his six fee rows just reference his id. Change the email in one place and every fee row stays correct. This avoids "update anomalies".

Self-referencing = a table points at itself. A mentor is also a student, so mentor_id is a foreign key back into the same students table:

SELECT s1.name AS student, s2.name AS mentor
FROM students s1
LEFT JOIN students s2 ON s1.mentor_id = s2.student_id;

🎮 What relationship is this?

For each scenario, pick the relationship type.

Rules · Game

Constraints — the table's rulebook

Constraints are rules the database enforces for you. Click one.

Click a constraint above.

🎮 Will the insert survive?

Our users table has these rules. Predict PASS or FAIL for each insert.

CREATE TABLE users (
  user_id   INT PRIMARY KEY AUTO_INCREMENT,
  name      VARCHAR(30),
  email     VARCHAR(50) NOT NULL UNIQUE,
  age       INT CHECK (age >= 15 AND age <= 30),
  is_active BOOLEAN DEFAULT TRUE
);
Concept · Game

NULL vs 0 vs empty string

NULL
No value at all — unknown or not applicable. Not zero, not blank text.
e.g. someone has no middle name
0
A real number. We know the value and it is exactly zero.
e.g. ordered quantity = 0
''
An empty string — valid text, just zero characters long.
e.g. user left nickname blank on purpose

🎮 Which one fits?

For each scenario, pick the right representation.

How they behave in queries

ConceptHow to checkIn AVG() / SUM()
NULLWHERE col IS NULLIgnored
0WHERE col = 0Included
'' (empty)WHERE col = ''Not a number
Never write WHERE col = NULL — it never matches. Use IS NULL. And COUNT(col) skips NULLs, while COUNT(*) counts every row.
Internals · Animated

Query lifecycle: parse → plan → execute

When you run a query, the database doesn't answer instantly. It goes through three stages.

1
Parse
Is the SQL valid? Do the tables & columns exist?
→
2
Plan
What's the fastest way? Index scan or full scan?
→
3
Execute
Read the rows, filter, return the result.
SELECT name, email
FROM customers
WHERE city = 'Mumbai';
Press run to watch the query flow through all three stages.
Performance · Game

Indexing — the book at the back

How an index works

You saw the speed difference in the find-Aisha game. Here's what's actually happening.

An index is a separate, automatically-sorted mini-table holding two things: a sorted copy of the column (e.g. all emails A→Z) and a pointer to the real row. Because it's sorted, the database uses binary search — 1,000,000 rows might need only ~20 lookups instead of a million.

-- jump straight to a value instead of scanning
CREATE INDEX idx_user_email ON users(email);

-- one index covering two columns searched together
CREATE INDEX idx_name_city ON users(full_name, city);

Full scan vs index lookup

FeatureFull table scanIndex lookup
Search speedSlow (linear)Fast (logarithmic)
Disk spaceMain table onlyExtra space for the index
Insert speedFastSlower (must update index)
Best forSmall tablesLarge tables
💡
The hidden cost (write penalty): every INSERT, UPDATE, or DELETE must also update every index on the table. 10 indexes = writes roughly 10× heavier. Only index columns you frequently filter or sort by.

🎮 Should you index this column?

Tap yes or no for each. Think about how often it's searched vs how often it changes.

Module 4

Query fundamentals

Reading data: SELECT, WHERE, operators, ORDER BY, LIMIT, DISTINCT, AS. Every playground below runs on a real countries table.

The table we'll use

A 5-row countries table. Every example below queries this.

idcountrypopulationarearegion
1India1,400,000,0003,287,000Asia
2China1,200,000,0004,287,000Asia
3Brazil214,000,0008,516,000South America
4Australia25,700,0007,692,000Oceania
5USA331,000,0009,834,000North America
Core · Playground

SELECT & FROM

The order the database really runs it

You write SELECT first, but the engine starts with FROM.

1 · FROM find the table → 2 · WHERE keep matching rows → 3 · SELECT pick the columns

🎮 Pick your columns

Tick the columns you want. The query and result update live.

SELECT country, population FROM countries;

SELECT can do more than fetch

-- math on the fly, named with AS
SELECT country, population / area AS density FROM countries;

-- a static label column
SELECT country, 'In Asia region' AS note FROM countries;
⚠️
Common mistakes: no comma after the last column (SELECT name, age, FROM… is wrong). Avoid SELECT * in real apps — fetching 50 columns when you need 2 wastes memory. Column order in your SELECT is the order they appear in the output.
Filter · Playground

WHERE — the gatekeeper

WHERE checks every row: condition true → row stays, false → row blocked. It runs before SELECT, so you can filter on a column you don't display.

SELECT * FROM countries WHERE region = 'Asia';

✓ = row kept, ✗ = row blocked by the gatekeeper.

⚠️
Watch out: text needs single quotes (region = 'Asia'), numbers don't. Matching is usually case-sensitive. And you can't use a SELECT alias in WHERE — WHERE runs before the alias exists.
Reference

Comparison operators

Swap any of these into the WHERE playground above.

OperatorMeaningExample
<Less thanpopulation < 50000000
>Greater thanarea > 5000000
<=Less than or equalarea <= 4287000
>=Greater than or equalpopulation >= 331000000
=Equal toregion = 'Asia'
!= / <>Not equal toregion != 'Asia'
⚠️
Common mistakes: SQL uses a single = for comparison (not == like Python). Don't quote numbers — price > '100' forces a slow text conversion. For <= / >= the equals sign comes second.
Combine · Playground

Logical AND / OR / NOT

Two conditions on every row. Flip the connector and watch which countries survive.

A: population > 50,000,000
B: area < 5,000,000
SELECT * FROM countries
WHERE population > 50000000 AND area < 5000000;

The precedence trap

SQL applies AND before OR. Without parentheses you can get surprises — always group OR conditions.

-- "Accessories or Cables, AND under 20"
WHERE (category = 'Accessories' OR category = 'Cables')
  AND price < 20;   -- brackets force OR first
⚠️
Common mistakes: repeat the column each time — region = 'Asia' OR 'Oceania' is wrong; write region = 'Asia' OR region = 'Oceania'. And region = 'Asia' AND region = 'Oceania' returns zero rows — one row can't be two regions.
Math · Playground

Arithmetic operators

OperatorDoesExample
+ - * /Add, subtract, multiply, divideSELECT 10 * 5;
%Modulus (remainder)SELECT 10 % 3; → 1
DIVInteger division (MySQL)SELECT 7 DIV 2; → 3

🎮 Compute population density

density = population / area. Hit the button to calculate it for every row on the fly.

⚠️
Division by zero: MySQL won't crash — it returns NULL plus a warning. The production-safe pattern is population / NULLIF(area, 0): if area is 0, NULLIF turns it into NULL, so you divide by NULL (= NULL) with no warning.
Sort · Playground

ORDER BY & LIMIT

Sort the rows, then keep only the top few. Classic "Top N" pattern.

Sort by
LIMIT 5
SELECT country, area FROM countries ORDER BY area ASC LIMIT 5;
💡
Good to know: default is ASC (small→large, A→Z). LIMIT goes last in the query. You can sort by multiple columns — ORDER BY region ASC, area DESC uses the second as a tie-breaker. You can even sort by an alias.
Clean up · Playground

DISTINCT & AS (aliases)

DISTINCT removes duplicate values. AS renames a column header for the output (the real table is unchanged).

rename header (AS):
SELECT DISTINCT region AS unique_region FROM countries;
⚠️
Common mistakes: DISTINCT goes right after SELECT. DISTINCT region, country dedupes unique pairs, not each column alone. And you can't use an alias in WHERE (it's created after filtering). Prefer underscores over spaces in alias names.
Capstone · Game

Predict the result

🎮 How many rows comes back?

Read each query against the 5-row countries table and pick the row count.

Module 5

Filtering essentials

Real data is messy — missing values, ranges, patterns. Meet the operators that handle it: IS NULL, IN, BETWEEN, LIKE (and their NOT forms).

A deliberately messy users table

Look at the city column: real values, a NULL, an empty string, even a single space. That's why we need special operators.

NULL ≠ empty string ≠ a space ≠ zero. NULL means "unknown / not recorded" — so it can't be compared with = or !=.
Missing data · Playground

IS NULL / IS NOT NULL

Pick a column and a mode. Toggle normalization to treat empty strings and spaces as "missing" too.

SELECT id, email, city FROM users WHERE city IS NULL;
💡
The normalize trick: production data has missing values in many forms. NULLIF(TRIM(city), '') IS NOT NULL trims spaces, turns the result into NULL if it's empty, and only keeps truly-present values — folding NULL, '', and ' ' all into "missing".
Membership · Playground

IN / NOT IN

IN is a clean shortcut for many ORs. Pick cities to build the list, then flip IN ↔ NOT IN.

SELECT id, email, city FROM users WHERE city IN ('Delhi', 'Mumbai');
⚠️
The NOT IN + NULL trap: if the list has a NULL — NOT IN ('Delhi', NULL) — you get zero rows, because the DB can't prove a value isn't equal to "unknown". Keep NULLs out of NOT IN lists.
Ranges · Playground

BETWEEN / NOT BETWEEN

Inclusive range on last_purchase_inr. Both ends count. NULLs (never purchased) never match a range.

from to
SELECT full_name, last_purchase_inr FROM users
WHERE last_purchase_inr BETWEEN 400 AND 800;
⚠️
Common mistakes: lower value first — BETWEEN 1000 AND 500 returns nothing. It's inclusive (both ends included). NULLs never match a range. With dates, '2025-12-31' means midnight, so events later that day get missed unless you include the time.
Patterns · Playground

LIKE / NOT LIKE

Type a pattern to match against email. % = any number of characters, _ = exactly one.

SELECT id, email FROM users WHERE email LIKE '%@tuf.com';
💡
Escaping wildcards: if your data literally contains _ or %, escape it with a backslash. LIKE 'test\_user%' matches a real underscore, so it finds test_user1 but not testXuser2.
Capstone · Game

Which operator fits?

🎮 Pick the right tool for the job

For each goal, tap the operator you'd reach for.

Module 6

Aggregation & grouping

Stop reading rows one by one. Bucket them, then summarize: GROUP BY, SUM/AVG/MIN/MAX, COUNT, and HAVING.

Core · Playground

GROUP BY & aggregates

The bucket model

Picture a pile of balls. GROUP BY color drops them into buckets by colour. Then an aggregate (count, sum, average…) runs inside each bucket and returns one row per bucket.

FROM→ WHERE filter rows→ GROUP BY make buckets→ aggregate→ HAVING filter buckets→ SELECT→ ORDER BY

The employees table

10 employees across 3 projects. Note Dave's NULL experience and Hank's NULL hours.

🎮 Group & summarize

Choose how to bucket the rows and what to compute in each bucket.

GROUP BY compute

      
💡
NULL vs 0 in averages: aggregates skip NULLs but count zeros. AVG(hours_logged) for Alpha = (100+120+105+0)/4 = 81.25 — Dave's 0 drags it down. If Dave's hours were NULL instead, he'd be skipped and the average would be higher.
⚠️
Common mistakes: you can only SELECT columns that are in GROUP BY or wrapped in an aggregate — SELECT employee_name … GROUP BY project fails (which name?). Don't group by a primary key (every bucket holds one row). SUM/AVG need numbers; MIN/MAX also work on text (alphabetical).
Counting · Playground

COUNT — four flavours

The registrations table

10 sign-ups. Some have NULL names or emails; Aisha registers several times.

🎮 Count it different ways

Same table, very different answers depending on what (and how) you count.


      
💡
Which COUNT? COUNT(*) = total volume (counts every row, NULLs included). COUNT(col) = data completeness (skips NULLs). COUNT(DISTINCT col) = true audience size (skips NULLs and duplicates). DISTINCT is slower on huge tables.
Filter groups · Playground

HAVING — filter the buckets

WHERE filters rows before grouping; HAVING filters buckets after the math. That's why WHERE SUM(...) > 100 fails — the sum doesn't exist yet at the WHERE step.

per project, keep where

      

✓ = bucket kept, ✗ = bucket filtered out by HAVING.

🎮 WHERE or HAVING?

For each filter, decide where it belongs.

Capstone · Game

Predict the result

🎮 What does each query return?

Read the aggregate query and pick the answer.

Module 7

Numeric & NULL functions

Reshape values per row: round and absolute, pick the biggest/smallest across columns, and tame missing data with IFNULL, NULLIF, and COALESCE.

The students table

Note the messy phone (NULL vs empty), and NULLs in fees, scores, and tests.

Math · Playground

ROUND & ABS

ROUND(n, d) rounds to d decimals — and d can be negative (round to tens, hundreds). ABS(n) drops the sign. NULL stays NULL for both.

decimals (d) 0

      
💡
Handy facts: ROUND(fee, -2) rounds to the nearest 100 (2804.45 → 2800). ABS is great for "size of change regardless of direction" — WHERE ABS(score_change) > 20 finds the biggest swings up or down. To cut decimals without rounding, use TRUNCATE / FLOOR instead.
Across columns · Playground

GREATEST / LEAST

Unlike MAX/MIN (which work down a column), these work across a row. If any value is NULL, the result is NULL — toggle the fix to see why.


      
Replace · Playground

IFNULL & NULLIF

IFNULL(col, x) swaps a NULL for x — but it does not catch empty strings. Toggle NULLIF(phone, '') to fold '' into NULL first, then watch Rohan/Imran/Tanya get caught too.

replacement:

      
⚠️
NULL ≠ '': IFNULL only replaces true NULLs. Empty string means "data exists but is blank", so it slips through. The combo IFNULL(NULLIF(phone, ''), 'Missing') handles both. And IFNULL changes the output, not the rows — to hide them you still need WHERE … IS NOT NULL.
Fallback chain · Playground

COALESCE

The coalesce_demo table

Three email columns with NULLs (and one empty string). COALESCE returns the first non-NULL.

Build a priority chain. Toggle the columns and order matters left-to-right. Add a default for when all are missing.

default (optional):

      
💡
COALESCE vs IFNULL: IFNULL takes exactly 2 args (value + one backup). COALESCE takes an unlimited list and returns the first non-NULL — perfect for fallback chains and safe math like COALESCE(hours, 0) + COALESCE(bonus, 0). Like IFNULL, it treats '' as a real value unless you NULLIF it first.
Capstone · Game

IS NULL vs IFNULL vs COALESCE vs NULLIF

Quick reference

ToolDoesUsed in
IS NULL / IS NOT NULLTests if a value is missing / presentWHERE (filter rows)
IFNULL(col, x)Replaces NULL with one backup valueSELECT (clean output)
COALESCE(a, b, …)Returns the first non-NULL from a listSELECT (clean output)
NULLIF(a, b)Returns NULL if a = b (e.g. turn '' into NULL)SELECT (pre-clean)

🎮 Pick the right tool

For each goal, tap the function you'd reach for.

Module 8

CASE & conditional logic

SQL's if/else. Checked top to bottom, returns the first match (or ELSE, or NULL). From simple labels to the pro "conditional aggregation" pivot trick.

The shape of a CASE

CASE
  WHEN condition1 THEN value1     -- checked first
  WHEN condition2 THEN value2     -- then this
  ELSE fallback_value             -- optional; NULL if omitted
END AS label                      -- must close with END
TypeMatchesBest for
Searched CASEBoolean conditions (>, <, BETWEEN, IS NULL)Thresholds, ranges, validations
Simple CASEEquality against fixed values (= 'P')Direct code → label mapping
Core · Playground

Classify with CASE

The products table

🎮 Price classifier — drag the thresholds

Move the cut-offs and watch every product re-label live. NULL price → "Missing Price".

Budget ≤ 20 · Premium > 100

      
💡
Top-down priority: SQL stops at the first true WHEN, so put the most specific rules first. Every CASE must close with END. Omit ELSE and unmatched rows become NULL. All THEN results should be the same data type.

🎮 Searched or Simple?

Ranges and IS NULL → Searched. Mapping fixed codes → Simple.

Decisions · Playground

Boolean verdicts & bonus tiers

🎮 PASS / FAIL threshold

The rules table. Slide the pass mark; NULL stays "Missing".

PASS if value ≥ 40

      

CASE + COALESCE: tiered bonus

NULL score → bonus 0 (via COALESCE), ≥90 → 20%, ≥75 → 10%, else 5%.

CASE
  WHEN COALESCE(performance_score, -1) = -1 THEN 0
  WHEN performance_score >= 90 THEN salary * 0.20
  WHEN performance_score >= 75 THEN salary * 0.10
  ELSE salary * 0.05
END AS bonus_amount
⚠️
NULL comparison logic: NULL = NULL is UNKNOWN, not true — so WHEN value = NULL never fires. Use IS NULL, or MySQL's null-safe <=>, or wrap with COALESCE as above. That's why the bonus rule uses COALESCE(score, -1) = -1 to catch missing scores.
Pro trick · Playground

Conditional aggregation (pivoting)

Put CASE inside SUM/COUNT to turn many rows into one side-by-side summary row — exactly how dashboards show "Revenue India vs USA" together. Pick a pivot:


      
⚠️
Common mistakes: inside SUM(CASE …) always add ELSE 0 so no-match doesn't become NULL. COUNT(CASE WHEN … THEN 1 END) works because COUNT ignores the NULLs from unmatched rows. EXISTS-in-CASE (node classification) is powerful but slow on big tables.
Capstone · Game

Predict the CASE output

🎮 What does CASE return?

Read each rule set and the row, then pick the result.

Module 9

String functions

Real text is messy — mixed case, accents, stray spaces. These functions normalize, slice, search, and join it. Type into any box and watch the output live.

Foundations · Playground

Collation — when are two strings "equal"?

Collation decides comparison rules. Four flags you'll see in names like utf8mb4_0900_ai_ci:

FlagMeaningcafé vs cafe / Apple vs apple
aiAccent-insensitivecafé = cafe
asAccent-sensitivecafé ≠ cafe
ciCase-insensitiveApple = apple
csCase-sensitiveApple ≠ apple

🎮 Compare under different collations

vs
Joining · Playground

CONCAT vs CONCAT_WS

The customers table

Neha and Aayush have no middle name (NULL). Watch what each function does.

CONCAT returns NULL if any argument is NULL (the NULL trap). CONCAT_WS(sep, …) sets the separator once and skips NULLs.

separator:

      
Type & see · Playground

String lab

Type anything — include spaces, an accent (é), or an emoji 😀 — and see every function applied at once. Brackets [ ] reveal where spaces are.

💡
LENGTH vs CHAR_LENGTH: CHAR_LENGTH counts characters (what humans see), LENGTH counts bytes. In UTF-8, English = 1 byte, é = 2 bytes, 😀 = 4 bytes. Use CHAR_LENGTH for password/length checks; LENGTH for storage limits. A row has special characters when LENGTH > CHAR_LENGTH.
Cut & find · Playground

LEFT / RIGHT / SUBSTRING & LOCATE / INSTR

🎮 Slice a string (1-based indexing)

LEFT n RIGHT n SUBSTRING start len

🎮 Find a position

Both return the 1-based position (0 if not found). Only the argument order differs: LOCATE(needle, haystack) vs INSTR(haystack, needle).

find in
⚠️
Gotchas: SQL strings are 1-based — the first character is position 1, not 0. 0 from LOCATE/INSTR means "not found". RIGHT(filename, 4) is the easy way to grab a .ext. SUBSTRING(code, 5, 4) pulls a year out of MKT-2023-001.
Replace & match · Playground

REPLACE & LIKE

🎮 REPLACE every occurrence

Swaps all matches (case-sensitive). Try a breadcrumb: replace / with > .

in replace with

      
⚠️
REPLACE gotchas: it's case-sensitive (REPLACE('Apple','apple','X') does nothing). It changes every occurrence — replacing cat in category gives dogegory. In a SELECT it only changes the output; use UPDATE to change stored data. NULL in → NULL out. Nest them: REPLACE(REPLACE(url,'tuf.com','takeuforward.org'),'/',' > ').

LIKE pattern matching — quick recap

% = any length (0+), _ = exactly one character. A comparison returns 1 (true) or 0 (false).

'plus_user'  LIKE 'plus%'      -- 1  (starts with plus)
'plus1user'  LIKE 'plus_us_r'  -- 1  (each _ matches one char)
'USR_8'      LIKE 'USR__'      -- 1  (one char after USR_)
'USR_99'     LIKE 'USR__'      -- 0  (two chars after)
'plus1user'  LIKE 'plus\_user' -- 0  (\_ = a literal underscore)

There's a full live LIKE playground (type a pattern, watch rows match) in the Filtering essentials module.

Capstone · Game

Predict the output

🎮 What does each call return?

Module 10

Editing data & tables

The write side of SQL: INSERT, UPDATE, DELETE, UPSERT, ALTER, TRUNCATE. Every panel below mutates one shared employees sandbox — break it, then hit Reset.

Add rows · Sandbox

INSERT

Add a row. emp_id is the primary key — a duplicate is rejected. Leave department blank to insert NULL.

💡
More INSERT moves: multiple rows — comma-separate value tuples. A quote in a name needs doubling: 'Kevin O''Brian'. Use functions inline: hire_date = CURRENT_DATE. Copy from another table: INSERT INTO employees (…) SELECT … FROM draft_hires;. Always list columns — never rely on table order.
Insert-or-update · Sandbox

UPSERT

"Insert if new, otherwise update." Needs a primary/unique key to detect the collision. Pick how a conflict is handled, then try an existing id (301) vs a new one.


      
Change rows · Sandbox

UPDATE — mind the WHERE

Set a column where a condition holds. Pick no WHERE to feel the "global update" danger — it hits every row.

SET =

      
Remove rows · Sandbox

DELETE & TRUNCATE


      

DELETE vs TRUNCATE vs DROP

FeatureDELETETRUNCATEDROP
RemovesSelected (or all) rowsAll rowsEntire table + structure
WHERE allowed?YesNoNo
Table remains?YesYesNo
SpeedSlower (row-by-row)Faster (bulk wipe)Fast (drops object)

🎮 The AUTO_INCREMENT difference

Same goal — empty the table, then insert "Raj". But the next id depends on how you cleared it. Watch the counter.

⚠️
DELETE safety & nuance: DELETE is logged row-by-row, so it's slower but you can wrap it in a transaction and ROLLBACK. TRUNCATE resets the AUTO_INCREMENT counter to 1; DELETE keeps counting. Foreign keys enforce referential integrity — the DB blocks deleting a parent row that children reference, unless you use ON DELETE CASCADE. And deleting a non-existent row isn't an error — just "0 rows affected".
Change structure · Sandbox

ALTER

Change the table's shape, not its rows. Add a column (NULL for existing rows), backfill it, then drop it.


      
⚠️
CHANGE vs MODIFY: MODIFY salary DECIMAL(12,2) changes the type/constraints, keeps the name. CHANGE salary monthly_salary DECIMAL(10,2) also renames (name written twice, type re-stated). Use MODIFY unless you specifically need to rename — accidental renames with CHANGE cause real production bugs. Also: RENAME TO employees_v2 renames the table.
💡
ALTER in the real world: RENAME COLUMN hire_date TO joining_date changes the name only — old queries break. New columns land at the end by default; MySQL's AFTER colname places them. Use DROP COLUMN IF EXISTS in cleanup scripts so they don't crash. On big tables ALTER can lock the table for minutes, so it's run during maintenance windows. Shrinking a type (VARCHAR(100)→VARCHAR(10)) can truncate existing data.
Capstone · Game

Which command?

🎮 Pick the right write operation

Module 11

Merging query results

Stack the rows of two SELECTs into one result. UNION removes duplicates; UNION ALL keeps them and is faster. There's no built-in intersection in older MySQL — so you build it with logic.

What "merging" really means

You are not appending table B to table A. You are creating one combined result set. SQL stacks the rows vertically — so both queries must line up.

The 3 rulesWhat it meansIf broken
Same number of columns2 columns can only merge with 2 columnsQuery errors out
Compatible data typesColumn 1 here ↔ column 1 there must matchError or junk data
Matched by positionSQL aligns by order, not by nameValues land in the wrong column
Core · Playground

UNION — the unique master list

The two office tables

Two offices, two staff tables. Neha Sharma works in both.

delhi_staff
mumbai_staff

🎮 Merge lab — watch duplicates appear & vanish

Flip between the two operators and choose which columns to merge. Rows SQL throws away are struck through.

· Columns:

      
💡
Duplicates = the whole row. SQL compares every selected column position-by-position, not just the name. With name, role selected, two people named "Neha Sharma" with different roles are not duplicates. Sort the final list with one ORDER BY at the very end — never inside an individual SELECT.
Speed · Playground

UNION ALL — keep every row

Sales come from two channels. To total revenue or count packages, every transaction must survive — even a repeated Mouse. UNION ALL just pastes the rows together: no sort, no compare, no filter.

online_sales
shop_sales

🎮 Consolidate sales — and why the total breaks under UNION

Same data, two operators. Notice the revenue total change when a real duplicate sale gets silently deleted.


      
⚠️
UNION ALL is the professional default. Use it whenever duplicates are impossible (e.g. orders_2023 + orders_2024) or meaningful (every sale, every log line). Reach for plain UNION only when you truly need a deduplicated list — its hidden sort is expensive on millions of rows.
Decide · Reference

UNION vs UNION ALL

FeatureUNIONUNION ALL
Duplicate rowsRemovedKept
PerformanceSlower — sort + distinctFaster — just append
Internal workSorts & compares every rowNo filtering at all
Best forUnique list (mailing list)Totals & raw logs
🃏
The two stacks of exam papers. UNION ALL dumps the second stack on the first — one move. UNION checks each paper against the pile, sorts them, and throws out copies. With millions of papers, that sorting is the "hidden cost".

Decision matrix

  1. Are duplicates even possible? No → UNION ALL (faster, nothing to remove).
  2. Do you want to keep them? Yes → UNION ALL (e.g. counting sales). No → UNION (e.g. a contact list).
Logic · Playground

Intersection & set difference

The set explorer

Two customer lists. Pick a set operation and watch which buckets it pulls from. store_customers = Raj, Neha, Sana · online_customers = Raj, Amit, Sana.

Only store
Both ∩
Only online

      

Building INTERSECT by hand

Older MySQL has no INTERSECT. The mindset: if SQL won't hand you a command, express it with logic. Stack both lists with UNION ALL, group, and keep anything that appears more than once.

SELECT customer_name
FROM (
    SELECT customer_name FROM store_customers
    UNION ALL                      -- must be ALL, or counts collapse to 1
    SELECT customer_name FROM online_customers
) AS combined_list
GROUP BY customer_name
HAVING COUNT(*) > 1;               -- appeared in both → intersection
  1. UNION ALL → [Raj, Neha, Sana, Raj, Amit, Sana]
  2. GROUP BY → Raj (2), Neha (1), Sana (2), Amit (1)
  3. HAVING COUNT(*) > 1 → Raj, Sana
⚠️
The classic trap: the manual method must use UNION ALL. Plain UNION dedupes first, so every count becomes 1 and the intersection silently returns nothing. For set difference ("in A but not B"), a LEFT JOIN ... WHERE b.col IS NULL does the job where EXCEPT / MINUS isn't available.
Capstone · Game

Which operator & what's the output?

🎮 UNION or UNION ALL?

Read each business goal and pick the right tool.

🎮 Predict the merged result

Lists A = [X, Y] and B = [Y, Z] unless noted. How many rows come back?

Module 12

Joins deep dive

Real data lives in separate tables that connect through shared keys. JOIN stitches their rows back together. Pick the join type, watch which rows survive, and learn the one trap that quietly turns every outer join into an inner one.

What a JOIN actually does

A join walks the rows of two tables and pairs them up wherever the ON condition is true. What you change is which unmatched rows are kept — and that single choice is the whole difference between INNER, LEFT, RIGHT and FULL.

JoinKeeps unmatched from…Plain-English ask
INNERNeither — matches only"Only rows that exist in both."
LEFTLeft table"All customers, orders if any."
RIGHTRight table"All orders, customer if known."
FULL OUTERBoth tables"Everything from either side."
CROSSNo ON — every pair"All combinations (m × n)."
Setup · Reference

The two tables

Everything below runs against these two. Note the deliberate gaps: customer 105/999 never existed, order 7 has a NULL customer, and several customers never ordered.

customers (left)
orders (right)
Core · Playground

Join visualizer — flip the type, watch the rows

🎮 One query, five join types

Same customers c + orders o, same ON c.customer_id = o.customer_id. Only the join word changes. Rows filled with NULL are the "kept anyway" rows — proof of what each join preserves.


      
🔁
LEFT and RIGHT are mirror images. A LEFT JOIN B is identical to B RIGHT JOIN A. Most people pick a side and stick with LEFT JOIN because reading parent → child top-to-bottom feels natural — but you'll meet RIGHT JOIN in other people's code, so know both.

INNER JOIN aggregates cleanly

Because INNER drops unmatched rows, there are no stray NULLs to poison a SUM or COUNT. Revenue + order count per customer, computed live:

SELECT c.customer_id, c.name,
       COUNT(*)            AS orders_count,
       SUM(o.total_amount) AS revenue
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name;
The trap · Playground

ON vs WHERE — the soft and hard filter

🎮 Same filter, two places, very different results

We LEFT JOIN customers to orders and want only PAID orders. Put that condition in ON (during the join) versus WHERE (after) and watch the "zero-order" customers vanish.


      
AspectON (join condition)WHERE (filter)
WhenDuring matchingAfter the join is built
Effect on unmatched rowsKept, filled with NULLThrown away (NULL ≠ value)
Use it to…Control what counts as a matchTrim the final result
⚠️
The classic outer-join killer. Writing WHERE o.status = 'PAID' on a LEFT JOIN silently deletes every customer with no order — their status is NULL, and NULL = 'PAID' is never true. Your LEFT JOIN just became an INNER JOIN. Want to keep them? Put the condition in ON, or filter for IS NULL on purpose (the "who never ordered?" query).
Both sides · Playground

FULL OUTER JOIN & the COALESCE fix

The audit tables

A merger-style audit: keep every student and every payment. Note Raj has a NULL id, and payment for student 4 is an orphan (no such student).

students
payments

🎮 Raw full join vs COALESCE master-id

MySQL has no FULL OUTER JOIN — you stitch LEFT JOIN + RIGHT JOIN with UNION. The raw version leaves the id NULL for the orphan payment (its student row doesn't exist). COALESCE falls back to the payment's id to build one clean key.


      
⚠️
FULL OUTER exposes bad data. The orphan payment for student 4 and the NULL-id Raj aren't query bugs — they're data-quality findings the join surfaced. A WHERE on the nullable side (e.g. WHERE s.name = 'Aisha') would quietly delete those rows and undo the whole "outer" point.
Hierarchies · Playground

Self join — a table joined to itself

The employees table

Employee and manager live in the same table. Two aliases (e and m) let SQL treat one table as two and walk the hierarchy.

🎮 Build the org chart


      
The rest · Reference

Cross, implicit & natural joins

CROSS JOIN — the Cartesian product

No ON. Every customer pairs with every order: 8 × 8 = 64 rows. Useful for generating grids (all customer–coupon pairs, all dates × products), dangerous by accident.

SELECT COUNT(*) FROM customers c CROSS JOIN orders o;   -- 64

🎮 Combination generator

The "combo meal" mindset: no key needed, you just want every pairing. Pick two small reference tables and watch the grid build itself.


      

IMPLICIT JOIN — the old comma style

Comma in FROM makes a Cartesian product; the WHERE condition trims it down to matches. Forget the WHERE and you've written an accidental CROSS JOIN.

SELECT c.customer_id, c.name, o.order_code, o.status
FROM customers c, orders o
WHERE c.customer_id = o.customer_id;   -- == INNER JOIN

NATURAL JOIN — auto-match by column name

Joins on every identically-named column automatically — here customer_id links customers to customer_loyalty. No ON needed.

SELECT c.customer_id, cl.tier
FROM customers c
NATURAL JOIN customer_loyalty cl
ORDER BY c.customer_id;

The multi-column trap

Now both customers and store_locations share two names — customer_id AND city. NATURAL JOIN silently matches on both, so a customer listed under a different city vanishes.

SELECT * FROM customers NATURAL JOIN store_locations;
⚠️
NATURAL JOIN is risky in real projects. If both tables later gain another same-named column (say name), the join silently starts matching on that too — changing results with zero warning. Prefer an explicit ON so the match key is always visible.
Capstone · Game

Pick the join & predict the rows

🎮 Which join answers the question?

Read each business goal and choose the right join.

🎮 How many rows come back?

Using the same customers (8 rows) and orders (8 rows) from this module.

Module 13

Permissions & access control

A database is only as safe as the keys you hand out. GRANT, REVOKE and ALTER USER decide who can do what, where — from a single table to the whole server. This is DCL: Data Control Language.

What is a privilege?

A privilege is a single permission that controls what a user can do — read rows, insert, update, delete, create tables, drop tables. The admin decides which user gets access to which database or table.

🔐
Real-life analogy. You can't give every team member access to sensitive tables like user_login_history or revenue. You grant access only to the people who actually need it — and ideally keep an audit trail. Least privilege is the whole game.

Three commands do all the work, and each is the mirror of another:

CommandDoesDirection word
GRANTGives a privilege or roleTO a user/role
REVOKERemoves a privilege or roleFROM a user/role
ALTER USERChanges the account itself (password, lock)—
Foundations · Reference

Users, hosts & roles

Where users live

MySQL stores every account — users and roles — in one system table: mysql.user. List them with SELECT user, host FROM mysql.user;. A user is defined not just by name, but by where they connect from — the host.

Click a host type

'raj'@'localhost' and 'raj'@'%' are different accounts. The host is half the identity.

Click a host type above.
⚠️
Creating a user grants nothing. CREATE USER 'raj'@'%' IDENTIFIED BY '123456'; lets raj connect — but he can't see or touch a single table until you GRANT him something.

User or role? Same table, different shape

Roles don't live in a separate table — they sit in mysql.user too. The tell isn't the table, it's the account properties: a user has a password and logs in; a role has no password and just holds privileges.

AccountHas password?Type
rootYesUser
rajYesUser
engineeringNoRole
supportNoRole

Roles are typically locked and password-less, so you can list them with:

SELECT user AS role_name, host
FROM mysql.user
WHERE account_locked = 'Y'
  AND (authentication_string IS NULL OR authentication_string = '')
ORDER BY user;
Core · Playground

GRANT — build the privilege set

Everything below protects this one table. Run all GRANT/REVOKE statements as root/admin — only an admin can create users and hand out privileges.

CREATE TABLE students (
  student_id INT PRIMARY KEY AUTO_INCREMENT,
  email      VARCHAR(120) NOT NULL UNIQUE,
  full_name  VARCHAR(80)  NOT NULL,
  city       VARCHAR(60),
  status     ENUM('ACTIVE','BANNED') NOT NULL DEFAULT 'ACTIVE',
  created_at DATETIME NOT NULL
) ENGINE=InnoDB;

🎮 Tick privileges, watch what raj can do

Pick what you GRANT to 'raj'@'%' on tuf_sql.students. The statement rebuilds, and every operation raj might try is re-checked live. Notice DROP never appears in the list — it was never granted, so it always fails.


      
🔎
Always verify. As admin, run SHOW GRANTS FOR 'raj'@'%'; to see exactly what's been granted. To truly confirm, log in as raj and try the operation — a missing privilege fails with "permission denied".
The blast radius · Playground

GRANT ALL — it's all about scope

🎮 Move the scope, watch the blast radius

GRANT ALL hands over every privilege — but its real power depends entirely on where you point it. Switch the scope and watch how much of the server lights up as accessible.


      
☢️
GRANT ALL ON *.* is God Mode. It makes the account a superuser — it can read, modify or DROP any database on the machine. Give it only to trusted admins, never to application users or developers.
Scale · Playground

Roles & delegation

Why roles?

Imagine 50 engineers. Granting SELECT to each one is 50 queries — and one typo is a security hole. Instead, grant once to a role, then link users to it. Change the role later and all 50 update instantly.

CREATE ROLE 'engineering';
GRANT SELECT, INSERT, UPDATE ON tuf_sql.students TO 'engineering';
GRANT 'engineering' TO 'raj_ip'@'192.168.65.1';

🎮 The "default role" trap

Granting a role to a user does not make it active at login. A common gotcha — toggle the switches and see whether raj_ip can actually run queries after connecting.

WITH GRANT OPTION — the power to delegate

By default a user can use a privilege but can't pass it on. Add WITH GRANT OPTION and they can re-grant the same privileges to others (but never privileges they don't hold themselves). You can test this toggle back in the GRANT playground.

GRANT SELECT, INSERT ON tuf_sql.students
TO 'raj'@'%'
WITH GRANT OPTION;
💥
Privilege explosion. Raj grants to Neha, Neha grants to an intern… suddenly everyone has access and the admin has lost track of who holds the keys. If you don't want re-sharing, simply omit the keyword — that one phrase is a massive difference in control.
Account management · Reference

ALTER USER — rotate & lock

GRANT changes what a user can do. ALTER USER changes the account itself — its password or login status. Admin-only.

1 · Password rotation

Change a password periodically, or immediately after a suspected leak. The old password dies instantly; new connections need the new one.

ALTER USER 'tuf_support'@'localhost' IDENTIFIED BY 'Temp#5678';

2 · Emergency lock

Offboarding, suspicious activity, or a compromised device — block login instantly without deleting the account. Roles, grants and history stay intact, so you can restore access later.

ALTER USER 'tuf_support'@'localhost' ACCOUNT LOCK;

3 · Unlock

The reverse. Login works again with the existing password; nothing was lost during the lockdown.

ALTER USER 'tuf_support'@'localhost' ACCOUNT UNLOCK;
🪤
Lock beats DROP. DROP USER erases the account's whole setup — if it was a false alarm or the employee returns, you rebuild from scratch. ACCOUNT LOCK just freezes login and preserves everything.
The undo · Reference

REVOKE — the opposite of GRANT

REVOKE removes privileges or roles. Same shape as GRANT, one keyword flips: TO becomes FROM. Run as admin.

Revoke a role from a user

REVOKE 'engineering' FROM 'raj_ip'@'192.168.65.1';

All privileges coming from that role vanish for the user.

Revoke specific privileges

REVOKE INSERT, UPDATE ON tuf_sql.students FROM 'raj'@'%';

Raj keeps anything still granted (e.g. SELECT) but loses the revoked ones — an INSERT now fails with "permission denied".

Capstone · Game

Lock it in

🎮 Which command?

Pick the right DCL / account command for each scenario.

🎮 Will it succeed?

raj has exactly SELECT, INSERT on tuf_sql.students — nothing else. Logged in as raj, does each statement work?

🎮 User or role?

Same mysql.user table — decide by the account's properties.

Module 14

Transactions & concurrency

A transaction makes many statements behave as one atomic unit — all succeed, or none do. Below: how connections carry transaction state, what COMMIT / ROLLBACK / SAVEPOINT really do, what InnoDB does under the hood, and how locks turn into timeouts and deadlocks.

Foundations · Playground

Connections & connection pools

What is a connection?

A connection is a single live channel between your app and the MySQL server. Every connection has a connection_id, and it is stateful — session variables and in-progress transaction state live inside that one connection. Kill the connection and that state is gone.

🔀
Why does connection_id keep alternating (73, 74, 75…)? Your tool is using a connection pool and handing out connections round-robin — so each query may run on a different connection.

🎮 Pool vs single connection

Opening a fresh DB connection every time is slow. A pool keeps several open and reuses them: it hands out a free one, runs the query, returns it. Fire three simultaneous requests each way and watch the difference.

Core · Playground

COMMIT & ROLLBACK

Outside a transaction MySQL auto-commits each statement. Inside one, nothing is final until you COMMIT — and ROLLBACK throws all of it away, restoring the last committed state.

START TRANSACTION;
  UPDATE students SET status = 'ACTIVE' WHERE student_id = 1;
  UPDATE students SET city = 'Delhi'   WHERE student_id = 1;
COMMIT;   -- or ROLLBACK to discard both

🎮 Run a transaction, then keep or discard it

Start a transaction, apply the two updates, then choose COMMIT or ROLLBACK. The right panel is what another session sees — proof that uncommitted changes stay invisible (MVCC).

Your session (in transaction)
Another session sees (committed)
Partial undo · Playground

SAVEPOINT — undo just part of the work

A SAVEPOINT is a bookmark inside a transaction. ROLLBACK TO SAVEPOINT undoes only the work after the bookmark and keeps everything before it — the transaction stays open.

🎮 Step through a savepoint

We insert an enrollment, bookmark, then attempt a balance transfer — but roll the transfer back to the savepoint while keeping the enrollment. Step through and watch which changes survive the final COMMIT.


      
accounts
enrollments (new this txn)
Under the hood · Reference

Inside InnoDB

What actually happens during a transaction?

Click each piece to see its job. Together they explain how InnoDB can change data, still let others read the old version, undo on rollback, and survive a crash.

Click a component above.

Undo log vs Redo log — don't mix them up

Undo logRedo log
PurposeReverse changesReplay changes
Used forROLLBACK + MVCC readsDurability after a crash
HoldsThe old row versionThe new change to re-apply
⚠️
Production warning. Transaction state lives on one connection. If you START TRANSACTION on one pooled connection and COMMIT on another, the commit does nothing useful — the state isn't there. Always run start → queries → commit/rollback on the same connection handle.
Concurrency · Playground

Timeout & deadlock

A write takes a lock on its rows until commit/rollback. If another transaction wants the same row it waits — and if it waits too long, it times out and fails instead of hanging forever.

🎮 Build a deadlock

Two transactions grab locks in the opposite order. Step through and watch them lock each other out — then see how MySQL breaks the tie.

Transaction 1
Transaction 2
Capstone · Game

Lock it in

🎮 Which keyword?

Pick the right transaction control for each goal.

🎮 What survives?

Reason about the final state after each transaction.

🎮 Concept check

Connections, logs, locks & concurrency.

Module 15

Subqueries

A subquery is just a SELECT living inside another statement. SQL runs the inner query first, then hands its result to the outer query to finish the job. You'll meet them in the SELECT list, inside IN / EXISTS, and — the powerful kind — correlated, re-running once per outer row.

Inner first, outer second

Read any subquery from the inside out. The inner SELECT produces a value, a list, or a yes/no — then the outer query uses it. The shape of what comes back decides where it can plug in.

Returns…Plugs into…Example
One value (scalar)SELECT list or = / > compare"the name for this order"
A list of valuesIN / NOT IN"users who appear in orders"
Rows (or none)EXISTS / NOT EXISTS"does any order exist?"
Setup · Reference

The three tables

A small e-commerce slice. Note the deliberate gaps: users 2, 5, 6 never order, user 7 has an empty name and 8 a NULL one, the Keyboard is never sold, and order 102 is CANCELLED. Orders also carry a status and a placed_at_utc timestamp we'll use later.

users
products
orders
Core · Playground

Scalar subquery in the SELECT list

🎮 One value per order row — no JOIN needed

When a subquery returns exactly one value, you can drop it straight into the SELECT list. It runs once per outer row, using that row's o.user_id. Pick what each order should look up:


      
🐢
Handy, but not free. A scalar subquery in SELECT re-runs for every output row — great for a quick lookup, usually slower than a JOIN on large tables.
Lists · Playground

Subquery with IN

🎮 The inner query builds the filter list

IN checks whether a value matches anything in a list. Instead of hardcoding IN (1, 3, 4), we let the subquery find the IDs. Watch the generated list, then the rows it keeps:


      

Subqueries vs JOINs — when to reach for each

Subqueries read like "Step 1, then Step 2"; JOINs are often faster for the engine. Reach for a subquery when you're filtering a WHERE against a separate list, or comparing each row to an aggregate (like an average).

Use a subquery when…Use a JOIN when…
Filtering by a list from another queryYou need columns from both tables in the output
Comparing a row to an aggregate valuePerformance matters on large tables
The logic reads cleaner as discrete stepsYou're stitching related rows together
Presence · Playground

EXISTS & NOT EXISTS

🎮 Does any matching row exist?

EXISTS returns TRUE the instant the correlated subquery finds one row — values don't matter, so SELECT 1 is the idiom. NOT EXISTS is the opposite: true when nothing is found. Flip between them and raise the bar:


      
⚡
EXISTS short-circuits. IN must build the full list of inner values first; EXISTS stops at the first match. And NOT EXISTS is NULL-safe — NOT IN can return zero rows if the inner list contains a single NULL.
Cases · Playground

EXISTS in the wild

🎮 Four real questions, one pattern

Same presence/absence idea aimed at different tables and conditions:


      
Pitfalls

Common mistakes to avoid

⚠️ The NOT IN + NULL trap — live

"Which products were never sold?" answered two ways. With clean data both agree on Keyboard. Now inject one refund-style order whose product_id is NULL and watch NOT IN collapse to nothing while NOT EXISTS stays correct.

NOT IN
SELECT p.product_id, p.product
FROM products p
WHERE p.product_id NOT IN (
  SELECT o.product_id FROM orders o
);
NOT EXISTS
SELECT p.product_id, p.product
FROM products p
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.product_id = p.product_id
);
MistakeWhy it bitesDo instead
SELECT * inside EXISTSWastes work — the values are never readUse SELECT 1
NOT IN over a NULL-able columnOne NULL makes it return zero rowsUse NOT EXISTS
Forgetting the correlation linkInner query gives the same answer for every rowTie it back: WHERE o.user_id = u.user_id
Correlated · Playground

Correlated subqueries — once per outer row

A correlated subquery references a column from the outer query, so it re-runs for every outer row — like a mini-query inside a loop. Think "for each employee, compute the average salary of their own department, then compare."

🎮 Each user's biggest order

Same idea, two angles — summarise the peak per user, or pull the actual winning order rows:


      
Cases · Playground

Row-by-row comparisons

🎮 The looping logic in action

Three correlated patterns you'll reuse constantly — last-seen, running counts, and beating a group average:


      
🐢
The hidden cost. A correlated subquery runs once per outer row — 1,000 rows means 1,000 inner runs. Modern optimizers often rewrite it as a JOIN, but for it to stay fast the inner WHERE column (e.g. user_id) must be indexed.
Reference · Cheat sheet

Simple vs correlated

FeatureSimple subqueryCorrelated subquery
ExecutionRuns onceRuns in a loop — once per outer row
DependencyIndependent; runs by itselfDepends on the outer row's values
PerformanceUsually fastCan be slow on large tables
Capstone · Game

Lock it in

🎮 Pick the right tool

Which clause fits each goal?

🎮 Predict the result

Reason about what each subquery returns over the tables above.

Module 16

CTEs & Temporary Tables

A CTE (Common Table Expression) names the result of a query so the next query can select from it like a table. Defined with WITH … AS (…), it lives only for that one statement — and it turns deeply nested subqueries into clean, sequential steps. The recursive flavour even walks tree-shaped data.

Core · Concept

Name a result set, then query it

The high-level idea

1) Run a query that generates a result set. 2) Give that result a name — like a nickname for a table. 3) Write a final query that selects from the named result as if it were real.

WITH cte_name AS (
  -- any SELECT that returns a table-like result
  SELECT ...
)
SELECT ...
FROM cte_name;

The keyword is WITH (not "width"). The CTE exists only for the duration of that one final query — nothing is stored on disk; the engine computes it, uses it, and throws it away.

Always tabular — even one value

A CTE's output is always rows × columns. "One value" just means 1 row × 1 column; it can also be 0 rows or many.

WITH e_row AS (
  SELECT 1 AS n
)
SELECT n FROM e_row;   -- → one row: n = 1

Why bother?

WinBecause…
ReadabilityLogic flows as named steps instead of deeply nested subqueries
MaintainabilityChange one step (one CTE) without untangling the whole query
ReusabilityReference the same computed result multiple times in the final query
💡
Naming. Use descriptive CTE names (sales_by_month, not demo), don't collide with real table names, and optionally list explicit columns for clarity: WITH user_summary (user_id, total_spent) AS (…).
Setup · Reference

The tables

Three teaching tables: restaurant ratings (note Burger Barn's NULL star — aggregates ignore it), a sales_date ledger, and a categories hierarchy where each row points at its parent. OrphanCat's parent (999) doesn't exist — deliberately broken.

ratings
sales_date
categories
Core · Playground

Basic CTE — compute once, filter after

🎮 Average ratings, then a clean filter

The CTE does the aggregation; the final query treats it like a table and filters. Pick a variant:


      
🐢
Skip ORDER BY inside a CTE. The final query usually reorders or filters anyway, so sorting inside the CTE is wasted work. Sort in the final query — unless you specifically need ordering with LIMIT inside the step.
Pipeline · Playground

Multi-step CTE pipeline

🎮 Top salesperson, one step at a time

Chain CTEs: each one uses the previous. Step through the pipeline and watch the intermediate results:


      
Compare

Subquery vs CTE — same answer, different shape

Restaurants above the overall average

Subquery version
SELECT restaurant, AVG(stars) AS avg_stars
FROM ratings
GROUP BY restaurant
HAVING AVG(stars) > (
  SELECT AVG(stars) FROM ratings
)
ORDER BY avg_stars DESC;
CTE version
WITH baseline AS (
  SELECT AVG(stars) AS overall_avg
  FROM ratings
),
avg_by_restaurant AS (
  SELECT restaurant, AVG(stars) AS avg_stars
  FROM ratings
  GROUP BY restaurant
)
SELECT a.restaurant, a.avg_stars
FROM avg_by_restaurant a
CROSS JOIN baseline b
WHERE a.avg_stars > b.overall_avg
ORDER BY a.avg_stars DESC;
Both return
Recursive · Playground

Recursive CTE — anchor + loop

A recursive CTE has two parts glued by UNION ALL: the anchor query runs once and seeds the result; the recursive query references the CTE's own name and keeps adding rows until its condition fails. No stop condition → infinite loop.

🎮 The simplest recursion: count to N

Change the stop condition and watch the rows grow:


      
Hierarchies · Playground

Walking the category tree

🎮 One pattern, five real questions

The recursive member joins categories back to the CTE — downwards on c.parent_id = ct.cat_id (children), upwards on c.cat_id = cp.parent_id (parents):


      
⚠️
Recursion safety. Always use UNION ALL (plain UNION dedupes every iteration — slow and error-prone), always have an anchor, index parent_id, and make sure the data has no cycles — A→B→A recurses forever.
Compare · Reference

CTE vs Temporary Table

FeatureCTETemporary table
DefinitionLogical named query blockPhysical temp table
StorageLogical / optimizer-dependentStored in temp storage
LifetimeSingle query onlyWhole session / until dropped
ReusabilityWithin the same query onlyAcross multiple queries
RecursionYesNo

Temporary table version of the ratings summary

CREATE TEMPORARY TABLE tmp_avg_ratings AS
SELECT restaurant,
       AVG(stars)   AS avg_stars,
       COUNT(stars) AS ratings_count
FROM ratings
GROUP BY restaurant;
Sticks around for the whole session — you can hit it with many later queries. And note COUNT(stars) counts only non-NULL values: Burger Barn's count is 1.
Pitfalls

Common mistakes to avoid

MistakeWhy it bitesDo instead
Semicolon after the CTEWITH … ; ends the statement — the final query loses its CTEOne statement, one semicolon at the very end
SELECT before WITHThe WITH clause must open the queryDefine all CTEs first, final query last
Comma after the last CTESyntax error — commas go between CTEs onlyWITH a AS (…), b AS (…) SELECT …
Reusing a CTE in the next statementScope is one query — it's gone after thatRedefine it, or use a temporary table
Treating a CTE as a variableIt's always a table shape, even 1 row × 1 columnSelect from it; join or CROSS JOIN a one-row CTE
50-line mega-CTEDefeats the whole readability purposeOne logical step per CTE
Capstone · Game

Lock it in

🎮 Pick the right tool

CTE, recursive CTE, or temp table?

🎮 Predict the result

Reason over the tables above.

Module 17

Dates & Times

Dates power every real report: who joined when, what sold this month, how fast did we deliver? You'll extract parts with YEAR() / MONTH() / EXTRACT(), filter ranges the index-friendly way, find first/latest events with MIN/MAX, and measure gaps with DATEDIFF and TIMESTAMPDIFF — plus the classic BETWEEN-on-DATETIME interview trap, live.

Core · Playground

Extracting date & time parts

🎮 Six functions, two spellings

Given @date_temp = '2029-08-19' and @datetime_temp = '2029-05-18 12:50:45', pull out any component. EXTRACT(part FROM value) is the SQL-standard spelling of the same thing:


      
💡
Gotchas. MONTH() starts at 1 for January (not 0). HOUR() on a date-only value gives 0 — there's no time to read. And EXTRACT needs its FROM: EXTRACT(YEAR joining_date) is a syntax error.
Cases · Playground

Joining dates in practice

The employees table — note Arjun's NULL joining date:

🎮 Break the date apart, then report on it


      
Performance · Rule

The BIG performance rule

Never wrap the column in a function to filter it

❌ Slow — full table scan risk
WHERE YEAR(joining_date) = 2029
  AND MONTH(joining_date) = 2;
✅ Fast — index range scan
WHERE joining_date >= '2029-02-01'
  AND joining_date <  '2029-03-01';

This is called being SARGable (Search-ARGument-able). Wrapping the column in YEAR() forces the engine to compute the year for every row before checking; a plain range lets it jump straight to the right slice of the index. Same results, wildly different speed on 10 million rows.

Ranges · Playground

Filtering with date ranges

The orders table (a DATE column — no time part):

🎮 Pick a window

BETWEEN is inclusive on both ends — fine on pure DATE columns. The half-open pattern (>= start AND < next-period-start) works everywhere and needs no month-length math:


      
Pitfall · Playground

The BETWEEN + DATETIME trap

⚠️ 🎮 Where did January 31st go?

On a DATETIME column, '2029-01-31' silently becomes '2029-01-31 00:00:00' — so BETWEEN keeps only the first second of the 31st. Try all three filters on the January logins and watch who survives:


      
🎯
The production-safe pattern: login_time >= '2029-01-01' AND login_time < '2029-02-01' — a half-open interval [start, next_start). It catches every timestamp including fractional seconds (23:59:59.500 slips past a <= 23:59:59 filter), never needs 28-vs-31-day math, and stays index-friendly.
Events · Playground

First & latest events with MIN / MAX

SQL sees dates as numbers on a timeline: MIN = oldest, MAX = newest. The logins table:

🎮 Five timeline questions


      
⚠️
MIN/MAX mistakes. Selecting device next to MIN(login_time) without grouping by it can return a device that doesn't match that minimum row. Dates stored outside 'YYYY-MM-DD' sort alphabetically, not chronologically. And both functions ignore NULLs — a user whose every login_time is NULL gets NULL, not an error.
Arithmetic · Playground

DATEDIFF & TIMESTAMPDIFF

🎮 Two rulers

DATEDIFF(end, start) always answers in whole days, ignoring time. TIMESTAMPDIFF(unit, start, end) counts complete units of your choice — note the argument order flips!


      
🕐
CURRENT_DATE makes reports live. Hardcode a date and the report is right for one day; use CURRENT_DATE / CURRENT_TIMESTAMP and the same query stays correct forever — that's what powers daily dashboards and alerts. Watch out for: server timezone ("today" in the US ≠ "today" in India — prefer UTC), portability (GETDATE() in SQL Server), and comparing CURRENT_DATE to a DATETIME column — SQL pads it to midnight, so later-in-the-day rows can slip out.
Capstone · Case study

Instant food delivery latency

Minutes to deliver + a CASE verdict

SELECT order_id, order_placed_at, delivered_at,
  TIMESTAMPDIFF(MINUTE, order_placed_at, delivered_at) AS delivery_minutes,
  CASE
    WHEN delivered_at IS NULL THEN 'Pending'
    WHEN delivered_at < order_placed_at THEN 'Bad Data'
    WHEN TIMESTAMPDIFF(SECOND, order_placed_at, delivered_at) <= 60 * 30 THEN 'On Time'
    ELSE 'Slow'
  END AS delivery_status
FROM deliveries;
The CASE guards fire top-down: NULL first (still pending), then reversed timestamps (bad data — order 9004 was "delivered" before it was placed), then the 30-minute promise. Orders 9007 and 9008 cross midnight and still compute correctly — timestamps don't care about day boundaries.
Capstone · Game

Lock it in

🎮 Pick the right tool

Function, pattern, or trap?

🎮 Predict the result

Reason over the tables above.