0%
Database Internals: Build the Engine
Durability
Concurrency Control
Query Processing
Access Paths
Distribution
B-Tree Concurrency, Latches and Splits
A B-tree that one thread touches at a time is straightforward. A B-tree that hundreds of threads read and modify concurrently is one of the genuinely difficult pieces of engineering inside a database, and the difficulty has a specific shape worth understanding before any of the solutions make sense.
What a reader needs to be true
A descent reads a parent, learns which child to visit, and reads that child. For the descent to be correct, the relationship the parent asserted must still hold when the child is read: the child must still exist, still be part of the tree, and still contain the key range the parent claimed.
Stating the requirement this precisely is what makes the problem visible. The reader does not need the whole tree to be stable, and it does not need the page it just left to be stable either. It needs one specific claim, made by a page it has already finished with, to remain true about a page it has not yet read. That gap between reading the parent and reading the child is where every B-tree concurrency bug lives.
What a writer does that breaks it
A split takes a full page, moves half its entries to a new page, and inserts a separator into the parent. For a moment there are two pages holding what one held, and the parent has not yet been told. A reader descending through the old parent during that moment will look for its key in the wrong half.
This is not a rare interleaving that careful coding avoids; it is the normal state of affairs during every split under load. A busy index splits pages continuously, and descents are happening continuously, so the overlap is constant rather than occasional. Any design that is merely usually correct will fail many times a second.
The structural change versus the content change
Modifying a page's contents affects only that page, so protecting the page protects the operation. A split changes the tree's shape, affecting at least three pages and the relationships between them, so protecting any one page protects nothing.
This distinction is the organising idea of the whole subject. Implementations are built around treating content modifications as cheap and local and structure modifications as expensive and global, and almost every technique in this lesson is a way of making the second class rarer, smaller or less visible rather than making it faster.
Why the obvious solution is unacceptable
A single lock over the whole tree makes everything correct immediately and limits the index to one operation at a time. Since every query touches the root, the root becomes a global serialisation point and the database cannot use more than one processor.
The reason this deserves saying out loud is that the acceptable solutions are all substantially more complicated, and the complexity is justified only because the simple answer is this bad. A correct, slow B-tree is not a usable B-tree, so the engineering is forced.
Why locking the path is almost as bad
A natural refinement holds a lock on each page from the root down, releasing nothing until the operation completes, so that the path is stable. Every operation then holds a lock on the root for its whole duration, which is the same bottleneck with more bookkeeping.
The failure teaches something general: in a tree where every operation starts at the same node, any protocol that holds the root for the duration is equivalent to a global lock. Progress requires releasing the upper levels early, and releasing them early is exactly what reintroduces the gap described above. The tension is fundamental rather than an artefact of a particular design.
The asymmetry that makes solutions possible
The upper levels of the tree are read constantly and modified almost never, since a node splits only once per fanout-many splits below it. The leaf level is modified constantly and each individual leaf rarely.
This asymmetry is the opening every workable design exploits. It means an optimistic approach is correct almost always and wrong rarely enough that an expensive recovery path is affordable. A design whose fast path assumes no concurrent structural change, and which detects and retries when the assumption fails, pays nearly nothing for safety because the assumption is nearly always true.
Why the problem differs from transaction locking
Transaction locks protect logical data for the duration of a transaction, which may be minutes, and must support rollback. The protection a descent needs lasts microseconds, concerns physical structure rather than logical rows, and never rolls back.
Conflating the two is one of the most common misunderstandings in this area. The two mechanisms coexist in every engine, operate at different granularities and timescales, and have different failure modes, and a performance problem caused by one has nothing in common with a problem caused by the other.
Why recovery makes it harder still
A split must be durable as a unit. If the system fails after the new page is written but before the parent's separator is logged, recovery must not leave a page that exists in storage and is unreachable from the tree.
So a structure modification is not merely a concurrency problem but a small atomicity problem, needing its own logging discipline so that replay produces a consistent tree. This coupling between the concurrency design and the logging design is why B-tree implementations are hard to change and why engines rarely replace them.
Why the leaf level is where the contention actually is
Having established that the upper levels are rarely modified, the pressure moves to the leaves and, specifically, to whichever leaves the workload is currently writing. Sequential insertion concentrates every writer on one leaf.
This is the practical contention problem in real systems rather than the theoretical one. An index on a monotonically increasing key has a single hot leaf that every insertion must modify, so the index becomes a serialisation point however good the general protocol is. The problem is in the key choice rather than the implementation.
What a correct design must deliver
Readers must never see an inconsistent tree, writers must be able to proceed concurrently with readers and with each other on different parts of the tree, the root must not be held for the duration of any operation, and structure modifications must be recoverable.
Listing the requirements together shows why the solutions look the way they do. Each technique in the next sections addresses one of these and composes with the others, and no single mechanism delivers all four, which is why a real implementation is a combination rather than one idea.
Why the difficulty concentrates in a handful of pages
The root and the first internal level are read by every operation in the system, so whatever protects them is executed more often than any other code path in the engine.
That concentration is why small constant factors matter here in a way they do not elsewhere. A protocol that costs a hundred extra nanoseconds on the root costs that on every lookup, every insertion and every scan, so it shows up as a percentage of total throughput rather than as a rounding error. It is also why these protocols are rewritten whenever hardware changes the relative cost of their primitives, and why an idea that was clearly correct for one generation of machines can be clearly wrong for the next without anything about the algorithm having been disproved.
What to take from the problem statement
Three things. The hazard is specifically the gap between reading a parent and reading the child it pointed to, during which a structure modification can make the parent's claim false, and that overlap is constant rather than occasional under load. Any protocol that holds the root for an operation's duration is a global lock in disguise, so progress requires releasing the upper levels early, which is what creates the gap in the first place. And the asymmetry between rarely-modified upper levels and constantly-modified leaves is the opening that makes optimistic designs correct almost always and cheap on the fast path.
Separate two mechanisms that are constantly confused. Latches protect the physical structure for microseconds, are taken and released within a single operation, never roll back, and are invisible to transactions. Locks protect logical rows for the duration of a transaction, participate in rollback, and are what deadlock detection operates on. A latch contention problem looks like high processor use with low throughput; a lock contention problem looks like idle sessions waiting. The fixes have nothing in common.