Block A · Topics 1–7
Modelling
The schema is the one decision that's still shaping your codebase in
five years. Everything here is about making the database structurally incapable
of holding contradictory data — and knowing when to give that up on purpose.
01
What a DBMS gives you
vs filesthree-schemadata independence
Why not just use files?
This is a real interview question and the answer is a list of things you'd
otherwise have to build yourself — badly. Naming five of them is the answer.
| Problem with files | What a DBMS provides |
| The same fact copied into several files, drifting apart | Controlled redundancy and integrity constraints |
| Every query is a hand-written scan | A declarative query language plus an optimiser that picks the access path |
| Two writers corrupt each other | Concurrency control — transactions and isolation |
| A crash mid-write leaves half a record | Atomicity and durability via the write-ahead log |
| Anyone who can read the file can read everything | Per-table and per-column access control |
| Changing a record layout breaks every program | Data independence — the schema is separate from the programs |
| Finding a row means reading the whole file | Indexes |
| Backup and restore is your problem | Point-in-time recovery |
The three-schema architecture
EXTERNAL (view) level what each user or app sees
e.g. a payroll view exposing no home addresses
↕ logical data independence
CONCEPTUAL (logical) level the whole schema: tables, columns, constraints
↕ physical data independence
INTERNAL (physical) level files, pages, indexes, compression
The two independences are the payoff, and are worth naming precisely:
· PHYSICAL data independence — add an index, change the storage engine,
repartition a table, and no query has to change.
· LOGICAL data independence — split a table in two, and existing views
can be redefined so applications querying them still work.
Physical independence is nearly complete in practice; logical
independence is only partial, which is why schema migrations are
still real work.
Terms to keep straight
Schema is the structure (the definition); instance
is the data in it right now. DBMS is the software;
database is the data it manages. RDBMS is a DBMS
that implements the relational model. And DDL defines structure,
DML manipulates data, DCL grants permissions,
TCL controls transactions.
02
The relational model & keys
candidate keyprimary keyforeign keyintegrity
Why keys matter
A key is how you identify a row, which is how you reference it, which
is how relationships exist at all. Get keys wrong and you get duplicate rows,
orphaned references, and a table you cannot reliably update. Most exam questions in
this topic are really testing whether you can distinguish six similar terms.
| Key | Definition | Example |
| Super key | Any set of attributes that uniquely identifies a row — including ones with extra useless attributes | {id}, {id, name}, {id, name, email} |
| Candidate key | A minimal super key — remove any attribute and it stops being unique | {id}, {email} |
| Primary key | The candidate key you chose. Implies NOT NULL + UNIQUE, and usually determines physical ordering. | {id} |
| Alternate key | The candidate keys you didn't choose | {email} |
| Composite key | A key made of more than one attribute | {order_id, line_no} |
| Foreign key | An attribute referencing another table's primary key. May be NULL (meaning "no relationship"). | orders.customer_id |
| Surrogate key | An artificial identifier with no business meaning | An auto-increment id, a UUID |
| Natural key | A real-world identifier used as the key | ISBN, email, national ID |
Surrogate or natural? A good tradeoff answer
Prefer a surrogate primary key, with a unique constraint on the
natural key. Reason: natural keys change — people change email addresses, countries
change postcodes, a "unique" supplier code turns out to be reused — and a changing
primary key means cascading updates through every referencing table. Surrogates are
also narrower, which matters because every secondary index stores the primary key.
Keep the natural key as a UNIQUE constraint so the database still
enforces the real-world rule.
Integrity constraints
- Entity integrity — the primary key is unique and not null.
- Referential integrity — a foreign key must either be NULL or
match an existing primary key. No orphan rows.
- Domain integrity — values fall in the column's type and
CHECK constraints.
- Key/unique constraints — enforce alternate keys.
-- Referential actions: what happens to children when a parent goes away
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT -- refuse the delete (the safe default)
ON DELETE CASCADE -- delete the children too — powerful and dangerous
ON DELETE SET NULL -- orphan them deliberately (column must be nullable)
ON DELETE SET DEFAULT -- point them at a placeholder row
ON DELETE NO ACTION -- like RESTRICT, but checked at end of statement
-- CASCADE is the one to be careful with: deleting one customer can
-- silently remove millions of rows across many tables, and it happens
-- inside your transaction, holding locks the whole time.
Two NULL facts that get tested
NULL means "unknown", not "empty" — it isn't zero and isn't an
empty string. And in a UNIQUE constraint, most databases (including
Postgres and MySQL) allow multiple NULLs, because two unknowns aren't
provably equal. So UNIQUE(email) does not stop you inserting a
thousand rows with a NULL email — a genuinely surprising result the first time it
bites.
03
ER modelling
entitiescardinalityweak entitiesmapping to tables
Why it exists
ER diagrams are a thinking tool for the step before SQL: agreeing what
things exist and how they relate, in a notation a non-engineer can
check. The examinable part is mechanical — mapping the diagram to tables — and the
rules are worth knowing exactly.
The vocabulary
| Concept | Means | Becomes |
| Entity | A thing with independent existence | A table |
| Attribute | A property of an entity | A column |
| Composite attribute | address = street + city + zip | Several columns |
| Multivalued attribute | A person's several phone numbers | A separate table — never a comma-separated column |
| Derived attribute | age, from date_of_birth | Computed, or a generated column — not stored raw |
| Relationship | An association between entities | A foreign key, or a join table |
| Weak entity | Can't exist or be identified without an owner | A table with a composite PK including the owner's key |
| Participation | Total (mandatory) vs partial (optional) | NOT NULL or not, on the FK |
Mapping cardinality to tables — the mechanical rules
1 : 1 Put the FK on either side — preferably the side with TOTAL
participation — and add UNIQUE to it.
e.g. employee ─ has ─ parking_space
parking_space(id, employee_id UNIQUE REFERENCES employee)
1 : N Put the FK on the N side. Always. No join table needed.
e.g. customer ─ places ─ order (one customer, many orders)
orders(id, customer_id REFERENCES customers)
M : N You must create a junction / associative table whose primary
key is the pair of foreign keys.
e.g. student ─ enrols ─ course
enrolments(student_id, course_id, grade, enrolled_at,
PRIMARY KEY (student_id, course_id))
-- and note the junction table is the natural home for
-- attributes OF THE RELATIONSHIP, like grade and enrolled_at
Weak entity
order_lines(order_id, line_no, product_id, qty,
PRIMARY KEY (order_id, line_no))
-- line_no alone means nothing; identity comes from the owner
Ternary relationships map to a table with three FKs — but check whether
it's genuinely ternary or really two binary relationships.
Modelling mistakes interviewers look for
- Repeating groups.
phone1, phone2, phone3 —
that's a multivalued attribute; give it a table. You'll always need a fourth.
- Comma-separated lists in a column. Unqueryable,
unindexable, unenforceable.
- Storing derived values like
age or
total without a plan to keep them correct.
- Missing the M:N. "A student has a course_id" collapses a
many-to-many into one-to-many and loses data.
- No history where the business needs it. If a price can
change, storing one
price on products means old orders
silently re-price themselves — you need the price captured on the order line.
This is the mistake most worth catching out loud.
04
Relational algebra
σ π ⋈set operationsdivision
Why bother
Two reasons. It's examined directly. And it's what the query optimiser actually
manipulates — when you read that the planner "pushed the predicate down", it means
it moved a σ below a ⋈, which is valid precisely because relational algebra has
algebraic equivalences. Understanding that makes query plans much less
mysterious.
| Operation | Symbol | Means | SQL |
| Selection | σcond(R) | Pick rows matching a condition | WHERE |
| Projection | πcols(R) | Pick columns — and removes duplicates | SELECT DISTINCT |
| Rename | ρ | Rename a relation or attribute | AS |
| Cartesian product | R × S | Every row paired with every row | CROSS JOIN |
| Natural join | R ⋈ S | Product, then match on common attributes, then drop duplicates of them | NATURAL JOIN |
| Theta join | R ⋈θ S | Join on an arbitrary condition | JOIN … ON |
| Union | R ∪ S | Rows in either (needs union-compatible schemas) | UNION |
| Intersection | R ∩ S | Rows in both | INTERSECT |
| Difference | R − S | Rows in R but not S | EXCEPT |
| Division | R ÷ S | "Rows in R associated with all of S" | No direct SQL — needs a double NOT EXISTS |
-- "Names of students enrolled in CS101"
πname( σcourse_id = 'CS101'( students ⋈ enrolments ) )
-- The optimiser's favourite rewrite: PREDICATE PUSHDOWN.
-- These are equivalent, but the second joins far fewer rows:
σcourse='CS101'( students ⋈ enrolments ) -- join everything, then filter
= students ⋈ σcourse='CS101'( enrolments ) -- filter first, then join ✓
-- DIVISION is the one that trips people up. "Students who take EVERY
-- course offered" — the SQL idiom is double negation:
SELECT s.name FROM students s
WHERE NOT EXISTS (
SELECT 1 FROM courses c
WHERE NOT EXISTS (
SELECT 1 FROM enrolments e
WHERE e.student_id = s.id AND e.course_id = c.id))
-- reads as: "no course exists that this student is not enrolled in"
Say this
"Relational algebra is the formal basis for SQL — selection picks rows,
projection picks columns, join combines relations. It matters practically because
the optimiser rewrites queries using its algebraic equivalences: the classic one is
predicate pushdown, applying the filter before the join rather than after, which is
provably equivalent but processes far fewer rows. Division is the awkward one — it
has no direct SQL operator and is expressed as a double NOT EXISTS."
05
Functional dependencies
FDsclosurecandidate keysArmstrong
Why it exists
Normalisation is defined in terms of functional dependencies, so you cannot do
normalisation properly without them. An FD is a rule about the real world
that the data must obey — and finding candidate keys from a set of FDs is a
reliably examined procedure.
X → Y ("X functionally determines Y") means: any two rows agreeing on X
must agree on Y.
student_id → name — one student has one name. ✓
name → student_id — two students can share a name. ✗
Trivial if Y ⊆ X ({a,b} → a is always true).
Partial dependency: a non-key attribute depends on part of a
composite key.
Transitive dependency: X → Y and Y → Z, so X → Z indirectly.
Armstrong's axioms
Reflexivity if Y ⊆ X then X → Y
Augmentation if X → Y then XZ → YZ
Transitivity if X → Y and Y → Z then X → Z
-- derived, and the ones you actually use:
Union if X → Y and X → Z then X → YZ
Decomposition if X → YZ then X → Y and X → Z
Pseudotransitivity if X → Y and WY → Z then WX → Z
Attribute closure — and finding candidate keys
R(A, B, C, D, E)
FDs: A → BC CD → E B → D E → A
Closure of A: start {A}
A → BC → {A,B,C}
B → D → {A,B,C,D}
CD → E → {A,B,C,D,E}
A⁺ = {A,B,C,D,E} = all attributes → A is a candidate key
Closure of E: {E} → E → A → {E,A} → then as above
E⁺ = all → E is a candidate key
Closure of B: {B} → B → D → {B,D}. Nothing else applies.
B⁺ = {B,D} ≠ all → B is NOT a key
Closure of CD: {C,D} → CD → E → {C,D,E} → E → A → {C,D,E,A}
→ A → BC → {A,B,C,D,E}
CD⁺ = all → CD is a candidate key
Candidate keys: A, E, CD
PRIME attributes (in some candidate key): A, C, D, E
NON-PRIME: B ← this distinction is what 2NF/3NF are defined on
The exam shortcut: an attribute that appears ONLY on the right-hand side
of every FD can never be part of a candidate key. An attribute that never
appears on any right-hand side must be in EVERY candidate key.
06
Normalisation
1NF2NF3NFBCNFanomalies
Why it exists
Redundancy isn't primarily a storage problem — it's a correctness
problem. If a fact is stored in three places, one update can miss one of them and
now your database contains a contradiction with no way to tell which copy is right.
Normalisation removes the possibility structurally, so the database can't be
inconsistent even if the application is buggy.
The three anomalies — always name these first
Update anomaly
A department's name is repeated on 500 employee rows. Renaming it means 500
updates; miss one and the database says two things at once.
Insertion anomaly
You can't record a new department until it has an employee, because department
data only exists on employee rows.
Deletion anomaly
Deleting the last employee in a department destroys the department's
information entirely.
The forms, worked on one example
Start — unnormalised
student_courses(student_id, student_name, course_id, course_name,
instructor, instructor_office, grades)
grades = "A,B,A" ← multiple values in one cell
PK = (student_id, course_id)
══ 1NF ══ atomic values, no repeating groups ══════════════════════
Split the multivalued attribute out.
student_courses(student_id, student_name, course_id, course_name,
instructor, instructor_office, grade)
-- one grade per row now. 1NF achieved.
══ 2NF ══ 1NF + no PARTIAL dependencies ══════════════════════════
(only relevant when the PK is composite)
FDs present:
student_id → student_name ← depends on PART of PK ✗
course_id → course_name, instructor ← PART of PK ✗
(student_id, course_id) → grade ← full PK ✓
Decompose:
students(student_id, student_name)
courses(course_id, course_name, instructor, instructor_office)
enrolments(student_id, course_id, grade)
══ 3NF ══ 2NF + no TRANSITIVE dependencies ═══════════════════════
(no non-prime attribute depends on another non-prime attribute)
In courses: course_id → instructor → instructor_office
instructor_office depends on course_id only TRANSITIVELY ✗
-- symptom: an instructor's office is repeated on every
-- course they teach, so moving office = many updates
Decompose:
courses(course_id, course_name, instructor)
instructors(instructor, instructor_office)
══ BCNF ══ every determinant is a superkey ═══════════════════════
The classic case 3NF misses. Suppose:
enrolments(student_id, course_id, instructor)
· each course is taught by many instructors
· a student takes a course from exactly one instructor
· each instructor teaches only ONE course
FDs: (student_id, course_id) → instructor ✓ superkey
instructor → course_id ✗ NOT a superkey!
This IS in 3NF — course_id is prime, and 3NF permits a non-superkey
determinant when the dependent attribute is prime. But it still has
redundancy. BCNF has no such exception:
teaches(instructor, course_id)
enrolments(student_id, instructor)
| Form | Requires | In one sentence |
| 1NF | Atomic values, no repeating groups | One value per cell. |
| 2NF | 1NF + no partial dependency on a composite key | Non-key attributes depend on the whole key. |
| 3NF | 2NF + no transitive dependency | …and on nothing but the key. |
| BCNF | Every determinant is a superkey | The strict version of 3NF, with no prime-attribute exception. |
| 4NF | BCNF + no multivalued dependency | Two independent multivalued facts don't belong in one table (or you get a cartesian product of them). |
| 5NF | 4NF + no join dependency | Rarely reached deliberately. |
The mnemonic, and the BCNF tradeoff
3NF: "every non-key attribute depends on the key, the whole key, and nothing
but the key." 2NF fixes "the whole key", 3NF fixes "nothing but the key".
The subtle point worth knowing: BCNF decomposition is always lossless
but may not be dependency-preserving — you can end up unable to enforce an
FD with a single-table constraint. 3NF decomposition can always be both lossless
and dependency-preserving. So there's a genuine tradeoff, and "3NF is usually
enough in practice" is a defensible position, not laziness.
Lossless decomposition
A decomposition of R into R1 and R2 is lossless if joining them
back gives exactly R — no spurious extra rows. The condition:
R1 ∩ R2 must be a superkey of R1 or of R2. If you split on something
that isn't, the rejoin manufactures rows that were never in the original, which is
strictly worse than the redundancy you were trying to remove. Always check this
when you decompose in an exam.
07
Denormalisation
deliberate redundancyread optimisationkeeping copies honest
Why break the rules
Normalisation optimises for write correctness and pays in joins. At some read
volumes — or across shards where joins are impossible — that bill is too high. The
skill isn't knowing that denormalisation exists; it's doing it deliberately,
in one direction, with a plan to keep the copies correct.
| Technique | What it does | Cost you accept |
| Duplicate a column | Copy customer_name onto orders to avoid a join | A rename must update both — or you decide the order should keep the name as it was, which is often correct |
| Precomputed aggregate | Store order_count on customers | Every order insert must update it, and it can drift; needs a periodic reconciliation job |
| Materialised view | Store a query's result and refresh it | Staleness, plus refresh cost — but the database maintains it, not your code |
| Merge 1:1 tables | Fold user_profiles into users | Wider rows, so fewer fit per page and scans read more |
| Repeat a group | Store the three most recent items inline | Reintroduces the anomalies 1NF removed — use sparingly and only for a bounded, display-only cache |
| Column-store copy | Stream to a warehouse for analytics | Two systems, replication lag — but it keeps analytics off the transactional database, which is usually the right call |
The rule that makes it safe
Normalise the source of truth; denormalise into derived structures you
can rebuild. If the copy is a materialised view, a cache, or a read model
fed by an event stream, then drift is recoverable — you re-derive it from
the truth. If instead you denormalise the primary tables and the copies diverge,
you have two candidate truths and no way to decide between them. That distinction
is the whole answer.
And sometimes the "duplicate" isn't a duplicate
Copying the product price onto the order line looks like denormalisation but
isn't: the order line records the price at the time of sale, which is a
genuinely different fact from the product's current price. Same for the delivery
address on an order. Recognising these as point-in-time facts
rather than redundancy is a sign of real modelling experience — and getting it
wrong means old invoices silently change when someone edits a product.
Say this
"I'd normalise to 3NF by default, because it makes inconsistency structurally
impossible and the schema enforces the rules rather than the application. Then I'd
denormalise where measurement shows a join is too expensive — but into derived
structures, like a materialised view or a read model, so drift is recoverable by
re-deriving from the source of truth. I'd avoid denormalising the primary tables
themselves, because then two copies disagree and neither is authoritative. And I'd
distinguish real denormalisation from point-in-time facts: the price on an order
line isn't a duplicate of the product price, it's the price at the time of
sale."