🐬 MySQL Mastery
Home
0%
🐬

The Complete MySQL Mastery Course

Structured Query Language · Database Design · Real-World SQL

From Absolute Beginner → Professional Database Developer

70+ Exercises Full Solutions Live Query Outputs Real-World Scenarios

MODULE 01

Introduction & Setup

1.1 What is MySQL?

MySQL is the world's most popular open-source relational database management system (RDBMS). It stores data in tables with rows and columns — like a highly structured, powerful spreadsheet — and you communicate with it using SQL (Structured Query Language).

It powers billions of applications — from WordPress to Airbnb, Twitter, Facebook, and YouTube.

1.2 Installing MySQL

BASH — INSTALLATION
# macOS (Homebrew)
brew install mysql
brew services start mysql

# Ubuntu / Debian
sudo apt update
sudo apt install mysql-server
sudo systemctl start mysql

# Windows: download installer from https://dev.mysql.com/downloads/installer/

# Verify install
mysql --version
# → mysql  Ver 8.0.35 for Linux on x86_64 (MySQL Community Server - GPL)

# Connect to MySQL shell
mysql -u root -p
💡 TIP — GUI Tools
  • MySQL Workbench — official, free GUI for all platforms
  • DBeaver — universal, excellent free database client
  • TablePlus — sleek, fast (Mac/Windows)
  • phpMyAdmin — browser-based, great for learning

1.3 Your First SQL Commands

SQL
-- Show all databases
SHOW DATABASES;

-- Show current date and time
SELECT NOW();

-- Simple calculation
SELECT 2 + 2 AS result;

-- MySQL version
SELECT VERSION();
▶ OUTPUT1 row
result
4

1.4 SQL Syntax Rules — Memorise These

RuleDetail
Case-insensitive keywordsSELECT = select = Select — convention: UPPERCASE keywords
Statements end with ;Always end with a semicolon
Single-line comment-- This is a comment
Multi-line comment/* This spans multiple lines */
String literalsUse single quotes: 'Alice' NOT "Alice"
NULLRepresents absence of a value — NOT zero, NOT empty string
Whitespace ignoredSQL ignores extra spaces and newlines

MODULE 02

Databases & Tables

2.1 Create & Select a Database

SQL
CREATE DATABASE bookstore;          -- create database
CREATE DATABASE IF NOT EXISTS bookstore; -- safe version
USE bookstore;                       -- switch to it
SHOW TABLES;                         -- list tables in current DB
DROP DATABASE bookstore;             -- delete (irreversible!)
▶ OUTPUT
Query OK, 1 row affected (0.01 sec) Database changed

2.2 Data Types — The Building Blocks

TypeDescriptionExample
INTWhole number (–2B to 2B)42, -7, 0
BIGINTLarge whole numberuser_id, order_id
TINYINT(1)Boolean (0 = false, 1 = true)is_active
DECIMAL(p,s)Exact decimal — use for money!DECIMAL(10,2) → 99999999.99
FLOAT / DOUBLEApproximate decimal (not for money)scientific data
VARCHAR(n)Variable-length string up to n charsVARCHAR(100)
CHAR(n)Fixed-length stringcountry_code CHAR(2)
TEXTLong text (up to 65KB)description, bio
DATEDate only (YYYY-MM-DD)'2024-03-15'
DATETIMEDate + time'2024-03-15 14:30:00'
TIMESTAMPAuto-records time of changecreated_at, updated_at
BOOLEANAlias for TINYINT(1)TRUE / FALSE
ENUMOne value from a fixed listENUM('S','M','L','XL')
JSONJSON documentsmetadata, settings

2.3 CREATE TABLE — Full Example

SQL
USE bookstore;

CREATE TABLE authors (
    author_id   INT            AUTO_INCREMENT PRIMARY KEY,
    first_name  VARCHAR(50)    NOT NULL,
    last_name   VARCHAR(50)    NOT NULL,
    email       VARCHAR(120)   UNIQUE,
    country     CHAR(2)        DEFAULT 'US',
    bio         TEXT,
    created_at  TIMESTAMP      DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE books (
    book_id     INT            AUTO_INCREMENT PRIMARY KEY,
    title       VARCHAR(200)   NOT NULL,
    author_id   INT            NOT NULL,
    genre       VARCHAR(50),
    price       DECIMAL(8,2)  NOT NULL,
    stock       INT            DEFAULT 0,
    published   DATE,
    FOREIGN KEY (author_id) REFERENCES authors(author_id)
                              ON DELETE CASCADE
);
▶ OUTPUT
Query OK, 0 rows affected (0.04 sec) Query OK, 0 rows affected (0.03 sec)

2.4 Constraints — Rules on Your Data

ConstraintWhat it enforces
PRIMARY KEYUnique + NOT NULL identifier for each row
AUTO_INCREMENTMySQL automatically assigns the next integer
NOT NULLColumn must have a value — never NULL
UNIQUEAll values in column must be distinct
DEFAULT valueValue used when INSERT doesn't specify
FOREIGN KEYValue must exist in the referenced table
CHECKValue must satisfy a condition: CHECK (price > 0)

2.5 ALTER TABLE — Modify Existing Tables

SQL
-- Add a column
ALTER TABLE books ADD COLUMN rating DECIMAL(3,2);

-- Modify column type
ALTER TABLE books MODIFY COLUMN genre VARCHAR(80) NOT NULL;

-- Rename a column
ALTER TABLE books RENAME COLUMN published TO publish_date;

-- Drop a column
ALTER TABLE books DROP COLUMN rating;

-- Show table structure
DESCRIBE books;
▶ DESCRIBE books7 rows
FieldTypeNullKeyDefaultExtra
book_idintNOPRINULLauto_increment
titlevarchar(200)NONULL
author_idintNOMULNULL
genrevarchar(80)NONULL
pricedecimal(8,2)NONULL
stockintYES0
publish_datedateYESNULL
✏️ EXERCISE — E-Commerce Schema

Scenario: Design a database for an online clothing store.

  1. Create a database called fashion_store
  2. Create a customers table: id, name, email (unique), phone, city, created_at
  3. Create a products table: id, name, category, price, size (ENUM S/M/L/XL), stock, description
  4. Create an orders table: id, customer_id (FK), order_date, status (ENUM), total_amount
  5. Add a discount_pct column to products with a CHECK that it's between 0 and 100
✅ ANSWER
CREATE DATABASE IF NOT EXISTS fashion_store;
USE fashion_store;

CREATE TABLE customers (
    customer_id  INT           AUTO_INCREMENT PRIMARY KEY,
    name         VARCHAR(100) NOT NULL,
    email        VARCHAR(150) UNIQUE NOT NULL,
    phone        VARCHAR(20),
    city         VARCHAR(80),
    created_at   TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE products (
    product_id   INT           AUTO_INCREMENT PRIMARY KEY,
    name         VARCHAR(150) NOT NULL,
    category     VARCHAR(60)  NOT NULL,
    price        DECIMAL(8,2) NOT NULL CHECK (price > 0),
    size         ENUM('XS','S','M','L','XL','XXL'),
    stock        INT           DEFAULT 0,
    description  TEXT
);

CREATE TABLE orders (
    order_id     INT           AUTO_INCREMENT PRIMARY KEY,
    customer_id  INT           NOT NULL,
    order_date   TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
    status       ENUM('pending','shipped','delivered','cancelled')
                              DEFAULT 'pending',
    total_amount DECIMAL(10,2) NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

ALTER TABLE products
    ADD COLUMN discount_pct DECIMAL(5,2) DEFAULT 0
    CHECK (discount_pct BETWEEN 0 AND 100);

MODULE 03

Inserting Data

3.1 INSERT INTO — Basic Syntax

SQL
USE bookstore;

-- Single row — specify all columns
INSERT INTO authors (first_name, last_name, email, country)
VALUES ('George', 'Orwell', 'gorwell@mail.com', 'GB');

-- Multiple rows in one statement (much faster!)
INSERT INTO authors (first_name, last_name, email, country)
VALUES
    ('J.K.',   'Rowling',  'jkr@mail.com',    'GB'),
    ('Harper', 'Lee',      'hlee@mail.com',   'US'),
    ('Gabriel','Garcia',   'gmarquez@mail.com','CO'),
    ('Agatha', 'Christie', 'achristie@mail.com','GB');

-- Get the auto-generated ID of last insert
SELECT LAST_INSERT_ID();
▶ OUTPUT
Query OK, 1 row affected (0.01 sec) Query OK, 4 rows affected (0.01 sec) Records: 4 Duplicates: 0 Warnings: 0
SQL — INSERT BOOKS
INSERT INTO books (title, author_id, genre, price, stock, publish_date)
VALUES
    ('1984',                    1, 'Dystopian',   12.99, 150, '1949-06-08'),
    ('Animal Farm',             1, 'Satire',      8.99,  200, '1945-08-17'),
    ("Harry Potter and the PS",  2, 'Fantasy',    14.99, 300, '1997-06-26'),
    ('To Kill a Mockingbird',    3, 'Classic',    11.99, 120, '1960-07-11'),
    ('One Hundred Years',        4, 'Magical Realism',13.99,80,'1967-05-30'),
    ('And Then There Were None', 5, 'Mystery',    9.99,  175, '1939-11-06'),
    ('Murder on the Orient',     5, 'Mystery',    10.99, 160, '1934-01-01');
▶ OUTPUT
Query OK, 7 rows affected (0.01 sec) Records: 7 Duplicates: 0 Warnings: 0

3.2 INSERT … ON DUPLICATE KEY UPDATE

SQL — UPSERT
-- Insert if not exists, update if duplicate key found
INSERT INTO books (book_id, title, price, stock)
VALUES (1, '1984', 12.99, 200)
ON DUPLICATE KEY UPDATE
    stock = stock + VALUES(stock),  -- add 200 to existing stock
    price = VALUES(price);            -- update price
▶ OUTPUT
Query OK, 2 rows affected (0.01 sec) (2 rows affected means: existing row was updated)

MODULE 04

SELECT Queries

4.1 Basic SELECT

SQL
-- Select all columns
SELECT * FROM books;

-- Select specific columns
SELECT title, price, stock FROM books;

-- Aliases — rename columns in output
SELECT
    title             AS 'Book Title',
    price             AS 'Price ($)',
    stock * price     AS 'Inventory Value'
FROM books;
▶ OUTPUT7 rows
Book TitlePrice ($)Inventory Value
198412.991948.50
Animal Farm8.991798.00
Harry Potter and the PS14.994497.00
To Kill a Mockingbird11.991438.80
One Hundred Years13.991119.20
And Then There Were None9.991748.25
Murder on the Orient10.991758.40

4.2 ORDER BY — Sort Results

SQL
-- Sort by price ascending (default)
SELECT title, price FROM books
ORDER BY price ASC;

-- Sort by price descending
SELECT title, price, stock FROM books
ORDER BY price DESC;

-- Sort by multiple columns
SELECT genre, title, price FROM books
ORDER BY genre ASC, price DESC;
▶ ORDER BY genre ASC, price DESC7 rows
genretitleprice
ClassicTo Kill a Mockingbird11.99
Dystopian198412.99
FantasyHarry Potter and the PS14.99
Magical RealismOne Hundred Years13.99
MysteryMurder on the Orient10.99
MysteryAnd Then There Were None9.99
SatireAnimal Farm8.99

4.3 LIMIT & OFFSET — Pagination

SQL
-- Top 3 most expensive books
SELECT title, price FROM books
ORDER BY price DESC
LIMIT 3;

-- Page 2 (rows 4-6) — OFFSET skips rows
SELECT title, price FROM books
ORDER BY price DESC
LIMIT 3 OFFSET 3;   -- skip first 3, return next 3
▶ Top 3 most expensive3 rows
titleprice
Harry Potter and the PS14.99
One Hundred Years13.99
198412.99

4.4 DISTINCT — Remove Duplicates

SQL
-- Unique genres in the books table
SELECT DISTINCT genre FROM books
ORDER BY genre;
▶ OUTPUT6 rows
genre
Classic
Dystopian
Fantasy
Magical Realism
Mystery
Satire

MODULE 05

Filtering & Operators

5.1 WHERE Clause

SQL
-- Comparison operators: = != < > <= >=
SELECT title, price FROM books
WHERE price > 12;

-- AND / OR / NOT
SELECT title, genre, price FROM books
WHERE genre = 'Mystery' AND price < 11;
▶ Mystery books under $111 row
titlegenreprice
And Then There Were NoneMystery9.99

5.2 BETWEEN, IN, NOT IN

SQL
-- BETWEEN is inclusive on both ends
SELECT title, price FROM books
WHERE price BETWEEN 10 AND 13;

-- IN — match any value in a list
SELECT title, genre FROM books
WHERE genre IN ('Mystery', 'Fantasy', 'Dystopian');

-- NOT IN — exclude values
SELECT title, genre FROM books
WHERE genre NOT IN ('Satire', 'Classic');
▶ BETWEEN 10 AND 133 rows
titleprice
To Kill a Mockingbird11.99
And Then There Were None9.99
Murder on the Orient10.99

5.3 LIKE — Pattern Matching

SQL
-- % matches any number of characters (including none)
-- _ matches exactly ONE character

SELECT title FROM books WHERE title LIKE '%Harry%';   -- contains "Harry"
SELECT title FROM books WHERE title LIKE '1%';        -- starts with "1"
SELECT title FROM books WHERE title LIKE '%Farm';     -- ends with "Farm"
SELECT email FROM authors WHERE email LIKE '%@mail.com';

-- RLIKE / REGEXP — full regular expressions
SELECT title FROM books
WHERE title REGEXP '^(Harry|Animal)';
▶ LIKE '%Harry%'1 row
title
Harry Potter and the PS

5.4 NULL — The Special Case

SQL
-- ALWAYS use IS NULL / IS NOT NULL — never = NULL
SELECT * FROM authors WHERE bio IS NULL;
SELECT * FROM authors WHERE bio IS NOT NULL;

-- IFNULL — replace NULL with a default
SELECT first_name, IFNULL(bio, 'No bio available') AS bio
FROM authors;

-- COALESCE — returns first non-NULL from a list
SELECT COALESCE(bio, email, 'Unknown') AS contact
FROM authors;
▶ IFNULL example5 rows
first_namebio
GeorgeNo bio available
J.K.No bio available
HarperNo bio available
GabrielNo bio available
AgathaNo bio available
✏️ EXERCISE — Filtering Practise

Using the bookstore database:

  1. Find all books published before 1960
  2. Find books where price is between $9 and $12 (inclusive)
  3. Find all books by Agatha Christie (author_id = 5)
  4. Find books whose title starts with "And" or contains "Hundred"
  5. Find books with stock greater than 150 sorted by stock descending
  6. Show all authors whose country is NOT 'US' or 'GB'
✅ ANSWERS
-- 1. Books before 1960
SELECT title, publish_date FROM books
WHERE publish_date < '1960-01-01'
ORDER BY publish_date;
-- → Animal Farm (1945), And Then There Were None (1939), Murder on the Orient (1934)

-- 2. Price between $9 and $12
SELECT title, price FROM books
WHERE price BETWEEN 9 AND 12
ORDER BY price;
-- → Animal Farm $8.99 (excluded), And Then $9.99, Murder $10.99, To Kill $11.99

-- 3. Agatha Christie books
SELECT title, price FROM books
WHERE author_id = 5;

-- 4. Title starts with "And" OR contains "Hundred"
SELECT title FROM books
WHERE title LIKE 'And%' OR title LIKE '%Hundred%';

-- 5. Stock > 150 descending
SELECT title, stock FROM books
WHERE stock > 150
ORDER BY stock DESC;

-- 6. Authors not from US or GB
SELECT first_name, last_name, country FROM authors
WHERE country NOT IN ('US', 'GB');
-- → Gabriel Garcia, CO

MODULE 06

Aggregate Functions

6.1 Core Aggregate Functions

SQL
SELECT
    COUNT(*)                  AS total_books,
    COUNT(genre)              AS books_with_genre, -- ignores NULLs
    SUM(stock)                AS total_stock,
    AVG(price)                AS avg_price,
    MIN(price)                AS cheapest,
    MAX(price)                AS most_expensive,
    ROUND(AVG(price), 2)      AS avg_price_rounded
FROM books;
▶ OUTPUT1 row
total_booksbooks_with_genretotal_stockavg_pricecheapestmost_expensiveavg_price_rounded
77118511.9900008.9914.9911.99

6.2 GROUP BY — Aggregate by Category

SQL
-- Count and total stock per genre
SELECT
    genre,
    COUNT(*) AS num_books,
    SUM(stock) AS total_stock,
    ROUND(AVG(price), 2) AS avg_price
FROM books
GROUP BY genre
ORDER BY total_stock DESC;
▶ OUTPUT6 rows
genrenum_bookstotal_stockavg_price
Fantasy130014.99
Satire12008.99
Mystery233510.49
Dystopian115012.99
Classic112011.99
Magical Realism18013.99

6.3 HAVING — Filter Groups (not rows)

SQL
-- WHERE filters rows BEFORE grouping
-- HAVING filters groups AFTER aggregation

SELECT genre, COUNT(*) AS num_books, AVG(price) AS avg_price
FROM books
WHERE stock > 50                     -- filter rows first
GROUP BY genre
HAVING AVG(price) > 10               -- then filter groups
ORDER BY avg_price DESC;
▶ Genres with avg price > $10 (stock > 50)4 rows
genrenum_booksavg_price
Fantasy114.990000
Magical Realism113.990000
Dystopian112.990000
Classic111.990000
💡 WHERE vs HAVING — The Golden Rule
  • WHERE — filters individual rows before grouping. Cannot use aggregate functions.
  • HAVING — filters groups after GROUP BY. Must use aggregate functions.
  • SQL execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
✏️ EXERCISE — Sales Analytics

Build a simple sales table and query it:

CREATE TABLE sales (
  sale_id INT AUTO_INCREMENT PRIMARY KEY,
  product VARCHAR(60), category VARCHAR(40),
  quantity INT, unit_price DECIMAL(8,2), sale_date DATE
);
INSERT INTO sales (product, category, quantity, unit_price, sale_date) VALUES
  ('Laptop','Electronics',3,899.99,'2024-01-15'),
  ('Phone','Electronics',10,499.99,'2024-01-20'),
  ('Desk','Furniture',2,299.99,'2024-02-01'),
  ('Chair','Furniture',5,149.99,'2024-02-10'),
  ('Tablet','Electronics',7,349.99,'2024-02-15'),
  ('Lamp','Furniture',8,49.99,'2024-03-01'),
  ('Headphones','Electronics',15,79.99,'2024-03-10'),
  ('Monitor','Electronics',4,399.99,'2024-03-20');
  1. Total revenue per category (quantity × unit_price)
  2. Number of sales and avg unit price per category
  3. Find categories where total revenue exceeds $2,000
  4. Find the most expensive product per category
  5. Count sales per month (use MONTH() function)
✅ ANSWERS
-- 1. Total revenue per category
SELECT
    category,
    SUM(quantity * unit_price) AS total_revenue
FROM sales
GROUP BY category
ORDER BY total_revenue DESC;
-- Electronics: 9,099.76 | Furniture: 1,549.87

-- 2. Count & avg price per category
SELECT
    category,
    COUNT(*) AS num_sales,
    ROUND(AVG(unit_price), 2) AS avg_price
FROM sales
GROUP BY category;

-- 3. Categories with revenue > $2,000
SELECT category, SUM(quantity * unit_price) AS revenue
FROM sales
GROUP BY category
HAVING revenue > 2000;
-- → Electronics only

-- 4. Most expensive product per category
SELECT category, MAX(unit_price) AS max_price
FROM sales
GROUP BY category;

-- 5. Sales count per month
SELECT
    MONTH(sale_date) AS month_num,
    MONTHNAME(sale_date) AS month_name,
    COUNT(*) AS num_sales
FROM sales
GROUP BY MONTH(sale_date), MONTHNAME(sale_date)
ORDER BY month_num;

MODULE 07

JOINs — Combining Tables

🔑 Core Concept

JOINs combine rows from two or more tables based on a related column. This is the heart of relational databases — normalised data spread across tables reunited by JOIN.

7.1 INNER JOIN — Only Matching Rows

SQL
-- Get books with their author names
SELECT
    b.title,
    CONCAT(a.first_name, ' ', a.last_name) AS author,
    b.genre,
    b.price
FROM books AS b
INNER JOIN authors AS a
    ON b.author_id = a.author_id
ORDER BY b.price DESC;
▶ OUTPUT7 rows
titleauthorgenreprice
Harry Potter and the PSJ.K. RowlingFantasy14.99
One Hundred YearsGabriel GarciaMagical Realism13.99
1984George OrwellDystopian12.99
To Kill a MockingbirdHarper LeeClassic11.99
Murder on the OrientAgatha ChristieMystery10.99
And Then There Were NoneAgatha ChristieMystery9.99
Animal FarmGeorge OrwellSatire8.99

7.2 LEFT JOIN — All Left Rows + Matching Right

SQL
-- All authors, including those with no books
SELECT
    CONCAT(a.first_name, ' ', a.last_name) AS author,
    COUNT(b.book_id) AS book_count,
    IFNULL(SUM(b.price), 0) AS total_value
FROM authors AS a
LEFT JOIN books AS b
    ON a.author_id = b.author_id
GROUP BY a.author_id, a.first_name, a.last_name
ORDER BY book_count DESC;
▶ OUTPUT — Authors with book counts5 rows
authorbook_counttotal_value
George Orwell221.98
Agatha Christie220.98
J.K. Rowling114.99
Gabriel Garcia113.99
Harper Lee111.99

7.3 JOIN Types Visual Reference

JOIN TypeReturnsUse When
INNER JOINOnly rows with matches in BOTH tablesYou only want complete records
LEFT JOINAll left rows + matching right (NULL if no match)Keep all records from the left table
RIGHT JOINAll right rows + matching left (NULL if no match)Keep all records from the right table
CROSS JOINEvery combination of rows (cartesian product)Generate all combinations
SELF JOINTable joined to itselfHierarchies, employee-manager

7.4 Multi-Table JOIN — 3+ Tables

SQL — Build an order system first
-- Create customers and orders for the bookstore
CREATE TABLE customers (
    customer_id INT AUTO_INCREMENT PRIMARY KEY,
    name        VARCHAR(100) NOT NULL,
    email       VARCHAR(150) UNIQUE NOT NULL,
    city        VARCHAR(80)
);
INSERT INTO customers (name, email, city) VALUES
    ('Alice Wang',   'alice@mail.com',   'New York'),
    ('Bob Martinez', 'bob@mail.com',     'Chicago'),
    ('Carol Smith',  'carol@mail.com',   'London');

CREATE TABLE orders (
    order_id    INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,
    book_id     INT NOT NULL,
    quantity    INT DEFAULT 1,
    order_date  DATE,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id),
    FOREIGN KEY (book_id)     REFERENCES books(book_id)
);
INSERT INTO orders (customer_id, book_id, quantity, order_date) VALUES
    (1, 1, 2, '2024-03-01'),  -- Alice ordered 1984 x2
    (1, 3, 1, '2024-03-02'),  -- Alice ordered Harry Potter
    (2, 4, 3, '2024-03-05'),  -- Bob ordered To Kill x3
    (3, 6, 1, '2024-03-08');  -- Carol ordered Christie

-- 3-table JOIN: Customer + Order + Book + Author
SELECT
    c.name                                            AS customer,
    b.title                                           AS book,
    CONCAT(a.first_name, ' ', a.last_name)         AS author,
    o.quantity,
    b.price,
    o.quantity * b.price                             AS total
FROM orders AS o
INNER JOIN customers AS c  ON o.customer_id = c.customer_id
INNER JOIN books     AS b  ON o.book_id     = b.book_id
INNER JOIN authors  AS a  ON b.author_id   = a.author_id
ORDER BY c.name;
▶ OUTPUT — Customer Orders Report4 rows
customerbookauthorquantitypricetotal
Alice Wang1984George Orwell212.9925.98
Alice WangHarry Potter and the PSJ.K. Rowling114.9914.99
Bob MartinezTo Kill a MockingbirdHarper Lee311.9935.97
Carol SmithAnd Then There Were NoneAgatha Christie19.999.99

7.5 SELF JOIN — Table Joined to Itself

SQL — Employee hierarchy example
CREATE TABLE employees (
    emp_id      INT PRIMARY KEY,
    name        VARCHAR(60),
    manager_id  INT,  -- references emp_id of this same table
    department  VARCHAR(50)
);
INSERT INTO employees VALUES
    (1, 'Sarah CEO',    NULL,'Executive'),
    (2, 'Alice',        1,   'Engineering'),
    (3, 'Bob',          1,   'Marketing'),
    (4, 'Carol',        2,   'Engineering'),
    (5, 'David',        2,   'Engineering');

-- Show each employee with their manager's name
SELECT
    e.name           AS employee,
    IFNULL(m.name, 'No Manager') AS manager
FROM employees AS e
LEFT JOIN employees AS m  -- same table aliased twice!
    ON e.manager_id = m.emp_id;
▶ Employee → Manager Hierarchy5 rows
employeemanager
Sarah CEONo Manager
AliceSarah CEO
BobSarah CEO
CarolAlice
DavidAlice

MODULE 08

Subqueries & CTEs

8.1 Subqueries in WHERE

SQL
-- Books priced above the average
SELECT title, price
FROM books
WHERE price > (SELECT AVG(price) FROM books)
ORDER BY price DESC;

-- Books by authors from Great Britain
SELECT title FROM books
WHERE author_id IN (
    SELECT author_id FROM authors
    WHERE country = 'GB'
);
▶ Books above average price ($11.99)3 rows
titleprice
Harry Potter and the PS14.99
One Hundred Years13.99
198412.99

8.2 Subqueries in FROM (Derived Tables)

SQL
-- Authors with more than 1 book (using derived table)
SELECT author, book_count
FROM (
    SELECT
        CONCAT(a.first_name, ' ', a.last_name) AS author,
        COUNT(b.book_id) AS book_count
    FROM authors AS a
    LEFT JOIN books AS b ON a.author_id = b.author_id
    GROUP BY a.author_id
) AS author_counts                 -- derived table MUST have alias
WHERE book_count > 1;
▶ Authors with 2+ books2 rows
authorbook_count
George Orwell2
Agatha Christie2

8.3 CTE — Common Table Expression (WITH)

SQL — CTEs are cleaner than subqueries
-- Same query as above using CTE — much more readable!
WITH author_counts AS (
    SELECT
        a.author_id,
        CONCAT(a.first_name, ' ', a.last_name) AS author,
        COUNT(b.book_id) AS book_count,
        SUM(b.price)    AS total_value
    FROM authors AS a
    LEFT JOIN books AS b ON a.author_id = b.author_id
    GROUP BY a.author_id
)
SELECT author, book_count, total_value
FROM author_counts
WHERE book_count > 1
ORDER BY total_value DESC;
▶ OUTPUT2 rows
authorbook_counttotal_value
George Orwell221.98
Agatha Christie220.98

8.4 Window Functions — Advanced Analytics

SQL — OVER() clause
-- ROW_NUMBER, RANK, DENSE_RANK
SELECT
    title, genre, price,
    ROW_NUMBER()   OVER (PARTITION BY genre ORDER BY price DESC) AS row_n,
    RANK()         OVER (ORDER BY price DESC)                   AS price_rank,
    AVG(price)     OVER (PARTITION BY genre)                       AS genre_avg,
    SUM(price)     OVER ()                                           AS grand_total
FROM books
ORDER BY genre, price DESC;
▶ Window Function Results7 rows
titlegenrepricerow_nprice_rankgenre_avggrand_total
To Kill a MockingbirdClassic11.991411.9983.93
1984Dystopian12.991312.9983.93
Harry Potter and the PSFantasy14.991114.9983.93
One Hundred YearsMagical Realism13.991213.9983.93
Murder on the OrientMystery10.991510.4983.93
And Then There Were NoneMystery9.992610.4983.93
Animal FarmSatire8.99178.9983.93

MODULE 09

Built-in Functions

9.1 String Functions

SQL
SELECT
    CONCAT('Hello', ' ', 'World')        AS concat_ex,
    UPPER('hello')                        AS upper_ex,
    LOWER('WORLD')                        AS lower_ex,
    LENGTH('Python')                      AS len_ex,
    TRIM('  hello  ')                    AS trim_ex,
    SUBSTRING('MySQL Mastery', 1, 5)    AS substr_ex,
    REPLACE('Hello World', 'World', 'SQL') AS replace_ex,
    LEFT('MySQL', 3)                      AS left_ex,
    RIGHT('MySQL', 3)                     AS right_ex,
    INSTR('Hello World', 'World')        AS pos_ex,
    LPAD('42', 6, '0')                   AS lpad_ex,
    REPEAT('AB', 3)                       AS repeat_ex;
▶ OUTPUT1 row
concat_exupper_exlower_exlen_extrim_exsubstr_exreplace_exleft_exright_expos_exlpad_exrepeat_ex
Hello WorldHELLOworld6helloMySQLHello SQLMySSQL7000042ABABAB

9.2 Date & Time Functions

SQL
SELECT
    NOW()                              AS current_datetime,
    CURDATE()                          AS current_date,
    CURTIME()                          AS current_time,
    YEAR('2024-03-15')                AS yr,
    MONTH('2024-03-15')               AS mo,
    DAY('2024-03-15')                 AS dy,
    DAYNAME('2024-03-15')             AS day_name,
    MONTHNAME('2024-03-15')           AS month_name,
    DATEDIFF('2024-12-31', CURDATE()) AS days_left,
    DATE_ADD('2024-01-01', INTERVAL 30 DAY) AS plus_30,
    DATE_FORMAT('2024-03-15', '%d/%m/%Y')  AS formatted;
▶ Date function results1 row
yrmodyday_namemonth_nameplus_30formatted
2024315FridayMarch2024-01-3115/03/2024

9.3 Math Functions

SQL
SELECT
    ROUND(3.14159, 2)   AS rounded,     -- 3.14
    CEIL(4.1)            AS ceiling,     -- 5
    FLOOR(4.9)           AS floored,     -- 4
    ABS(-42)             AS absolute,    -- 42
    POWER(2, 10)         AS two_to_ten,  -- 1024
    SQRT(144)            AS sq_root,     -- 12
    MOD(17, 5)           AS modulus,     -- 2
    RAND()               AS random_num;  -- 0 to 1
▶ OUTPUT1 row
roundedceilingflooredabsolutetwo_to_tensq_rootmodulus
3.1454421024122

9.4 CASE WHEN — Conditional Logic

SQL
SELECT
    title,
    price,
    stock,
    CASE
        WHEN price < 10   THEN 'Budget'
        WHEN price < 13   THEN 'Mid-range'
        ELSE                    'Premium'
    END AS price_tier,
    CASE
        WHEN stock = 0    THEN 'Out of Stock'
        WHEN stock < 100  THEN 'Low Stock'
        WHEN stock < 200  THEN 'In Stock'
        ELSE                    'Well Stocked'
    END AS stock_status
FROM books
ORDER BY price;
▶ OUTPUT — Price & Stock Categorisation7 rows
titlepricestockprice_tierstock_status
Animal Farm8.99200BudgetWell Stocked
And Then There Were None9.99175BudgetIn Stock
Murder on the Orient10.99160Mid-rangeIn Stock
To Kill a Mockingbird11.99120Mid-rangeIn Stock
198412.99150Mid-rangeIn Stock
One Hundred Years13.9980PremiumLow Stock
Harry Potter and the PS14.99300PremiumWell Stocked

MODULE 10

UPDATE & DELETE

⚠️ CRITICAL SAFETY RULE

Always run a SELECT with the same WHERE clause FIRST to verify which rows will be affected before running UPDATE or DELETE. And always work with transactions when modifying data.

10.1 UPDATE

SQL
-- ALWAYS check what you're about to update first!
SELECT * FROM books WHERE book_id = 1;

-- Update a single row
UPDATE books
SET
    price = 13.99,
    stock = stock + 50   -- increment existing value
WHERE book_id = 1;

-- Update multiple rows — 10% price increase for all Mystery books
UPDATE books
SET price = ROUND(price * 1.10, 2)
WHERE genre = 'Mystery';

-- Conditional update with CASE
UPDATE books
SET price = CASE
    WHEN genre = 'Fantasy'  THEN price * 1.05
    WHEN genre = 'Classic'  THEN price * 0.95  -- discount classics
    ELSE price
END;
▶ OUTPUT
Query OK, 1 row affected (0.01 sec) -- single update Rows matched: 1 Changed: 1 Warnings: 0 Query OK, 2 rows affected (0.01 sec) -- mystery update Rows matched: 2 Changed: 2 Warnings: 0

10.2 DELETE

SQL
-- Delete a specific row
DELETE FROM books WHERE book_id = 99;

-- Delete with condition
DELETE FROM books
WHERE stock = 0 AND publish_date < '1950-01-01';

-- TRUNCATE — delete ALL rows (very fast, cannot roll back)
TRUNCATE TABLE temp_table;

-- Soft delete pattern (industry best practice)
-- Instead of deleting, mark as deleted:
ALTER TABLE books ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;

UPDATE books
SET deleted_at = NOW()
WHERE book_id = 5;   -- "soft deleted" — still in DB, hidden from queries

-- Query only active records
SELECT * FROM books WHERE deleted_at IS NULL;
▶ OUTPUT
Query OK, 0 rows affected (0.00 sec) -- no matching rows for book_id=99 Query OK, 1 row affected (0.01 sec) -- soft delete applied

MODULE 11

Indexes & Performance

11.1 What is an Index?

An index is a data structure that speeds up data retrieval at the cost of additional storage and slightly slower writes. Think of it as a book's index — instead of reading every page, you jump directly to the right page.

11.2 Creating Indexes

SQL
-- Single-column index
CREATE INDEX idx_genre   ON books(genre);
CREATE INDEX idx_price   ON books(price);

-- Unique index (enforces uniqueness)
CREATE UNIQUE INDEX idx_email ON authors(email);

-- Composite index (columns used together in WHERE)
CREATE INDEX idx_genre_price ON books(genre, price);

-- Full-text index (for LIKE '%...%' type searches)
CREATE FULLTEXT INDEX idx_title_ft ON books(title);

-- Show all indexes on a table
SHOW INDEX FROM books;

-- Drop an index
DROP INDEX idx_genre ON books;
▶ SHOW INDEX FROM books (sample)
TableKey_nameColumn_nameNon_uniqueIndex_type
booksPRIMARYbook_id0BTREE
booksidx_genregenre1BTREE
booksidx_priceprice1BTREE
booksidx_title_fttitle1FULLTEXT

11.3 EXPLAIN — Analyse Query Performance

SQL
-- Prepend EXPLAIN to see MySQL's query plan
EXPLAIN
SELECT title, price
FROM books
WHERE genre = 'Mystery'
ORDER BY price;
▶ EXPLAIN output
typekeykey_lenrowsExtra
refidx_genre2032Using index condition; Using filesort
EXPLAIN typeMeaningPerformance
system / constSingle row match (PK/unique lookup)⚡ Best
refNon-unique index used✅ Good
rangeIndex used for BETWEEN / > / <✅ Good
indexFull index scan⚠️ OK
ALLFull table scan — no index used!🔴 Fix this
💡 Index Best Practices
  • Index columns used in WHERE, JOIN ON, and ORDER BY
  • Don't index every column — each index slows down INSERT/UPDATE/DELETE
  • Composite indexes: put the most selective column first
  • LIKE '%term' (leading wildcard) cannot use a B-tree index — use FULLTEXT instead
  • Foreign key columns should almost always be indexed

MODULE 12

Views & Stored Procedures

12.1 Views — Saved Queries

SQL — CREATE VIEW
-- Create a view = saved SELECT query
CREATE VIEW v_book_details AS
SELECT
    b.book_id,
    b.title,
    CONCAT(a.first_name, ' ', a.last_name) AS author,
    a.country,
    b.genre,
    b.price,
    b.stock
FROM books b
JOIN authors a ON b.author_id = a.author_id;

-- Query the view just like a table!
SELECT * FROM v_book_details WHERE genre = 'Mystery';

-- List all views
SHOW FULL TABLES WHERE Table_type = 'VIEW';

-- Drop a view
DROP VIEW v_book_details;
▶ SELECT from v_book_details WHERE genre = 'Mystery'2 rows
book_idtitleauthorcountrygenrepricestock
6And Then There Were NoneAgatha ChristieGBMystery10.99175
7Murder on the OrientAgatha ChristieGBMystery12.09160

12.2 Stored Procedures

SQL — Stored Procedure with parameters
DELIMITER //   -- change delimiter so ; inside proc doesn't end it

CREATE PROCEDURE get_books_by_genre(
    IN  p_genre      VARCHAR(50),
    IN  p_max_price  DECIMAL(8,2),
    OUT p_count      INT
)
BEGIN
    SELECT title, price, stock
    FROM books
    WHERE genre = p_genre
      AND price <= p_max_price
    ORDER BY price;

    SELECT COUNT(*) INTO p_count
    FROM books
    WHERE genre = p_genre AND price <= p_max_price;
END//

DELIMITER ;   -- restore delimiter

-- Call the procedure
CALL get_books_by_genre('Mystery', 15.00, @count);
SELECT @count AS books_found;
▶ Procedure results
titlepricestock
And Then There Were None10.99175
Murder on the Orient12.09160
books_found: 2

12.3 Triggers

SQL — Auto-update stock on order
DELIMITER //

CREATE TRIGGER trg_reduce_stock
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    -- Automatically reduce book stock when an order is placed
    UPDATE books
    SET    stock = stock - NEW.quantity
    WHERE  book_id = NEW.book_id;
END//

DELIMITER ;

-- Show all triggers
SHOW TRIGGERS;
▶ Trigger created
Query OK, 0 rows affected (0.02 sec) -- Now every INSERT into orders automatically decrements books.stock!

MODULE 13

Transactions & ACID

13.1 ACID Properties

PropertyMeaningExample
AtomicityAll operations succeed or all fail — never partialBank transfer: debit AND credit both happen, or neither
ConsistencyData always moves from one valid state to anotherAccount balance never goes negative if business rule forbids it
IsolationConcurrent transactions don't interfere with each otherTwo users booking the last seat can't both succeed
DurabilityCommitted changes survive crashesConfirmed order stays after server restart

13.2 Transactions — COMMIT & ROLLBACK

SQL — Bank Transfer Example
-- Setup
CREATE TABLE accounts (
    account_id INT PRIMARY KEY,
    name       VARCHAR(60),
    balance    DECIMAL(12,2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts VALUES (1, 'Alice', 1000.00), (2, 'Bob', 500.00);

-- ── Successful transfer ─────────────────────────────
START TRANSACTION;

UPDATE accounts SET balance = balance - 200 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 200 WHERE account_id = 2;

COMMIT;   -- permanently saves both changes

SELECT * FROM accounts;
▶ After COMMIT2 rows
account_idnamebalance
1Alice800.00
2Bob700.00
SQL — ROLLBACK on error
START TRANSACTION;

UPDATE accounts SET balance = balance - 2000 WHERE account_id = 1;
-- ↑ This would make Alice's balance = -1200 → violates CHECK constraint!

ROLLBACK;   -- undo everything since START TRANSACTION

-- Verify: Alice's balance is unchanged
SELECT * FROM accounts;
▶ After ROLLBACK — balances unchanged2 rows
account_idnamebalance
1Alice800.00
2Bob700.00

13.3 SAVEPOINTs

SQL
START TRANSACTION;

INSERT INTO orders (customer_id, book_id, quantity, order_date)
VALUES (1, 2, 1, CURDATE());

SAVEPOINT after_order;   -- bookmark this point

UPDATE books SET stock = stock - 99999 WHERE book_id = 2;
-- Oops! Too large a deduction

ROLLBACK TO SAVEPOINT after_order;   -- go back to savepoint, keep order insert

UPDATE books SET stock = stock - 1 WHERE book_id = 2;
-- Correct deduction

COMMIT;
▶ OUTPUT
Query OK, 1 row affected (0.00 sec) -- INSERT Query OK, 0 rows affected (0.00 sec) -- SAVEPOINT set Query OK, 1 row affected (0.01 sec) -- bad UPDATE Query OK, 0 rows affected (0.00 sec) -- ROLLBACK TO SAVEPOINT Query OK, 1 row affected (0.00 sec) -- correct UPDATE Query OK, 0 rows affected (0.00 sec) -- COMMIT

MODULE 14

Capstone Projects

🏨 Project 1 — Hotel Booking System
  • Tables: hotels, rooms, guests, bookings, payments
  • Queries: Available rooms for a date range, revenue per hotel per month
  • View: v_booking_summary with guest + room + hotel details
  • Stored Procedure: book_room(guest_id, room_id, check_in, check_out)
  • Trigger: Auto-update room availability status on booking insert/delete
  • Transaction: Payment processing with rollback on failure
🛒 Project 2 — E-Commerce Analytics Platform
  • Tables: users, products, categories, orders, order_items, reviews
  • Window functions: Product ranking by revenue within each category
  • CTE: Monthly revenue trend with month-over-month growth %
  • Complex JOIN: Customer lifetime value calculation
  • Subquery: Find customers who ordered but never reviewed
  • Index optimisation: Use EXPLAIN to speed up slow queries
🏥 Project 3 — Hospital Management System
  • Tables: doctors, patients, appointments, prescriptions, wards, staff
  • Self JOIN: Doctor-to-supervisor hierarchy
  • CASE WHEN: Patient risk scoring based on age and diagnosis
  • Pivot: Appointments per doctor per weekday (using conditional aggregation)
  • Stored Procedure: schedule_appointment with conflict checking
  • Transaction: Transfer patient between wards atomically

APPENDIX

Quick Reference

A.1 SQL Execution Order

SQL — How MySQL Processes a Query
-- Writing order (what you type):
SELECT    columns
FROM      table
JOIN      other_table ON condition
WHERE     row_filter
GROUP BY  grouping_column
HAVING    group_filter
ORDER BY  sort_column
LIMIT     n;

-- Execution order (what MySQL does):
-- 1. FROM + JOIN    → determine all rows
-- 2. WHERE          → filter rows
-- 3. GROUP BY       → create groups
-- 4. HAVING         → filter groups
-- 5. SELECT         → choose columns + aliases
-- 6. DISTINCT       → remove duplicates
-- 7. ORDER BY       → sort
-- 8. LIMIT/OFFSET   → paginate

A.2 Essential SQL Commands

CommandPurpose
CREATE DATABASE / TABLECreate new database or table
USE db_nameSwitch to a database
SHOW DATABASES / TABLESList all databases or tables
DESCRIBE tableShow table structure
INSERT INTO … VALUESAdd new rows
SELECT … FROM … WHEREQuery data
UPDATE … SET … WHEREModify existing rows
DELETE FROM … WHERERemove rows
ALTER TABLE … ADD/MODIFY/DROPChange table structure
DROP TABLE / DATABASEDelete table or database (permanent!)
CREATE INDEXAdd an index for performance
EXPLAIN SELECT …Analyse query execution plan
START TRANSACTION / COMMIT / ROLLBACKTransaction control
CREATE VIEWSave a query as a virtual table
CREATE PROCEDURE / CALLCreate and execute stored procedures

A.3 String Functions Reference

FunctionExample → Result
CONCAT(a, b)CONCAT('Hello',' World') → Hello World
CONCAT_WS(sep, a, b)CONCAT_WS('-','2024','03','15') → 2024-03-15
LENGTH(s)LENGTH('Hello') → 5
UPPER(s) / LOWER(s)UPPER('hello') → HELLO
TRIM(s)TRIM(' hi ') → hi
SUBSTRING(s,pos,len)SUBSTRING('MySQL',1,2) → My
REPLACE(s,old,new)REPLACE('aababc','ab','X') → aXXc
INSTR(s, sub)INSTR('Hello','ll') → 3
LEFT(s,n) / RIGHT(s,n)LEFT('MySQL',2) → My
LPAD(s,n,pad) / RPADLPAD('42',6,'0') → 000042
FORMAT(n, decimals)FORMAT(1234567.89,2) → 1,234,567.89

A.4 Date Functions Reference

FunctionExample → Result
NOW()2024-03-15 14:30:00
CURDATE() / CURTIME()2024-03-15 / 14:30:00
YEAR(d) / MONTH(d) / DAY(d)2024 / 3 / 15
DAYNAME(d) / MONTHNAME(d)Friday / March
DATEDIFF(a, b)DATEDIFF('2024-12-31','2024-01-01') → 365
DATE_ADD(d, INTERVAL n UNIT)DATE_ADD('2024-01-01', INTERVAL 1 MONTH)
DATE_SUB(d, INTERVAL n UNIT)DATE_SUB(NOW(), INTERVAL 7 DAY)
DATE_FORMAT(d, fmt)DATE_FORMAT(NOW(),'%d/%m/%Y') → 15/03/2024
STR_TO_DATE(s, fmt)STR_TO_DATE('15/03/2024','%d/%m/%Y')
TIMESTAMPDIFF(unit, a, b)TIMESTAMPDIFF(YEAR, dob, NOW())

A.5 Common Interview Questions

SQL — Classic interview patterns
-- 1. Nth highest salary / price
SELECT MAX(price) FROM books
WHERE price < (SELECT MAX(price) FROM books);  -- 2nd highest

-- 2. Find duplicates
SELECT email, COUNT(*) AS cnt
FROM authors
GROUP BY email
HAVING cnt > 1;

-- 3. Delete duplicates, keep lowest ID
DELETE FROM authors
WHERE author_id NOT IN (
    SELECT MIN(author_id)
    FROM authors
    GROUP BY email
);

-- 4. Running total
SELECT
    title, price,
    SUM(price) OVER (ORDER BY price) AS running_total
FROM books;

-- 5. Rows where value > group average (classic!)
SELECT title, genre, price
FROM books b
WHERE price > (
    SELECT AVG(price)
    FROM books
    WHERE genre = b.genre  -- correlated subquery
);

-- 6. Pivot (conditional aggregation)
SELECT
    SUM(CASE WHEN genre = 'Mystery'  THEN 1 ELSE 0 END) AS mystery_count,
    SUM(CASE WHEN genre = 'Fantasy'  THEN 1 ELSE 0 END) AS fantasy_count,
    SUM(CASE WHEN genre = 'Classic'  THEN 1 ELSE 0 END) AS classic_count
FROM books;
🐬

You're now a SQL Developer!

Practice every day. Write queries for real datasets. Build something.