Skip to main content
Database Design and Applications

B+ Trees, Transactions, and ACID

Published: 2026-08-06
Level: postgraduate
Audience: Postgraduate students in Database Design and Applications

Prerequisite Knowledge

This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.

Previously Covered in This Subject

  • Indexes, B-trees, and B+ trees — covered in Lecture 10 and Lecture 13
  • Functional dependencies and normalization — covered in Lecture 4 and Lecture 5
  • Transactions and the ACID properties — covered in Lecture 1 and Lecture 12
  • Transaction states, commit, and the log — covered in Lecture 12
  • Schedules and conflict serializability — covered in Lecture 1 and Lecture 12

14.1 The B+ Tree: Structure and Node Capacity

14.1.1 Why the B+ Tree Exists

Hook: You can find any record in a B+ tree with only two or three disk reads, even in a database with millions of rows. How did indexing evolve to get there — and what was broken along the way?

The indexing story so far: a single-level index was fine until it consumed too much memory, so we built a multi-level index out of it. A multi-level index tells us where the actual block is on disk, and within that block we can locate the actual data — we studied several variants of this, including dense index, sparse index, and so on. But multi-level indexing had a serious flaw: insertion and deletion were a pain. Every time a record was inserted or deleted, the index had to be rebuilt or rearranged, and that was an expensive affair.

The B-tree was the response: insertion and deletion became relatively fine. Adding or removing values and making whatever changes were required in a B-tree was no longer impossible or prohibitively expensive. But even the B-tree left one gap — range queries were not easy. Whenever we had to answer a range query (give me all keys from to ), things became very, very difficult.

Intuition — the three-stage lineage. Think of the index as a stack of filing drawers.

  1. Single-level index: one master drawer lists every drawer below it. Simple, but the master list itself outgrows memory.
  2. Multi-level index: the master list is itself indexed — drawers within drawers. Locating is fast, but every insertion or deletion forces re-labelling many drawers: rebuild or rearrange, expensive.
  3. B-tree: the drawers become self-balancing — split and merge locally, so insertion and deletion stop being painful. But asking "everything from K2 to KQ" still means climbing up and down between drawers.

The B+ tree exists because of exactly this: it keeps the B-tree's cheap insertion and deletion, and it adds cheap range queries. The extra ingredient is a single chain: the last slot of every leaf holds a pointer to the next leaf block. Once you are at the first qualifying leaf, every later key is one pointer walk away, in sorted order, without climbing back up the tree. And since range queries are everywhere in real life, most commercial applications, whenever they create an index, create a B+ tree out of it.

The analogy's break point: a B+ tree is not a fixed set of drawers — its blocks split and merge, so the structure grows and shrinks with the data. The "filing drawer" picture captures the layout, not the dynamics; the dynamics come in Sections 14.2 and 14.3.

Real-world: any real database system that creates an index through SQL creates a B+ tree under the hood — that is the default structure behind the create index statement. Oracle, SQL Server, and PostgreSQL all ship B+ tree–style indexes as their workhorse index type, precisely because real workloads are dominated by range scans ("all orders between these two dates"), not just point lookups.

14.1.2 The Node Layout: Keys and Pointers

A B+ tree node is described by a single number called the p value (spoken as "p-value"): p tells us how many pointers a particular block can hold.

  • In the root or at any intermediate level, a node holds keys and, between and around them, pointers. With keys there are pointers:

Each pointer in an internal node is a block pointer: it points to another block of the tree — the next level of the index. The keys between two pointers are the routing values: everything in the subtree under a pointer is bounded by the keys around that pointer.

Formalize — why , never or . An internal node is a set of separators that cut the key range into buckets. Think of walls dividing a corridor: with walls you get rooms. Key separates everything before it from everything after it, does the same further right, and so on:

  • Subtree 1 (left of ): all keys
  • Subtree 2 (between and ): all keys
  • Subtree (right of ): all keys

The number of buckets is always one more than the number of separators. That is why keys force pointers — the geometry of "walls make rooms" cannot produce any other number.

  • At the leaf level (also called the child level), the node contains the actual data values, which we again call keys, and each key has a record pointer pointing to the physical block on disk that contains the record with that key. That is the crucial structural difference: in internal nodes the pointers are block pointers (they point to the next level of index), but at the leaf level the pointers are record pointers (they point to the actual records). The last slot of a leaf node holds one more pointer — a pointer to the next leaf block coming after it.
Node level Contents Pointers are Purpose
Root / internal Keys Block pointers (to child nodes) Routing a search down to the right leaf
Leaf (child) Keys with full data values Record pointers (to data blocks) + one next-leaf pointer Finding the actual records; walking a range

That next-leaf pointer is what makes range queries easy. For a range query from to spanning different blocks, we start at the leaf level from the block holding , follow the record pointers, and when one leaf is exhausted we just walk the next-leaf pointer to the next block, without climbing back up the tree. We have all the record pointers for the whole range laid out in order. This is how searching in a B+ tree becomes easier for range queries.

Pitfalls — three common confusions about the layout.

  1. Internal keys are not the data. A key in an internal node is a boundary value for routing; the actual record for that key lives in a leaf. Searching for key 9 does not stop when you see 9 in a root node — you still descend to the leaf that holds the real entry.
  2. Record pointers exist only at the leaves. If you see a "record pointer" in an internal node, the drawing is wrong. Internal nodes route; only leaves point at records.
  3. The next-leaf pointer is not an index key. It sits in the last slot of a leaf and never carries a value — it is the chain that makes range scans linear instead of logarithmic.

14.1.3 The Capacity Rules

The capacity rules of a B+ tree follow directly from p:

  • Maximum keys in a block: .
  • Minimum keys in a block: (the spoken description was "four divided by two, ceiling minus one", which for gives ).

For our running example, : at most we can store three keys in a block, and the pointers are at most four. The minimum number of keys is one — fall below one and the block is in trouble, which is what triggers deletion restructuring (see Section 14.3).

Formalize — where the two numbers come from. The block size is fixed by disk geometry: a node is a disk block, and a disk block fits pointers plus the keys between them. The rules then write themselves:

  • Maximum keys: with pointer slots there are at most key gaps, so at most separator keys. Inserting one more key than this makes the block overflow.
  • Minimum keys: a block that falls below half its pointer capacity is wasted space and a symptom of imbalance — the tree stops being "bushy". The (ceiling) handles odd : e.g., gives minimum keys.

Worked example — reading the rules off .

  • : max keys; min key.
  • : max keys; min keys.
  • : max keys; min keys.

Sense-check: with , a node holding 1 key is illegal — it must borrow or merge (Section 14.3); a node holding 6 keys is illegal — it must split (Section 14.2). The boundaries are exactly what drive every restructure, so getting these two numbers right per is the whole game.

These are not idle bookkeeping numbers: they are the boundaries that decide when a block must split on insertion and when it must merge on deletion.

Assumptions & scope. The capacity rules assume the classic B+ tree of order-style design: fixed-size nodes (one disk block per node), all keys of comparable size, and duplicates handled inside leaves. The rules change slightly in practice: commercial engines relax the strict minimum (T2's treatment uses an order parameter with a minimum occupancy, and typical trees run around 67% space occupancy), and variable-length keys make "keys per block" a budget question rather than a fixed count. For this course, the professor's convention — = max pointers per block — is the one that appears on the exam; use it consistently.

Recap + bridge. A B+ tree block holds between and keys; internal nodes route with block pointers, leaves hold the real data with record pointers and a next-leaf chain. The rules exist to keep the tree balanced — exactly what the next section exercises by hand: building a whole tree by inserting values one at a time with .

(Real-world connection: the same rules are why a B+ tree on a 8 KB disk block keeps its height at 2–4 levels even for tables with millions of rows — each level multiplies the fan-out, so log-like search cost translates directly into a handful of physical disk reads.)

14.2 Building a B+ Tree by Hand (p = 4): The Worked Construction

14.2.1 The Values and the Rules

Purpose. We are about to watch — and build — the exact mechanism that every commercial index uses when rows arrive: keys land in leaves, leaves fill up and split, splits push copies upward, and the tree grows from the root down one level at a time. The rules that follow are the only two decisions the designer must fix before the first insertion; everything else is mechanical.

We construct a B+ tree from the values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, and later add 21, 13, and 11, with . Two conventions must be fixed before we start, because a B+ tree split has freedom in it:

  1. Median ordering. When a block overflows and has to split, we pick the median value to promote. With an even number of keys there are two middle values, and we may choose either — left ordering or right ordering. In the previous session left ordering was used; today we use right ordering: always take the right value when there is a clash. Whatever we decide, we must use it consistently throughout the tree.
  2. Equal-to rule. When a promoted key value equals a value in the leaves, we must decide which side the equal value lives on. The rule used today: whenever a value is equal to a promoted key, it stays on the left-hand side block. (Last time it was placed on the right-hand side — either is fine as long as it is applied everywhere.)

Pitfall — the conventions are global, not local. Choosing right-ordering median and equals-on-left "for this split only" corrupts the tree: the same key can land in different leaves depending on which split produced it, and searches will still find it (leaves are chained in order), but the tree's structure becomes unpredictable and exams will mark it wrong. Decide once, apply everywhere. A useful self-check after each split: ask "which median did I promote, and did the equal value stay on the left?"

A strong piece of advice given before the walkthrough: attempt the construction yourself along with the walkthrough, even if you get it wrong. The attempt is what makes you attentive, and it exposes what you don't know you don't know — a mistake made before seeing the solution is worth more than a silent agreement with it.

14.2.2 Inserting 1 Through 4: The First Split

Inputs & outputs. Input: a stream of key values with their record pointers, plus (so max 3 keys per leaf, min 1). Output: a height-balanced tree in which every leaf holds between 1 and 3 keys and every internal node between 1 and 3 keys.

We start with a leaf block (a child node) and insert in sorted order.

  • Insert 1: leaf holds plus its record pointer.
  • Insert 2: leaf holds .
  • Insert 3: leaf holds — still exactly at the maximum of three keys, no problem.

Step 1 — the split. As soon as we insert 4, the block holds — four keys, which is one over the maximum. This is an overflow, so we take the median. With right ordering, the median of (middle values 2 and 3, take the right one) is 3. The value 3 is copied up and becomes the root node; it stays in the leaf too, because in a B+ tree the data always resides in the leaf, with its record pointer. The root's single block pointer points to the leaf block holding the values less than 3 — and by our equal-to rule, the value equal to 3 goes on the left-hand side as well. So:

  • Root: (one key, two block pointers).
  • Left leaf: , each with its record pointer.
  • Right leaf: with its record pointer.

The tree now has two levels:

             [ 3 ]
            /     \
      [1 2 3]     [4]

Trace checkpoint — why 3 is copied, not moved. In a B+ tree the leaf keeps its copy of 3 (the data lives at the leaves), and the root's 3 is a second copy used for routing. That duplication at the leaf-to-internal boundary is normal; it is only at internal levels that promoted keys are not duplicated (see the deepest split in 14.2.5). A quick check: the left leaf's equal value 3 stays on the left because the professor's equals-on-left rule was applied from the very first split.

14.2.3 Inserting 5 Through 7: The Second Split

Insert 5 and 6 — no problem, the right leaf becomes . (From here on the record pointers and block pointers are understood; they are implicit.)

Step 2 — the second split. Inserting 7 overflows the right leaf: . Right ordering — middle values 5 and 6, take the right one — promotes 6. The root gains 6: root is now . The bounds split the range:

  • Leaf for everything less than 3.
  • Leaf for everything greater than 3 and less than or equal to 6 (the professor's phrasing: "greater than three, not equal to; greater than three and less than or equal to six").
  • Leaf for everything greater than 6.
            [ 3   6 ]
           /    |    \
     [1 2 3] [4 5 6] [7]

Boundary reading. The interval structure is: , , . Every value in a middle leaf is strictly greater than the left separator and less than or equal to the right separator. The "strict on the left, inclusive on the right" pattern comes straight from the equals-on-left rule — the equal value 6 lives in the middle leaf, so its upper bound is inclusive.

14.2.4 Inserting 8 Through 10: The Third Split

Insert 8 and 9 — no problem, the last leaf becomes . Inserting 10 overflows it: , middle values 8 and 9, right ordering promotes 9. The root becomes , and the leaves are:

             [ 3   6   9 ]
            /    |    |    \
      [1 2 3] [4 5 6] [7 8 9] [10]

Step 3 — pattern recognition. All three splits so far share one shape: a leaf overflows at four keys, the right-of-the-two-middle values is promoted, the equal value stays left, and a new leaf opens with a single value. The root simply collects one promoted value per split. Notice what has not happened yet: no internal node has overflowed, because the root has absorbed exactly one key per split.

14.2.5 Adding 21, 13, and 11: The Deepest Split

The tree now has more values arriving: 21 and 13 (existing values that belong after 10), then 11. The last leaf holds , then — three keys, still fine. When 11 arrives it is inserted in sorted order: . That is an overflow, so we take the median: middle values 11 and 13, right ordering promotes 13.

Where does 13 go? Into the root, which currently holds . Adding 13 gives the root four keys — — and that is another overflow, so the root itself must split. With right ordering, the median of the four values is 9 (middle values 6 and 9, take the right one). 9 rises to a brand new root, and — this is the key detail — 9 is not repeated in the lower levels: "even though 9 is at the top, 9 will not be repeated in the lower levels." The second level takes the remaining two keys: the left node holds and the right node holds .

Trace checkpoint — why 9 is copied up without duplicating it below. The leaf split in 14.2.2 copied 3 into the root and kept it in the leaf, because leaf entries are data. But this is an internal split: the node is an index node, and index nodes never duplicate promoted keys — the promoted value becomes the separator in the new root, and the lower-level nodes keep only the keys that still bound their own subtrees. That is exactly why the level-2 nodes are and , not and . The leaf below them keeps its own copy of 9 because that leaf entry is data, not an index separator — two copies of 9 exist in the whole tree, but in different kinds of nodes.

The professor's narration at this point is compressed ("so now 8 and 21 will go above it") — the mechanics were garbled in the recording — but the reconstruction above is the unique right-ordering outcome: median of is 9 (middle values 6 and 9, right one), 9 becomes the new root, on the left, on the right, matching the professor's explicit statement that 9 rises to the top and is not duplicated below.

The final B+ tree for the values 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 21, 13, 11 therefore looks like this:

  • Root (level 1):
  • Level 2: node and node
  • Leaves (level 3):
  • under : (equal-to-3 on the left, consistent with the rule), ,
  • under : on its left (12 was never one of the values), and on its right
                     [ 9 ]
                    /     \
              [ 3  6 ]    [ 13 ]
             /   |   \     /    \
       [1 2 3] [4 5 6] [7 8 9] [10 11]  [21]

Notice that the leaf stays exactly as it was after the third split — equal values remain on the left-hand side, so nothing moved when 9 was promoted. The leaf ordering then preserves sorted order end to end, which is what the next-leaf pointers exploit for range queries.

Pitfalls — the four mistakes that break a hand-built tree.

  1. Promoting the wrong median. With four keys, right ordering always promotes the third value (index 3 of the sorted list); left ordering always the second. If you switch mid-construction, the tree still "works" but violates the chosen convention.
  2. Duplicating a promoted key in the internal node it came from. Index-level splits never duplicate; leaf-level splits always keep the copy. Mixing the two up produces internal nodes with an extra key and a broken pointer count.
  3. Forgetting the pointer balance. After any split, each new node must carry exactly (number of keys + 1) pointers. A leaf split produces two leaves of sizes 3 and 1 here — not 2 and 2 — because the equal value stays left.
  4. Forgetting that only the leaf split duplicates. "9 not repeated below" is only true for the index level. The leaf keeps its 9 — a fact students misquote when they oversimplify the rule.

Recap + bridge. Insertion is a two-level loop: keep adding to a leaf until it overflows at keys, promote the median (right-ordering) with the equal value left, and if the promoted key overflows the parent, keep promoting upward until a new root is born. The construction above produced a tree of height 3 — with barely any insertions are needed for the tree to grow tall. What happens when we remove values instead? The same boundaries now force merges — Section 14.3.

(Real-world connection: this hand-run trace is the literal algorithm executed by a DBMS on every INSERT into an indexed table — the only difference is that the DBMS chooses the node size from the disk block size, so a real B+ tree with millions of keys stays at 2–4 levels and a single insertion touches only the blocks on the path from root to leaf.)

14.3 Deleting from a B+ Tree

14.3.1 Deleting 5 and 4: No Restructuring Needed

Purpose. Deletion must remove a key and still keep every invariant: each node between and keys, all leaves at the same depth, leaves chained in order. The good news, shown here first, is that most deletions are local — they touch one leaf and nothing else.

Deletion uses the same boundaries as insertion: in any block, at most 3 keys, at least 1 key (for ). As soon as a block falls below 1, we have a problem and must restructure.

Take the finished tree from Section 14.2 (root ; level 2: and ; leaves ) and delete 5. The value 5 lives only in the leaf ; it is not part of any internal node (no index value to clean up). The leaf becomes — two keys, still above the minimum. No problem at all: the deleted value is simply not part of the record any more, and the tree keeps working.

The real problem comes across only when we miss the boundaries. Delete 4 as well: the leaf becomes — one key, still exactly at the minimum. Still no problem.

Trace — the local case in full.

Step Action Leaf contents Key count Verdict
Start 3 at max
Delete 5 remove from leaf 2 above min (1)
Delete 4 remove from leaf 1 exactly at min

Sense-check: neither deletion crossed the minimum of 1 key, so neither touched the root, the level-2 nodes, or the other leaves. "Deletion is mostly local" is not a slogan — it is this table.

14.3.2 Deleting 6: Underflow and Restructuring

Now delete 6. The leaf becomes empty — that is below the minimum of 1, so this entire block cannot exist any more, and whatever is remaining must be restructured and refactored properly.

What remains in this part of the tree: the leaves and , plus the upper levels. The first decision is which way to merge: we had the option of merging on the right-hand side or merging on the left-hand side. Either way, we end up with a single pool of data values, and then we create the index out of them as well.

Formalize — the merge decision. An empty leaf cannot stay in the tree, but the values around it still exist. The remedy is to merge the emptied block with a neighbor — the left sibling or the right sibling — into one pool of values, then rebuild the small piece of index above that pool. Both directions are legal; the choice of direction is part of the "decide once" convention. What must never happen is leaving a zero-key leaf in place with its pointer dangling.

14.3.3 Merge Options and the Empty Pointer

Merging produces a configuration where the values less than 3 are still fine, and the pointer that used to cover the deleted range — the one between 3 and 6 — becomes an empty pointer. The structure at the above level can still work ahead with whatever is presently there, so this is not a very significant change: only one block goes away (the emptied leaf), and the index above it remains workable.

The caveat: if the value at that boundary also disappears, then again the entire thing needs to be restructured properly. The takeaway from the walkthrough is that deletion in a B+ tree is mostly local — you delete keys, and only when a block underflows (falls below keys) do you merge with a neighbor and rebuild the small index over the merged pool. The boundaries, not the individual keys, are what force the work.

Pitfall — "empty pointer" is a sign, not a fix. After a merge, the parent's pointer that pointed at the vanished block is dangling. Two mistakes follow: leaving the dangling pointer in the diagram, and believing the merge is done before the parent's own key count is checked. The parent may now hold one pointer fewer — and if the parent itself drops below its minimum, the merge must continue one level up.

14.3.4 Worked Deletion: The Complete Mechanics, With Merges

The syllabus asks for the deletion and node-merging mechanics in full, so this supplement runs one complete deletion sequence on the finished tree of section 14.2 — root ; level 2 nodes and ; leaves — and exercises every rule: plain leaf deletion, separator replacement, leaf merging, index-node underflow, and tree shrink. The boundary rule for throughout: every node must hold between 1 and 3 keys.

Step 1 — delete 8 (plain leaf deletion). The value 8 lives only in the leaf . Removing it leaves — two keys, still above the minimum. No restructuring. This is the common case: most deletions are exactly this, a removal inside one leaf.

Step 2 — delete 9 (separator replacement). The leaf becomes — exactly at the minimum, still legal. But 9 was also the root separator: the root's single key 9 no longer exists anywhere in the leaves, and a B+ tree separator must be a real boundary. The rule: replace the deleted separator with the smallest key of the leaf to its right — the leaf — so the root becomes . The tree is unchanged in shape; one key changed value at the root.

Step 3 — delete 10 (separator replacement again). Leaf becomes — still legal. The root separator 10 is again gone, so it is replaced by the smallest key of the right leaf: the root becomes .

Step 4 — delete 11 (leaf underflow → merge). The leaf becomes empty — zero keys, below the minimum of 1. The block cannot exist. Two remedies exist; this walkthrough takes the merge path. Merge the empty leaf with its right sibling , drop the separator 13 that stood between them, and collapse the two leaf pointers in the parent node into one. The parent now holds zero keys — it has underflowed as an index node. Because the parent is below the minimum, it merges in turn with its sibling : the merged index node holds — three keys, exactly at the maximum — and the root , now pointing to a single child, holds zero keys.

Step 5 — the empty root (tree shrink). A root with zero keys is discarded, and its only child becomes the new root. The tree has shrunk by one level.

The final tree: root with the leaves . Every leaf still holds at least one key, every index node holds between one and three keys, all leaves sit at the same depth — the B+ tree invariants are restored, and the whole sequence of five steps is what "deletion and node merging" means mechanically.

Trace — the five steps as one table.

Step Operation Leaf state Internal state Restructure?
1 (delete 8) plain leaf deletion (2 keys) unchanged no
2 (delete 9) separator replacement (1 key) root 9 → 10 no shape change
3 (delete 10) separator replacement (1 key) root 10 → 11 no shape change
4 (delete 11) leaf merge + index merge node 13 → 0 keys; merges with 3,6 → ; root 11 → 0 keys yes — two merges
5 tree shrink empty root discarded; node becomes root yes — height drops
Before:          [ 9 ]                       After:
             /          \                    [ 3  6  13 ]
         [ 3 6 ]        [ 13 ]              /    |    |    \
        /  |   \        /    \      →   [1 2 3] [4 5 6] [7] [21]
 [1 2 3] [4 5 6] [7 8 9] [10 11] [21]

Sense-check: count keys bottom-up. Leaves: 3, 3, 1, 1 — all between 1 and 3. Index node: 3 keys, 4 pointers. One root, no empty nodes, every leaf at depth 3 → invariants hold.

14.3.5 The Alternative: Borrowing (Redistribution) Instead of Merging

Merging is not the only remedy for underflow — and it is not always allowed. Before merging, the standard algorithms try redistribution: if a sibling has more than the minimum number of keys, borrow one key from it. The borrowed key moves across the sibling boundary, and the separator in the parent is updated to the new boundary value; no parent node ever loses a pointer, so no merge is needed at the upper levels. The rule of thumb: redistribute (borrow) when the sibling can afford it, merge when both siblings are at the minimum. In the walkthrough above, the leaf at step 4 had exactly one key — the minimum — so borrowing was impossible and merging was forced. Had the sibling held , the leaf could have borrowed, say, 9 or 10, the separator would move, and the parent would never have underflowed. Merging and redistribution together are the complete deletion toolkit, and an exam answer should name both and say when each applies.

Formalize — redistribution's one rule. A borrow is legal only if the lending sibling still satisfies its own minimum afterwards. For , a sibling with 3 keys can lend one (leaving 2, still above minimum 1); a sibling with 1 key cannot lend at all — that is precisely the situation that forces a merge. The separator between the two leaves must then be rewritten to the new boundary value, because separators are real boundary keys, never stale ones.

Recap + bridge. Deletion keeps every node between and keys. If a block empties, redistribute from a rich sibling (no parent change) or merge with a sibling (parent may underflow and cascade), and if the root empties, the tree shrinks. This completes the B+ tree as a structure. The rest of the session turns to a completely different problem: what happens when many users operate on the database at once — the world of transactions.

(Real-world connection: redistribution is what keeps B+ tree space occupancy near 67% in production systems — a tree that merges eagerly burns fewer disk blocks, and a tree that borrows avoids expensive parent cascades; commercial indexes combine both to keep block I/O low on every delete-heavy workload.)

14.4 Functional Dependencies First: The Normalization Prerequisite

14.4.1 Normalization Cannot Begin Without Functional Dependencies

Hook: A student held up a table with no visible key and asked, "What normal form is this?" The answer — "we cannot even have the discussion" — turned out to be the most important vocabulary lesson of the session.

The first student question of the session was about a table that looks like a weak entity — a table where no key value is visible. What normal form is such a table in? Could we call it 3NF, or better?

Q: Suppose I have tables where we don't see any key value — a weak entity where no key is visible. What will be its normal form? Can we say it is already in 3NF or something better?

A: Whenever we make an ER diagram and convert it to a relational model, that part is fine. The confusion begins when people think the number of tables tells you the normal form — "I was making six tables, now I have five, so it is in 3.5NF." That is wrong. The discussion of normalization only happens when we have written down the functional dependencies — "there are four attributes, and within the four attributes these are the functional dependencies between them." Unless I have written down the functional dependencies, no discussion of any normal form occurs at all. I cannot say it is in second normal form because I haven't even specified the functional dependencies.

The crucial rule, restated three ways in the exchange: normalization can only occur whenever there are functional dependencies. Without the explicit mention of functional dependencies, we can never say a relation is in 2NF, 3NF, BCNF, or 4NF — the discussion point does not even arise. We can never prove or disprove any normal form unless the table states what functional dependencies are there.

Formalize — why the normal forms are defined over functional dependencies. Each normal form is a property of the dependency set, not of the rows:

  • 2NF: no attribute that depends on only part of a composite key.
  • 3NF: no transitive dependency — no non-key attribute determined through another attribute.
  • BCNF: every determinant (left side of every functional dependency) is a superkey.
  • 4NF: no non-trivial multivalued dependency.

Without a written set of functional dependencies there is nothing for these definitions to act on — "is this 3NF?" has the same logical status as "how tall is this color?" The normal form is a conclusion drawn from the functional dependencies, and you cannot draw a conclusion from an empty premise.

But is it even possible to have no functional dependency at all? A follow-up pushed on exactly this: what if the table has no known key and no functional dependency?

14.4.2 No Key at All? There Is Always a Primary Key

Q: Even if you say there is no functional dependency in the table — just some series of rows where we don't know the key — what do you use then? What normal form?

A: That is highly difficult. We will use a surrogate entry, which will make a primary key — we need to have one primary key at all; without it we cannot exist. And whenever there is a primary key, that means it has a functional dependency to all other attributes individually. So at least some functional dependency always exists.

The chain of reasoning is worth spelling out: a relation cannot exist without a primary key; a primary key functionally determines every other attribute individually; therefore every relation has at least one functional dependency. Once that minimum exists, the normal-form discussion becomes possible.

Formalize — the surrogate key. A surrogate key (the professor's "surrogate entry") is a manufactured attribute with no business meaning — typically an auto-incrementing integer or a system-assigned unique id. Because it is unique for every row, it is a candidate key by construction: the surrogate determines every other attribute of the relation, written

So a relation with any content always has at least the functional dependencies "primary key → each other attribute." The "no functional dependencies at all" scenario is physically impossible once the relation exists with a primary key.

Q: So suppose we introduce such a primary key. Can we then say the relation is in 3NF, because we have only one key?

A: Of course. Now you have some functional dependency written down, and you have made the effort to enumerate every functional dependency — any attribute or combination of attributes that repeats, you have written everything down. In this case you are clear that there is only one key, no other keys at all, and you are writing that this key determines everything individually — it is a unique attribute. With only one attribute as the key and no other functional dependencies apart from that, it might be in BCNF as well, no problem. But you need to write down, explicitly, that this is what it is.

So the answer has a pleasant shape: a relation with one key and no other dependencies is a candidate for BCNF — the same answer the professor gave when asked "can we say 3NF?" — the point being the normal form is whatever your written functional dependencies prove, never whatever you assert by table counting.

Pitfall — asserting a normal form is not proving it. Saying "it is 3NF" on the paper is worth nothing; the evaluation looks for the lines — the enumerated functional dependencies and the explicit argument that no partial and no transitive dependency exists. A one-key relation with the dependencies written down can honestly claim "3NF, and in fact BCNF"; the same relation without the FD lines cannot claim anything.

The final question in this cluster was about the other extreme — a table that contains nothing but foreign keys.

14.4.3 Foreign Keys Are Constraints, Not Functional Dependencies

Q: Sometimes when we decompose, we end up with one table that has just foreign keys pointing to some other entity. In that case, do we have to think about functional dependencies for the newly created table? Is a foreign key a functional dependency?

A: Foreign key constraints are just like normal constraints. We have a referential integrity constraint; we have foreign key constraints. They say the value in this field can only occur whenever there is a corresponding value occurring in the other field — I cannot have anything whimsically there. And if the entry that the foreign key references is deleted, a decision must be taken: should I add a default value, should I stop the deletion, or should I delete that entry as well? That decision is the constraint we need to take care of. Apart from that, the foreign key has less to do with the normalization portion — normalization is about functional dependencies, not about foreign keys.

This is a vocabulary correction in miniature: a foreign key is a constraint, not a functional dependency. It restricts which values may appear in a column and what happens on deletion of the referenced row; it does not participate in the 2NF/3NF/BCNF/4NF discussion.

Formalize — the three delete behaviors, named. When the referenced row is deleted, the foreign key constraint forces a decision, chosen when the constraint is declared:

  • Default: replace the referencing value with a default value.
  • Restrict: stop the deletion while referencing rows exist (the professor's "stop the deletion").
  • Cascade: delete the referencing entries as well (the professor's "delete that entry as well").

These are referential-integrity behaviors — they govern which values may exist and what happens on deletion — while functional dependencies govern what determines what inside a relation. A foreign key can be present in a table that is in BCNF or in no normal form at all; it contributes to neither claim.

Why does all this matter in practice? Functional dependencies tell us what type of redundancies occur within a relation. We may remove redundancy across relations, but within a relation, if functional dependencies are present, there is a high chance of redundancies, of storing things separately, and of anomalies inside the relation. We can normalize the relation to make sure we have lesser redundancies and lesser chances of a lot of null entries. This is a discussion that many people fail to reciprocate, and the professor explicitly called it out: people who have heard this correction are at least an order better than those who have not.

Assumptions & scope. The "one key ⇒ maybe BCNF" conclusion assumes the enumeration of functional dependencies was complete — every repeating attribute or attribute combination written down. Miss one dependency and the conclusion can flip: a hidden functional dependency that does not go through the key is exactly what would break BCNF and possibly 3NF. The normal form is as good as the dependency list it is drawn from.

Recap + bridge. Normal forms are conclusions drawn from written functional dependencies, never from table counts; a relation always has at least the primary-key functional dependencies (a surrogate key guarantees this); foreign keys are constraints about allowed values and delete behavior, not functional dependencies. This correction is exactly what the assignment will demand in practice — as the next section shows, the evaluator wants the FD lines on paper.

(Real-world connection: every real schema — a university database, an e-commerce catalogue, a banking ledger — ships with both kinds of lines: functional dependencies that the design team wrote down during modeling, and referential-integrity constraints (default/restrict/cascade) enforced by the DBMS at runtime. Tooling like the MySQL ON DELETE clauses and ER tools' "identifying relationships" are the operational forms of these two ideas.)

14.5 Assignment Guidance: What Is Expected

14.5.1 Diagrams and Report Format

The assignment has no mandatory tool. You can draw your ER diagram with your hand — a clear hand drawing that communicates the structure is more than fine. If you prefer, use PowerPoint, eDraw, or some other tool where you can nicely draw the boxes and arrows — that is also perfectly fine. The report part can be handwritten or typed; both are accepted.

The SQL queries must be pasted as they are, and for each query you need to provide a snapshot of the output. Write a few important queries that are relevant and practical — four, five, six queries is fine; less than four would be very less. The purpose is that you write a handful of important, practical queries. Make sure you have a good combination: DDL (data definition language) queries, and data manipulation queries that actually do something — inserting, deleting, updating; even just searching is okay. Whatever queries you write, share their working through snapshots inside the documentation, so the query is visible, the database is visible, and the running output is visible.

Exam note: the submission checklist is exactly: (1) ER diagram — hand or tool, (2) report — handwritten or typed, (3) 4–6 practical SQL queries with output snapshots, mixing DDL and DML. Fewer than four queries is explicitly too few.

14.5.2 The Index-Theory Component

A query-related question that came up: when we create an index through SQL, what is expected about indexes in the assignment? The answer is important — you are not expected to write any SQL index creation query. The index part is a theoretical discussion.

Q: (Prashant) Whenever I create an index, will it be there every time? Do I need to create it on the fly each time I require an index?

A: Yes, you will create that index once and you keep on using it throughout — you need to maintain and preserve it throughout the lifetime of the attribute or combination of attributes. Unless you specifically delete it, it exists like that only. That's a very relevant question.

Formalize — the lifetime of an index. An index is a persistent structure with its own lifecycle: CREATE INDEX builds it once; the DBMS maintains it incrementally on every INSERT/UPDATE/DELETE (each change touches the index path, as Sections 14.2–14.3 showed); it survives as long as the attribute or combination it covers exists. It is dropped only by an explicit DROP INDEX. It is never "recreated on demand" per query — that would defeat its purpose, since the whole point is that the structure already exists when the query arrives.

The theoretical discussion expected: suppose you create an index on attribute Q. This will significantly impact my retrieval time. So you need to write the balance — to create an index on Q I need to do such and such things, in terms of the space, the computation power, and the time needed to create and maintain that index; and on the reward side, how much faster retrieval becomes for Q. Go into the depth of why I am having the index on Q: why it is a B+ tree, why it is a B-tree, why it is just a multilevel index or a single-level index, what a hash index or a bitmap index would be — all of that is an addition. A numerical number quantifying the time saving would be really, really great; even otherwise, whatever value you write, it is critical and will be accepted with both hands — and given the fact that you would anyway be doing this value addition in the comprehensive examination, it is worth writing a proper perspective on why you are creating an index and how it may impact retrieval.

Scope — the index comparison you should write. The assignment wants the trade-off table, not the SQL:

  • B+ tree: balanced, handles range queries and point lookups; the default choice for most columns — this is the structure of Sections 14.1–14.3.
  • B-tree: cheaper insertion/deletion history but awkward range queries — the predecessor.
  • Multi-level / single-level index: fixed levels; multi-level scales memory, both pay for every update.
  • Hash index: exact-match lookups in near-constant time, but useless for ranges — hash on the key you always look up by equality.
  • Bitmap index: compact for low-cardinality columns (e.g., gender, status) with set-like operations — efficient for counts and AND/OR conditions, costly to maintain under heavy updates.

The cost side is space (index blocks), computation, and maintenance time on every write; the reward side is the retrieval speed-up for the queries that use Q — quantify it with a number if you can.

14.5.3 Tools, Platforms, and the Virtual Lab

You may use any SQL application — SSMS (though the professor had not personally used it and could not comment on it), MySQL, Microsoft SQL Server, Oracle, or any other platform. Microsoft Access also provides some way to do it. There are many tools and softwares that allow you to create a database and write queries on it; some are open source, some are paid. Use any of them.

Q: (Anusha) Can we use an online SQL editor?

A: Generally, online SQL editors don't allow that provision — that capability we have in other softwares. If you are thinking of using an online editor, please reserve a time slot in the university's virtual lab — that will be a better, more professional one; you get a real-time interface as well. I want on-campus and off-campus courses to remain of the same gravity and same theory — conceptually they are the same; the number of things done may be less, but the quality is not compromised. So please use professional softwares or the virtual lab. I will not accept the excuse that "I used an online editor, that's why I could not get the result" — make sure it works, make sure you are using all your four, five, six, seven, eight relations throughout, and you are writing a query on that. Use the tool that properly emulates a real-time editor — Microsoft, Oracle, or the installed ones.

A note on user-friendly tools that write the SQL for you: make sure you are writing the SQL query yourself. The query must be visible to the evaluator — the database visible, the running query visible, the output visible — which is why the snapshot requirement exists. The deadline for the assignment is the 30th; whatever the deadline is, ensure you submit within that point of time.

Exam note: use any professional platform — MySQL, SQL Server, Oracle, Access, SSMS — or the university's virtual lab; online editors are not an accepted excuse, and the query, database, and output must all be visible in snapshots. Deadline: the 30th.

14.5.4 Functional Dependencies and the 3NF Proof

A direct question settled one requirement firmly:

Q: Is it required to write down the functional dependencies for each entity and normalize to 3NF?

A: Yes, it is expected. 3NF means all the relations are in 3NF, for which you need to mention what the functional dependencies are for every relation, and for every relation prove that it is in 3NF.

The final exchange of the session came from a student who had already submitted and worried the missing functional dependencies would sink the submission.

Q: (Sagar) Do we have to add the functional dependency in the assignment? Everyone has clarified that if you convert ER to relational schema, it will be already in 3NF. I did the same — I converted everything, so it is 3NF only.

A: Not necessarily. Not just by specifying on paper "it is in 3NF" can anyone believe that it is in 3NF. Understand that when you say "I have an ER diagram, I have converted them into a relational schema that is in 3NF" — you haven't specified any functional dependencies till now. The discussion for 3NF, 2NF, BCNF comes only when we are writing functional dependencies. Functional dependencies cannot be represented in an ER diagram — completely, all of them. Yes, these functional dependencies are real-time, realistic. So when we work with an ER diagram and convert it to a relational schema, that's okay — but in real life we still need to make sure the functional dependencies are captured, are realized, and that is the point where normalization comes across. Before that, from ER diagram to relational form, we can never say it is 2NF, 3NF, BCNF, whatever it may be, because we have not specified the functional dependencies — we have not captured all the other constraints that might be there.

And even if you say the conversion "is always in 3NF", still you need to specify why it is in 3NF, because 3NF specifies that there are functional dependencies in which there is no partial functional dependency and there is no transitive dependency at all — that is still something you need to prove across there.

The professor's closing position: if you say it is in 3NF, you must have proven it is in 3NF. Just by specifying that it is in 3NF, how do I believe it? The functional dependency lines — the "FD" keyword — were missing. You need to add the extra lines of functional dependencies for your entities.

Formalize — the proof template the assignment wants. For every relation in the schema:

  1. List its attributes and its key(s).
  2. Write the complete functional dependency set for (the "FD" lines).
  3. Show no partial dependency: no attribute depends on a proper subset of a composite key — this is 2NF.
  4. Show no transitive dependency: no non-key attribute is determined through another attribute — this is 3NF.

Only after all four steps is " is in 3NF" a claim backed by argument rather than by assertion. ER diagrams cannot carry this information — an ER diagram shows entities, attributes, and relationships, but the functional dependencies inside a relation are written in the relational schema's FD lines.

Exam note: the FD lines are mandatory in the submission. "ER-to-relational is always 3NF" is not accepted as a proof; the evaluator wants, for every relation, the functional dependencies plus the explicit no-partial and no-transitive argument. This mirrors Section 14.4's rule: normal forms are conclusions from written dependencies.

14.6 Transactions: Why Concurrent Access Is a Problem

14.6.1 Every Real Application Is Multi-User

Hook: Your bank's core software and a railway booking site run on the same fundamental idea — a database accessed by thousands of people at once. What could possibly go wrong when one user's request is interrupted mid-way by a power cut, or when the processor runs two users' operations in the "wrong" order?

This is the point where the course turns practical. Whatever application you create — a desktop application, a mobile application, any such application — it will be concurrently accessed by multiple people. Think of a banking application, railway reservation, hotel reservation, a library system, or the more practical giants: Facebook, Amazon, and other marketplaces. Every mobile application you can look at, from Coursera or Udemy onward, from Gmail to messaging applications, from social media to entertainment to learning applications — there is a database involved. That database is accessed by multiple people simultaneously, through whatever medium — mobile phone, desktop, through the internet — and it is hosted at a particular IP address, on a central server machine, or on a cloud application, or any blockchain application.

Why does that deserve study? A natural pushback: if the database is accessible to multiple people, so what? If a hundred people ask what my marks are, I can reply to all of them. If a hundred people check IRCTC status, they look. If a hundred people ask whether a book is available in the library, we answer yes or no. If 20 or 30 people access a banking application, each accesses their own account. Isn't the database already handling complex SQL and ACID queries? Why can't it handle simple multi-user access?

That pushback is exactly the setup — and the professor insisted students genuinely agree there is a problem before moving on, because the entire rest of the course (transactions, concurrency control, recovery) is the answer to it.

14.6.2 What a Database Write Really Does: Blocks, Memory, and Persistence

To see the problem, walk through what a money transfer physically does. A transaction is a unit of a program — a unit of a set of instructions taken at a time. Say I transfer some amount, 50 rupees, from account A to account B. The processor works in main memory. The value of A is not in main memory — it is in a separate block on secondary storage. So:

  1. Read the entire block in which A resides into main memory — irrespective of whether there is an index or not, bring the block in.
  2. Read the value of A from that block, make it minus 50, and write the block back to the same secondary storage.
  3. Read the next block, where the value of B resides.
  4. Increase the value of B by 50 and write the block back into the hard disk wherever it resides.

Why write back at all? Because changes happening in main memory are not persistent. If there is a power failure, a hardware failure, or a system crash, the main-memory changes vanish. Changes in secondary storage are persistent, so we write the changed block back into the database.

This block-level detail matters: we never update "the value of A" directly — the entire block containing one record is read, one record in it is updated, and the whole block is written back.

Formalize — the read-modify-write cycle. Every database write is three physical steps, never one:

Two consequences follow directly. First, main memory is volatile — power loss erases un-written changes — while secondary storage is persistent, which is exactly why the write-back exists. Second, the unit of transfer is the block, not the record — one record's update moves the entire block that contains it. A transfer from A to B therefore spans at least two read-modify-write cycles, on two different blocks, at two different moments in time — and between those moments the system is wide open to failure.

14.6.3 The Power-Failure Nightmare: Debited but Not Credited

Now the failure: you have read A's block, made the changes, written the value of A back to the hard disk — and before you could read the block where B resides, there is a power failure; the system crashes. The value of A has been debited from its place, but the value of B has not been increased.

You are now in a precarious situation. The client says: "My hard-earned money has been debited from my account and not credited to the other account — how are you making sure of this?" A bank cannot defend this by explaining buffer management, blocks, and main memory; that explanation will never be acceptable to clients. The professor's framing was blunt: if the transfer from Ganesh's account to Priti's account debits one side and never credits the other, all of you will chase the bank asking what is happening — and no technical excuse about where the write happened will satisfy anyone.

Worked example — the 50-rupee transfer, step by step.

Initial state: A holds 100, B holds 0. Blocks: A's account record lives in block 1, B's in block 2.

Step Physical action A on disk B on disk
1 Read block 1 (with A) into main memory 100 0
2 A − 50 in main memory; write block 1 back 50 0
3 Read block 2 (with B) into main memory 50 0
4 B + 50 in main memory; write block 2 back 50 50
Failure point Power fails after step 2, before step 4 50 0

The result is indefensible: A was debited, B was never credited, and the ledger lost 50 rupees with no trace. Sense-check: a correct transfer must leave the sum unchanged — 100 + 0 before, 50 + 50 after. The failed run leaves 50 + 0 = 50, and that sum change is precisely the signature of the broken atomicity the rest of the course fixes.

Pitfall — "the database handles ACID already, why worry?" The pushback "the database already handles complex SQL/ACID, why worry about multi-user access" is wrong — the debit-without-credit scenario is indefensible to clients. "ACID" is a guarantee the database implements through exactly the machinery this course studies; it is not a magic property that appears by itself. The person who understands the failure modes is the person who can design the guarantee.

This single scenario is the entire motivation for transactions: a group of instructions where the two or three operations depend on each other, and where a failure at the wrong moment leaves the database in a state no one can defend.

14.6.4 The Ordering Problem and the Penalty

There is a second, subtler problem: order of execution. Say four people are sending money to you and you are sending money to three people, and the amounts are neck to neck — you receive 50, you send 50. You are careful: you first receive the four, then you send the three. But the order can get shifted, because these are requests submitted to the operating system. We cannot even say the priority of the first one is higher or lower — everything sits in a queue, and the processor will take any of the processes to execute. We cannot guarantee which executes first and which later.

The processor can execute one query at a time, one process at a time — even with a multi-core processor, every core processes one operation at a time. If the order shifts, you suddenly have a penalty levied on you: "insufficient balance, and still you paid the other person" — a heavy penalty, and the bank account shows the charge with no explanation you can defend.

Worked example — the ordering problem with real numbers.

Suppose your balance is 0. Four people each send you 50 (incoming: +50 each, total +200) and you send three payments of 50 (outgoing: −150). Executed in your intended order — all four receipts, then the three payments — the balance climbs and never goes negative:

Intended order Balance
receive 1 (50) 50
receive 2 (50) 100
receive 3 (50) 150
receive 4 (50) 200
send 1 (−50) 150
send 2 (−50) 100
send 3 (−50) 50

Now let the OS reorder the queue — say your first send runs before the first receipt. The very first operation reads a balance of 0, subtracts 50, and the bank rejects the payment or levies the penalty: insufficient balance — and still you paid. Sense-check: the final state is identical (the same seven operations all executed) — only the order differed — yet one order produces a penalty and the other does not. That is the ordering problem in one line.

So there are two enormous reasons to study concurrent execution: hardware failures can interrupt a dependent sequence of operations, and the concurrent execution of multiple transactions in the same order as they were requested is very, very important — but we are not in control of the processor. The order in which we specify the instructions must be the order in which they execute, and that guarantee is exactly what transaction management provides.

Recap + bridge. Two failure modes — interrupted dependent operations (power failure mid-transfer) and reordered independent operations (OS queue reordering) — both leave states no one can defend. The fix is the transaction: a unit of instructions executed with guarantees. Those guarantees have a famous name — ACID — and they are the next section.

(Real-world connection: the 50-rupee transfer is, at heart, how every bank's ledger works — debit and credit as two block writes — and why banking cores run on transactional databases; the ordering problem is why payment systems validate against a live balance and why your bank statement can show a penalty even when "the money arrived later".)

14.7 The ACID Properties

The four properties — atomicity, consistency, isolation, durability (the "ACID" properties; the spoken name in the session is sometimes garbled as "asset properties", but the four named properties below are stated explicitly) — are the contract a transaction offers. Because of them, we group a unit of instructions into a program in which either everything executes or nothing executes, and whatever happens, consistency, isolation, and durability are respected.

14.7.1 Atomicity: Everything or Nothing

The atomicity requirement: not only for three or six of the instructions — either all of them execute or none of them should execute. I do not want my money debited from my account and not created in the other person's account. Either the money is transferred completely, reflecting on the other bank account, or nothing is changed — the status quo before and after is the same.

That is a very basic requirement, and it is exactly what failed in the power-failure scenario: the transaction executed till a point, the processor changed B's value in main memory — it was reflecting for a greater point of time — but it did not write the block back to the hard disk, so physically, after the power failure, the 50 rupees was never added to B's account. Atomicity forbids this state.

Worked example — the all-or-nothing contract. Take the transfer of Section 14.6.2 with A = 100, B = 0.

  • Atomic execution (everything): A − 50 and B + 50 both become durable → final state (50, 50).
  • Atomic execution (nothing): neither write survives → final state (100, 0) — exactly the starting state.
  • Forbidden: only the debit survives → (50, 0), money vanishes.

Sense-check: in both allowed outcomes the total stays 100; the forbidden outcome changes the total to 50. Atomicity is the rule that a transaction may leave behind either its full effect or no effect at all — never a fraction.

14.7.2 Consistency: No Money from Thin Air

Before and after a transaction, the sum of money should be the same — the total sum of balances should be the same. You should not create money out of thin air, and you should not remove money into thin air. Whatever constraints exist need to be respected at no point of time should we see that before and after, things are not consistent.

There is an honest caveat: the logical part of consistency is written by the programmer — the constraint logic itself is your code. But it should not be a problem in execution; the database system must not let a sudden execution order create money or lose it. "We have burned the money and we don't know where it has gone" — that is precisely the situation consistency prevents at the system level.

Formalize — consistency as an invariant. The database holds invariants — statements that must be true of every database state, such as "total balance across all accounts is unchanged by a transfer" or "number of booked seats never exceeds number of seats". Consistency means: if the database satisfies the invariants before a transaction, it satisfies them after the transaction too. The invariant itself (what "consistent" means for your application) is the programmer's job — that is the honest caveat; the DBMS's job is never to break a valid invariant through scheduling or failure.

14.7.3 Isolation: You Should Feel Alone

Isolation is very important: at whatever point of time, if things are working, somebody needs the value of A, somebody needs the value of B — say someone prints the value of A and B. Who might that be? A person calculating interest for both accounts; an auditor auditing the bank account; a joint account where a spouse reads because both persons have access; a company account where all the directors have access. Whoever it is, when they print A plus B, they must not see an intermediate state — they should see a consistent snapshot.

How would T2 (the reader) even know that another transaction is concurrently changing things? If the value visibly moves between two reads, that is not a healthy sign. The guarantee needed: whenever I am working, I must appear to be working in isolation — as if I am the only one accessing the database, starting from the beginning of the transaction till its end.

Intuition — who needs the snapshot? Interest calculators add A and B to compute interest; auditors compare A and B against bank records; joint-account holders and company directors both read the same account. Every one of them needs the same consistent snapshot — if the reader could see A after the debit but before the credit, the "sum" they print would be wrong, and an auditor would flag a discrepancy that never actually existed. Isolation is the guarantee that nobody ever sees a transaction's half-finished work.

The airline ticket is the everyday proof that this matters. When I access the price of an airline ticket, I see a price; when I click the same ticket moments later, the price fluctuates. Somebody else is manipulating the ticket price — someone is reserving seats. But here is the legitimate mechanism behind the fluctuation: the airline may have decided that the first 25% of seats are sold at one price, the next 50% at another, the next 15% at another, and the last 10% at a very high price. When I first look, the seat is available at the current tier; when I look again, someone has booked the flight, and even though the seat is still available to me, it is available at a different price. That is a legitimate business rule, and the fluctuation tells me I am not the only one working.

The violation to prevent is different: if I start booking, I am allowed to pay, and then I am told the seat is not available — that is a gross violation. From the moment I press the first amount till the seat is booked to me, nobody should be able to take the seat away. That is isolation: the first time I am working in isolation, the seat should be there for me. Either I get the seat or the transaction never happened — but never "seat available, payment taken, seat gone."

Worked example — price tiers explain fluctuation; isolation prevents theft of the seat. A flight has 200 seats. The airline's pricing rule: first 25% of seats (0–50 sold) at ₹4,000; next 50% (51–150) at ₹6,500; next 15% (151–180) at ₹9,000; last 10% (181–200) at ₹14,000.

  • You search at noon: 60 seats sold → tier 2 → you see ₹6,500.
  • At 12:01 someone books seat 61; the count reaches 61 → still tier 2. Nothing changes.
  • At 12:02 three more book → count 64. Still tier 2. Prices move only when a tier boundary is crossed.
  • Later, 151 seats are sold → tier 3 → the same class now shows ₹9,000. The price fluctuation is legitimate — a business rule reacting to seat counts, exactly the 25/50/15/10 model.

The violation isolation blocks: you begin booking while the seat is available in tier 2, pay, and are then told the seat is gone because another booking raced ahead mid-transaction. Isolation guarantees your booking runs as if you were alone: from the moment your booking transaction starts until it ends, the seat cannot be taken by anyone else. Sense-check: the price may legally move between searches (two separate transactions), but it may never move within your single booking transaction.

14.7.4 Durability: Earthquake-Proof Changes

Once I have done it, it needs to be persistent, durable. Whether an earthquake happens, a tsunami, a power failure, a hard disk failure, or a main memory failure — main memory is an electronic device, it can fail — my database should not be changeable; the change should survive. We read blocks into main memory and the processor works there; unless we require to write back, we don't write back. Because of power failures and other failures, the durability of my system must not depend on main memory — once committed, the change must be in physical storage and stay there.

Every client wants this package: the payment application must be atomic, the reservation must appear as if executed in isolation, the seat I reserved must be mine at the airport or railway station (it must be durable), and before and after, the database must be consistent — the number of seats must never become more than the number of seats that were actually possible at the starting point.

Assumptions & scope — what ACID does and does not cover. ACID assumes failures are eventual — crashes stop the machine, then it restarts and recovery runs. It does not cover: data destroyed by disk failure before a backup exists (that is the backup/DR story), or application-level bugs that commit wrong values consistently (consistency of your invariants is the programmer's job, as the professor's caveat says). Durability is "once committed, survive any failure" — with the recovery mechanism (log files, shadow paging) studied later in the course.

Recap + bridge. Atomicity — all instructions or none; consistency — invariants preserved, no money from thin air; isolation — every user feels alone on the database; durability — committed changes survive any failure. The natural next question: since concurrency is dangerous, why not just run transactions one at a time? Section 14.8 answers — and the answer is about the human experience of waiting.

(Real-world connection: every payment app, reservation system, and marketplace runs on this exact contract — UPI transfers, IRCTC bookings, and airline seats are all ACID transactions; the seat-tier pricing story is literally how revenue-management systems price flights.)

14.8 Why We Cannot Just Run Transactions One at a Time

14.8.1 The Price of a Serial Queue

If concurrency is so dangerous, why not simply queue the requests — ten people want to access the database, so let them go one by one, one transaction at a time? The answer is practical, not theoretical.

First, consider a read: a person wants the current status of the number of books available on Amazon or any marketplace. You cannot tell them: "Another person is accessing the database, you are not allowed to even read the database now." Nobody will accept waiting an hour just to learn the price of a particular book or a particular phone — "I may go back to the local store and come back by that point of time." Response time is very important; I cannot stop and wait until every other person has executed 100%.

Second, consider a write: if one transaction has started writing, it cannot withhold all other transactions — including readers — just because it has begun. Context switching makes this unavoidable: the processor shares time, the process gets its share, then the processor does context switching, another process starts executing and wants to read the database — but the previous process was already writing. The second process goes back through the processor; it cannot do something now, and the other process does not have access to the database. Until the first process completes all its instructions, the others become redundant and can do nothing at all — that is a very bad situation for the entire operation.

Intuition — serial execution is a queue with one window. Imagine a single-counter bank where the teller finishes one customer completely before calling the next. A customer who only wants a balance enquiry stands behind a customer who is opening an account, filling forms, and getting signatures. The enquiry — one glance at a screen — waits a full hour. Nobody accepts that for a book price on a marketplace, and nobody should: reads cannot be withheld behind a long write. And if the first customer is a writer (a deposit), every customer behind them — even pure readers — is blocked, because until the writer's instructions complete, the other processes get nothing done. Response time for the majority collapses.

14.8.2 Why Concurrency Wins

So serial execution is unacceptable because it destroys response time and starves everyone behind a writer. The benefits of allowing multiple transactions to execute simultaneously: the average time is reduced, the performance of the processor is better, and disk utilization is better. For these reasons, we must allow multiple transactions to execute simultaneously — and then manage the correctness of that concurrency. (A related personal note from the professor: handling multiple threads and accesses is a tedious task in traditional languages like Python and Java; that is why dedicated database software must be designed to handle it appropriately — just as a separate software layer was needed over the file access system in earlier sessions because of the complex constraints specific to the database system.)

Formalize — the concurrency trade-off. Three quantities improve with concurrency:

  • Average response time: a short transaction finishing while a long one runs completes in seconds instead of queuing behind the long one for minutes. With transactions of similar length, serial execution has an expected waiting time that grows roughly as times the transaction length, while interleaving lets short transactions slip through.
  • Processor utilization: while one transaction waits for a disk read (an I/O wait), another transaction's instructions can use the CPU — otherwise the CPU idles during every I/O.
  • Disk utilization: the same logic applies to the I/O subsystem; overlapping requests keep the disk busy.

The cost side is exactly what the rest of the course is about: interleaving can produce the debited-but-not-credited and reordering disasters of Section 14.6, so the concurrency must be managed — the schedules must be checked for safety (Sections 14.10–14.11) and the DBMS must enforce the check with protocols.

And the deeper motivation, aimed squarely at the student: the industry does not pay you for work — the industry pays you for the value you are adding there. Understanding, practicing, and becoming skillful at concurrency control is exactly the kind of value the industry pays for.

Pitfall — "the processor executes queries in parallel, so order does not matter." Even with multiple cores, every core processes one operation at a time; the OS decides the order, and the ordering disasters of Section 14.6.4 happen precisely when the interleaving is unmanaged. Concurrency is a reward that must be earned — the speedup is real, but only safe when schedules are validated.

Recap + bridge. Serial execution destroys response time, starves readers behind writers, and wastes CPU and disk; concurrency wins on all three — if the interleaving is checked for correctness. That check needs precise vocabulary: what is a schedule, which schedules are serial, and when is an interleaved schedule equivalent to a serial one — Sections 14.10 and 14.11. First, the next section defines the life of a single transaction: its states, its commit, and the log that records its changes.

(Real-world connection: this is why real systems use thousands of concurrent sessions — every banking, railway, and marketplace application depends on interleaved execution; and it is why database engineering (locking, isolation levels, schedulers) is a paid specialization: the value added is precisely the ability to make concurrency safe.)

14.9 The Life of a Transaction: States and the Log

14.9.1 The State Transitions

Hook: Is a transaction "done" the moment its last instruction executes? No — and the difference between "the last instruction ran" and "the changes reached the disk" is the entire life of a transaction. The professor flagged this discussion as "super, super, super important."

A transaction passes through distinct states, and the states define what guarantees hold at each moment:

  • Active. The transaction has been submitted and has got its fair share of the processor — it is executing.
  • Partially committed. The transaction has executed its last statement — all instructions are done — but the changes are done only in the main memory; nothing has been written to physical secondary storage yet.
  • Committed. Not only has the last instruction executed, the changes have been made in the physical, secondary storage as well. Only then is the stage committed.
  • Failed. Due to some reason — network failure, power failure, system failure, or hardware failure — the transaction could not execute all its instructions. If a transaction is a program of 10 instructions and we wanted the whole program to run completely or nothing at all, but after three, five, or six instructions an exception or error occurred, the entire program (transaction) has failed.
  • Aborted. The failed transaction is rolled back: every change made so far is undone, so the database returns to exactly the state it was in at the start of the transaction. The state before and after is the same.

The transitions form a clear flow: a transaction starts, and either it gets partially committed or it fails. If it fails, we abort — rollback. If it is partially committed, it must ultimately result in the committing stage very, very soon — the changes need to be returned back to the secondary storage. The key phrase the professor emphasized repeatedly: either I write the entire changes or I write none of the changes at all. "This is super, super, super important."

Formalize — the state diagram. The five states and their transitions:

                 partially
   start         committed
     │               │
     ▼               ▼
   ACTIVE ────────────► PARTIALLY ─────► COMMITTED
     │               COMMITTED
     │                   │
     │  failure/error    │  (writes forced
     ▼                   ▼   to secondary storage)
   FAILED ──────────► ABORTED
        (rollback,
         undo all changes)

Read the diagram in pairs. Success path: active → partially committed → committed. Failure path: active → failed → aborted (rolled back). The decisive step in both paths is the same: when the changes physically leave main memory and reach secondary storage. That single event converts "partially committed" into "committed", and its absence is why a failed transaction must be aborted. A committed transaction cannot be aborted afterwards; an aborted transaction can be restarted (as a fresh active transaction) if the abort was due to a transient error.

Pitfall — confusing "executed" with "committed". A transaction can have executed every one of its instructions and still be only partially committed: the changes sit in volatile main memory. Commit is a storage fact, not an execution fact — the dirty pages must reach the disk. This is the exact trap of the Section 14.6 power failure: every instruction had run, nothing was committed, and the debit survived while the credit vanished.

14.9.2 Commit versus Partial Commit

Q: (Ganesh) When we say committed, it means the transaction is stored. Is that right?

A: Yes, exactly. Till the time it is partially committed, all the instructions of the transaction are executed, but the changes are still in the main memory. As soon as I say committed, the changes are written back to the physical or secondary storage as well. That's correct.

So "committed" is a storage fact, not a logical one: the last instruction executed is not enough — the dirty pages must reach the disk.

14.9.3 Rollback and the Log File

Q: (Ganesh) When we say roll back, are the database systems automatically designed to do the exact opposite of what is done? For example, I'm inserting something, the insertion is partially done, it fails — will the DBMS automatically write a delete query to delete whatever was inserted?

A: Great question — but nothing happens automatically. Whatever we are saying in the computer system, somebody must have done whatever is specified. Even if it looks automated to the normal person, the entire science is that if a failure happens, we ensure the changes are written back and undone. The changes I have done in the database, I need to undo — I need to make sure the changes are not reflected after that point of time. So since the starting of the transaction, we may use a log file to keep writing what changed: "I changed the value of A from 5 to 10" or "the value of B from 10 to 5." We keep track through log files, and there must be something written — committed or aborted. If it is aborted, as soon as we restart we see that the transaction started, it is not committed, and we need to undo everything there. If the changes were only in main memory, nothing happened on disk — no problem. If the changes reached the secondary storage, we need to ensure they are re-updated — the changes were made, but the transaction did not commit, so we write abort and undo the changes that happened in the physical storage.

There is a proper process for recovery and failure that we will study — maybe a couple of sessions later — and that is the process we will use: log files to keep mentioning what changes we made in the physical storage. The log is a separate piece of writing that we keep writing and that is persistent — whatever failure may happen, we can still read that particular piece of the database.

Formalize — the log-based undo, in one trace. Suppose a transaction changes A from 5 to 10 and B from 10 to 5. The log is an append-only, persistent record:

<T, start>
<T, A, 5, 10>     "A changed from 5 to 10"
<T, B, 10, 5>     "B changed from 10 to 5"
<T, commit>       or <T, abort>

If the system crashes before <T, commit> is written, then on restart the recovery process reads the log, finds a started-but-uncommitted transaction, and undoes it: restore A to 5, restore B to 10 (using the old-value fields). Nothing "automatically" writes an opposite query — the log entries are the record, and the recovery logic is the science. If the changes were still only in main memory, there is nothing on disk to undo; if they reached the disk, the old values in the log restore them.

The correction here is a valuable one: rollback is not magic. Someone designed the log, someone wrote the recovery logic; the DBMS's apparent automatism is the product of the log-based recovery process — which is precisely the topic (recovery, shadow paging) coming later in the course.

Pitfall — "the DBMS will figure it out on its own." The inverse-operation misconception: on a failed insert, does the DBMS "automatically" delete the partial insert? No — the DBMS does nothing by itself; the persistent log records every change (old value and new value), and the recovery process on restart uses those records to undo uncommitted work. If the log were absent, there would be no way to know what to restore.

Recap + bridge. A transaction travels active → partially committed → committed, or active → failed → aborted; commit means the changes physically reached secondary storage; rollback is performed by the recovery process using the persistent log file. These states and this log are the foundation of the recovery and shadow-paging machinery later in the course — and they are exactly why the professor called the states discussion super, super important.

(Real-world connection: every commercial DBMS — Oracle, SQL Server, MySQL, PostgreSQL — implements exactly this design in its transaction log (redo/undo log); a DBA reading a crash log is reading the modern form of "A changed from 5 to 10".)

14.10 Schedules and Serial Schedules

14.10.1 What a Schedule Is

With multiple transactions allowed to run concurrently, we need vocabulary for their interleaving. A schedule is the chronological order in which instructions of multiple transactions execute — the order in which T1 started executing something, T2 started executing, T3 started executing. Any such ordering is a schedule:

  • T1 completes entirely, then T2 completes entirely — a schedule.
  • T2's instructions execute first, then T1's — also a perfectly valid schedule.
  • T1 does some instructions, T2 does some, T1 does some more, T2 does some more — also a schedule.

The second kind is the serial schedule: everything for T1 first, till committing, then everything for T2, till committing. A serial schedule always follows the ACID properties — if everything executed first for T1 and it committed, then everything for T2, that is definitely atomic and definitely consistent. If a failure happens at any point, we roll back to the last commit — at every failure point the properties hold.

Formalize — schedule vocabulary. Given transactions , a schedule is any ordering of their instructions that preserves each transaction's internal order (inside , its own instructions keep their relative order). Three kinds matter:

  • Serial schedule: the complete instructions of , then the complete instructions of , then — no interleaving. There are different serial schedules for transactions.
  • Interleaved (non-serial) schedule: instructions of different transactions alternate.
  • Complete schedule: every transaction either commits or aborts by the end.

The key fact: every serial schedule obeys ACID. Since only one transaction runs at a time, atomicity is automatic, isolation is trivial, and consistency follows because each transaction preserves the invariants. The student's instincts are then captured in a single question — the next section's question: is a given interleaved schedule equivalent to some serial schedule?

14.10.2 The Equivalence Test

So the central question becomes: when a jumbled, interleaved schedule is proposed, is it equivalent to a serial schedule? If it is equivalent to a serial schedule, we know it follows ACID, because serial schedules always follow ACID. If it is not equivalent to a serial schedule, we are not confident that it follows ACID, and we should hold and stop that schedule. Some non-serial schedules do follow ACID — but for the sake of confidence, the test is: can we find an equivalent serial schedule? For my sake of confidence, at least find out if the schedule is equivalent to a serial one; then it is easier to find out that ACID will be followed.

Intuition — why equivalence is enough. The serial schedule is the gold standard: whatever it produces is safe, because ACID holds by construction. If an interleaved schedule is equivalent to some serial schedule — meaning it produces the same final database state — then the interleaved schedule inherits the gold standard's safety. This is a sufficiency test: passing it guarantees ACID; failing it does not prove ACID is broken (some interleaved schedules are safe without being serializable), but we cannot prove safety, so prudence says hold and stop the schedule.

Recap + bridge. A schedule is the chronological order of instructions; serial schedules — one transaction fully, then the next — always follow ACID; the test for any interleaved schedule is equivalence to a serial one, and the failing case is held and stopped. The next section makes "equivalent" precise — with the conflict definition, the swap rule, and the precedence graph, all worked on real schedules.

(Real-world connection: the "is this schedule safe?" question is asked millions of times a day inside a DBMS — the schedulers and isolation-level engines of Oracle, PostgreSQL, and SQL Server are implementing exactly this equivalence check, with locking protocols that guarantee serializable schedules.)

14.11 Conflict Serializability

14.11.1 The Conflict Definition and Swap Rule

Two instructions are conflicting when they come from two different transactions, operate on the same data item, and at least one of them is a write:

Formally, the conflicting pairs on the same data item are read–write, write–read, and write–write across two different transactions. Two reads never conflict — "read, read... one of them is not a write, so there is no conflict here." And two operations on different data items never conflict — "even though there are two different transactions, but on a different data item, I can swap them easily."

To test whether an interleaved schedule is equivalent to a serial schedule, we keep swapping adjacent instructions — but we may only swap non-conflicting instructions. The instructions we can never swap are the conflicting ones: two different transactions on the same data item where one of them is a write. If by swapping non-conflicting instructions we can rearrange the schedule into a fully serial one, the schedule is conflict serializable, and we allow it, because it will follow the ACID property.

Formalize — why these three pairs conflict and the fourth does not. Two adjacent operations may be swapped without changing the final state only if their order does not matter:

Pair (on same item X, different transactions) Conflict? Why
read–read No Both only observe X; swapping changes nothing about the value stored or read
read–write (R-W) Yes The reader may observe the writer's value or not, depending on order
write–read (W-R) Yes The reader may read the old or the new value
write–write (W-W) Yes The final value of X depends on which write lands last

Two operations on different items never conflict (their order cannot affect each other). So the swap rule is: an adjacent pair may be swapped if and only if it is not a conflicting pair. Repeatedly applying legal swaps to a schedule and reaching a serial schedule is the proof of conflict serializability.

There is a second way to run the same test, the precedence graph method: write the transactions as nodes T1, T2, and whenever there is a conflicting operation where T1's operation happened first and T2's later, draw an arrow from T1 to T2. After exhausting all the instructions: if the graph has no cycle, the schedule is equivalent to a serial schedule; if there is a cycle, it is not equivalent to any serial schedule.

14.11.2 Worked Check: The Serializable Schedule

Consider two transactions over data items a and b, each doing a read and a write on both items:

  • T1: read a, write a, read b, write b
  • T2: read a, write a, read b, write b

The interleaving to check runs: T1 read a; T1 write a; T2 read a; T2 write a; T1 read b; T1 write b; T2 read b; T2 write b.

Worked example — conflict check by pairs, then the graph. Lay out the schedule and examine every adjacent conflicting pair, then every non-adjacent pair that could matter (two conflicting operations can be forced together by swaps):

  T1: r(a)  w(a)                 r(b)  w(b)
  T2:              r(a)  w(a)              r(b)  w(b)
order: 1    2     3     4      5    6      7     8

Conflicts on a: T1's r(a) (pos 1) is before T2's w(a) (pos 4) → arrow T1→T2. T1's w(a) (pos 2) is before T2's r(a) (pos 3) → arrow T1→T2. T1's w(a) before T2's w(a) → same arrow T1→T2. From this point onwards, T2's read a and write a: are there conflicts back to T1? T1 has already finished its a operations, so no.

Conflicts on b: T1's r(b) (pos 5) before T2's w(b) (pos 8) → arrow T1→T2; T1's w(b) (pos 6) before T2's r(b) (pos 7) → arrow T1→T2; T1's w(b) before T2's w(b) → same arrow.

      T1 ──────► T2      (all arrows one direction, no cycle)

All arrows point T1 → T2 only; the graph has no cycle, so this schedule is equivalent to the serial schedule T1 then T2. The schedule is conflict serializable and allowed.

The same conclusion can be reached by the swap test: this read can go all the way up, that instruction can be swapped with this one, then this one — the entire block of T1's non-conflicting instructions can be taken up, and T2's block goes down, until the schedule is T1 in full followed by T2 in full. Equivalent to a serial schedule — allowed.

Worked example — the swap test, step by step. Take the same schedule and bubble T1's instructions upward past T2's. Only conflicting pairs (same item, different transactions, one write) block a swap.

S:  r1(a) w1(a) r2(a) w2(a) r1(b) w1(b) r2(b) w2(b)

1.  r1(b) w1(b) swap up past r2(a) w2(a):
    r1(b) vs r2(a): different items — legal
    r1(b) vs w2(a): different items — legal
    w1(b) vs r2(a): different items — legal
    w1(b) vs w2(a): different items — legal
    →  r1(a) w1(a) r1(b) w1(b) r2(a) w2(a) r2(b) w2(b)

    T1's four instructions now all precede T2's four. Done — SERIAL.

Sense-check: the final layout is the serial schedule , and T2's internal order (r2(a), w2(a), r2(b), w2(b)) is untouched. The swap test and the precedence graph agree, as they must: a cycle-free graph and a successful serialization are the same fact viewed twice.

14.11.3 Worked Check: The Schedule with a Cycle

Now change the interleaving so the a-operations cross: T1 read a; T2 read a; T1 write a; T2 write a (with the b operations arranged similarly).

Worked example — the crossing schedule, and why it must stop.

  T1: r(a)        w(a)
  T2:      r(a)        w(a)
order: 1    2     3     4

First conflict: T1's read a (pos 1) versus T2's write a (pos 4) — T1 happened first, T2 later — arrow T1 → T2.

Move to the next pair: T2's read a (pos 2) versus T1's write a (pos 3) — this is two different transactions on the same data item with one write, so a conflict — and this time T2 happens first before T1, so we draw the arrow from T2 → T1.

      T1 ──────► T2
      ▲          │
      └──────────┘   cycle!

At this point we do not have to move at all: there is already a circle here — a cycle. The cycle means the schedule is not equivalent to any serial schedule, so we cannot claim it follows ACID; we hold and stop that schedule. Not conflict serializable — rejected.

14.11.4 The Precedence Graph, Summarized

The method, cleanly: mark an arrow from where to where the conflicting operations are; if after exhausting all the transactions the graph does not have any cycle, the schedule is equivalent to a serial schedule; if not, it is not. This is the entire point of the session's work — we just evaluated, for two schedules, whether they are conflict serializable. Conflict serializable schedules we can allow, because they are equivalent to a serial schedule and they will follow the ACID property. That is the basic point.

Formalize — the precedence graph algorithm. Given a schedule over transactions :

  1. Create one node per transaction.
  2. For every pair of conflicting operations (of ) and (of ) on the same item, where executes before , add a directed edge (skip if the edge already exists).
  3. No cycle → the schedule is conflict serializable (equivalent to some serial schedule, obtainable by a topological order). Cycle → not conflict serializable.

The topological order in the cycle-free case tells you which serial schedule the interleaving is equivalent to: here, T1 then T2.

Pitfalls — three exam traps.

  1. The cycle check must cover all conflicting pairs, not just adjacent ones. In 14.11.3 the T1→T2 arrow came from a non-adjacent pair (positions 1 and 4). Skipping non-adjacent pairs misses real cycles.
  2. Two reads are never a conflict. The pair r(a), r(a) — different transactions, same item, no write — is swappable. Students often "see" a conflict where the definition allows none.
  3. A cycle means not conflict serializable — and the schedule is stopped. Do not patch the schedule after the fact; the test's verdict is final: no cycle → allow; cycle → hold and stop.

Exam note: the conflict serializability test was worked fully in the session — expect it: define conflict (two transactions, same item, one write), swap only non-conflicting instructions (or draw the precedence graph), and conclude from the presence or absence of a cycle. The exam guidance for this lecture flags the schedule analysis — view serializability, recoverability, cascading rollbacks — as the road ahead in Section 14.12.

Recap + bridge. Two instructions conflict when they come from different transactions, touch the same item, and one is a write; a schedule is conflict serializable when legal swaps (or an acyclic precedence graph) show equivalence to a serial schedule — allowed, because serial schedules follow ACID; a cycle rejects the schedule. Next session continues schedule analysis with view serializability, recoverability, and cascading rollbacks.

(Real-world connection: database engines use locking protocols (e.g., strict two-phase locking) precisely to force every executed schedule to be serializable — the cycle-free graph is what the lock manager protects at runtime; SQL isolation levels (READ COMMITTED, REPEATABLE READ, SERIALIZABLE) are the user-facing knobs of this machinery.)

14.12 Where the Discussion Goes Next

14.12.1 The Roadmap So Far

The roadmap laid out in the opening: hashing (internal, external, dynamic) is done; indexing (single-level, multi-level) is done; the B-tree and B+ tree discussion is now complete. From here the course moves fully into transactions: this session covered why transactions exist and the ACID properties; the next sessions continue with schedules — view serializability (what it is and how we test it) comes next, then recoverability: if there is a failure, is the schedule recoverable or not recoverable; then cascading rollbacks — does the schedule have cascading rollbacks or not; and finally the judgment of whether a schedule should be allowed to run through or not. After the transaction discussion, recovery continues with shadow paging, and the last part works on the challenges as well.

The arc in one line. Indexing gets records to you fast; transactions keep the database correct while thousands use it at once; recovery (shadow paging and the log machinery of Section 14.9) makes the guarantees survive crashes. The roadmap's compressed phrasing ("continue for recovery shadow paging... work on flow and challenges") is the standard course outline: recovery — with shadow paging as the headline technique — and its challenges close out this unit.

The sequence ahead, in order:

  1. View serializability — what it is and how we test it: a weaker sibling of conflict serializability, based on what each transaction sees, not on which operations conflict.
  2. Recoverability — if a failure happens, is the schedule recoverable or not?
  3. Cascading rollbacks — does the schedule force cascading rollbacks or not?
  4. The judgment — whether a schedule should be allowed to run through or not.

14.12.2 The Evaluative Components

The evaluative components are also on the horizon — quiz 3 is live and should be attempted within time, and mid-semester results are delayed this time (a separate team takes care of the evaluation).

Exam note: quiz 3 is live — attempt it within its time window; mid-semester results are delayed this time (a separate team handles the evaluation). For the exams ahead: expect the conflict serializability test (swap rule or precedence graph + cycle check), and watch for view serializability, recoverability, cascading rollbacks, and recovery with shadow paging in the coming sessions.

Recap + bridge. The session closed the indexing arc (hashing and B+ trees done), opened the transactions arc (why they exist, ACID, schedules, conflict serializability), and pointed forward: view serializability, recoverability, cascading rollbacks, then recovery and shadow paging with the system-wide challenges.

(Real-world connection: the topics ahead — recoverability and cascading rollbacks — are the difference between a database that silently corrupts on crash and one that comes back to a defensible state; the judgment "allow this schedule or not" is executed every millisecond by the isolation engines of every commercial database.)

Exam Guidance Summary

  • Assignment — functional dependencies are mandatory: For every relation, write down its functional dependencies and prove the relation is in 3NF. Saying "ER to relational conversion is always 3NF" is not enough — the FD lines (the "FD" keyword) must be present; 3NF means no partial functional dependency and no transitive dependency, and that must be shown, not asserted. (See Sections 14.4 and 14.5.4.)
  • Assignment — what to submit: The ER diagram (hand-drawn, or PowerPoint/eDraw, all acceptable), the report (handwritten or typed), and 4–6 practical SQL queries pasted as-is with snapshots of their working output. Include a good mix of DDL and DML (insert, delete, update, search).
  • Assignment — the index-theory component: No SQL index creation query is expected. Write the theoretical balance: why you create an index on a chosen attribute, why it would be a B+ tree versus a B-tree versus a multi-level or single-level index versus a hash or bitmap index, the space/computation/time cost to create and maintain it, and the retrieval-time reward. A concrete numerical figure for the time saving is appreciated; this is value addition that you would anyway be doing in the comprehensive examination. (See Section 14.5.2.)
  • Assignment — platforms: Any professional SQL platform (MySQL, Microsoft SQL Server, Oracle, Microsoft Access, SSMS) or the university's virtual lab. Online SQL editors are not accepted as an excuse for missing functionality — the query, the database, and the output must all be visible.
  • Assignment — deadline: the 30th; submit within that point of time.
  • Quiz 3 should be attempted within its time window.
  • Mid-semester results are delayed this time; a separate team handles the evaluation.
  • Core concept to master for exams: the transaction states (active → partially committed → committed, or failed → aborted) and the meaning of commit (changes written to physical/secondary storage) versus partial commit (changes only in main memory) were flagged as super, super important. Expect the conflict serializability test (swap non-conflicting instructions, or precedence graph + cycle check) in some form — it was worked fully in the session. (See Sections 14.9 and 14.11.)
  • Structure to remember for the B+ tree questions: capacity rules maximum and minimum keys per block; right-ordering median with equals-on-left applied consistently; index splits never duplicate the promoted key, leaf splits do; deletion merges/redistributes only below the minimum. (See Sections 14.1–14.3.)
  • Upcoming topics to watch: view serializability, recoverability, cascading rollbacks, recovery and shadow paging, and the challenges of the whole system.

Key Industry Applications

  • Real-world: banking applications are the canonical transaction use case — fund transfers must be atomic (debit without credit is unacceptable), consistent (total balance constant), isolated (auditors, interest calculators, joint-account holders must see consistent snapshots), and durable (survive power failure). The 50-rupee transfer of Section 14.6 is the physical shape of every wire transfer and UPI payment.
  • Real-world: railway reservation (IRCTC), hotel reservation, and library systems are classic concurrent-access systems where reads must never be blocked by writes and seats/rooms must never be double-booked — the isolation guarantee of Section 14.7 in production.
  • Real-world: airline ticket pricing — the 25% / 50% / 15% / 10% seat-tier pricing model explains why prices fluctuate between two clicks; the system must still guarantee that once you start booking, the seat cannot be taken away.
  • Real-world: marketplaces like Amazon, social platforms like Facebook, mail (Gmail), messaging, entertainment, and learning applications (Coursera, Udemy) — every one of them runs on a database accessed concurrently by thousands, which is exactly why the concurrency and correctness machinery of this lecture exists.
  • Real-world: SQL index creation in every commercial database system creates a B+ tree under the hood, because range queries dominate real workloads — the p-capacity rules of Section 14.1 decide how many disk blocks the index needs and how few reads a lookup costs.
  • Real-world: traditional languages (Python, Java) can implement multi-threaded database access, but it is a tedious task — which is why database software exists to handle concurrency and recovery properly, just as a dedicated software layer was needed over the file access system.
  • Real-world: log files are the persistent, append-style record of every change (e.g., "A changed from 5 to 10") that makes recovery possible after any failure — the basis of the recovery and shadow-paging techniques coming later in the course; every commercial engine (Oracle, SQL Server, MySQL, PostgreSQL) ships one.

DDA Lecture 14 notes · B+ Trees, Transactions, and ACID

Database Design and Applications· postgraduate· 2026-08-06

Sections Breakdown

114.1 The B+ Tree: Structure and Node Capacity

14.1 The B+ Tree: Structure and Node Capacity

214.2 Building a B+ Tree by Hand (p = 4): The Worked Construction

14.2 Building a B+ Tree by Hand (p = 4): The Worked Construction

314.3 Deleting from a B+ Tree

14.3 Deleting from a B+ Tree

414.4 Functional Dependencies First: The Normalization Prerequisite

14.4 Functional Dependencies First: The Normalization Prerequisite

514.5 Assignment Guidance: What Is Expected

14.5 Assignment Guidance: What Is Expected

614.6 Transactions: Why Concurrent Access Is a Problem

14.6 Transactions: Why Concurrent Access Is a Problem

714.7 The ACID Properties

14.7 The ACID Properties

814.8 Why We Cannot Just Run Transactions One at a Time

14.8 Why We Cannot Just Run Transactions One at a Time

914.9 The Life of a Transaction: States and the Log

14.9 The Life of a Transaction: States and the Log

1014.10 Schedules and Serial Schedules

14.10 Schedules and Serial Schedules

1114.11 Conflict Serializability

14.11 Conflict Serializability

1214.12 Where the Discussion Goes Next

14.12 Where the Discussion Goes Next

13Exam Guidance Summary

The professor's exam guidance: assignment deliverables, quiz 3, delayed mid-semester results, and the core exam topics of transaction states and the conflict serializability test.

14Key Industry Applications

Real-world connections: banking transfers, railway and hotel reservations, airline seat-tier pricing, B+ trees behind every CREATE INDEX, and log files as the basis of recovery.

Postgraduate students in Database Design and Applications

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

The B+ Tree: Structure and Node Capacity

Must-know: A B+ tree block holds between ceil(p/2)-1 and p-1 keys; internal nodes hold block pointers, leaves hold record pointers plus a next-leaf pointer; n keys imply n+1 pointers.

⚠️ Top pitfall: Thinking internal-node keys are the data itself; internal keys are routing boundaries, record pointers exist only at leaves.

Self-check: For p = 6, what are the maximum and minimum keys per block?

Connects to: Building a B+ Tree by Hand (p = 4): The Worked Construction, Deleting from a B+ Tree

Building a B+ Tree by Hand (p = 4): The Worked Construction

Must-know: Overflow at p keys -> promote median; right ordering takes the right of the two middle values; equals stay left; leaf splits duplicate the promoted key, index splits do not.

⚠️ Top pitfall: Duplicating a promoted key in an internal node (index splits never duplicate; leaf splits always keep the copy).

Self-check: Inserting 13 into a root holding 3, 6, 9: which value becomes the new root and why?

Connects to: The B+ Tree: Structure and Node Capacity, Deleting from a B+ Tree

Deleting from a B+ Tree

Must-know: Underflow triggers redistribution (borrow from a sibling with more than the minimum) or merge; a deleted separator is replaced by the smallest key of the right leaf; an empty root is discarded and the tree shrinks.

⚠️ Top pitfall: Merging when redistribution was possible, or leaving a dangling pointer from the parent after a merge.

Self-check: When is borrowing impossible and merging forced?

Connects to: The B+ Tree: Structure and Node Capacity, Building a B+ Tree by Hand (p = 4): The Worked Construction

Functional Dependencies First: The Normalization Prerequisite

Must-know: No normal form can be claimed without written functional dependencies; a primary key (surrogate if needed) always determines every other attribute so at least one FD always exists; a foreign key is a constraint (default/restrict/cascade on delete), not an FD.

⚠️ Top pitfall: Counting tables to claim a normal form ('six tables reduced to five is 3.5NF') or treating a foreign key as a functional dependency.

Self-check: A relation with one surrogate key and no other FDs: which normal forms can it honestly claim?

Connects to: Assignment Guidance: What Is Expected

Assignment Guidance: What Is Expected

Must-know: FD lines + 3NF proof are mandatory for every relation; index part is theoretical (B+ vs B-tree vs hash vs bitmap, cost vs retrieval reward, numerical figure appreciated); online SQL editors are not accepted as an excuse; deadline the 30th.

⚠️ Top pitfall: Asserting 3NF without writing the functional dependencies, or submitting without visible query/output snapshots.

Self-check: Why can an ER diagram alone never prove a relation is in 3NF?

Connects to: Functional Dependencies First: The Normalization Prerequisite

Transactions: Why Concurrent Access Is a Problem

Must-know: A write is READ block -> MODIFY record -> WRITE block; main memory is volatile, secondary storage persistent; failure between two dependent writes debits without crediting; the processor cannot guarantee request order.

⚠️ Top pitfall: Assuming ACID comes for free because 'the database handles it', or forgetting that a record is never updated directly - the whole block moves.

Self-check: Why does a power cut after step 2 of a transfer leave the ledger inconsistent?

Connects to: The ACID Properties, Why We Cannot Just Run Transactions One at a Time

The ACID Properties

Must-know: ACID: atomicity (all or nothing), consistency (invariants preserved), isolation (each transaction feels alone - no intermediate states visible), durability (committed changes survive failures).

⚠️ Top pitfall: Confusing legitimate price fluctuation between two searches with an isolation violation inside one booking transaction.

Self-check: Why must an auditor reading A plus B never see the intermediate state of a transfer?

Connects to: Transactions: Why Concurrent Access Is a Problem, Why We Cannot Just Run Transactions One at a Time

Why We Cannot Just Run Transactions One at a Time

Must-know: Serial execution destroys response time and starves readers behind a writer; concurrency improves average time, processor performance, and disk utilization - so we allow it and manage correctness.

⚠️ Top pitfall: Believing multi-core processors guarantee a safe order; every core still executes one operation at a time and the OS controls the order.

Self-check: Why must a reader not be forced to wait for a writer to finish?

Connects to: Transactions: Why Concurrent Access Is a Problem, Schedules and Serial Schedules

The Life of a Transaction: States and the Log

Must-know: Five states with two paths: active -> partially committed -> committed, or active -> failed -> aborted; commit = changes written to secondary storage; rollback is log-based undo, never automatic.

⚠️ Top pitfall: Thinking the DBMS automatically generates the opposite operation on rollback; nothing happens without the log and the recovery process.

Self-check: A transaction's last instruction executed but the disk was never written. Which state is it in?

Connects to: Transactions: Why Concurrent Access Is a Problem, Where the Discussion Goes Next

Schedules and Serial Schedules

Must-know: Serial schedules always follow ACID; an interleaved schedule is acceptable only if equivalent to a serial schedule; non-equivalent schedules are held and stopped.

⚠️ Top pitfall: Believing every interleaved schedule is unsafe, or that failure of the equivalence test proves ACID is violated - it only means we cannot prove safety.

Self-check: Why is every serial schedule automatically atomic and consistent?

Connects to: Conflict Serializability

Conflict Serializability

Must-know: Conflicting pair = two different transactions, same data item, one write (R-W, W-R, W-W). Conflict serializable = swappable to a serial schedule via non-conflicting swaps = acyclic precedence graph. Cycle -> not serializable -> stop the schedule.

⚠️ Top pitfall: Missing non-adjacent conflicting pairs in the graph, or treating two reads as a conflict (they never conflict).

Self-check: In schedule r1(a) r2(a) w1(a) w2(a), which two arrows exist and why is the schedule rejected?

Connects to: Schedules and Serial Schedules, Where the Discussion Goes Next

Where the Discussion Goes Next

Must-know: Coming next: view serializability, recoverability, cascading rollbacks, then recovery and shadow paging; quiz 3 is live; mid-semester results are delayed.

Self-check: Which two schedule-safety properties come after conflict serializability in the roadmap?

Connects to: Conflict Serializability

Exam Guidance Summary

Must-know: FD lines + 3NF proof per relation; ER diagram + report + 4-6 queries with snapshots; index theory with numerical value; professional platform or virtual lab; deadline the 30th; states and conflict serializability are the core exam concepts.

Connects to: Assignment Guidance: What Is Expected, The Life of a Transaction: States and the Log, Conflict Serializability

Key Industry Applications

Must-know: Banking transfers are the canonical ACID case; reservation systems need isolation; B+ trees back every SQL index; log files enable recovery in every commercial engine.

Connects to: Transactions: Why Concurrent Access Is a Problem, The ACID Properties, The Life of a Transaction: States and the Log

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.