Proofs: Log-structured merge tree
These are the human proofs of Log-structured merge tree, at full length.
The leveled LSM tree returns the newest entry of each key full
This proof shows: correctness, termination.
We use the pseudocode of the page, with and . Number the updates in order. Every entry in the memtable or in a run is a copy of some update of its key, an entry of age if it comes from update . Let be the state of in the abstract dictionary: absent if was never updated or its last update is a delete, and present with value if its last update is put(k, v).
Define the sequence of components as the memtable, then level 1, level 2, and so on. The found entry of a key is its first entry in this sequence, if any. We prove that between operations:
- (M1) every run is sorted with distinct keys and has at most entries at level , and the memtable has fewer than entries;
- (M2) for every key , if has entries in two components, the one earlier in the sequence is younger;
- (N) for every key , if has a found entry, it is the last update of , or has no found entry and is absent; and if the found entry is a tombstone, is absent;
- (M3) the deepest nonempty level has no tombstones.
We prove (N) in the form: for every key that was ever updated, either its found entry is of the age of its last update, or has no entry and its last update is a delete.
Base. All components are empty and no key was updated.
GET. Lines L8 to L11 look at the memtable and then at the levels in order and return the first entry of , which is the found entry. The search inside one run is a binary search inside the block chosen by the fence keys: the block whose first key is the largest fence at most holds if any block does, because the run is sorted by (M1). If the found entry is a put of , it is the last update of by (N), so is present with value . If it is a tombstone, the last update was a delete, and is absent. If there is no entry, is absent by (N). GET ends after at most one step per level.
UPDATE, line L1. The new entry of is the youngest of all entries, and it replaces the older entry of in the memtable, if any. So has its found entry in the memtable, of the age of its last update, and (N) holds for . Other keys are not changed. (M2) holds because the memtable is first in the sequence and its entry of is the youngest. If the memtable now has entries, line L2 calls FLUSH.
FLUSH. We prove a loop invariant for lines L4 to L7. At the start of each pass with index :
- (F1) is a sorted run with distinct keys, and the memtable and the levels are empty;
- (F2) for each key, its entry in is younger than its entries in levels ;
- (F3) for each key, if we take the sequence , level , level , and so on, the first entry of each key is the found entry it had before FLUSH, or there is no entry and the found entry before was a tombstone that was dropped.
At the first pass, and is the memtable in key order, so (F1) to (F3) hold by (M2) and the definition.
Line L5. MERGE takes the union of the entries of and of level ; on a key in both, it keeps the one of , which is younger by (F2), and drops the older one, which was not the first entry of the key in the sequence. So the first entry of each key in the sequence (new , level , ...) is the same as in (old , level , level , ...). The result is sorted with distinct keys, as a merge of two sorted runs with equal keys combined. If all levels below are empty, a tombstone in the result is the only entry of its key in the whole tree, so dropping it changes the first entry of that key from a tombstone to none, which (F3) allows. So (F1) to (F3) hold for the new , with level now merged into it.
Line L6. If fits, it becomes level . Then the sequence is: empty memtable, empty levels , level , levels unchanged. By (F3), the found entry of each key is its old found entry, or none for a dropped tombstone. So (N) holds: an old found entry is of the age of the last update, and a dropped tombstone means the last update was a delete. (M2) holds by (F2) and by (M2) for the deeper levels, which were not touched. (M1) holds: has at most entries, and the memtable is empty. For (M3): if all levels below are empty, is the deepest nonempty level and has no tombstones, since they were dropped; otherwise the deepest nonempty level is unchanged.
Line L7. If is too big, level becomes empty, and the loop goes on with . (F1) holds, since level is now empty and its entries are in . (F2) holds for level and deeper by (F2) and (M2) before. (F3) holds by the argument of line L5.
Termination of FLUSH. Each pass either returns or increases . When the pass at level starts, holds at most the entries of the memtable and of the levels , so at most entries, since . Once is past the last nonempty level, level is empty, the merge adds nothing, and line L6 returns. So the pass at the first empty level returns at the latest, and FLUSH ends after at most one pass per level plus one.
Range queries. RANGE reads, in each run, the entries with keys in the range, from the oldest level to the newest and then the memtable, letting newer entries overwrite older ones. So it keeps for each key the found entry, and it returns the keys whose found entry is a put, which by (N) are exactly the present keys in the range.
So every GET and RANGE returns the state of the abstract dictionary, every operation ends, and the invariants hold between operations.