Storage 2 and 3 Recap Q&A — Relational vs. non-Relational
🔒 Answers locked
Part 1 · Normal forms
Q1
From an Unnormalised Table to 3NF
A secretariat keeps its course registrations in the following table. Each student belongs to one programme, each programme has one director, each course is taught by one lecturer, and each lecturer has one office.
StudentID
Name
Programme
Director
Courses (code, title, lecturer, office, grade)
S01
Léa Martin
Computer Science
Prof. Laurent
DB101
Databases
Dr. Rossi
C-210
16
IR201
Information Retrieval
Dr. Rossi
C-210
14
S02
Hugo Bernard
Mathematics
Prof. Moreau
DB101
Databases
Dr. Rossi
C-210
12
S03
Inès Petit
Computer Science
Prof. Laurent
ML301
Machine Learning
Dr. Chen
B-105
18
DB101
Databases
Dr. Rossi
C-210
15
(a) Is this table in first normal form? Rewrite it in 1NF and give its primary key.
(b) List the functional dependencies that hold between the attributes.
(c) Decompose the table into 2NF, then into 3NF. Draw the final schema with its primary and foreign keys.
(d) Give one insertion, one update and one deletion anomaly that the 1NF table suffers from and that your 3NF schema avoids.
(a) First normal form
No. A relation is in 1NF if and only if no attribute domain has relations as elements. Here the Courses attribute holds a small table (a repeating group) in each cell. To flatten it, we write one row per (student, course) pair:
StudentID
Name
Programme
Director
CourseCode
Title
Lecturer
Office
Grade
S01
Léa Martin
Computer Science
Prof. Laurent
DB101
Databases
Dr. Rossi
C-210
16
S01
Léa Martin
Computer Science
Prof. Laurent
IR201
Information Retrieval
Dr. Rossi
C-210
14
S02
Hugo Bernard
Mathematics
Prof. Moreau
DB101
Databases
Dr. Rossi
C-210
12
S03
Inès Petit
Computer Science
Prof. Laurent
ML301
Machine Learning
Dr. Chen
B-105
18
S03
Inès Petit
Computer Science
Prof. Laurent
DB101
Databases
Dr. Rossi
C-210
15
Primary key: (StudentID, CourseCode). Neither attribute alone identifies a row.
(b) Functional dependencies
StudentID → Name, Programme
Programme → Director
CourseCode → Title, Lecturer
Lecturer → Office
(StudentID, CourseCode) → Grade
(c) 2NF, then 3NF
2NF removes partial dependencies, meaning non-key attributes that depend on only part of the key. Name, Programme and Director depend on StudentID alone, and Title, Lecturer and Office depend on CourseCode alone:
Students(StudentID, Name, Programme, Director)
Courses(CourseCode, Title, Lecturer, Office)
Enrolments(StudentID, CourseCode, Grade)
3NF removes transitive dependencies. StudentID → Programme → Director and CourseCode → Lecturer → Office are both transitive, so Director and Office move to their own tables. I also give lecturers a surrogate key, since two lecturers could share a name.
Programmes
programme
director
Computer Science
Prof. Laurent
Mathematics
Prof. Moreau
Lecturers
lecturer_id
name
office
L1
Dr. Rossi
C-210
L2
Dr. Chen
B-105
Students
student_id
name
programme
S01
Léa Martin
Computer Science
S02
Hugo Bernard
Mathematics
S03
Inès Petit
Computer Science
Courses
course_code
title
lecturer_id
DB101
Databases
L1
IR201
Information Retrieval
L1
ML301
Machine Learning
L2
Enrolments
student_id
course_code
grade
S01
DB101
16
S01
IR201
14
S02
DB101
12
S03
ML301
18
S03
DB101
15
(d) Anomalies avoided
Update: if Dr. Rossi moves to office C-305, four rows of the 1NF table must change. If one is missed, the database holds two offices for the same person. In 3NF this is a single row in Lecturers.
Deletion: if Hugo drops DB101, his only course, deleting the row also erases the facts that Hugo exists and that Mathematics is directed by Prof. Moreau.
Insertion: a new course "NoSQL Systems" cannot be recorded before at least one student enrols, because StudentID is part of the primary key and cannot be NULL.
Q2
Designing a Relational Schema from a Specification
A regional train operator asks you to design the database behind its booking website. The product owner sends you the following list of what must be stored:
Passengers: name, e-mail, date of birth, age, one or more phone numbers, and an optional loyalty card. Each card has a tier (Bronze, Silver, Gold), and each tier gives a fixed discount rate.
Stations: a unique code (e.g. RNS), a name, the city it is in, and the city's region.
Trains: a train number (e.g. TER 8412) that always follows the same route, which is an ordered list of stations with a scheduled departure time at each. Each train uses a type of rolling stock, which determines the number of 1st- and 2nd-class seats.
A train runs on many dates. The run of one train on one date is called a service.
Bookings: a reference, the passenger who made the booking, the booking time, the payment method and the total amount paid. A booking contains one or more tickets. Each ticket is for one traveller (not necessarily the person who booked), on one service, from one station to another, in one class, with a coach and seat number and the price paid.
Design a relational schema in 3NF. For each table, give its primary key and foreign keys, and justify every split with the dependency that motivates it. Some of the requested attributes should not be stored as such. Which ones, and why?
A 3NF schema
Arrows point from a foreign key to the primary key it references.
Justification of the main decisions
PassengerPhone: a passenger has several phone numbers. A multi-valued attribute violates 1NF, so it gets its own table with key (passenger_id, phone).
Tier and LoyaltyCard: passenger → card_no → tier → discount_rate is a transitive chain. Storing discount_rate with the passenger would duplicate it and risk inconsistency when the operator changes the Gold rate. card_no is UNIQUE and nullable in Passenger, because the card is optional and belongs to one person.
City: station → city → region is transitive, so the region is stored once per city.
RollingStock: the seat counts depend on the stock type, not on the train (train_no → stock_type → seats_1st).
RouteStop: a route is an ordered list. The order is itself data (stop_order), so the list is not packed into one column. Key (train_no, stop_order), plus UNIQUE(train_no, station_code).
Service: separates the train, which is static, from its daily runs. Surrogate key, plus UNIQUE(train_no, run_date).
Booking vs Ticket: the booker and the traveller are two distinct foreign keys to Passenger. The ticket carries the trip facts (service, from, to, class, seat).
Attributes that should not be stored as such
Age is derived from birth_date and the current date. Stored, it becomes wrong every birthday, which is an update anomaly caused by time.
Total amount paid is derived: SUM(price_paid) over the booking's tickets. Storing it creates a dependency between rows that has to be maintained. It is acceptable only as a deliberate denormalisation, kept correct by a trigger or by the application.
price_paid on Ticket is not a 3NF violation. It records what was paid at that moment, and it is not a function of class or route, because fares change over time. Replacing it with a lookup into a current fare table would rewrite history.
Part 2 · Distribution, CAP & consistency
Q3
CP, AP or CA?
Three distributed key-value stores handle reads and writes as follows.
System A: majority replicas
A key is stored on 3 replicas. A write is acknowledged only once at least 2 of the 3 replicas have stored it. A read asks at least 2 replicas and returns the value with the highest version number. A replica that cannot reach at least one other replica answers every request with an error.
System B: local first
Each of the 3 replicas accepts any read or write and answers immediately from its local copy. Writes are forwarded to the other replicas in the background whenever they are reachable. When two versions of a key meet, the one with the latest timestamp wins.
System C: one rack, all-or-nothing
Two replicas sit in the same rack, linked by a dedicated cable. A write is applied on both replicas with a two-phase commit before being acknowledged, and a read can be served by either one. The designers assume the cable never fails: the protocol has no timeout and no rule for a replica that stops answering.
(a) Classify each system as CP, AP or CA. Justify each answer by describing what happens when the network between replicas is cut.
(b) Which system would you pick for (i) the "like" counter of a social network, (ii) the seat inventory of a train booking system, and (iii) a bank system?
(a) Classification
System A is CP. With $N=3$, $W=2$ and $R=2$ we have $R+W>N$, so every read quorum intersects every write quorum and a read always sees the latest acknowledged write (consistency). During a partition, the majority side keeps working (partition tolerance), but the isolated replica refuses requests, so some non-failing nodes do not respond (no availability).
System B is AP. Every non-failing replica answers every request (availability) and the system keeps running when messages are dropped (partition tolerance). During a partition, both sides accept writes on the same key and diverge, so a read can return a stale value (no consistency). After the partition heals, last-writer-wins makes the replicas converge, which makes B eventually consistent, and one of the concurrent writes is silently discarded.
System C is CA. As long as the cable works, two-phase commit keeps both copies identical (C) and both replicas answer (A). Nothing in the design handles a partition. If the cable is cut, the coordinator waits forever for the other vote, so the system blocks. It has no partition tolerance.
(b) Use cases
Like counter → B (AP). A slightly stale count harms no one, and the page must always load.
Train seats → A (CP), at least when the train is nearly full, so that the same seat is never sold twice. When booking has just opened and the train is almost empty, an AP approach is acceptable, because the risk of conflict is negligible.
Bank ledger → A (CP). An error message is better than a balance that is wrong or a double spend.
Q4
Making Two Systems Eventually Consistent
Consider two replicated systems that store a single value x (initially 0). The diagrams show each system in normal operation.
System 1. A client writes to any replica, which applies the write locally and acknowledges immediately. The replica then sends the new value to the other replica in one UDP message, without waiting for any reply.
System 2. Leader–follower with synchronous replication. The leader handles writes one at a time, in arrival order, and commits a write only after every follower has acknowledged it. Reads can go to any follower. Follower F2 is in another data centre.
(a) In System 1, Client B first reads 0, then reads 5. Is System 1 eventually consistent? More generally, which consistency level seen in class (strong, eventual, low) does each system provide? Think about what can go wrong, not only about the normal case shown.
(b) In System 2 nothing fails, yet Client B waits a long time for its write. Where does this latency come from, and how does it grow with the number and distance of followers? Which theorem from the lecture describes this trade-off?
(c) Modify both systems so that they become eventually consistent.
(a) Look at the failure cases
System 1 has low (weak) consistency, not eventual. The diagram shows convergence only because the message happened to arrive. Eventual consistency is a guarantee: if writes stop, every replica must end up with the last value. Here, if the single UDP message is lost, R2 keeps x = 0 forever:
Two concurrent writes on R1 and R2 can also cross each other, so that each replica ends with the other's value and they stay different forever.
System 2 has strong consistency. A write is committed only when it is on every replica, so any follower returns the latest committed value. The price shows up when one follower becomes unreachable: the leader waits forever, and every later write is blocked too. The system is consistent but not available. It is CP pushed to the extreme, where the cluster is only as available as its least available node.
(b) Latency without any failure: PACELC
Each write costs a round trip to the slowest follower: latency ≈ max over followers of the round-trip time. With one follower in another data centre, that is tens of milliseconds instead of under a millisecond. It grows with distance, and with the number of followers, since the maximum of more round-trip times is larger. Because writes are handled in order, Client B also waits for A's write to finish: queueing adds up.
This is the PACELC theorem: if there is a Partition, choose A or C; Else, choose Latency or Consistency. System 2 is PC/EC: it gives up availability during a partition, and pays latency the rest of the time. CAP only describes the first half.
(c) Fix for System 1: resend until acknowledged, and resolve conflicts deterministically
Each write gets a version, for example a timestamp plus the replica id.
The receiving replica acknowledges each update. The sender keeps the update and resends it every τ seconds until it gets the acknowledgement. Periodically comparing the replicas' contents and exchanging the differences is a backstop.
All replicas apply the same rule when two versions meet, for example "the highest version wins", so they reach the same final state whatever order the messages arrive in.
(c) Fix for System 2: switch to asynchronous replication
The leader commits locally and acknowledges immediately, then replicates to the followers in the background.
A follower that comes back asks the leader what it missed, and the leader re-sends those updates. Writes no longer wait for F2, and reads from followers may return Data(t−1).
An intermediate design is semi-synchronous replication: wait for one follower, not all of them.
Part 3 · ACID: atomicity, isolation, durability
Recall
Atomicity: a transaction is either carried out entirely or not at all. If it fails, the database returns to the state before it started. Isolation: concurrent transactions cannot compromise each other while they are in progress. The result must be as if they ran one after the other.
Q5
Breaking Atomicity
A logistics company moves 50 units of stock from its Paris warehouse (A = 80) to its Lyon warehouse (B = 30). The transfer is one transaction T made of two threads of execution, which the DBMS may interleave in any order. Each Write updates the value in place on disk. There is no log.
#
Thread 1
Thread 2
1
Read(A)
Read(B)
2
A = A − 50
B = B + 50
3
Write(A)
Write(B)
The invariant is A + B = 110. The machine can lose power at any moment.
(a) How many different execution orders (interleavings of the two threads) are possible?
(b) Give one execution order and one moment of failure such that, after restart, the database is in a state that violates atomicity. Give the values of A and B on disk.
(c) Is there an execution order that is safe whatever the moment of failure?
(a) Number of interleavings
The order inside each thread is fixed, so we only choose which 3 of the 6 time slots belong to Thread 1: $\binom{6}{3} = 20$ orders.
(b) A failing execution
t
Thread 1
Thread 2
Disk
1
Read(A) → 80
A=80, B=30
2
Read(B) → 30
A=80, B=30
3
A = 30
A=80, B=30
4
Write(A)
A=30, B=30
5
B = 80
A=30, B=30
⚡ power failure, before Write(B)
↻
restart
restart
A=30, B=30 → A+B = 60
Half of the transaction has been applied: 50 units have disappeared. After restart, the system cannot tell that A = 30 is a partial result, and it has no record of the old value 80 to go back to.
(c) No safe order exists
In every one of the 20 orders, the two writes happen at two distinct instants, so there is always a window between the first and the second write. A failure inside that window leaves exactly one of them on disk. Reordering can shorten the window, for example by computing both values first and writing them back to back, but it can never remove it. Atomicity therefore needs extra information on disk to undo or redo the work. That is the role of the log (Q6).
Q6
The Write-Ahead Log: Why It Works and Why It Is Fast Enough
We now add a write-ahead log (WAL), an append-only file on disk. For the transfer of Q5, the DBMS appends the following records:
(a) Replay the failure from your answer to Q5(b) with the WAL. What is in the log at the time of the crash, and what does the recovery procedure do at restart? Do the same for a crash that happens just after <T, COMMIT> reaches the disk, but before any data page is written.
(b) Why does the log store the old and new values (old=80, new=30) rather than the operation (A −= 50)?
(c) In the lecture we saw that disk is slow. Yet the WAL adds one more disk write per update. Is WAL still a good solution?
(a) Recovery
Crash after Write(A), before Write(B). Because of the write-ahead rule, <T,START> and <T,A,80,30> are on disk, since A's page could not be written before its log record. There is no <T,COMMIT>, so T is a loser. Recovery scans its records backwards and undoes them by writing the old values: A := 80, and B := 30 if its record exists. The result is A = 80, B = 30, as if T never ran.
Crash after <T,COMMIT>, before the pages are written. T is successfull: the client was told the transfer succeeded. Recovery scans forward and redoes it by writing the new values: A := 30, B := 80. Each outcome is all or nothing, and which one applies is decided by a single atomic event: whether the commit record is on disk.
(b) Crash during recovery
Recovery simply starts again. Undo and redo set values (A := 80) and are therefore idempotent: applying them twice gives the same result as applying them once. A logical record like A −= 50 would be applied twice and corrupt the data. Real systems such as ARIES also store a log sequence number (LSN) on each page, so that a change already present on the page is not redone.
(c) Why an extra disk write makes things faster
The key insight is that the WAL turns many random in-place writes at commit time into one sequential append. The data pages are still written, but later, in the background, batched and optimized.
Q7
Isolation Levels and Their Anomalies
Recall the four isolation levels, in their lock-based implementation:
Level
Write locks
Read locks
Range locks
0 · Read uncommitted
held until the end of the transaction
none
none
1 · Read committed
held until the end of the transaction
released right after the read
none
2 · Repeatable read
held until the end of the transaction
held until the end of the transaction
none
3 · Serializable
held until the end of the transaction
held until the end of the transaction
on ranged WHERE clauses
A transaction (T1, T2, …) is everything between BEGIN and COMMIT or ROLLBACK. A read lock (shared) can be held by several transactions at once. A write lock (exclusive) excludes every other lock on the same data, so a transaction that asks for a conflicting lock waits. Level 3 adds range locks on top of level 2; we come back to them in Q8.
Table accounts(id, owner, balance) initially contains (1, 'Alice', 100) and (2, 'Bob', 50). Each scenario below starts from this state. For each scenario:
give the value marked ? under the three first levels (and say when a transaction has to wait);
give the lowest isolation level that prevents it.
Scenario 1
t
T1
T2
1
BEGIN;
2
BEGIN; UPDATE accounts SET balance = 0 WHERE id = 1;
3
SELECT balance FROM accounts WHERE id = 1; -- ?
4
ROLLBACK;
5
COMMIT;
Scenario 2
t
T1
T2
1
BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 100
2
BEGIN; UPDATE accounts SET balance = 150 WHERE id = 1; COMMIT;
3
SELECT balance FROM accounts WHERE id = 1; -- ?
4
COMMIT;
Scenario 3: Bob deposits 30 (T1) while a payment of 20 is debited (T2). The application reads the balance, computes the new value, and writes it back.
t
T1 (deposit +30)
T2 (debit −20)
1
BEGIN; SELECT balance FROM accounts WHERE id = 2; -- 50
2
BEGIN; SELECT balance FROM accounts WHERE id = 2; -- 50
3
UPDATE accounts SET balance = 80 WHERE id = 2; COMMIT;
4
UPDATE accounts SET balance = 30 WHERE id = 2; COMMIT;
5
Final balance of Bob? -- ?
Scenario 1: dirty read → Read committed
Level
T1 reads
Why
Read uncommitted
0
No read lock, so T1 sees T2's uncommitted value, which is then rolled back. This is a dirty read.
Read committed
100
T2 holds a write lock on row 1. T1 waits until the ROLLBACK, then reads 100.
Repeatable read
100
Same as above.
Serializable
100
Same as above.
Scenario 2: non-repeatable read → Repeatable read
Level
2nd read
Why
Read uncommitted
150
The same row returns two different values within T1.
Read committed
150
T1's read lock was released after step 1, so T2 can update and commit.
Repeatable read
100
T1 keeps its read lock on row 1. T2's UPDATE waits until T1 commits, then sets 150.
Serializable
100
Same as above.
Scenario 3: lost update → Repeatable read
Any serial execution gives 50 + 30 − 20 = 60.
Level
Final balance
Why
Read uncommitted
30
Both read 50, and T2 overwrites T1's deposit: a lost update.
Read committed
30
Read locks are released immediately, so nothing stops it.
Repeatable read
60
Both hold a read lock on row 2. T1's UPDATE waits for T2 and T2's UPDATE waits for T1: a deadlock. The DBMS aborts one (say T2), which retries, reads 80 and writes 60.
Serializable
60
Same as above.
Summary: is the anomaly possible?
Anomaly
Read uncommitted
Read committed
Repeatable read
Serializable
Dirty read
yes
no
no
no
Non-repeatable read
yes
yes
no
no
Lost update
yes
yes
no
no
Q8
What Exactly Should We Lock?
In Q7, level 3 (Serializable) was defined as level 2 plus range locks on ranged WHERE clauses. This question looks at why row locks are not enough, and what a range lock actually is.
Table employees(id, name, dept, salary) has a B-tree index on salary and contains:
id
1
2
3
4
5
6
7
name
Ana
Bilal
Chloé
David
Emma
Farid
Gaëlle
salary
2100
2800
3100
3400
3900
4500
6000
A reporting transaction T1 runs the following query twice, and must get the same answer both times:
SELECT COUNT(*), SUM(salary) FROM employees WHERE salary BETWEEN 3000 AND 4000; -- 3, 10400
Between the two executions, the following transactions try to run:
T2: INSERT (8, 'Hugo', 'IT', 3500)
T3: UPDATE employees SET salary = 6200 WHERE id = 7 (6000 → 6200)
T4: UPDATE employees SET salary = 3200 WHERE id = 2 (2800 → 3200)
T5: INSERT (9, 'Inès', 'HR', 5200)
T6: INSERT (10, 'Jules', 'IT', 4200)
Two locking procedures are proposed for T1's SELECT:
Procedure A: take a shared lock on the whole table until commit. Any writer needs an exclusive lock on the table.
Procedure B: take a shared lock on each row that satisfies the WHERE clause until commit. A writer needs an exclusive lock on the rows it modifies or inserts.
(a) For each procedure, which of T2–T6 are blocked, and what does T1's second SELECT return?
(b) Show that one procedure is too strong and the other too weak.
(c) Design a procedure C in between.
(a) Blocked transactions and results
T2 ins 3500
T3 6000→6200
T4 2800→3200
T5 ins 5200
T6 ins 4200
2nd SELECT
A · table
blocked
blocked ✗
blocked
blocked ✗
blocked ✗
3, 10400
B · rows
passes ✗
passes
passes ✗
passes
passes
5, 17100
C · next-key
blocked
passes
blocked
passes
blocked (false positive)
3, 10400
✗ marks a wrong decision: blocking something unrelated (A), or letting T1's result change (B). Under B the second SELECT sees Hugo (3500) and Bilal's new salary (3200): 3100 + 3400 + 3900 + 3500 + 3200 = 17100.
(b) Too strong, too weak
A is too strong. It is correct, but every writer on the table waits for a report that may last minutes, including T3 and T5, which have nothing to do with the range. Concurrency collapses to one transaction at a time on the table.
B is too weak. It locks data, but the query is about a predicate. When T1 takes its locks, Hugo's row does not exist yet, and you cannot lock a row that does not exist. Bilal's row exists but does not match the predicate, so it is not locked either, and T4 moves it into the range.
Rows that appear in the result of a query re-run inside the same transaction are called phantoms, and this anomaly is a phantom read. Procedure B is exactly level 2 (Repeatable read), which is why level 2 allows phantoms. Level 3 (Serializable) must prevent them, and that is what range locks are for.
(c) Procedure C: lock the range in the index (next-key locking)
This is how lock-based DBMSs implement level 3, for example MySQL/InnoDB and SQL Server's key-range locks.
Scan the index on salary from 3000 to 4000. Lock every index entry found and the gap before it, then lock the first entry after the range (4500) together with its gap. The locked region is (2800, 4500].
Any writer that wants to insert a key, or move a key, into a locked gap must first request an "insert-intention" lock on that gap, which conflicts with T1's lock, so it waits. T2 (3500 ∈ (3400, 3900]) and T4 (3200 ∈ (3100, 3400]) are blocked.
Keys outside (2800, 4500] are untouched: T3 and T5 proceed.
It needs an index on the column. Without one, the DBMS has to scan the whole table, so it locks every row and every gap, which degenerates into procedure A.
(d) Why the margin? Why not lock exactly [3000, 4000]?
A lock manager can only lock things that have a name. It is essentially a hash table: resource → who holds it, in which mode. Checking for a conflict is one hash lookup, so it costs $O(1)$. A table, a page, a row and an index key all have names. The interval [3000, 4000] does not: no row and no key in the database is called 3000 or 4000.
Next-key locking borrows names that already exist: the index keys. Each gap between two consecutive keys is named after the key that closes it, so the gap (3900, 4500] "belongs to" key 4500. To insert 4200, the writer has to walk down the B-tree to where 4200 goes anyway, and it lands right before 4500. Checking the lock on 4500 is one extra hash lookup. The price is that only whole gaps can be locked. The gap containing 4000 is (3900, 4500], and cutting it at 4000 would need a key 4000 that does not exist. That is the margin: (2800, 3100] at the bottom and (3900, 4500] at the top.
Locking exactly the range means predicate locking. The DBMS stores the condition salary BETWEEN 3000 AND 4000 itself, and checks every write against it. For this single interval, that would actually be cheap. In general it is not:
A predicate is not a name, so a conflict cannot be found with a hash lookup. Every write has to be tested against every predicate held by every active transaction.
Predicates are arbitrary SQL, for example dept = 'IT' AND salary * 1.1 > bonus. Deciding whether two arbitrary predicates can be true for the same row is hard in general.
Q9
Durability: The Order of Operations
A client sends UPDATE accounts SET balance = 80 WHERE id = 2; COMMIT; to a modern DBMS. The arrows below are labelled with letters in random order (both arrows marked G belong to the same step):
A: append <T, COMMIT> to the log and flush it to disk (fsync)
B: write the "new" page to the database files
C: the client sends the SQL query
D: return "OK" to the client
E: apply the change to the page in the buffer pool and mark the page "new"
F: write a checkpoint record in the log
G: look up the page in the buffer pool; if it is not there, read it from the database files and load it into the buffer
H: append the change record <T, page, old=50, new=80> to the log
(a) Put the steps in an order that guarantees atomicity and durability. can we apply any step in the background?
(b) For durability, can we keep the database only in memory? Can we keep the log only in memory?
(c) What goes wrong if B happens before H? What goes wrong if D happens before A?
(d) The power fails while B is writing several "new" pages, so half of them are on disk. What does the DBMS do at restart?
(e) What is step F for? What would happen without it?
(a) The order
C → G → E → H → A → D, then in the background B → F.
#
Step
Where
1
C · receive the query
client → process
2
G · fetch the page (from disk on a miss)
buffer / disk
3
E · modify it in the buffer, mark it "new"
RAM
4
H · log record (old and new values)
log
5
A · COMMIT record, fsync: the commit point
disk
6
D · answer "OK"
process → client
7
B · flush "new" pages (background, batched)
disk
8
F · checkpoint record
log
E and H may be swapped. The real constraints are that H is on disk before B (write-ahead), and that A is on disk before D (commit before acknowledging).
(b) Memory only?
Database in memory: no. A power cut erases RAM, so the data of committed transactions would be lost. Log in memory: no. The log is the durable proof of the commit. If it only lives in RAM, a crash after D loses a transaction the client was told was committed, and recovery has nothing to undo or redo from. Any trade-off here might reduce consistency.
(c) Breaking the order
B before H: the disk holds an uncommitted value with no log record describing it. If the transaction then aborts or the machine crashes, recovery does not know the old value (50) and cannot undo the change, so atomicity is broken. This is the case the write-ahead rule exists to prevent.
D before A: the client is told "committed", but the commit record is not yet on disk. A crash at that moment makes recovery treat T as a loser and undo it, so durability is broken.
(d) Power failure in the middle of B
Read the log from the last checkpoint.
Redo every committed transaction: set the new values. Pages that were already written are simply rewritten, or skipped.
Undo every transaction without a COMMIT record by restoring the old values stored in the log.
(e) Checkpoints
A checkpoint records "every change up to this point is in the database files". Recovery can start from there, and the log before it can be recycled. Without checkpoints the log grows without bound, and recovery must replay it from the very beginning, which can take hours after a crash.
Part 4 · Graph databases
Q10
Finding a Route with Relational Databases
Go back to the 3NF train schema of Q2. Routes are stored in RouteStop(train_no, stop_order, station_code, dep_time), one row per stop. An excerpt:
train_no
stop_order
station_code
dep_time
TER 8412
1
NTE (Nantes)
06:10
TER 8412
2
RNS (Rennes)
07:25
TGV 8612
1
RNS (Rennes)
07:40
TGV 8612
2
LAV (Laval)
08:07
TGV 8612
3
LMN (Le Mans)
08:30
TGV 8612
4
PMO (Paris-Montparnasse)
09:07
TGV 9575
1
PST (Paris-Est)
10:25
TGV 9575
2
STG (Strasbourg)
12:12
The network has about 3,000 stations and 10,000 trains of about 15 stops each, so roughly 150,000 rows. A customer asks: "How do I get from one station to another?"
(a) Can you write a query that lists the trains going directly from Rennes to Le Mans?
(b) Now the customer accepts changes: find any itinerary from Nantes to Paris-Montparnasse. Can you write such a query?
(c) Can you write a query that find paths from Rennes to Strasbourg?
(a) Direct trains: one self-join (or a group-by).
SELECT a.train_no
FROM RouteStop a JOIN RouteStop b ON a.train_no = b.train_no
WHERE a.station_code = 'RNS' AND b.station_code = 'LMN'
AND a.stop_order < b.stop_order; -- TGV 8612
This is easy because the path has a known length: one train, so one join.
(b) Any number of changes: recursion
With a fixed number of joins we can only find paths up to a fixed length: 1 join for one hop, 2 for two, and so on. Reachability, meaning a path of any length, cannot be expressed in plain relational algebra. We need to use recursive queries.
(c) Transfers are a second kind of edge
The schema only knows "the next stop of the same train". Walking between two stations, or changing trains with a minimum connection time, is not stored anywhere. We need a new table, for example Transfer(from_station, to_station, minutes), and the recursive step must now follow either a Hop (that needs to be properly defined) or a Transfer:
FROM reach r JOIN (SELECT src, dst FROM Hop
UNION ALL
SELECT from_station, to_station FROM Transfer) e
ON e.src = r.station
Every new kind of relationship (bus, ferry, walking path) means one more table and one more branch in the recursion. If we also want the times to be consistent (the next train must leave after we arrive, plus the transfer time), the recursion must carry the current time and compare it at each step. The query quickly becomes hard to write and to optimise.
In general
The relational model can answer the question, so it is not a matter of expressiveness (since the introduction of recursive queries). It is a matter of fit: the question is about relationships and paths of unknown length, while the model is built around sets of rows combined by joins. That is the motivation for graph databases (Q11).
Q11
The Railway Network as a Graph
The operator decides to store its network in a graph database, to answer route queries like those of Q10.
(a) Propose a labelled property graph (LPG) for the network. What are the nodes, the relationships, their labels and their properties? Draw the part of the network that is necessary to answer questions from Q10.
(b) Can we move everything (passengers, bookings, tickets, payments) to the graph database?
(a) A labelled property graph
Nodes labelled :Station with properties code and name, and :City with name and region.
Relationships:NEXT between two consecutive stops of a train, with properties train_no, dep, arr and class. :WALK between two stations you can walk between, with minutes. :IN from a station to its city.
Relationships are stored, not recomputed: the "next stop" from Q10's view now exists physically. A new kind of connection (bus, ferry) is a new relationship type, with no schema migration.
For timetable-exact routing ("leave after 07:00, arrive as early as possible"), an alternative is a time-expanded graph, with one node per (train, stop, time) event. It is bigger, but a shortest path in it is automatically consistent with the times.
Important to note
Index-free adjacency (discussed in class) is a property of the storage engine, not of the data model. A relational DBMS that speaks a graph query language might still uses indexes underneath.
Pointers make graph databases great at local traversals but poor at global scans and aggregates ("average ticket price per region"), and harder to partition across machines, because cutting the graph cuts pointers.
(d) Everything in the graph?
It might be possible, but it is not appropriate here. Bookings, tickets and payments are records: they need ACID transactions, constraints (no double-booked seat), aggregates. That is exactly what the relational model is good at. The network, used for routing, is a graph. A common design is a multi-model approach: the relational database stays the source for tickets and passengers, and the graph of stations and connections is derived from RouteStop and kept in sync.
🔑 Professor Access
Enter the unlock code to reveal model answers after class discussion.