⚡Quick Reference
If you remember only 5 things:
- Execution order:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT NULL = NULLreturns UNKNOWN. Always useIS NULL.WHEREfilters rows;HAVINGfilters groups.JOIN ... ON, notJOIN ... WHEREfor the join condition.- Aliases work in
ORDER BYbut NOT inWHERE.
📦Database Basics
| Term | Meaning |
|---|---|
| Database | Collection of related tables |
| Table | Grid of rows and columns (like a spreadsheet) |
| Row (record) | One entity instance (one customer, one order) |
| Column (field) | One attribute (name, age, salary) |
| Primary Key (PK) | Unique identifier per row · NOT NULL + UNIQUE · one per table |
| Foreign Key (FK) | Column pointing to PK in another table · enforces referential integrity |
| Composite Key | PK made of multiple columns |
| RDBMS | Relational DB Management System (MySQL, PostgreSQL, Oracle, SQL Server, SQLite) |
🗂️SQL Command Categories
| Type | Stands for | Commands | Purpose |
|---|---|---|---|
| DDL | Data Definition | CREATE ALTER DROP TRUNCATE RENAME | Structure |
| DML | Data Manipulation | INSERT UPDATE DELETE | Data changes |
| DQL | Data Query | SELECT | Read data |
| DCL | Data Control | GRANT REVOKE | Permissions |
| TCL | Transaction Control | COMMIT ROLLBACK SAVEPOINT | Transactions |
🎯 Interview trap:
TRUNCATE looks like DML but is DDL — it deallocates pages, can't be rolled back, resets identity. DELETE is DML — row-by-row, logged, can be rolled back.
CRUD = Create Read Update Delete = INSERT SELECT UPDATE DELETE
⚡Execution Order
You write SQL in one order. The database runs it in a completely different order.
| # | Step | What it does |
|---|---|---|
| 1 | FROM / JOIN | Get rows from tables |
| 2 | WHERE | Filter individual rows |
| 3 | GROUP BY | Group rows together |
| 4 | HAVING | Filter groups |
| 5 | SELECT | Pick columns, compute expressions, assign aliases |
| 6 | ORDER BY | Sort the result |
| 7 | LIMIT | Trim to N rows |
Memory: "From Where Groups Have Selected Ordered Limits"
Why this matters:
•
•
•
•
WHERE can't use SELECT aliases (alias doesn't exist yet)•
ORDER BY CAN use SELECT aliases (it runs after)•
WHERE can't use aggregate functions (no groups yet) — use HAVING
📝SELECT — the master skeleton
SELECT column1, column2, COUNT(*)
FROM table1
JOIN table2 ON table1.id = table2.fk
WHERE row_filter_condition
GROUP BY column1
HAVING group_filter_condition
ORDER BY column1 ASC, column2 DESC
LIMIT 10;
Each clause is optional except SELECT. Use only what you need.
🔍WHERE Operators
| Operator | Use | Example |
|---|---|---|
= != <> | Equal / not equal | city = 'Lahore' |
> < >= <= | Comparison | amount >= 5000 |
BETWEEN x AND y | Range, inclusive both ends | age BETWEEN 18 AND 65 |
IN (a, b, c) | Match any in list | city IN ('Lahore', 'Multan') |
NOT IN | None in list | status NOT IN ('cancelled') |
LIKE | Pattern: %=any chars, _=one char | name LIKE 'A%' |
IS NULL | Check NULL | phone IS NULL |
IS NOT NULL | Check non-NULL | phone IS NOT NULL |
AND OR NOT | Combine conditions | x > 5 AND y < 10 |
🎯 NEVER write
= NULL — it always returns no rows. Use IS NULL.
AND > OR precedence:
A OR B AND C means A OR (B AND C). Use parentheses to be safe.
LIKE patterns:
'A%' starts with A · '%A' ends with A · '%A%' contains A · '_A%' A is 2nd character
🔢ORDER BY · LIMIT
SELECT name, salary
FROM Employees
ORDER BY salary DESC, name ASC
LIMIT 5;
ASC= ascending (default, small→large)DESC= descending (large→small)- Can sort by multiple columns — secondary sort breaks ties
LIMIT N= first N rows ·LIMIT N OFFSET M= skip M then take N- ORDER BY can use SELECT aliases (e.g.,
ORDER BY total DESC)
LIMIT without ORDER BY = unpredictable. Different runs may give different rows.
🔗JOINs — all six types
| Type | Returns | Use when |
|---|---|---|
| INNER JOIN | Only matched rows | You want pairs that exist on both sides |
| LEFT JOIN | All from left + matches from right (NULL if none) | "Show all customers, with orders if any" |
| RIGHT JOIN | All from right + matches from left | Mirror of LEFT — rarely used |
| FULL OUTER JOIN | All rows from both, NULL fills gaps | Show everything from both sides |
| CROSS JOIN | Cartesian product (every × every) | Rare — usually a bug |
| SELF JOIN | Table joined to itself | Hierarchies: employees + their managers |
-- Basic INNER JOIN
SELECT c.name, o.amount
FROM Customers c
INNER JOIN Orders o ON c.customer_id = o.customer_id;
-- Find customers who never ordered (LEFT JOIN trick)
SELECT c.name
FROM Customers c
LEFT JOIN Orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
-- SELF JOIN: each employee with their manager
SELECT e.name AS employee, m.name AS manager
FROM Employees e
LEFT JOIN Employees m ON e.manager_id = m.emp_id;
🎯 Use
ON for join conditions, NOT WHERE. WHERE filters AFTER the join.
Pattern — "find rows with no match": LEFT JOIN +
WHERE right_side_col IS NULL
📊GROUP BY · Aggregates · HAVING
| Function | Returns | NULL behavior |
|---|---|---|
COUNT(*) | Count of rows | Counts ALL rows including NULL |
COUNT(col) | Count of non-NULL values | Skips NULL |
COUNT(DISTINCT col) | Count of unique values | Skips NULL |
SUM(col) | Total | Skips NULL · returns NULL if all NULL or empty |
AVG(col) | Mean | Skips NULL — denominator changes! |
MIN(col) / MAX(col) | Smallest / largest | Skips NULL · works on numbers, dates, strings |
SELECT department, COUNT(*), AVG(salary)
FROM Employees
WHERE status = 'active' -- filter rows BEFORE grouping
GROUP BY department
HAVING COUNT(*) > 5; -- filter groups AFTER aggregation
🎯 Rule: Every non-aggregated column in SELECT must appear in GROUP BY.
Wrong:
Wrong:
SELECT name, COUNT(*) FROM emp GROUP BY dept — name isn't grouped.
WHERE vs HAVING: WHERE for individual rows. HAVING for grouped results. WHERE can't use aggregates; HAVING can.
Empty set behavior:
•
•
Wrap with
•
COUNT(*) → 0•
SUM/AVG/MIN/MAX → NULL (not 0!)Wrap with
COALESCE(SUM(x), 0) to force 0.
🪆Subqueries
Scalar subquery — returns one value
-- Employees above company average
SELECT name, salary
FROM Employees
WHERE salary > (SELECT AVG(salary) FROM Employees);
IN-list subquery — returns a list
SELECT name FROM Customers
WHERE customer_id IN (SELECT customer_id FROM Orders WHERE amount > 5000);
Correlated subquery — references outer query
-- Employees earning above their OWN department average
SELECT name, salary
FROM Employees e1
WHERE salary > (
SELECT AVG(salary) FROM Employees e2
WHERE e2.dept_id = e1.dept_id -- references outer row
);
EXISTS vs IN: Both work for "matches in another table." EXISTS is often faster for big tables.
EXISTS (SELECT 1 FROM ... WHERE ...)
🧱CTEs (WITH clause)
A CTE is a named temporary result set. Use it to break complex queries into readable layers.
WITH customer_totals AS (
SELECT customer_id, city, SUM(amount) AS total
FROM Orders JOIN Customers USING(customer_id)
GROUP BY customer_id
)
SELECT name, total
FROM customer_totals
WHERE total > (
SELECT AVG(total) FROM customer_totals
);
Why CTEs: Avoid repeating the same subquery, build queries in layers, easier to debug. Equivalent to subqueries in performance for most databases.
∪UNION · DISTINCT · Set Ops
| Operation | Effect |
|---|---|
UNION | Combine rows, remove duplicates (slower) |
UNION ALL | Combine rows, keep all duplicates (faster) |
INTERSECT | Only rows in both queries |
EXCEPT / MINUS | Rows in first query but not in second |
DISTINCT | Remove duplicate rows from a single query |
SELECT name FROM Customers
UNION
SELECT name FROM Suppliers;
SELECT DISTINCT city FROM Customers;
UNION rules: Both queries must have the same number of columns with compatible data types.
⚠️NULL Handling
NULL means "unknown value" — not zero, not empty string. Special rules apply.
| Expression | Result |
|---|---|
NULL = NULL | UNKNOWN (treated as FALSE) |
NULL = 5 | UNKNOWN |
NULL != 5 | UNKNOWN |
5 + NULL | NULL |
'A' || NULL (concat) | NULL (or 'A' in Oracle) |
WHERE phone = NULL | Always 0 rows — wrong! |
WHERE phone IS NULL | Correct way to check |
Replacing NULL with a default
COALESCE(column, default_value) -- ANSI standard, multiple args
IFNULL(column, default_value) -- MySQL, SQLite
NVL(column, default_value) -- Oracle
ISNULL(column, default_value) -- SQL Server
NULL ≠ empty string.
WHERE x IS NULL won't match rows where x = ''. To catch both: WHERE x IS NULL OR x = ''.
🔐Keys & Constraints
| Constraint | What it enforces | NULL allowed? |
|---|---|---|
PRIMARY KEY | Unique + non-null · 1 per table | No |
FOREIGN KEY | Must match a PK in parent table | Yes |
UNIQUE | No duplicates · multiple allowed per table | Yes (usually 1 NULL) |
NOT NULL | Value must be provided | No |
CHECK (cond) | Custom Boolean condition | Depends on condition |
DEFAULT val | Fallback when no value given | — |
CREATE TABLE Employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
dept_id INTEGER,
salary REAL CHECK (salary > 0),
status TEXT DEFAULT 'active',
FOREIGN KEY (dept_id) REFERENCES Departments(dept_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
FK behaviors:
CASCADE (propagate), SET NULL (clear FK), RESTRICT/NO ACTION (block change).
🧬Normalization
| Form | Rule (in plain English) |
|---|---|
| 1NF | One value per cell. No lists, no repeating groups. (atomic) |
| 2NF | 1NF + no partial dependencies. (Only relevant for composite PKs — every non-key column must depend on the FULL key.) |
| 3NF | 2NF + no transitive dependencies. (No non-key column depends on another non-key column.) |
| BCNF | 3NF + every determinant is a candidate key. (Edge case of 3NF.) |
Codd's line: Every non-key attribute depends on "the key, the whole key, and nothing but the key, so help me Codd."
• The key → 1NF
• The whole key → 2NF
• Nothing but the key → 3NF
• The key → 1NF
• The whole key → 2NF
• Nothing but the key → 3NF
Single-column PK? Then your table is automatically in 2NF (no part of key to partially depend on).
Anomalies normalization prevents
- Insert anomaly — can't add data without dummy values
- Update anomaly — same fact stored in multiple rows; must update everywhere
- Delete anomaly — deleting one row loses unrelated info
📇Indexes
| Type | How it works | Per table |
|---|---|---|
| Clustered | Physically orders rows by index column · PK usually creates this | 1 max |
| Non-clustered | Separate structure with pointers to rows | Many allowed |
| B-tree (default) | Supports equality + range queries · balanced tree | — |
| Hash | Equality only · no range queries | — |
| Composite | Index on multiple columns · order matters | — |
| Unique | Enforces uniqueness + speeds up lookups | — |
When to index: columns frequently in WHERE, JOIN, ORDER BY, or with high uniqueness.
Cost of indexes: Every INSERT/UPDATE/DELETE updates every relevant index. 10 indexes ≈ 10× slower writes. Don't over-index.
🛠️Views · Triggers · Procedures · Functions
| Object | What it is | How invoked |
|---|---|---|
| View | Stored SELECT query — virtual table | Queried like a table |
| Stored Procedure | Named code block · can have parameters · multiple statements | CALL proc_name(args) |
| Function | Returns one value · used inside expressions | SELECT my_func(x) |
| Trigger | Auto-runs BEFORE/AFTER INSERT/UPDATE/DELETE | Automatic on event |
| Sequence / Auto-increment | Auto-generates unique IDs | Used in INSERT |
-- View
CREATE VIEW active_customers AS
SELECT id, name, email FROM Customers WHERE status = 'active';
-- Trigger (logs every salary change)
CREATE TRIGGER log_salary_change
AFTER UPDATE ON Employees
FOR EACH ROW
BEGIN
INSERT INTO audit_log VALUES (OLD.emp_id, OLD.salary, NEW.salary, NOW());
END;
🛡️ACID Properties
| Letter | Property | Guarantees | Memory trigger |
|---|---|---|---|
| A | Atomicity | All steps succeed or all are rolled back | "All or nothing" |
| C | Consistency | Database moves from valid state to valid state | "No broken rules" |
| I | Isolation | Concurrent transactions don't see each other's incomplete work | "Two users at once" |
| D | Durability | Committed changes survive crashes | "Survives power cut" |
The bank transfer story: Ahmad sends 3000 to Sara.
• A: if crediting Sara fails, debiting Ahmad is reversed.
• C: total money in the bank stays the same.
• I: two simultaneous transfers don't corrupt each other.
• D: once committed, a power cut doesn't undo it.
• A: if crediting Sara fails, debiting Ahmad is reversed.
• C: total money in the bank stays the same.
• I: two simultaneous transfers don't corrupt each other.
• D: once committed, a power cut doesn't undo it.
NoSQL alternative: BASE = Basically Available, Soft state, Eventual consistency. Trades correctness for scale.
💸Transactions
BEGIN TRANSACTION; -- or START TRANSACTION
UPDATE accounts SET balance = balance - 3000 WHERE name = 'Ahmad';
UPDATE accounts SET balance = balance + 3000 WHERE name = 'Sara';
COMMIT; -- save permanently
-- or ROLLBACK to undo everything since BEGIN
SAVEPOINT — partial rollback
BEGIN TRANSACTION;
UPDATE ...;
SAVEPOINT step1;
UPDATE ...; -- if this is wrong:
ROLLBACK TO step1; -- undoes only after the savepoint
COMMIT;
🔒Isolation Levels & Read Phenomena
The 3 read anomalies
| Anomaly | What happens |
|---|---|
| Dirty read | Read another transaction's uncommitted changes |
| Non-repeatable read | Same row read twice → different values (someone updated it) |
| Phantom read | Same query returns different rows (someone inserted new ones) |
4 isolation levels (weakest → strongest)
| Level | Dirty | Non-rep | Phantom |
|---|---|---|---|
| Read Uncommitted | ❌ | ❌ | ❌ |
| Read Committed | ✅ | ❌ | ❌ |
| Repeatable Read | ✅ | ✅ | ❌ |
| Serializable | ✅ | ✅ | ✅ |
✅ = prevents the anomaly · ❌ = allows it
🎯Top 15 Gotchas
NULL = NULLreturns UNKNOWN — always useIS NULLWHEREfilters rows;HAVINGfilters groups- SELECT aliases NOT available in WHERE (WHERE runs before SELECT)
- SELECT aliases ARE available in ORDER BY (runs after SELECT)
- INNER JOIN uses
ON, notWHEREfor the join condition - GROUP BY: every non-aggregated column in SELECT must be grouped
SUM/AVG/MIN/MAXon empty/all-NULL = NULL, not 0AVGignores NULLs — both numerator AND denominator changeCOUNT(*)counts NULLs;COUNT(column)doesn'tLIMITwithoutORDER BY= unpredictable rowsTRUNCATEis DDL (no rollback);DELETEis DML (can rollback)CROSS JOIN(no ON) = Cartesian product — usually a bug- Single quotes for text values:
'Lahore', not"Lahore" - LIKE:
%= any chars,_= exactly one char UNIONremoves duplicates;UNION ALLkeeps them (faster)
🎤Interview Q&A — prepared answers
Q: What's the difference between INNER JOIN and LEFT JOIN?
INNER JOIN returns only rows that have a match in both tables. LEFT JOIN returns all rows from the left table, plus matched rows from the right; if no match exists, right columns become NULL. Use LEFT when you need to keep unmatched rows.
Q: Primary key vs Foreign key?
Primary key uniquely identifies each row in a table — NOT NULL, UNIQUE, one per table. Foreign key references the primary key in another table, enforcing referential integrity — you can't insert an FK value that doesn't exist in the parent.
Q: What is normalization?
Restructuring tables to reduce redundancy and prevent anomalies. 1NF requires atomic values. 2NF removes partial dependencies (relevant for composite keys). 3NF removes transitive dependencies. Each form prevents update, insert, and delete anomalies.
Q: Explain ACID.
Four properties of reliable transactions. Atomicity — all or nothing. Consistency — moves from valid state to valid state. Isolation — concurrent transactions don't interfere. Durability — committed data survives crashes.
Q: DELETE vs TRUNCATE?
DELETE is DML, removes rows individually, logged, can be rolled back, keeps identity counter. TRUNCATE is DDL, deallocates entire table pages, can't be rolled back in most engines, resets identity, much faster. Use TRUNCATE for clearing whole tables.
Q: What is an index? When would you use one?
A sorted lookup structure that speeds up queries on indexed columns. Use on columns frequently used in WHERE, JOIN, or ORDER BY. Cost is slower writes and extra storage. Most use B-trees, which support both equality and range queries.
Q: WHERE vs HAVING?
WHERE filters individual rows before grouping. HAVING filters groups after aggregation. WHERE can't use aggregate functions; HAVING can. WHERE runs earlier in execution order.
Q: What's a stored procedure?
A named, pre-compiled block of SQL stored in the database, callable by name. Benefits: faster execution due to pre-compilation, code reuse, security through permission control, reduced network traffic.
Q: View vs Table?
A table physically stores data. A view is a stored SELECT query that behaves like a virtual table — no data is duplicated, the underlying query runs each time you query the view. Used for security (hide columns) and simplification (hide complex joins).
Q: How would you find duplicate rows?
Use GROUP BY on the columns that define duplication, with HAVING COUNT(*) > 1:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;Q: Explain a transaction with a real example.
A bank transfer of 3000 from Ahmad to Sara. Two UPDATE statements: subtract from Ahmad, add to Sara. Both must succeed together. If either fails, ROLLBACK reverses both. COMMIT makes both permanent. Without transactions, a failure between the two updates would lose 3000 from the system.
Q: What's the difference between clustered and non-clustered indexes?
A clustered index physically orders the table rows by the index column — only one per table because rows can only be physically sorted one way. A non-clustered index is a separate structure with pointers back to rows — many allowed per table.
📚Bonus topics — quick mention
CASE WHEN — inline if/else
SELECT name,
CASE
WHEN salary < 50000 THEN 'Junior'
WHEN salary < 100000 THEN 'Mid'
ELSE 'Senior'
END AS level
FROM Employees;
Window functions — aggregate without collapsing rows
SELECT name, salary,
AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg,
RANK() OVER (ORDER BY salary DESC) AS rank
FROM Employees;
Common window functions: ROW_NUMBER(), RANK(), DENSE_RANK(), LAG(), LEAD(), SUM/AVG/COUNT OVER(...).
Date functions (vary by database)
NOW() -- current timestamp
CURRENT_DATE -- today's date
DATE('2024-01-15') -- cast to date
YEAR(date_col) -- extract year
DATEDIFF(d1, d2) -- difference in days
String functions
UPPER(s) LOWER(s) -- case conversion
LENGTH(s) -- string length
SUBSTR(s, start, len) -- substring
TRIM(s) -- remove leading/trailing spaces
CONCAT(s1, s2) or s1 || s2 -- combine
REPLACE(s, old, new) -- replace substring
NoSQL families (recognition only)
| Type | Example |
|---|---|
| Document | MongoDB, CouchDB |
| Key-value | Redis, DynamoDB |
| Column-family | Cassandra, HBase |
| Graph | Neo4j |