Schedules and Serializability

Normalization and Transactions · 30 min

Core CS · DBMS

Swap two operations, lose twenty-two rupees

Two transactions, eight operations, and one interleaving that is safe sitting next to one that is not. You will build the precedence graph edge by edge and watch a single swapped operation put an arrow in the wrong direction.

Run both schedules, one operation at a time
A concurrent schedule is correct when it leaves the database in a state that some serial order would also have left it in. Not when the totals look plausible, and not when nothing crashed.

01 The idea

Serial is correct, and serial is far too slow

A transaction is one unit of work: a group of reads and writes that must all take effect or none of them. A schedule is the order in which the database actually executes the operations of several transactions, with each transaction’s own operations kept in its own order. If the schedule finishes one transaction completely before starting the next, it is a serial schedule, and a serial schedule is correct by definition, because no transaction ever sees another one’s half-finished work.

So why not run everything serially. Because a transaction spends most of its life waiting: for a page to come off disk, for a network round trip, for a lock somebody else is holding. Serial execution leaves the processor idle through every one of those waits, and it makes a one-row balance check queue behind a report that scans a million rows. Interleaving fixes both at once. Throughput goes up because the machine has something to do during the waits, and average response time comes down because a short transaction no longer sits behind a long one.

What interleaving costs you is the guarantee. Once operations from two transactions land between each other, some orders produce a database state that no serial run could have produced. Two transactions of four operations each can be interleaved 8! / (4! × 4!) = 70 ways while keeping each transaction’s own order intact, and exactly two of those seventy are serial. So “just run them serially” is not a usable rule, and “it looked fine afterwards” is not a test. What you need is a procedure that takes any one of the seventy and answers a single question: is its effect the same as some serial order?

Serialisability is the standard, not serial execution. Interleave as much as you like, as long as the result is one that some serial order would also have produced. The rest of this lesson is one test for that, and the protocol real systems follow instead of the test.
ScheduleThe order the database executes the reads and writes of two or more transactions in. Operations of different transactions may be interleaved freely; operations inside one transaction may never be reordered.
Serial scheduleA schedule with no interleaving at all: each transaction runs start to finish before the next begins. There are n! of them for n transactions, and every one is correct, which is why they are the yardstick.
ConflictTwo operations conflict when they belong to different transactions, touch the same data item, and at least one of them is a write. Two reads never conflict, and operations on different items never conflict.

02 Worked example

One transfer, one interest run, eight operations

Asha has 100 rupees in her savings row A and 50 in her current row B. Two transactions want both rows tonight. T1 is a transfer: it moves 20 from A to B. T2 is the interest run: it adds ten per cent to A, then ten per cent to B. Each is four operations, a read and a write per row. Here is one interleaving of them, with the value on disk after every single operation.

-- T1  move 20 from A to B        r1(A) w1(A) r1(B) w1(B)-- T2  add 10% to A, then to B    r2(A) w2(A) r2(B) w2(B) --   schedule S1                              A      B--   before anything runs                    100     50 1   r1(A)    T1 reads A = 100            100     50 2   w1(A)    T1 writes A = 100 - 20       80     50 3   r2(A)    T2 reads A = 80              80     50 4   w2(A)    T2 writes A = 80 + 8         88     50 5   r1(B)    T1 reads B = 50              88     50 6   w1(B)    T1 writes B = 50 + 20        88     70 7   r2(B)    T2 reads B = 70              88     70 8   w2(B)    T2 writes B = 70 + 7         88     77--   final state                              88     77

Nothing in those numbers tells you whether the schedule was safe. 88 and 77 look reasonable, and so will the numbers the broken version of this schedule produces in section 04. The test does not look at values at all. It looks only at which pairs of operations were forced into an order, and it runs like this.

1 · Take the schedulethe eight operations in the order the database ran them, with nothing reordered inside a transaction
2 · Find the conflicting pairsdifferent transactions, same row, at least one write. r1(A) and r2(A) are both reads, so they are not a pair
3 · Draw one edge per paira node per transaction, and an arrow from whichever operation came first to the other: w1(A) before r2(A) draws T1 to T2
4 · Look for a cyclewith two transactions a cycle needs one arrow each way; with three or more you have to walk the graph properly
5 · Read the verdictacyclic means conflict serialisable, and any topological order of the graph is a serial schedule it matches

Apply that to S1. Row A gives three conflicting pairs: r1(A) with w2(A), w1(A) with r2(A), and w1(A) with w2(A). Row B gives three more of exactly the same shape. In all six of them the T1 operation is the one that came first, so all six arrows read T1 to T2, and six copies of the same arrow are still one edge. The graph is two nodes with a single arrow between them. There is no cycle, so S1 is conflict serialisable, and the only topological order of that graph is T1 then T2.

Check it by hand. Run T1 on its own and A becomes 80 and B becomes 70. Now run T2 on those values: 80 + 8 = 88 and 70 + 7 = 77. The same pair of numbers S1 produced. The graph told you that before you did any arithmetic, and it would still tell you on a schedule of forty operations where doing the arithmetic is not an option.

Now move one operation, and only one. Swap operations 6 and 7, which are w1(B) and r2(B), so that T2 reads B before T1 writes it, and change nothing else. That single adjacent swap turns one of the six pairs around, and with T1 to T2 already on the graph, one arrow pointing the other way closes a cycle. Section 04 runs both schedules a step at a time so you can watch the graph change under the swap.

03 Mechanics

Three ways it goes wrong, and three grades of schedule

The whole test rests on the conflict rule, so start there. Two operations conflict when they are from different transactions, on the same data item, and at least one is a write. Three conditions, all of which have to hold. For one row and two transactions the entire rule fits in four cells.

T1 on row A, down · T2 on row A, acrossT2 reads AT2 writes A
T1 reads Ano conflict · swap the two and nothing changesconflict · whoever went first fixes the direction of the edge
T1 writes Aconflict · the reader sees a different value depending on the orderconflict · the write that happens second is the one that survives

Only read against read is free, and that single exception is what makes read-heavy workloads cheap to run concurrently. Operations on different rows never conflict whatever they are, which is why a schedule spread over ten rows usually draws far fewer edges than it has operations. Now the three failures those edges exist to catch. All three use the same two rows and the same starting values, 100 and 50.

ProblemThe interleavingWhat the database is left holdingWhat the graph says
Lost update r1(B) r2(B) w1(B) w2(B) Both read 50. T1 writes 70. T2 writes 50 + 5 = 55, computed from the value T1 had already replaced. B ends at 55 and the 20 rupees T1 moved are gone. No operation failed and no error was raised. r2(B) before w1(B) draws T2 to T1; r1(B) before w2(B) draws T1 to T2. Cycle. Rejected
Dirty read
(uncommitted dependency)
r1(A) w1(A) r2(A) abort1 w2(A) T1 writes 80, T2 reads that uncommitted 80, T1 aborts and A goes back to 100, then T2 writes 80 + 8 = 88. T1 never happened, so A should read 110. T2 charged interest on a number that was never in any committed state. every edge runs T1 to T2. Acyclic. Conflict serialisable, and still wrong
Unrepeatable read r3(B) w2(B) commit2 r3(B) T3 is an audit, a third transaction that reads each row twice to check its own total. It reads B = 50, T2 writes 55 and commits, T3 reads B = 55. One transaction, two answers, and neither is wrong at the moment it was taken. r3(B) before w2(B) draws T3 to T2; w2(B) before the second r3(B) draws T2 to T3. Cycle. Rejected

The middle row is the one that decides interviews. Its precedence graph is acyclic, so the schedule passes the serialisability test, and it still leaves the database holding interest charged on a value that was rolled back. Serialisability is a statement about the order of reads and writes. It says nothing at all about commits and aborts, and “T2 read something from a transaction that later died” is not a fact any cycle can express. That gap is covered by three separate properties, in increasing strength.

GradeThe ruleDoes the dirty-read schedule pass?What it buys you
Recoverable If Tj reads a value Ti wrote, Ti must commit before Tj commits. no · T1 aborted, so it never commits, and T2 must not be allowed to commit either You never have to undo a transaction that already told the user it succeeded. This is the floor. Below it the word “committed” means nothing.
Cascadeless
(avoids cascading aborts)
Tj may read a value Ti wrote only after Ti has committed. no · r2(A) read the 80 that T1 had not committed One abort never drags other transactions down with it. Without this, aborting T1 forces T2 to abort, which may force T3, and so on down the chain.
Strict No transaction may read or write an item until the transaction that last wrote it has committed or aborted. no · the same read at r2(A) Undo becomes a plain restore of the before image: put back the value that was there. Nothing else can have overwritten it in the meantime.

Strict implies cascadeless implies recoverable, and the implication never runs the other way. None of the three implies serialisability, and serialisability implies none of them. The unrepeatable-read schedule r3(B) w2(B) commit2 r3(B) is strict, because nobody read or wrote B while another writer was still open, and its graph still has a cycle. You are being asked two independent questions about every schedule and you need both answers ready.

There is a weaker condition than conflict serialisability, called view serialisability: a schedule is view serialisable if there is some serial schedule in which every read reads from the same write and the final write of each item is the same one. It accepts everything conflict serialisability accepts plus a few extra schedules, and every one of those extras contains a blind write, meaning a transaction writing an item it never read. Nobody tests for it, because deciding view serialisability is NP-complete while the precedence graph is a cycle check on a graph with one node per transaction.

And a running database does neither test, because both of them need the finished schedule and a live system has to decide before each operation runs. What it follows instead is a protocol: two-phase locking. Every transaction takes every lock it will ever need before it releases any of them, so its life splits into a growing phase where it only acquires and a shrinking phase where it only releases. The instant it takes its last lock is called its lock point. Any schedule that a set of two-phase transactions can produce is conflict serialisable, and that is the whole trade: you stop asking whether a schedule was safe and follow a rule that cannot draw a cycle. Strict two-phase locking additionally holds every exclusive lock until commit or abort, which hands you strict schedules on top. What 2PL does not hand you is freedom from deadlock, and that is its price.

The precedence graph is the definition and two-phase locking is the implementation. One tells you whether a schedule that already happened was safe; the other stops an unsafe one from happening. An interview answer that has the first and not the second, or the second and not the first, is half an answer.

05 Cheat sheet

The whole test on one card

Two independent questions get asked about every schedule, in this order: is it serialisable, and what happens if somebody aborts. The first five rows answer the first question, the next three answer the second, and the last row is what a real system does instead of asking either.

TermThe testThe verdict it gives
Conflictdifferent transactions, same item, at least one writeread against read is the only free pair
Serial scheduleno interleaving at alln! of them, every one correct
Precedence graphone node per transaction, one edge per conflicting pair, pointing away from the operation that came firsta cycle means reject, and there is nothing else left to check
Conflict serialisablethe precedence graph is acyclicequivalent serial order = any topological sort of the graph
View serialisablesame reads-from pairs and same final write per item as some serial schedulea strictly larger set, and NP-complete to decide
RecoverableTi commits before Tj commits, whenever Tj read what Ti wroteno committed transaction ever has to be undone
CascadelessTj reads what Ti wrote only after Ti has committedno dirty reads, so one abort stays one abort
Strictno read or write of an item until its last writer has committed or abortedundo is a restore of the before image
Two-phase lockingevery lock acquired before any lock is releasedguarantees conflict serialisability, guarantees nothing about deadlock
Acyclic is if and only ifThe precedence graph test is not a heuristic. A schedule is conflict serialisable exactly when its graph is acyclic, in both directions of the implication. A cycle is proof of rejection, no cycle is proof of acceptance, and no further check exists. Say “if and only if” out loud when you are asked, because candidates who say “usually” get followed up on.
Serialisable does not mean safeThe dirty-read schedule in section 03 has an acyclic graph and still leaves interest charged on a value that was rolled back. Serialisability constrains the order of reads and writes and nothing else. Recoverable, cascadeless and strict are what cover aborts, and you will be asked for both sets of words inside the same answer.
Two transactions hide the hard caseWith two transactions a cycle can only be arrows in both directions, so the test collapses to “did one pair put T1 first while another pair put T2 first”. If every arrow runs the same way, whichever way that is, the graph is acyclic and the schedule is serialisable. With three or more, a graph can cycle as T1 to T2 to T3 to T1 with no two nodes pointing at each other at all. That is why exam questions almost always use three transactions, and why you walk the graph rather than scan for a mutual pair.

06 Where & why

Which database actually tests this

No mainstream engine builds a precedence graph while you type. Each one picks a protocol that makes cycles impossible, or a cheaper protocol that lets some through and documents which. Knowing which is which is what turns this from a chapter into something you can say about a system you have run. Every engine below that lets two transactions interleave ships with a default level that is not serialisable, and the one that is serialisable by default gets there by refusing to interleave at all.

PostgreSQL
A graph test, just not this graph

SERIALIZABLE here is serialisable snapshot isolation. Transactions run against a snapshot and the engine watches for the read-write dependency pattern that every cycle has to contain, then aborts one transaction with a serialization_failure rather than making anybody wait. Readers never block writers, so the cost lands in your retry loop instead of your throughput. The default is READ COMMITTED, which stops dirty reads and nothing else, so the unrepeatable read from section 03 is allowed there by design.

MySQL · InnoDB
Strict two-phase locking underneath

InnoDB takes row locks and holds every exclusive lock until commit or rollback, which is strict 2PL, and is why its rollback is a plain undo. Its default is REPEATABLE READ, where a plain SELECT reads a consistent snapshot, so the unrepeatable read does not happen there but write skew still does. Ask for SERIALIZABLE and InnoDB quietly turns every plain SELECT into a locking read, which is the textbook protocol at textbook cost.

Oracle · SQL Server
One keyword, two different protocols

Oracle’s SERIALIZABLE is snapshot isolation: readers are never blocked, a write collision raises ORA-08177, and write skew is permitted, which the standard’s definition does not allow. SQL Server’s SERIALIZABLE takes key-range locks and holds them to the end of the transaction, which is rigorous two-phase locking, while its default READ COMMITTED releases each shared lock as soon as the statement ends. One word, two protocols, and the gap between them is a standard interview question.

SQLite
Serialisable by refusing to interleave

In rollback-journal mode SQLite allows one writer and no concurrent reader while that write is in progress, so its schedules are serial and the question never arises. In WAL mode readers run against a snapshot while one writer works, and a second writer gets SQLITE_BUSY instead of an interleaving. It is the honest reminder that the cheapest way to guarantee serialisability is to refuse concurrency, and that refusing concurrency is exactly the throughput you are buying back on a bigger engine.

When an interviewer asks how a database guarantees serialisability, name the protocol rather than the property, then say what the default isolation level actually gives you. On every engine that lets two transactions run at once, that default is something weaker than serialisable, and knowing that is the difference between having read the chapter and having run the system.

07 Interview questions

What they actually ask

This topic arrives straight after ACID, and it is usually answered with three memorised nouns and no test at all. Whoever can draw the graph, get the arrows the right way round, and then say what serialisability does not cover is answering a different question from everybody else in the queue.

What is a schedule, and what makes one serial?
A schedule is the order the database actually executes the reads and writes of several transactions in, with each transaction’s own operations kept in its own order. It is serial if no transaction begins until the previous one has finished, so nobody ever sees anybody else’s half-finished work. Every serial schedule is correct by definition, which is exactly why serial schedules are the yardstick the whole theory measures against.
If serial execution is correct, why interleave at all?
Because a transaction spends most of its life waiting: for a page off disk, for a network round trip, for a lock. Serial execution leaves the processor idle through every one of those waits, and it makes a one-row balance check queue behind a report that scans a million rows. Interleaving raises throughput and cuts average response time, and concurrency control exists to buy that back without giving up the correctness serial had for free.
When exactly do two operations conflict?
When they are from different transactions, they touch the same data item, and at least one of them is a write. All three conditions have to hold, so read against read never conflicts and nothing on different items ever conflicts. The definition is shaped that way because two adjacent non-conflicting operations can be swapped without changing any outcome, and two conflicting ones cannot, so the conflicting pairs are precisely the pairs whose order is forced.
What does it mean for a schedule to be conflict serialisable?
That you can turn it into some serial schedule by repeatedly swapping adjacent non-conflicting operations, however many swaps that takes. The testable form is the precedence graph: the schedule is conflict serialisable if and only if that graph is acyclic. That “if and only if” is between the graph and conflict serialisability, not between the graph and correctness: conflict serialisability is sufficient for correctness but not necessary, so a schedule can fail the graph test and still be view serialisable.
How do you test a schedule for conflict serialisability?
Draw one node per transaction. Walk the schedule, and for every conflicting pair draw an edge from the transaction whose operation came first to the other one. Then look for a cycle. Acyclic means conflict serialisable, and any topological ordering of the graph is a serial schedule the original is equivalent to. Cyclic means reject, and there is nothing further to check.
Name the three problems concurrency causes.
Lost update: two transactions read the same item, both write it, and the second write is computed from the value the first one replaced, so the first update disappears with no error raised. Dirty read: one transaction reads an item another has written but not yet committed, and the writer then aborts. Unrepeatable read: a transaction reads an item, somebody else writes and commits it, and the first transaction reads it again inside the same transaction and gets a different number.
A schedule with a dirty read in it turns out to have an acyclic precedence graph. Is it safe?
No, and holding those two facts together is the point of the question. Serialisability constrains the order of reads and writes; it says nothing about commits and aborts, so a conflict serialisable schedule can be unrecoverable. The properties that cover aborts are separate ones: recoverable, cascadeless and strict. The implication fails in both directions, because a strict schedule can also have a cycle in its graph.
Recoverable, cascadeless, strict. What is the difference?
Recoverable is the weakest: if Tj read something Ti wrote, Ti has to commit before Tj commits, so you never have to undo a transaction that already reported success. Cascadeless is stronger: Tj may not read Ti’s write until Ti has committed at all, so one abort never drags others down with it. Strict is strongest: nobody may read or write an item until the transaction that last wrote it has ended, which makes undo a plain restore of the before image. Strict implies cascadeless implies recoverable, and never the reverse.
What is view serialisability, and why does no system use it?
A schedule is view serialisable if there is a serial schedule in which every read reads from the same write and the final write of each item is the same one. It accepts everything conflict serialisability accepts plus some extras, and every extra contains a blind write, meaning a transaction writing an item it never read. Nobody tests for it because deciding view serialisability is NP-complete, whereas the precedence graph is a cycle check on a graph with one node per transaction.
A running database cannot draw a precedence graph. So how does it guarantee serialisability?
With a protocol instead of a test, because the graph needs the finished schedule and a live system has to decide before each operation runs. Two-phase locking splits every transaction into a growing phase where it only acquires locks and a shrinking phase where it only releases them, and any schedule a set of two-phase transactions can produce is conflict serialisable. Strict two-phase locking additionally holds every exclusive lock until commit or abort, which gives strict schedules on top.
What does two-phase locking cost you?
Waiting, and deadlock. Holding locks to the end of a transaction means the second writer of a row sits idle for as long as the first one runs, so throughput tracks lock hold time rather than machine speed. And 2PL does not prevent deadlock: two transactions that take the same two rows in opposite order each end up holding what the other wants. The database finds that as a cycle in its wait-for graph, which is a different graph from the precedence graph, aborts one transaction, and your code has to retry it.
Writing ordinary application code, how much of this do you actually touch?
Almost none of it directly, and saying so honestly is better than pretending otherwise. You choose an isolation level and you write retry loops for the errors that level throws: serialization_failure on PostgreSQL, ORA-08177 on Oracle, a deadlock victim on SQL Server. What the theory buys you is the ability to read your default level and know which anomaly it still permits, and to recognise a lost update in a bug report instead of filing it as a race condition and moving on.

08 Practice problems

Six schedules to take apart

Work each one on paper with the graph drawn out. The most common mistake by far is drawing an edge from the wrong end of the pair, and it stays invisible until the verdict comes out backwards.

Three pairs, one verdict

Easy
Take the schedule r1(A) r2(A) w1(A) r1(B) w2(A) w1(B) over data items A and B. List every conflicting pair in it, say which edge each pair draws, and give the verdict.
Follow-up
One of the two data items contributes no pairs at all, and saying why is half the answer. On the other item you have to draw all three pairs before you can say anything, because the verdict here turns entirely on the direction of one of them.
Show the hint
Take the items one at a time, and inside an item take the pairs in the order the operations appear on the page. The pair people get wrong is the one where the read happened before the write.

Count the interleavings

Easy
T1 has three operations and T2 has two. Keeping each transaction’s own operations in order, how many distinct schedules of the two exist, and how many of those are serial? Then answer both questions again for three transactions of two operations each.
Follow-up
The count depends only on how many operations each transaction has, never on what those operations are or which items they touch. Say what that fact implies about checking every possible schedule, and therefore about why no database does concurrency control that way.
Show the hint
You are choosing which positions in the combined sequence belong to which transaction. Once those positions are chosen, each transaction has only one order it is allowed to fill them in.

Fix it with one move

Medium
The schedule r1(A) r2(A) w2(A) w1(A) r1(B) w1(B) is not conflict serialisable. Move exactly one operation to a different position, without changing the relative order of operations inside either transaction, so that it becomes conflict serialisable, and name the serial order it then matches.
Follow-up
Two different single moves work, and they give two different serial orders. With T1 subtracting 20 from A, T2 adding ten per cent to A, and A starting at 100, work out what A holds under each of them and say why the theory is content with either answer.
Show the hint
You only have to remove the edges running in one of the two directions. Pick a direction, list every pair that draws it, and look for one position change that kills all of them at once.

Grade three schedules

Medium
For each of these three schedules over item A, say whether it is recoverable, whether it is cascadeless, and whether it is strict, and name the exact operation at which each failing property first breaks: (a) w1(A) r2(A) c1 c2; (b) w1(A) r2(A) c2 c1; (c) w1(A) c1 r2(A) c2.
Follow-up
One of the three has all three properties and one has none of them. The middle one is the interesting case, because the single property that separates it from the first is the only one of the three that is about the order of two commits rather than about a read.
Show the hint
Build two lists for each schedule: which operations read a value whose writer had not yet ended, and which commit happens before which. All three properties are answered from those two lists and nothing else.

A cycle with no mutual pair

Medium
Construct a schedule of three transactions T1, T2 and T3 over just two data items A and B whose precedence graph is exactly the cycle T1 → T2 → T3 → T1, with no two of the three edges running between the same pair of nodes. Write the schedule out operation by operation and name the conflicting pair that draws each of the three edges.
Follow-up
With two transactions a cycle can only be arrows in both directions, so this is the shape you cannot practise on a two-transaction example. Your graph must have exactly three edges and no two of them between the same pair, which rules out the obvious construction of chaining two lost updates together.
Show the hint
Decide the three edges first and write down, for each one, the two operations that have to draw it. Then look for a single ordering of all those operations that still respects each transaction’s own internal order.

Prove the protocol, then break it

Hard
Two-phase locking guarantees conflict serialisability. Prove it: assume the precedence graph of a schedule produced by two-phase transactions contains an edge from Ti to Tj, state what that edge forces about the two transactions’ lock points, then extend the argument all the way around a cycle. After that, write out the lock and unlock operations of two two-phase transactions that deadlock, and say precisely why the deadlock is not a counterexample to what you just proved.
Follow-up
The proof turns on one moment in each transaction’s life that the protocol names but the schedule itself never shows. Find that moment before you write a line. The deadlock half then forces you to state exactly what the theorem claims and, more importantly, what it does not.
Show the hint
For an edge from Ti to Tj to exist, Ti must have released its hold on that item before Tj could touch it. Ask what releasing anything at all means about whether Ti was still allowed to acquire.