Every query below runs against these tables. Refer back here anytime.
| id | name | last_name | country | city | age | |
|---|---|---|---|---|---|---|
| 1 | Arjun | Patel | India | Mumbai | 28 | arjun@mail.com |
| 2 | Sara | Doe | USA | New York | 32 | NULL |
| 3 | Kenji | Tanaka | Japan | Tokyo | 24 | kenji@mail.com |
| 4 | Anna | Klein | Germany | Berlin | 30 | anna@mail.com |
| 5 | James | Doe | USA | Chicago | 22 | james@mail.com |
| 6 | Priya | Sharma | India | Delhi | 27 | NULL |
| 7 | David | Brown | UK | London | 35 | david@mail.com |
| 8 | Sakura | Ito | Japan | Osaka | 19 | sakura@mail.com |
| id | name | price | category |
|---|---|---|---|
| 1 | Laptop | 1200 | Electronics |
| 2 | Headphones | 150 | Electronics |
| 3 | Notebook | 8 | Stationery |
| 4 | Desk Chair | 450 | Furniture |
| 5 | Monitor | 600 | Electronics |
| 6 | Pen Set | 25 | Stationery |
| id | customer_id | product_id | quantity |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 3 | 2 | 2 |
| 3 | 4 | 4 | 1 |
| 4 | 2 | 5 | 3 |
| 5 | 1 | 3 | 5 |
| id | name | score |
|---|---|---|
| 1 | Ravi | 92 |
| 2 | Mia | 85 |
| 3 | Chen | 73 |
| 4 | Emma | 61 |
| 5 | Kai | 45 |
| id | order_id | status |
|---|---|---|
| 1 | 1 | Delivered |
| 2 | 2 | In Transit |
| 3 | 4 | Delivered |
| 4 | 99 | Returned |
What databases are, why we need them, and how keys keep data organized.
A database is an organized collection of data stored so it can be easily accessed, managed, and updated. Think of it as a digital filing cabinet — but one that can sort, search, and cross-reference instantly.
A DBMS (Database Management System) is the software that sits between users and the database. It enforces rules that protect the data:
| Concern | What it means |
|---|---|
| Data Integrity | Prevents duplicate, incorrect, or broken data from being stored |
| Security | Protects sensitive data from unauthorized access or misuse |
| Concurrency | Allows multiple users to read/write simultaneously without inconsistency |
| Type | How data is organized | Best for |
|---|---|---|
| Relational | Tables with rows and columns, linked by keys | Structured data with clear relationships |
| NoSQL | Documents, key-value pairs, graphs, or wide columns | Flexible schemas, large-scale unstructured data |
Every relational database table must follow four rules:
No two rows can have identical values across all columns.
| name | age | id_number |
|---|---|---|
| Himanshu | 80 | 801046122222 |
| Rahul | 92 | 801034569367 |
| Krish | 95 | 801025683456 |
| Rahul | 92 | 801034569367 |
If a column is defined as INT, every value in it must be a number. You can't mix types.
| name | age |
|---|---|
| Himanshu | 80 |
| Rahul | 92 |
| Rohit | Very Old |
Each cell holds exactly one value. No lists, no comma-separated entries.
| name | phone |
|---|---|
| Himanshu | 8010461222 |
| Krish | 8010256834, 9427823456 |
No two columns in the same table can share a name. This ensures every column can be referenced unambiguously.
Keys are columns (or combinations of columns) that uniquely identify rows and create relationships between tables. Without keys, you can't reliably tell one "Rahul" from another.
A column (or group of columns) that uniquely identifies each row. Two rules: values must be UNIQUE and never NULL. Each table can have only one primary key.
| emp_id (PK) | name | licence | passport |
|---|---|---|---|
| 101 | Ravi | DL-4421 | P-9981 |
| 102 | Mia | DL-5532 | P-7762 |
| 103 | Chen | DL-6643 | P-5543 |
Every emp_id is unique and non-null — it can pinpoint exactly one employee.
Any column (or set of columns) that could serve as the primary key — it's unique and non-null. The primary key is chosen from the candidates.
In the table above, emp_id, licence, and passport are all candidate keys because each uniquely identifies a row.
Any set of columns that can identify a row — including extra columns beyond what's strictly needed. Every candidate key is a super key, but not every super key is minimal.
{emp_id} -- minimal (candidate key)
{emp_id, name} -- super key (emp_id alone is enough)
{licence, name} -- super key
{emp_id, passport, name} -- super key
Any set containing at least one candidate key is a super key.
A column in one table that references the primary key of another table, creating a link between them. Unlike primary keys, foreign keys can have duplicates and NULLs.
| order_id (PK) | customer_id (FK) | product_id | quantity |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 3 | 2 | 2 |
| 3 | 4 | 4 | 1 |
| 4 | 2 | 5 | 3 |
| 5 | 1 | 3 | 5 |
Here, customer_id in orders points to id in the customers table. Customer 1 (Arjun) has two orders. The foreign key is what makes that relationship possible.
| Key | Unique? | NULL? | Per table |
|---|---|---|---|
| Primary | Yes | No | Exactly one |
| Candidate | Yes | No | One or more |
| Super | Yes (may be non-minimal) | Depends | Many |
| Foreign | No (duplicates allowed) | Yes | Zero or more |
Creating tables, inserting data, and writing your first queries.
DDL commands define the structure of your database — creating, modifying, and deleting databases and tables. They don't touch the data inside.
| Command | What it does |
|---|---|
| CREATE DATABASE db; | Creates a new database |
| SHOW DATABASES; | Lists all databases on the server |
| USE db; | Switches to a database (makes it active) |
| SELECT DATABASE(); | Shows which database is currently selected |
| SHOW TABLES; | Lists all tables in the active database |
| DROP DATABASE db; | Deletes an entire database (irreversible) |
Defines a new table with columns and their data types.
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
last_name VARCHAR(50),
country VARCHAR(50),
city VARCHAR(50),
age INT,
email VARCHAR(100)
);
Each column needs a name and a data type. Common types:
| Type | Stores | Example |
|---|---|---|
| INT | Whole numbers | age, quantity, id |
| VARCHAR(n) | Variable-length text up to n chars | name, email |
| TEXT | Long text (no limit specified) | description, bio |
| DECIMAL(p,s) | Precise decimal numbers | price, salary |
| DATE | Date values | order_date |
Rules you attach to columns to control what data is allowed.
| Constraint | Rule |
|---|---|
| PRIMARY KEY | Unique + Not Null. Identifies each row. |
| NOT NULL | Column cannot be empty |
| UNIQUE | All values must be different |
| DEFAULT val | Uses a default value if none is provided |
| FOREIGN KEY | References a primary key in another table |
| CHECK | Validates a condition (e.g., age > 0) |
Modifies an existing table — add, rename, change, or drop columns.
ALTER TABLE customers ADD phone VARCHAR(10);
ALTER TABLE customers RENAME COLUMN city TO location;
ALTER TABLE customers MODIFY COLUMN age VARCHAR(3);
ALTER TABLE customers DROP COLUMN email;
ALTER TABLE customers RENAME TO clients;
Permanently deletes a table and all its data.
DROP TABLE customers;DML commands change the data inside tables — inserting new rows, updating existing ones, or deleting them.
Adds new rows to a table.
INSERT INTO customers (id, name, last_name, country, city, age, email)
VALUES (9, 'Liam', 'Park', 'Korea', 'Seoul', 26, 'liam@mail.com');
| id | name | last_name | country | city | age | |
|---|---|---|---|---|---|---|
| 1 | Arjun | Patel | India | Mumbai | 28 | arjun@mail.com |
| ... rows 2–8 ... | ||||||
| 9 | Liam | Park | Korea | Seoul | 26 | liam@mail.com |
INSERT INTO customers (id, name, last_name, country, city, age, email)
VALUES
(9, 'Liam', 'Park', 'Korea', 'Seoul', 26, 'liam@mail.com'),
(10, 'Zara', 'Ali', 'India', 'Pune', 23, 'zara@mail.com');
Modifies existing rows. Always use WHERE to target specific rows.
UPDATE customers
SET city = 'Manchester'
WHERE id = 7;
| id | name | city (before) | city (after) |
|---|---|---|---|
| 7 | David | London | Manchester |
Removes rows from a table.
DELETE FROM customers
WHERE country = 'UK';
| id | name | country |
|---|---|---|
| 1 | Arjun | India |
| 2 | Sara | USA |
| 3 | Kenji | Japan |
| 4 | Anna | Germany |
| 5 | James | USA |
| 6 | Priya | India |
| 8 | Sakura | Japan |
| DELETE FROM table; | TRUNCATE TABLE table; |
|---|---|
| Removes all rows one by one | Drops and recreates the table (faster) |
| Can use WHERE to target specific rows | Always removes all rows |
| Can be rolled back (if inside a transaction) | Cannot be rolled back |
DQL (Data Query Language) is really just one command — SELECT — with many clauses that shape what comes back.
Retrieves data from a table. Use * for all columns or list specific ones.
SELECT name, country FROM customers;
| name | country |
|---|---|
| Arjun | India |
| Sara | USA |
| Kenji | Japan |
| Anna | Germany |
| James | USA |
| Priya | India |
| David | UK |
| Sakura | Japan |
Filters rows based on a condition. Only matching rows appear in the result.
SELECT name, age, country FROM customers
WHERE country = 'USA';
| name | age | country |
|---|---|---|
| Sara | 32 | USA |
| James | 22 | USA |
Used inside WHERE to compare values. They return 1 (true) or 0 (false).
| Operator | Meaning | Example |
|---|---|---|
| = | Equal to | WHERE age = 25 |
| != or <> | Not equal to | WHERE age != 25 |
| > | Greater than | WHERE age > 25 |
| < | Less than | WHERE age < 25 |
| >= | Greater or equal | WHERE age >= 25 |
| <= | Less or equal | WHERE age <= 25 |
Gives a column a temporary display name in the result set.
SELECT name AS customer_name, age AS years_old
FROM customers;
| customer_name | years_old |
|---|---|
| Arjun | 28 |
| Sara | 32 |
| Kenji | 24 |
The alias only exists in the query output — it doesn't rename the actual column.
Sorts results. ASC (ascending, default) or DESC (descending).
SELECT name, age FROM customers
ORDER BY age ASC;
| name | age |
|---|---|
| Sakura | 19 |
| James | 22 |
| Kenji | 24 |
| Priya | 27 |
| Arjun | 28 |
| Anna | 30 |
| Sara | 32 |
| David | 35 |
SELECT name, country, age FROM customers
ORDER BY country ASC, age DESC;
| name | country | age |
|---|---|---|
| Anna | Germany | 30 |
| Arjun | India | 28 |
| Priya | India | 27 |
| Kenji | Japan | 24 |
| Sakura | Japan | 19 |
| David | UK | 35 |
| Sara | USA | 32 |
| James | USA | 22 |
LIMIT caps how many rows are returned. OFFSET skips rows before starting.
SELECT name, age FROM customers
ORDER BY age ASC
LIMIT 3;
| name | age |
|---|---|
| Sakura | 19 |
| James | 22 |
| Kenji | 24 |
SELECT name, age FROM customers
ORDER BY age ASC
LIMIT 3 OFFSET 2;
| name | age |
|---|---|
| Kenji | 24 |
| Priya | 27 |
| Arjun | 28 |
Returns only unique values, removing duplicates.
SELECT DISTINCT country FROM customers;
| country |
|---|
| India |
| USA |
| Japan |
| Germany |
| UK |
SELECT DISTINCT country, city FROM customers;
| country | city |
|---|---|
| India | Mumbai |
| USA | New York |
| Japan | Tokyo |
| Germany | Berlin |
| USA | Chicago |
| India | Delhi |
| UK | London |
| Japan | Osaka |
Logical operators, aggregate functions, and conditional expressions.
Logical operators combine or negate conditions in WHERE clauses. They return 1 (true) or 0 (false).
Selects data only if all conditions are true.
SELECT * FROM customers
WHERE country = 'India' AND age > 25;
| id | name | country | age |
|---|---|---|---|
| 1 | Arjun | India | 28 |
| 6 | Priya | India | 27 |
Selects data if any one condition is true.
SELECT * FROM customers
WHERE country = 'India' OR country = 'Japan';
| id | name | country |
|---|---|---|
| 1 | Arjun | India |
| 3 | Kenji | Japan |
| 6 | Priya | India |
| 8 | Sakura | Japan |
Reverses a condition — selects rows where it's false.
SELECT * FROM customers WHERE NOT country = 'USA';
| id | name | country |
|---|---|---|
| 1 | Arjun | India |
| 3 | Kenji | Japan |
| 4 | Anna | Germany |
| 6 | Priya | India |
| 7 | David | UK |
| 8 | Sakura | Japan |
IN matches against a list (shorthand for multiple ORs). NOT IN excludes the list.
SELECT * FROM customers
WHERE country IN ('India', 'Japan', 'Germany');
| id | name | country |
|---|---|---|
| 1 | Arjun | India |
| 3 | Kenji | Japan |
| 4 | Anna | Germany |
| 6 | Priya | India |
| 8 | Sakura | Japan |
Selects values within a range (inclusive).
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
| id | name | price |
|---|---|---|
| 2 | Headphones | 150 |
| 4 | Desk Chair | 450 |
Pattern-matches strings.
| Wildcard | Meaning | Example |
|---|---|---|
| % | Zero or more characters | 'S%' — starts with S |
| _ | Exactly one character | '_r%' — r is 2nd letter
|
| [] | Any one char in brackets | '[UK]%' |
| ! | Exclude chars | '[!DR]%' |
SELECT * FROM customers WHERE name LIKE 'S%';
| id | name | country |
|---|---|---|
| 2 | Sara | USA |
| 8 | Sakura | Japan |
Checks for missing values. You cannot use = or != for NULL.
SELECT * FROM customers WHERE email IS NULL;
| id | name | |
|---|---|---|
| 2 | Sara | NULL |
| 6 | Priya | NULL |
Aggregate functions calculate a single value from a set of rows. Often paired with GROUP BY.
| Function | What it does |
|---|---|
| MIN() | Smallest value |
| MAX() | Largest value |
| COUNT() | Number of rows |
| SUM() | Total of a numeric column |
| AVG() | Average of a numeric column |
SELECT MAX(price) AS highest FROM products;
SELECT MIN(price) AS lowest FROM products;| highest | lowest |
|---|---|
| 1200 | 8 |
Nested SELECT to get the full row:
SELECT * FROM products
WHERE price = (SELECT MAX(price) FROM products);| id | name | price | category |
|---|---|---|---|
| 1 | Laptop | 1200 | Electronics |
SELECT COUNT(*) AS total FROM customers; -- 8
SELECT COUNT(email) AS has_email FROM customers; -- 6
SELECT COUNT(*) FROM customers WHERE country = 'India'; -- 2
SELECT COUNT(DISTINCT country) FROM customers; -- 5
| Counted | Skipped (NULL) |
|---|---|
| Arjun, Kenji, Anna, James, David, Sakura | Sara, Priya |
SELECT SUM(quantity) AS total_items FROM orders; -- 12
SELECT AVG(price) AS avg_price FROM products; -- 405.50| total_items | avg_price |
|---|---|
| 12 | 405.50 |
Groups rows, then runs aggregates per group.
SELECT country, COUNT(*) AS total
FROM customers GROUP BY country;| country | total |
|---|---|
| India | 2 |
| USA | 2 |
| Japan | 2 |
| Germany | 1 |
| UK | 1 |
Filters groups after aggregation (WHERE can't do this).
SELECT country, COUNT(*) AS total
FROM customers GROUP BY country
HAVING COUNT(*) > 1;| country | total |
|---|---|
| India | 2 |
| USA | 2 |
| Japan | 2 |
| WHERE | HAVING |
|---|---|
| Filters rows before grouping | Filters groups after aggregation |
| Cannot use aggregate functions | Designed for aggregate functions |
| Comes before GROUP BY | Comes after GROUP BY |
Works like if-else inside a query. Evaluates conditions and adds a new column based on which one matches first.
SELECT name, price,
CASE
WHEN price > 1000 THEN 'Expensive'
WHEN price > 500 THEN 'Mid-range'
END AS price_category
FROM products;| name | price | price_category |
|---|---|---|
| Laptop | 1200 | Expensive |
| Headphones | 150 | NULL |
| Notebook | 8 | NULL |
| Desk Chair | 450 | NULL |
| Monitor | 600 | Mid-range |
| Pen Set | 25 | NULL |
ELSE catches all unmatched rows so nothing becomes NULL.
SELECT name, score,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 70 THEN 'C'
WHEN score >= 60 THEN 'D'
ELSE 'F'
END AS grade
FROM students;| name | score | grade |
|---|---|---|
| Ravi | 92 | A |
| Mia | 85 | B |
| Chen | 73 | C |
| Emma | 61 | D |
| Kai | 45 | F |
Combining data from multiple tables and nesting queries inside queries.
So far, every query has worked with a single table. JOIN combines rows from two or more tables using a common column, letting you pull related data together in one result.
| Join Type | What it returns |
|---|---|
| INNER JOIN | Only rows that match in both tables |
| LEFT JOIN | All rows from the left table + matches from the right (NULLs if no match) |
| RIGHT JOIN | All rows from the right table + matches from the left (NULLs if no match) |
| FULL JOIN | All rows from both tables (NULLs where no match on either side) |
Returns only rows with matching values in both tables. Rows that don't match are excluded entirely.
SELECT c.name, c.country, o.product_id, o.quantity
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
| name | country | product_id | quantity |
|---|---|---|---|
| Arjun | India | 1 | 1 |
| Arjun | India | 3 | 5 |
| Sara | USA | 5 | 3 |
| Kenji | Japan | 2 | 2 |
| Anna | Germany | 4 | 1 |
James, Priya, David, and Sakura have no orders — they don't appear at all.
Returns all rows from the left table, plus matches from the right. Unmatched right-side columns become NULL.
SELECT c.name, c.country, o.product_id, o.quantity
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
| name | country | product_id | quantity |
|---|---|---|---|
| Arjun | India | 1 | 1 |
| Arjun | India | 3 | 5 |
| Sara | USA | 5 | 3 |
| Kenji | Japan | 2 | 2 |
| Anna | Germany | 4 | 1 |
| James | USA | NULL | NULL |
| Priya | India | NULL | NULL |
| David | UK | NULL | NULL |
| Sakura | Japan | NULL | NULL |
Customers without orders still appear — their order columns are filled with NULL.
COALESCE() replaces NULL with a default value — useful after LEFT/RIGHT joins.
SELECT c.name,
COALESCE(o.quantity, 0) AS quantity
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;Now instead of NULL, customers without orders show 0.
Opposite of LEFT — returns all rows from the right table, plus matches from the left.
SELECT o.id AS order_id, o.quantity, s.status
FROM orders o
RIGHT JOIN shipping s ON o.id = s.order_id;
| order_id | quantity | status |
|---|---|---|
| 1 | 1 | Delivered |
| 2 | 2 | In Transit |
| 4 | 3 | Delivered |
| NULL | NULL | Returned |
The "Returned" shipping record references order_id 99 which doesn't exist — LEFT columns become NULL.
Returns all rows from both tables. NULLs fill in wherever there's no match on either side.
SELECT o.id AS order_id, o.quantity, s.status
FROM orders o
FULL OUTER JOIN shipping s ON o.id = s.order_id;
| order_id | quantity | status |
|---|---|---|
| 1 | 1 | Delivered |
| 2 | 2 | In Transit |
| 3 | 1 | NULL |
| 4 | 3 | Delivered |
| 5 | 5 | NULL |
| NULL | NULL | Returned |
Orders 3 and 5 have no shipping. Shipping id=4 has no matching order. Both sides are kept.
Chain multiple JOINs to connect 3+ tables.
SELECT c.name, p.name AS product, s.status
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN products p ON o.product_id = p.id
LEFT JOIN shipping s ON o.id = s.order_id;
| name | product | status |
|---|---|---|
| Arjun | Laptop | Delivered |
| Kenji | Headphones | In Transit |
| Anna | Desk Chair | NULL |
| Sara | Monitor | Delivered |
| Arjun | Notebook | NULL |
Joins a table with itself. Requires aliases to distinguish the two copies.
SELECT c1.name AS customer_1,
c2.name AS customer_2,
c1.country
FROM customers c1
INNER JOIN customers c2
ON c1.country = c2.country
AND c1.id < c2.id;
| customer_1 | customer_2 | country |
|---|---|---|
| Arjun | Priya | India |
| Sara | James | USA |
| Kenji | Sakura | Japan |
c1.id < c2.id prevents duplicates (Arjun–Priya appears once, not Priya–Arjun too).
A CTE creates a temporary named result set you can reference in the main query. Makes complex queries more readable.
WITH cte_name AS (
SELECT ...
)
SELECT * FROM cte_name;
WITH indian_customers AS (
SELECT id, name FROM customers
WHERE country = 'India'
)
SELECT ic.name, o.product_id, o.quantity
FROM indian_customers ic
INNER JOIN orders o ON ic.id = o.customer_id;
| name | product_id | quantity |
|---|---|---|
| Arjun | 1 | 1 |
| Arjun | 3 | 5 |
Priya has no orders, so she doesn't appear. The CTE acts like a temporary table that exists only for this query.
A subquery is a query nested inside another query. The inner query runs first, and its result is used by the outer query. Subqueries can return:
| Type | Returns | Use with |
|---|---|---|
| Single row | One value | = > < >= <=
|
| Multiple rows | A list of values | IN, ANY, ALL |
| Multiple columns | Row(s) with several columns | Multi-column comparison |
Returns one value — used with comparison operators (=, >, <, etc.).
SELECT * FROM customers
WHERE age = (SELECT MIN(age) FROM customers);
| id | name | country | age |
|---|---|---|---|
| 8 | Sakura | Japan | 19 |
When the subquery returns multiple rows, use IN to match against the list.
SELECT name, country FROM customers
WHERE id IN (SELECT customer_id FROM orders);
| name | country |
|---|---|
| Arjun | India |
| Sara | USA |
| Kenji | Japan |
| Anna | Germany |
ANY returns true if the condition matches at least one value from the subquery.
SELECT name, age FROM customers
WHERE age < ANY (
SELECT age FROM customers WHERE country = 'India'
);
| name | age |
|---|---|
| Kenji | 24 |
| Priya | 27 |
| James | 22 |
| Sakura | 19 |
< ANY effectively means "less than the largest value in the list."
ALL returns true only if the condition matches every value from the subquery.
SELECT name, age FROM customers
WHERE age > ALL (
SELECT age FROM customers WHERE country = 'India'
);
| name | age |
|---|---|
| Sara | 32 |
| Anna | 30 |
| David | 35 |
> ALL effectively means "greater than the largest." < ALL means "less than the smallest."
EXISTS checks whether a subquery returns any rows at all. It doesn't care about values — just "is there at least one row?"
SELECT name, country FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id
);
| name | country |
|---|---|
| Arjun | India |
| Sara | USA |
| Kenji | Japan |
| Anna | Germany |
Many subqueries can be rewritten as JOINs and vice versa. The result is the same — pick whichever is more readable.
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders);
SELECT DISTINCT c.name
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
The JOIN version needs DISTINCT because Arjun has two orders and would appear twice otherwise.
UNION combines the results of two SELECT statements into one result set, removing duplicates.
SELECT name FROM customers WHERE country = 'India'
UNION
SELECT name FROM products WHERE category = 'Electronics';
| name |
|---|
| Arjun |
| Priya |
| Laptop |
| Headphones |
| Monitor |
Same as UNION but keeps duplicates. Faster because it skips the dedup step.
SELECT country FROM customers WHERE id <= 3
UNION ALL
SELECT country FROM customers WHERE id >= 3;
| country |
|---|
| India |
| USA |
| Japan |
| Japan |
| Germany |
| USA |
| India |
| UK |
| Japan |
| Rule | Why |
|---|---|
| Same # of columns | Both SELECTs must return the same number of columns |
| Matching data types | Columns must be compatible types (both INT, both VARCHAR, etc.) |
| Same column order | Columns are matched by position, not by name |
| UNION | JOIN |
|---|---|
| Stacks rows vertically | Combines columns horizontally |
| Tables don't need a common column | Requires a common column to match on |
| Must have same number of columns | Can have different number of columns |
SQL doesn't execute in the order you write it. Understanding the real execution order explains why certain things work (or don't).
-- YOU WRITE: -- ENGINE EXECUTES:
SELECT ... 5 FROM ... 1
FROM ... 1 WHERE ... 2
WHERE ... 2 GROUP BY ... 3
GROUP BY ... 3 HAVING ... 4
HAVING ... 4 SELECT ... 5
ORDER BY ... 6 ORDER BY ... 6
LIMIT ... 7 LIMIT ... 7
This is why you can't use a column alias from SELECT inside WHERE — WHERE runs before SELECT. But you can use it in ORDER BY, since that runs after.
| id | name | last_name | country | city | age | |
|---|---|---|---|---|---|---|
| 1 | Arjun | Patel | India | Mumbai | 28 | arjun@mail.com |
| 2 | Sara | Doe | USA | New York | 32 | NULL |
| 3 | Kenji | Tanaka | Japan | Tokyo | 24 | kenji@mail.com |
| 4 | Anna | Klein | Germany | Berlin | 30 | anna@mail.com |
| 5 | James | Doe | USA | Chicago | 22 | james@mail.com |
| 6 | Priya | Sharma | India | Delhi | 27 | NULL |
| 7 | David | Brown | UK | London | 35 | david@mail.com |
| 8 | Sakura | Ito | Japan | Osaka | 19 | sakura@mail.com |
| id | name | price | category |
|---|---|---|---|
| 1 | Laptop | 1200 | Electronics |
| 2 | Headphones | 150 | Electronics |
| 3 | Notebook | 8 | Stationery |
| 4 | Desk Chair | 450 | Furniture |
| 5 | Monitor | 600 | Electronics |
| 6 | Pen Set | 25 | Stationery |
{emp_id} -- candidate (minimal)
{emp_id, name} -- super key
{licence, name} -- super key
| Key | Unique? | NULL? | Per table |
|---|---|---|---|
| Primary | Yes | No | 1 |
| Candidate | Yes | No | 1+ |
| Super | Yes | Depends | Many |
| Foreign | No | Yes | 0+ |
CREATE DATABASE shop;
SHOW DATABASES;
USE shop;
SELECT DATABASE();
SHOW TABLES;
DROP DATABASE shop;
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100)
);
ALTER TABLE t ADD col VARCHAR(10);
ALTER TABLE t RENAME COLUMN old TO new;
ALTER TABLE t MODIFY COLUMN col VARCHAR(3);
ALTER TABLE t DROP COLUMN col;
ALTER TABLE t RENAME TO new_t;
INSERT INTO customers (id, name, country, age)
VALUES (9, 'Liam', 'Korea', 26);
-- Multiple rows at once:
VALUES (9, 'Liam', ...), (10, 'Zara', ...);
UPDATE customers
SET city = 'Manchester'
WHERE id = 7;
DELETE FROM customers WHERE country = 'UK';
SELECT name, country FROM customers; -- specific cols
SELECT * FROM customers; -- all cols
SELECT * FROM customers
WHERE country = 'USA'; -- filtered
SELECT name AS customer_name FROM customers;
ORDER BY age ASC; -- youngest first (default)
ORDER BY age DESC; -- oldest first
ORDER BY country, age; -- sort by country, then age
LIMIT 3; -- first 3 rows
LIMIT 3 OFFSET 2; -- skip 2, take next 3
SELECT DISTINCT country FROM customers; -- 5 unique
WHERE country = 'India' AND age > 25;
WHERE country = 'India' OR country = 'Japan';
WHERE NOT country = 'USA';
WHERE country IN ('India', 'Japan', 'Germany');
WHERE price BETWEEN 100 AND 500;
WHERE name LIKE 'S%'; ↳ Sara, Sakura
WHERE name LIKE '%an%'; ↳ Anna
WHERE email IS NULL; ↳ Sara, Priya
WHERE email IS NOT NULL; ↳ 6 rows
SELECT country, COUNT(*) AS total
FROM customers GROUP BY country;
| country | total |
|---|---|
| India | 2 |
| USA | 2 |
| Japan | 2 |
| Germany | 1 |
| UK | 1 |
HAVING COUNT(*) > 1;
CASE
WHEN price > 1000 THEN 'Expensive'
WHEN price > 500 THEN 'Mid-range'
END AS price_category
| name | price | category |
|---|---|---|
| Laptop | 1200 | Expensive |
| Headphones | 150 | NULL |
| Monitor | 600 | Mid-range |
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 70 THEN 'C'
WHEN score >= 60 THEN 'D'
ELSE 'F'
END AS grade
| name | score | grade |
|---|---|---|
| Ravi | 92 | A |
| Mia | 85 | B |
| Chen | 73 | C |
| Emma | 61 | D |
| Kai | 45 | F |
SELECT c.name, o.quantity
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
| Join | Returns |
|---|---|
| INNER | Only matching rows |
| LEFT | All left + matching right |
| RIGHT | All right + matching left |
| FULL | All from both sides |
SELECT c1.name, c2.name, c1.country
FROM customers c1
INNER JOIN customers c2
ON c1.country = c2.country
AND c1.id < c2.id;
WITH indian_customers AS (
SELECT id, name FROM customers
WHERE country = 'India'
)
SELECT ic.name, o.quantity
FROM indian_customers ic
INNER JOIN orders o ON ic.id = o.customer_id;
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN products p ON o.product_id = p.id
LEFT JOIN shipping s ON o.id = s.order_id;
SELECT * FROM customers
WHERE age = (SELECT MIN(age) FROM customers);
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders);
WHERE age < ANY (SELECT age ... WHERE country='India');
WHERE age > ALL (SELECT age ... WHERE country='India');
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.customer_id = c.id);
SELECT name FROM customers WHERE country='India'
UNION
SELECT name FROM products WHERE category='Electronics';
1. FROM → which table
2. WHERE → filter rows
3. GROUP BY → group them
4. HAVING → filter groups
5. SELECT → pick columns
6. ORDER BY → sort
7. LIMIT → cap results