Section6 Database SQL
Section6 Database SQL
July 2026
Table of Contents
departments
dept_id
10
20
30
40
employees
emp_id
1
2
3
4
5
6
7
8
9
projects
project_id
100
101
102
103
assignments
emp_id
1
2
3
2
1
4
5
7
8
Three deliberate features of this data, each of which drives a classic exam question:
• Farah Khan has a NULL dept_id — she is an employee belonging to no department.
This makes inner joins lose a row and breaks NOT IN.
• Research (dept 40) has no employees — this is what distinguishes a LEFT JOIN
from an INNER JOIN.
• Divya Rao and Karan Malhotra earn identical salaries, and Karan is on no project
— these drive the “Nth highest salary” and NOT EXISTS questions.
The distinction most often tested: every candidate key is a super key, but not every
super key is a candidate key — minimality is the difference. In employees, {emp_id} is a
candidate key; {emp_id, emp_name} is a super key but not a candidate key.
Integrity constraints:
• Entity integrity — no part of a primary key may be NULL.
• Referential integrity — a foreign key must either match an existing primary key
value or be NULL.
• Domain integrity — values must be from the attribute’s domain (enforced by data
type and CHECK).
Traps
• A foreign key CAN be NULL (Farah Khan’s dept_id); a primary key cannot. This is a
favourite true/false question.
• A primary key is chosen, not discovered — a table can have several candidate keys.
• UNIQUE permits one NULL in most systems; PRIMARY KEY permits none.
count_star
9
COUNT(*) counts rows (9). COUNT(dept_id) skips Farah Khan’s NULL (8).
COUNT(manager_id) skips both Asha Menon and Farah Khan (7).
rows_
4
Note the average is 4,500,000 ÷ 3 = 1,500,000, not ÷ 4. If you wanted NULL treated as
zero, you would need AVG(COALESCE(budget, 0)), which gives 1,125,000. Assessment
questions exploit this difference constantly.
Traps
• WHERE dept_id = NULL returns no rows, ever. It is not a syntax error, which is
what makes it dangerous.
• COUNT(column) ≠ COUNT(*) whenever the column is nullable.
• AVG divides by the count of non-NULL values.
• NULL sorts as either first or last depending on the database — never assume.
Worked query
“Show the headcount and average salary for departments with at least three
employees, highest average first.”
SELECT d.dept_name,
COUNT(e.emp_id) AS headcount,
ROUND(AVG([Link]),2) AS avg_salary
FROM departments d
JOIN employees e ON e.dept_id = d.dept_id
GROUP BY d.dept_name
HAVING COUNT(e.emp_id) >= 3
ORDER BY avg_salary DESC;
Actual output:
dept_name
Engineering
Sales
Finance (2 employees) and Research (0) are excluded by the HAVING clause — and
Research would have been excluded anyway by the inner join.
A second query — hours per project:
SELECT p.project_name,
SUM([Link]) AS total_hours,
COUNT(DISTINCT a.emp_id) AS people
FROM projects p
JOIN assignments a ON a.project_id = p.project_id
GROUP BY p.project_name
ORDER BY total_hours DESC;
project_name
Atlas
Cirrus
Borealis
Delta
Traps
• Putting an aggregate in WHERE — WHERE COUNT(*) > 3 is invalid. It must go in
HAVING.
• Forgetting a non-aggregated column in GROUP BY.
• COUNT(*) in a LEFT JOIN counts the NULL-filled row as 1 — use
COUNT(right_table.key) to get 0. This is exactly the difference in the next chapter.
• HAVING without GROUP BY is legal and treats the whole table as one group.
5. Joins
The join types
Join Returns
INNER JOIN Only rows matching in both tables
LEFT (OUTER) JOIN All rows from the left table; NULLs where
the right has no match
RIGHT JOIN All rows from the right table; the mirror
image
FULL OUTER JOIN All rows from both, NULL-padded where
unmatched
CROSS JOIN Cartesian product — every left row with
every right row
SELF JOIN A table joined to itself, using aliases
inner_rows
8
Eight, not nine — Farah Khan (NULL dept) is dropped. And departments with no
employees vanish entirely:
SELECT d.dept_name, COUNT(e.emp_id) AS headcount
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.dept_id
GROUP BY d.dept_name
ORDER BY d.dept_name;
dept_name
Engineering
Finance
Research
Sales
Research appears with a headcount of 0 only because of the LEFT JOIN and because we
counted e.emp_id rather than *. Had we written COUNT(*), Research would show 1 —
counting its single NULL-padded row. That pair of decisions is one of the most commonly
tested points in SQL.
employee
Asha Menon
Rohit Verma
Neha Kulkarni
Imran Sheikh
Divya Rao
Karan Malhotra
Sunita Pillai
Vikram Bose
Farah Khan
The LEFT JOIN is essential — with an inner join, Asha Menon (who has no manager)
would disappear from her own report.
Cross join
SELECT (SELECT COUNT(*) FROM departments) * (SELECT COUNT(*) FROM
employees) AS cross_rows;
cross_rows
36
• RIGHT JOIN is just a LEFT JOIN with the tables swapped; some databases
do not support FULL OUTER JOIN at all.
Scalar subquery
“Who earns more than the company average?”
SELECT emp_name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
emp_name
Asha Menon
Imran Sheikh
Sunita Pillai
Rohit Verma
Correlated subquery
“Who earns more than the average for their own department?” The inner query now
references the outer row, so it is recomputed for each employee.
SELECT e.emp_name, e.dept_id, [Link]
FROM employees e
WHERE [Link] > (SELECT AVG([Link]) FROM employees e2 WHERE
e2.dept_id = e.dept_id)
ORDER BY e.dept_id;
emp_name
Asha Menon
Imran Sheikh
Sunita Pillai
Note Rohit Verma (92,000) appears in the first result but not the second — he is above the
company average but below his own department’s average of 118,333. That contrast is the
point of the topic.
Farah Khan is absent from both: her department is NULL, so the correlated subquery
compares against an empty group.
emp_name
Karan Malhotra
Farah Khan
EXISTS stops at the first matching row, so SELECT 1 is conventional — the select list is
never evaluated.
rows_returned
0
Zero rows — even though Research (dept 40) genuinely has no employees. The subquery
returns a list containing a NULL (Farah Khan’s). 40 NOT IN (10, 20, 30, NULL) evaluates
to 40 <> 10 AND 40 <> 20 AND 40 <> 30 AND 40 <> NULL, and that last
comparison is UNKNOWN — so the whole expression is UNKNOWN, never TRUE, and no
row qualifies.
The fix:
SELECT dept_name FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS
NOT NULL);
dept_name
Research
Traps
• NOT IN with a nullable subquery column silently returns nothing. Use NOT
EXISTS, or filter NULLs explicitly. NOT EXISTS is immune to this problem, which is
why experienced developers prefer it.
• IN with NULL is safe for finding matches but not for excluding them — the asymmetry
is the whole trap.
• A correlated subquery runs once per outer row and can be slow; a join is often
equivalent and faster.
• A scalar subquery returning more than one row is a runtime error, not a silent one.
All require the same number of columns with compatible types. Column names come from
the first query.
This returns the same three employees as the correlated subquery in Chapter 6, but
computes each department’s average once instead of once per row.
Recursive CTEs walk hierarchies — the standard use is an org chart:
WITH RECURSIVE chain AS (
SELECT emp_id, emp_name, manager_id, 1 AS level
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.emp_id, e.emp_name, e.manager_id, [Link] + 1
FROM employees e JOIN chain c ON e.manager_id = c.emp_id
)
SELECT * FROM chain ORDER BY level, emp_id;
Traps
• UNION sorts and deduplicates, which costs time. If you know there are no
duplicates, UNION ALL is correct and faster — this is a standard interview question.
• ORDER BY may appear only once, at the very end of a set operation.
• A recursive CTE without a terminating condition runs forever; the anchor member
must not be recursive.
8. Window functions
Increasingly tested, because they distinguish candidates who have written real queries.
A window function computes across a set of rows without collapsing them — unlike
GROUP BY.
Function
ROW_NUMBER()
RANK()
DENSE_RANK()
LAG / LEAD
SUM() OVER (…)
Divya and Karan tie on 64,000, so both get rank 2. Had there been a fourth person in Sales,
RANK() would give them 4 (skipping 3) while DENSE_RANK() would give 3.
ROW_NUMBER() would have arbitrarily assigned 2 and 3 to the tied pair.
Traps
• RANK skips, DENSE_RANK does not, ROW_NUMBER never ties. The
difference is asked constantly.
• Window functions are evaluated after WHERE and GROUP BY, so you cannot filter
on a window function in WHERE — wrap the query in a CTE or derived table first.
• PARTITION BY divides the rows; ORDER BY inside OVER defines the ordering
within each partition. They are independent of the outer ORDER BY.
second_highest
110000
For a general N, the window-function form is cleaner and handles ties explicitly:
WITH ranked AS (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT DISTINCT salary FROM ranked WHERE rnk = 3;
Find duplicate values
SELECT salary, COUNT(*) AS n FROM employees
GROUP BY salary HAVING COUNT(*) > 1;
salary
64000
Traps
• The MAX(... WHERE salary < MAX) trick gives the second distinct salary. If the
question wants the second-highest employee including ties, use DENSE_RANK.
• For “Nth highest”, LIMIT 1 OFFSET N-1 works but ties behave differently — read
the question’s intent.
• DELETE without WHERE empties the table and is a favourite trick option in MCQs.
Type
Removes
WHERE allowed
Rollback
Resets identity counter
Speed on large tables
Constraints
NOT NULL · UNIQUE · PRIMARY KEY · FOREIGN KEY · CHECK · DEFAULT
Referential actions on a foreign key define what happens when the parent row changes:
Action
NO ACTION / RESTRICT
CASCADE
SET NULL
SET DEFAULT
Traps
• TRUNCATE cannot be rolled back in most systems and takes no WHERE.
• A CHECK constraint is not violated by NULL — CHECK (salary > 0) passes when
salary is NULL, because the condition evaluates to UNKNOWN rather than FALSE.
• UNIQUE allows NULLs (usually more than one); PRIMARY KEY does not.
• You cannot drop a table that is referenced by a foreign key without dropping or
altering the referencing constraint first.
Indexes
An index is a separate structure (typically a B-tree) that speeds lookups at the cost of
storage and slower writes.
• Clustered index — determines the physical row order. One per table.
• Non-clustered index — a separate structure pointing at rows. Many per table.
• Creating a PRIMARY KEY or UNIQUE constraint automatically creates an index.
When an index does not help:
• A query returning most of the table (a full scan is cheaper).
• A column wrapped in a function: WHERE UPPER(emp_name) = 'ASHA MENON'
cannot use an index on emp_name.
• A leading wildcard: LIKE '%menon' cannot use a B-tree index; LIKE 'menon%' can.
• A composite index on (a, b) helps queries filtering on a, or on a and b — but not on b
alone. (The “leftmost prefix” rule.)
Traps
• Indexes slow down INSERT, UPDATE and DELETE. “Index every column” is always
the wrong MCQ answer.
• Only one clustered index per table.
• The leftmost-prefix rule for composite indexes is a standard interview question.
12. Normalization
Why
Redundancy causes three anomalies:
• Insertion anomaly — you cannot record a fact without also recording an unrelated
one (cannot add a course with no students enrolled).
• Update anomaly — one fact stored in many rows must be changed in all of them, or
the data becomes inconsistent.
• Deletion anomaly — deleting one fact destroys another (deleting the last enrolment
loses the course’s existence).
Functional dependencies
X -> Y means: for any two rows agreeing on X, they must agree on Y. “X determines Y.”
• Partial dependency — a non-key attribute depends on only part of a composite key.
• Transitive dependency — a non-key attribute depends on another non-key attribute.
The memorable summary: every non-key attribute must depend on the key, the whole key,
and nothing but the key — which is 1NF, 2NF and 3NF in order.
Worked decomposition
Start with an unnormalized enrolment table:
STUDENT_COURSE (student_id, student_name, course_id, course_name,
instructor, instructor_office, grade)
Functional dependencies:
• student_id -> student_name
• course_id -> course_name, instructor
• instructor -> instructor_office
• (student_id, course_id) -> grade
Step 4 — BCNF check. Every determinant is now a key of its own table: student_id,
course_id, instructor, (student_id, course_id). All are super keys of their relations, so the
schema is in BCNF.
What this bought us: an instructor’s office is now stored once. Changing it is a single
update rather than one per course row, and a new course can be recorded before any
student enrols.
Traps
• A table can be in 3NF but not BCNF. The classic case is a relation with two
overlapping candidate keys where a non-key attribute determines part of a key.
• 2NF violations require a composite key — a table with a single-attribute primary key
is automatically in 2NF.
• Denormalization is deliberate and legitimate — reporting and analytics systems
trade redundancy for read speed. “Always normalize to the highest form” is the wrong
answer in a design question.
• Decomposition should be lossless (rejoining recovers the original) and ideally
dependency-preserving. 3NF can always achieve both; BCNF can sometimes achieve
only losslessness.
13. ER modelling
Components
Concept
Entity
Weak entity
Attribute
Key attribute
Multivalued attribute
Derived attribute
Relationship
Cardinality and how it maps to tables
Cardinality Example Implementation
1:1 Employee — Parking space Foreign key in either table,
with a UNIQUE constraint
1:N Department — Employees Foreign key on the N side
M:N Employees — Projects A junction table with both
foreign keys as a composite
primary key
Traps
• Every M : N relationship needs a third table. You cannot implement it with a foreign
key alone — a standard exam question.
• Attributes on a relationship (the hours in assignments) belong in the junction table,
not either entity.
• A weak entity’s primary key includes the owner’s key.
Isolation levels
Level
Read Uncommitted
Read Committed
Repeatable Read
Serializable
Learn this table as a staircase — each level prevents one more anomaly than the last, at
the cost of concurrency. That structure makes it far easier to recall than the four levels
individually.
Traps
• Higher isolation costs throughput. Serializable is not “the right answer” by default.
• Repeatable Read prevents non-repeatable reads but not phantoms — this precise
distinction is the most-asked question in the topic.
• Consistency in ACID means constraint consistency, not the “eventual consistency” of
distributed systems. The words are the same; the meanings differ.
• A deadlock is resolved by the database aborting one transaction — it is not prevented
by higher isolation.