Interactive course · ~4 hours

Inside a Database

Half the hard problems in computing are database problems wearing a disguise. The page that takes nine seconds is a missing index. The site that froze at lunchtime is a lock. The order that vanished is a transaction that only half happened. So build the engine room yourself, starting from an empty file, and then pull its power out in the middle of a sentence and watch it put itself back together.

How this works

There is a working storage engine in this page. It has pages, a tree you can watch split, a log, and a table of locks. Every number you meet is counted while the engine runs, not copied out of a book. When you press Pull the plug it really does throw away the work that was not finished, and Recover really does read the log back and redo the work that was.

What you need before you start

Nothing. No programming, no maths past counting and halving, no idea what a database is. Words like byte, index, page, buffer and transaction are all explained at the point where they first turn up. If you already write software, the same pages work as a refresher, and the engine underneath is real enough to argue with.

The steps

Step 1

What a table really is

A database begins as something disappointing: a file with rows in it. A table is a list of things that all have the same shape. One shop's orders. One school's pupils. One library's books. Each single thing is a row, and each named slot inside a row is a column.

The shop below has four columns: id, customer, city and total. One row is one order. None of that needs a computer, and you could keep it in a notebook. The question that turns it into engineering is where those rows actually sit, because a disk cannot store a row. A disk stores bytes, in one long line, from the start of a file to the end.

What a bit and a byte are

A bit is the smallest thing a computer can hold: one yes or no, written as 1 or 0. Eight bits standing together are called a byte. Eight yes-or-no answers can be arranged in 256 different patterns, so one byte can stand for a whole number from 0 to 255, or for a single letter of English text.

A file is a long line of bytes with a length. There is byte number 0, then byte number 1, and so on to the end. That position, counted from the start, is called an offset. Almost everything in this course comes back to one question: which offset do I jump to.

So the file has to turn rows into a line of bytes, and there are two honest ways to do it. Give every column a fixed width and pad the leftovers, or stick a separator between the values and let each row be as long as it needs. The first wastes space. The second, as you are about to see, costs something worse.

Why the shop stores the total as a number and the city as text

Every column is declared with a type: the kind of value it is allowed to hold. A whole number, a piece of text of at most so many letters, a date. The type is what lets the engine decide how many bytes the column needs and how to compare two of them.

The type matters for sorting more than for storage. The text"100" comes before the text"99", because "1" comes before"9" in the alphabet of characters. The number 100 comes after the number 99. If the engine did not know which one it had, it could not put a column in order, and the whole of Part 2 depends on being able to do exactly that.

Lab 1 · One row, byte by byte
Try this firstPress Comma separated and watch the row get shorter. Then type a longer name into the customer box: the byte count follows what you type. Press Fixed width again and the count stops moving, whatever you type.
The dots are padding. In fixed-width layout every customer name is given 12 bytes whether it needs them or not, so a three-letter name throws nine bytes away. Comma separated throws
Twelve bytes for a name sounds tiny. Why care?

It is tiny once. Ten million orders is ten million times tiny. Nine wasted bytes per row becomes 90 megabytes of nothing, and that 90 megabytes has to be read off the disk along with everything else, because the wasted space sits in between the parts you wanted.

Real engines do not choose one layout for everything. They usually store short fixed-size columns inline and put long text somewhere else with a pointer to it, which is a way of having both. The trade-off in the lab is real even when the compromise is more clever.

Lab 2 · Jump to row 5
Try this firstLeave the layout on Fixed width, drag row number to 5, and press Read the row. Read the bytes-read count. Now press Comma separated and press Read the row again with the slider still on 5. Same request, far more reading.
Fixed width bought you arithmetic. Row 5 starts at offset 5 × 30 = 150, so the engine skips straight there and reads 30 bytes. With separators there is no sum to do. The only way to find the sixth row is to start at byte 0 and count separators until you have passed five of them, which means reading everything in front of it.
The rows are fixed width at 30 bytes each. Somebody asks for row number 100, counting from 0 like the file does. Which byte does the engine jump to?
3000. Row 0 occupies bytes 0 to 29, row 1 occupies 30 to 59, and so on, so row n starts at n × 30. Row 100 starts at 3000 and ends at 3029. The tempting answer is 3030, which is where row 101 starts: that is what you get if you count row 0 as the first row and then multiply by 101 without noticing.
Step 2

Finding one row without reading them all

Step 1 gave you a file of rows and a way to jump to row number 5. Now the real question. The table has 50,000 orders. Somebody asks for the one with id 34,981. Not row number 34,981: the row whose id column happens to hold that value, which could be anywhere.

With only what Step 1 built, there is one honest answer. Look at row 0. Not it. Look at row 1. Not it. Keep going until you find it or run out of table. That is a full scan, and its cost tracks the size of the table exactly. Double the rows, double the work.

What exactly is being counted here, and why not seconds

Seconds depend on which machine you are sitting at, what else it is doing, and how warm the disk is. A count of work does not. So the widgets here count the thing the engine actually does: rows compared, and later pages read. Those numbers are the same on your laptop and on a server in a rack.

This kind of counting has a name. When people say one approach is faster than another, what they usually mean is that the count grows more slowly as the data grows. A full scan of a table twice the size does twice the work, every time, on every machine.

Sorting changes everything. If the rows are in order by id, you can open the table in the middle and ask one question: is 34,981 above or below the value sitting there? Whichever the answer, half the table is gone. Ask again, half of what is left is gone. This is binary search, and it is the reason databases care so much about keeping things in order.

Lab 3 · Scan against lookup, raced
Try this firstPress Race them and read the two counts. Then drag table size from 10,000 to 20,000 and press Race them again. One bar roughly doubles. The other one grows by about one.
Both searches really run. The scan walks the rows and the binary search halves the range, and each reports how many rows it actually looked at. Try a value that is not in the table at all: the scan has to look at every single row before it can say no, which is its worst case and the one that turns up most often in real life.
The word "index" is about to mean two different things

In Step 1,"row number 5" was a position counted from the start. Programmers call that position an index too, and you will hear "the index of that item" all the time. That meaning is not going away.

From here on, though, index mostly means the other thing: the sort of index at the back of a book. A separate, small, sorted list that says "this value is at that place", kept beside the real data so you can find a page without reading the book. When this course says "add an index on customer", it means build one of those. Both meanings are standard, and the sentence around the word always tells you which one is meant.

Lab 4 · How many halvings
Try this firstBefore you press anything, guess: how many times must you halve one million to get down to one? Type your guess in the box and press Check my guess. Then press Show the ladder to see the halving actually done.
Almost everybody guesses too high. Twenty halvings takes a million to one, and forty takes a million million. That growth-by-one-step is what a sorted structure buys, and it is the reason a lookup in a huge table can be as fast as a lookup in a small one.
If sorted is so good, why is anything ever unsorted?

Because a table can only be kept in one order at a time, and because keeping it in that order costs something on every insert. A new order with id 34,982 belongs after 34,981. If the file is sorted, everything after it has to shift along to make room, and shifting 20,000 rows to insert one is a poor trade.

Steps 3 and 4 are about the structure that gets around this. It keeps values in order without ever shifting a whole file, and it charges a small, fixed fee per insert instead. You can safely skip this note: the next step arrives at the same place by a different road.

A sorted table of one million rows needs about 20 halvings for a lookup. The shop grows to two million rows. Roughly how many halvings now?
About 21. Doubling the data adds exactly one halving, because the very first question you ask throws away the extra million. Forty is the answer you get if you assume the cost doubles when the data doubles, which is true for a full scan and untrue here: that difference is the whole point.
Step 3

Fat, shallow trees

Binary search sounds like the end of the story. It is not, because of one awkward fact about storage: you cannot read a byte. You read a page.

A disk, and the operating system above it, hand over a fixed-size block whatever you asked for. Four thousand and ninety-six bytes is the usual size, sometimes eight thousand. Ask for one byte and you still pay for the whole page. So the number that decides how fast a lookup is turns out not to be comparisons at all. It is page reads. And binary search over a sorted file is cruel here, because every halving lands somewhere else entirely, and each landing is a fresh page.

Why on earth does the hardware work in pages?

Because finding the data is the expensive part and copying it is not. A spinning disk has to move an arm and wait for the platter to come round, which takes milliseconds. Once the head is there, streaming the next few thousand bytes costs almost nothing extra. A solid state drive has no arm, but it is built from cells that are erased and written in blocks, so it has the same shape of cost for its own reasons.

The operating system settled on a page as the unit for the same reason, and keeps recently used pages in memory in case you want them again. Everything above the hardware, including this course, therefore counts in pages. A design that reads three pages beats a design that reads thirty, even if the second one does fewer comparisons.

The fix is a shape called a B-tree, and nearly every database in the world uses one. The idea is to stop halving and start dividing by a hundred. Fill one page with as many keys as it will hold, say 100, and keep beside each one a pointer to the page that holds the values in between. One page read now narrows the search 101 ways instead of two.

Tree, node, root, leaf, depth: the words

A tree here is a set of boxes joined by arrows that always point downward and never loop back. Each box is a node. The one at the top with nothing above it is the root. The boxes at the bottom with nothing below them are leaves. A box directly below another is its child.

Depth is how many boxes you pass through going from the root to a leaf. In a B-tree one node is one page, so the depth is exactly the number of page reads a lookup costs. That single sentence is why the rest of this step is about making trees shorter.

Lab 5 · The page-size slider
Try this firstDrag keys per page all the way left to 2 and read the depth figure. Then drag it right to 64. The tree collapses from tall and thin to short and wide, and the page reads for one lookup fall with it. Type an id into the box and press Look it up to see the path light up.
The tree is really built, one insert at a time, every time you move the slider. The nodes drawn are the nodes that exist, and the path marked read is the path the lookup actually walked. Notice that the reads never depend on where in the table the row sits, only on the depth.
Where does the row itself live?

In the design shown here, and in most real ones, the actual rows sit in the bottom row of nodes and nowhere else. The nodes above hold only keys and arrows: signposts, not data. That is why the upper nodes can pack in so many keys, and why the tree stays short.

A separate index works the same way, except the bottom nodes hold the key and the place to find the row, instead of the row. The lookup then costs the depth of the index, plus one more page read to go and fetch the row itself. Step 6 gets a lot of use out of that extra read.

Lab 6 · Skinny against fat, in page reads
Try this firstPress Race them. Both trees hold the same 10,000 keys and both do a real lookup. Compare the two bars, then drag keys per page and race again to watch only one of them move.
The binary tree is not doing anything stupid. It is a perfectly good structure and it does fewer comparisons per level. It loses because each of its nodes holds one key, so each level costs a whole page read, and it has about fourteen levels. Fatness, not cleverness, is what wins here.
A B-tree page holds 100 keys, so each node has up to 101 children. Roughly how many rows can a tree of depth 3 hold before it needs a fourth level?
About a million. The root splits 101 ways, each of those splits 101 ways again, and the bottom level holds 100 rows per page: roughly 101 × 101 × 100, which is a bit over a million. The tempting answer is 300, from adding the levels instead of multiplying them. Multiplying is the whole trick, and it is why three page reads is enough for a table most shops will never outgrow.
Step 4

What an index costs you

Indexes look like free money in Step 3. They are not, and the bill arrives somewhere the reader was not looking: on writes. An index is a second copy of one column, kept in order. Copies have to be kept true. Put a new order in the table and it has to go into every index on that table as well, in the right place.

Usually that is cheap. The engine walks down the tree, finds the leaf page the key belongs in, slots it in, writes that one page back. But pages are a fixed size, so eventually the leaf is full, and then something more interesting happens.

What happens when a page has no room left

The engine performs a split. It takes the full page, hands out roughly half the keys to a brand new page, and then tells the parent about the new page by inserting one separator key. So one insert has turned into three page writes rather than one: the old page, the new page and the parent.

The catch is that the parent can now be full too, which splits it, which tells its parent. In the worst case a single insert splits every level up to the root, and the root splitting is the only way a B-tree ever gets deeper. It grows from the top, not the bottom, which is exactly why every leaf stays the same distance from the root.

Lab 7 · Watch a page split
Try this firstPress Insert the next key four times and watch the single page fill up. Press it once more. The page splits, a level appears above it, and the write count jumps by more than one.
Count the writes, not the inserts. Most inserts cost one page write. The ones that land in a full page cost three or more, and the engine cannot tell in advance which insert that will be. This is why the time taken by inserts into a real database is lumpy rather than steady.
Is a split a disaster? It sounds like one.

No, and the arithmetic says why. A page holding 100 keys splits about once every 50 inserts, because after a split each half has room for 50 more. So the extra writes are spread across 50 cheap inserts, which works out at well under one extra write each on average.

Splits are only a problem when they are constant, and there is one shape that causes exactly that: keys arriving in increasing order, which is what an auto-numbered id column does. Every insert lands in the last page, so that one page splits over and over while the others sit half empty. Real engines special-case this by cutting the last page unevenly instead of down the middle.

Lab 8 · Three indexes, and the bill
Try this firstPress Run the workload with no indexes turned on and read both bars. Now press id to add an index on the id column and run it again: reads collapse, writes creep up. Then turn on all three and run it once more.
Compare the total workload, not one query. The first index saves more read work than it adds to inserts. The third speeds up a selective status query, but that query runs only 20 times while every insert maintains the index. An index can improve its target query and still make the full workload slower.
How do real teams decide which indexes to keep?

They measure. Every serious database can report how many times each index has actually been used since it was last restarted, and dropping the ones with a count of zero is one of the quickest improvements available. It is common to find indexes added years ago for a screen that no longer exists.

The other half of the job is looking at the slow queries and asking which index would have helped. Step 13 is that job, in miniature, with the costs counted for you. Safe to skip until then.

A table gets 1,000 inserts an hour and 5 searches a day, always on the same column. Somebody suggests adding an index on that column. Which choice best fits this workload?
Probably not. Twenty-four thousand inserts a day would each pay the index maintenance fee, in order to speed up five searches. Unless those five searches are painfully slow and somebody is waiting on them, the trade is bad. The last option is the one to argue with: indexes never make inserts faster, they only ever add work to a write.
Step 5

Putting two tables together

Real data is never in one table. The shop keeps orders in one table and customers in another, and the order table does not repeat the customer's name and address on every line. It stores the customer's id and nothing else. That small stored id, pointing at a row in another table, is called a foreign key.

Which means the obvious question,"show me every order with the customer's name next to it", cannot be answered from one table. The engine has to pair each order with the matching customer row. That pairing is a join, and it is the operation databases spend most of their effort on.

Why split it into two tables in the first place?

Because a customer who moves house should move house once. If the address is copied onto all 300 of their past orders, then changing it means finding and rewriting 300 rows, and if you miss one the database now holds two different truths. Storing each fact in exactly one place is the single most useful habit in database design.

The price is that answering a question now needs two tables instead of one. That price is the join, and the rest of this step is about how much it costs and how to make it cost less.

There are three standard ways to do it, and none of them is best. The engine picks one each time it runs a query, based on how big the two sides are and what indexes exist.

What a hash table is, since one of the three uses it

Imagine a row of numbered buckets. To file a customer, run their id through a small fixed sum that turns any value into a bucket number, and drop them in that bucket. That sum is called a hash. To find them again, run the same sum, go to that one bucket, and look through the handful of items there. You never look in any other bucket.

So a hash table finds things in a roughly fixed number of steps no matter how many items it holds, which is better than a tree. What it cannot do is answer "give me everything between 100 and 200", since the hash scatters neighbouring values into unrelated buckets on purpose. Trees keep order, hashes throw it away for speed. Both exist because both trades are sometimes right.

Lab 9 · Three joins, raced
Try this firstPress Race them and read the three bars. One of them is enormous. Then drag customers to the far right and race again: the enormous one grows much faster than the other two.
All three joins really run and really produce the same answer. The nested loop compares every order with every customer, so its count is one table's size multiplied by the other's. The hash join builds buckets once and then does one lookup per order. Sort-merge sorts both sides and then walks them in step, like matching two ordered lists of names by hand.
If the nested loop is so bad, why does any engine use it?

Because when one side is tiny, the multiplication is tiny too. Joining 5 rows to 5 million with a hash join means building a table of 5 million buckets first. The nested loop skips that entirely and does 5 lookups. Engines choose it constantly for small inputs.

And there is a second reason, which Lab 10 is about. If the inner table has an index on the joining column, the nested loop stops scanning the inner table and starts doing a tree lookup instead. That turns the worst plan into one of the best ones without changing the plan's name.

Lab 10 · The same loop, with an index under it
Try this firstPress Race them with the index switch off, and note the nested loop's number. Press Index on customers.id to turn it on, then race again. The same plan, doing a fraction of the work.
This is where the two halves of the course meet. A join is not slow or fast on its own. Its cost is decided by what the inner side can do when it is asked to find one row, which is exactly the question Steps 2 and 3 answered. Most real join problems are index problems.
A nested loop join between 10,000 orders and 2,000 customers, with no indexes, does about 20 million comparisons. The shop doubles in size, so both tables double. Roughly how many now?
About 80 million. The cost is one size multiplied by the other, so doubling both sides multiplies the total by four, not two. Forty million is the answer you get by doubling once and forgetting the other table also grew. This four-times-worse-when-twice-as-big shape is why an unindexed join is the classic cause of a system that was fine last year.
Step 6

Questions an index cannot answer quickly

An index is a sorted list. Everything it can do, and everything it cannot, comes from that one fact. Sorted means you can jump to a value, and read forward from there in order. It does not mean anything else.

So "find the customer named Perera" is easy: jump to P, take what is there. And "find every customer whose name ends in era" is impossible for the same index to help with, because the sorted order is by first letter, and names ending in era are scattered from Alwis to Zoysa. The engine has no choice but to read the whole table. The query looks almost identical to the fast one. It is not.

Why a sorted list only helps from the left

Think of a paper phone book. Finding Perera takes seconds, because the book is ordered by the first letter, then the second, then the third. Now find everyone whose name ends in "era". The book gives you nothing. You read all of it.

Nothing about a computer changes this. A database index is that same book. Any condition that pins down the start of the value can use it. Any condition that only describes the middle or the end cannot, and no amount of hardware fixes that, because the ordering it needs was never built.

Lab 11 · Which of these can use the index?
Try this firstPress the first condition, name is 'Perera', and read how many rows the engine had to examine. Then press name ends with 'era'. Same table, same index, and the count goes from a handful to all of it.
Two of these are traps that look harmless. Wrapping the column in a calculation, such as asking for rows where total × 2 is over 1,000, blinds the index completely: the index holds total, not total × 2, so it cannot jump anywhere. Rewriting the same condition as total > 500 gets the index back. That rewrite is one of the most common real fixes there is.
What "selectivity" means

Selectivity is just the share of the table a condition keeps. Asking for one exact id out of 10,000 rows is very selective: it keeps 0.01 percent. Asking for orders whose status is not cancelled might keep 95 percent, which is barely selective at all.

This one number decides more query plans than anything else, and it is why the engine keeps rough statistics about your data. It has to guess how many rows a condition will keep before it runs it. When those statistics go stale, the engine guesses badly and a query that ran in a blink starts taking a minute, with no code having changed.

Lab 12 · Where the index stops being worth it
Try this firstDrag rows that match to the far left, so only a few rows match, and press Ask the planner. It picks the index. Now drag it slowly to the right, pressing Ask the planner as you go, and find the point where the planner changes its mind.
The crossover is real and it is lower than people expect. An index lookup fetches matching rows one at a time, and each one can be on a different page, so a few thousand scattered matches end up touching nearly every page the table has. A full scan reads every page exactly once, in order. Past roughly a tenth of the table, reading everything in order beats jumping about.
Is there any way to answer "ends with era" quickly?

Yes, by storing something else. Build a second index on the name spelled backwards, and "ends with era" becomes "starts with are" on that index, which is a jump. Some engines will do this for you if you ask for an index on a calculation rather than on a column.

The general lesson is worth more than the trick. When a question cannot be answered quickly, the fix is almost never a faster machine. It is storing a different arrangement of the same data, chosen so that the question you keep asking becomes a jump. That is what every index in this course has been.

A query filters on city and there is an index on city. The table has 100,000 rows and 60,000 of them are in Colombo. Somebody searches for Colombo and it is slow. What is the most likely reason?
Sixty percent of the table matches. The engine has to produce 60,000 rows whatever it does, and fetching them one at a time through an index is worse than reading the table straight through. The index is fine and it is on the right column. It simply has nothing useful to offer for a condition this unselective, and a good planner will ignore it and scan.
Step 7

Pulling the plug

Everything so far has been about speed. This part is about something else entirely: whether the answer is true. Move 500 rupees from Amali's account to Nuwan's. That is two writes. Take 500 off one row, add 500 to the other.

Now cut the power in between. Not a bug, not bad code: a substation trips, or somebody kicks the cable. The first write is on the disk. The second one never happened. Five hundred rupees have left the world.

What actually survives a power cut, and what does not

A machine has two kinds of storage. Memory is fast and forgets everything the instant the power goes. Disk is slow and remembers. A running program keeps almost everything in memory, and writes to disk only when it decides to.

So "the power went off" means precisely this: everything in memory is gone, and whatever had already reached the disk is still there. The trouble is that the program does not get to choose the moment. The cut lands wherever it lands, including exactly halfway through a job that only makes sense whole.

Lab 13 · Break the bank
Try this firstPress Do the next write once, so 500 has left Amali. Then press Pull the plug instead of pressing it again. Read the total at the bottom. Press Start over and try cutting at a different moment.
The total is the thing to watch. Before the transfer it is 3,000. Afterwards it should still be 3,000, because moving money between two accounts creates none and destroys none. A rule like that, which must be true before and after but is allowed to be false in the middle, is called an invariant. Every interesting database bug is an invariant that got left broken.
Could you not just do both writes at the same instant?

Not really. The two rows are in different places on the disk, so they are two separate operations down at the hardware, and something can always happen between them. Even if they landed in the same page, a page write can be interrupted partway and leave half-old, half-new bytes behind, which is worse than either outcome.

So the answer cannot be "make the gap small". It has to be "make the gap survivable". That means writing down what you intend to do, somewhere that survives, before you start doing it, which is Step 8 in one sentence.

What is needed is a way to say: these writes are one thing. Either all of them happen or none of them do, and there is no state of the world in which half of them are visible. A group of writes with that promise attached is a transaction. You mark the start, do the writes, and then either commit, which means make it all real, or roll back, which means pretend none of it happened.

Lab 14 · Four steps, and a plug you place yourself
Try this firstDrag cut the power after step to 2 and press Run it. The widget lists which of the shop's four rules are now broken. Try every setting from 0 to 4 and find the two that leave the shop consistent.
Only the ends are safe. Cutting before anything happened is fine, and cutting after everything happened is fine. Every point in between leaves the shop holding a paid order with no stock reserved, or stock reserved for an order nobody paid for. A transaction is a promise that only those two safe states are ever visible from outside.
You will see the word ACID. What are the four letters?

They are the four promises a transaction makes, and each one has a step of this course behind it. A is atomic: all of it happens or none of it does, which is this step. C is consistent: the invariants that held before still hold after. I is isolated: two transactions running at once do not see each other half done, which is Steps 9 to 11.

D is durable: once you are told it committed, it survives a power cut, which is Steps 8 and 12. The word is used loosely in marketing, so it is worth being able to ask which of the four letters somebody actually means.

A transaction does three writes and the machine loses power after the second one. When it comes back up, what should a correct database show?
None of the three. The transaction never committed, so its promise was never made, and the only correct outcome is that nothing of it is visible. The first option is exactly the failure Lab 13 shows. The problem is worse than lost information: the database is incomplete and is now stating something false about the money.
Step 8

Writing it down before you do it

Here is the trick that solves Step 7, and it is not clever, which is why it works. Before touching any row, write a note on the disk saying what you are about to do. Only then do it. If the power goes, the note is still there when you come back. You can finish the job or undo it.

That file of notes is the write-ahead log, and "write-ahead" is the entire rule: the note goes to disk ahead of the change it describes. The log is a single file that only ever grows at the end, which matters more than it sounds, because appending to the end of one file is the cheapest thing a disk does.

Why appending is so much cheaper than updating rows

Ten rows changed by a transaction may live in ten different pages, scattered across the disk. Writing them means ten trips. Writing ten log records means one trip, because they all go to the same place: the end of the log, which the disk head is already sitting on.

So the engine gets to be safe immediately and tidy later. The moment the log record is on disk the transaction can be declared committed, and the real pages can be written whenever it suits, in a batch, in a sensible order. Being allowed to be late is what makes it fast.

Recovery then has one job. Read the log from the start. Note every transaction that has a commit record. Redo the writes belonging to those, and ignore the writes belonging to transactions that never committed. Amali's 500 either has a commit record after it or it does not, and there is no third case.

Lab 15 · The log, the crash, the recovery
Try this firstPress Next step until the transfer has committed, then press Pull the plug and then Recover. The money is intact. Now press Start over, press Next step only twice, and pull the plug before the commit.
Watch the three panels, not the buttons. The middle panel is memory, which the plug empties completely. The bottom panel is the disk, which it does not touch. Recovery only ever reads the bottom panel, and it rebuilds the middle one from what it finds. Whether your work survives comes down to one question: had a commit record reached the log before the lights went out.
Does the log grow forever?

It would, so the engine takes a checkpoint now and then: it writes all the changed pages out to the data file properly, then records in the log that it has done so. Everything before that point is now safely in the data file, so those log records can be thrown away.

Checkpoints are also what keeps recovery short. Without one, coming back from a crash would mean replaying the log from the day the database was created. With one, it means replaying from the last checkpoint, which is usually seconds of work. The cost is a burst of disk writes each time, which is why a database can feel briefly slower every few minutes.

Lab 16 · Break the rule on purpose
Try this firstLeave it on Log first and press Run and crash, then Recover: the accounts come back correct. Now press Data first and do the same two presses. Recovery has no idea anything happened, and the money is wrong.
The order is the whole mechanism. With the data written first and the crash landing before the log record, the disk holds a change that the log never mentions. Recovery reads the log, finds nothing to undo, and leaves the damage in place, reporting success. A silent wrong answer is the worst failure a database can have, and one rule about ordering is what prevents it.
What is written in one log record?

Five small things: which transaction, which row, which column, the value before, and the value after. The value after is what recovery uses to redo committed work. The value before is what a rollback uses to undo work that was abandoned, whether the abandoning was your choice or a crash's.

That is why a log is much smaller than the data it protects, and why it can be written in one place while the rows it describes are scattered. A record is a sentence about a change, not a copy of the page.

A transaction writes three rows and commits. Its log records are on disk, but none of the three data pages have been written out yet, because the engine was going to do that later. The power goes. What happens?
Recovery replays the log. This is the normal case, not an edge case: at any moment a busy database has thousands of committed changes that exist only in the log. The commit record is the promise, and the log record holds the value, so both the promise and the information needed to keep it are already safe. The pages are just a tidier copy that can be rebuilt.
Step 9

When an update disappears

Crashes are not the only way to break an invariant. Two people at once will do it on a machine that never fails at all. Amali's account holds 500. A shop charges her 50 and, in the same second, a refund of 30 arrives. Both are ordinary, correct pieces of work.

Each one does the same three things: read the balance, work out the new balance, write it back. That shape, read then change then write, is where almost all concurrency bugs live, because there is a gap in the middle where the value you are holding can go stale.

What "at the same time" actually means here

Usually it does not mean two things happening in the same instant. It means the machine is switching between them, running a bit of one, then a bit of the other, over and over, far too fast to see. That switching back and forth is called interleaving, and the order it produces is different every time.

Which is why this class of bug is so unpleasant. In the tiny six-step model below, nearly every overlapping order is wrong because the read-to-write gap takes up most of each transaction. A real transaction may do thousands of unrelated steps around a much smaller dangerous gap, so the overlap can be rare. Rare does not mean safe: enough traffic eventually finds every possible order.

Lab 17 · Deal the steps yourself
Try this firstPress Run a step of T1 three times, then Run a step of T2 three times. The balance ends at 480, which is right. Press Start over, then alternate the two buttons instead, and watch 50 rupees quietly vanish.
Nothing failed. No error was raised, no write was refused, both transactions reported success, and the answer is wrong. T2 read 500 before T1 had written 450, so when T2 wrote 530 it overwrote a change it had never seen. This is called a lost update, and it is the plainest of all the concurrency faults.
What would "correct" even mean when two things run at once?

There is a clean definition and it is worth learning. An outcome is correct if it matches what you would have got by running the transactions one after another, in some order, with no overlap at all. That is called a serial order.

Notice it does not say which order. Charge then refund gives 480, and refund then charge gives 480 as well. Either is acceptable. What is not acceptable is 530 or 450, because no ordering of the two whole transactions produces those. A schedule that gives the same answer as some serial order is called serializable, and that word is the goal of the next two steps.

Lab 18 · Every possible order at once
Try this firstPress Try all 20 orders. The widget runs every way the six steps can be dealt out between the two transactions and shows the final balance for each. Count how many give 480.
Only two orders are safe in this stripped-down model. Those are the two serial orders, with one whole transaction before the other. Every overlap lets both transactions read the same old balance. Real systems make the dangerous window a smaller fraction of the work, which changes how often the bug appears but does not remove any unsafe ordering.
The same bug without any money in it

A web page keeps a counter of how many times it has been viewed. Each visit reads the counter, adds one, writes it back. Exactly the shape from this step. Two visits landing together lose a count, and nobody notices, because nobody knows what the true number was.

This is worth holding on to, because it shows why the bug survives. With money somebody eventually complains. With counters, likes, stock levels and "number of retries so far", the number is simply a little bit wrong forever, and the wrongness grows with traffic.

Two transactions both do read-modify-write on the same row. A developer says the fix is to make each transaction faster, so the gap between its read and its write is tiny. Is that a fix?
No. Shrinking the gap lowers the chance of an unlucky interleaving without removing any of them, so the bug survives, appears less often, and is now much harder to reproduce. That is strictly worse than a bug that happens every time. The gap has to be made impossible to interrupt, not small, which is what Step 10 does.
Step 10

Locks, and the jam they cause

The fix for a lost update is blunt. Before a transaction reads a row it means to change, it takes a lock on that row. While it holds the lock, nobody else may touch that row. Anybody who tries is made to wait until the lock is released, which happens at commit.

That closes the gap from Step 9 completely. T2 cannot read the balance while T1 is in the middle of changing it, so T2 cannot be holding a stale copy. It simply waits, and then reads the value T1 wrote.

Two kinds of lock, because readers can share

Locking every row against everybody would be far too strict. Two transactions that only want to read the same row cannot possibly upset each other, so they are allowed to hold the row together. That is a shared lock, and any number of transactions can hold one at once.

A transaction that means to write needs an exclusive lock, which cannot be held alongside anything else, shared or exclusive. So the rule is short: readers do not block readers, and everything else waits. Most database tuning arguments are about how long an exclusive lock is held.

Lab 19 · The same two transactions, with a lock table
Try this firstPress Locks on, then alternate Run a step of T1 and Run a step of T2 exactly as you did in Lab 17. T2's second press does nothing except join the wait queue, and the balance ends at 480 anyway.
The lock table is the whole mechanism, and it is small. One line per locked row saying who holds it, in which mode, and who is queued behind them. Every database has this table inside it, and when a site freezes at lunchtime, this table is the first thing an engineer asks to see.

Locks fix the lost update and immediately introduce a new failure of their own. If T1 holds row A and wants row B, while T2 holds row B and wants row A, then both wait forever. Neither can give way, because giving way means releasing a lock, and neither is allowed to release before it commits. That is a deadlock.

How the engine notices and what it does about it

It keeps a small picture called a wait-for graph: an arrow from each waiting transaction to the one it is waiting on. A deadlock is exactly a loop in that picture, and looking for a loop is quick, so the engine checks every second or so.

When it finds one, there is no polite option. It picks one transaction, calls it the victim, rolls it back completely and releases its locks, which frees everybody else. The victim gets an error saying it was chosen. Well-written software catches that error and simply tries the whole transaction again, and the second attempt almost always succeeds.

Lab 20 · Build a deadlock, then design it out
Try this firstPress Test every interleaving with the order left as it starts. Some of the runs deadlock. Now press B then A or A then B to change the order T2 takes its locks in, test again, and press Check my answer when no run deadlocks.
The rule that falls out of this is the one real fix. If every transaction in a system takes its locks in the same agreed order, a loop cannot form, so a deadlock cannot happen. It costs nothing at runtime. It is a decision made once, when the code is written, and it is why teams argue about lock ordering in code review.
Why not lock the whole table and avoid all this?

You can, and it is correct. It is what some small engines do. The cost is that only one transaction can work on the table at a time, so a shop with a hundred cashiers becomes a shop with one. Locking a single row instead lets the other ninety-nine carry on.

The choice of how big a thing to lock is called granularity, and it is a trade like every other one here. Fine locks allow more work at once but need a bigger lock table and more checking. Engines usually lock rows, and quietly promote to a table lock when one transaction has taken so many row locks that tracking them costs more than the parallel work is worth. Safe to skip.

Your application logs one deadlock every few hours. Each time, one transaction is rolled back and the program retries it successfully. What is the right response?
Normal, but worth fixing. A detected deadlock is the mechanism working: the victim is rolled back whole, so no invariant is broken and no data is damaged. It costs some wasted work and some latency. The last option is the dangerous one: turning locking off does not remove the conflict, it removes the detection, and you are back to silent lost updates from Step 9.
Step 11

How much mess you agree to live with

Locking everything until commit is correct and slow. Every reader waits for every writer, so a single long-running report can bring a busy system to a halt. So databases offer a dial. You can ask for less protection in exchange for less waiting, and the dial has four standard settings called isolation levels.

What matters is not the names but the honest bit: each setting tells you exactly which kinds of wrong answer it still permits. Choosing a level is choosing which anomalies you are prepared to see.

The three anomalies, in plain words

A dirty read is reading a value another transaction has written but not committed. If that transaction then rolls back, you acted on a number that never existed. A non-repeatable read is reading the same row twice in one transaction and getting two different values, because somebody committed a change in between.

A phantom is the same problem for a group rather than a row. You count the orders from Kandy and get 12. Somebody inserts a new Kandy order and commits. You count again and get 13, inside the same transaction. No row you read has changed. A new one appeared.

Lab 21 · Provoke each anomaly on purpose
Try this firstLeave the level on read uncommitted, keep the anomaly on dirty read, and press Run the pair. Read what T1 saw. Now press read committed and run it again: the same two transactions, and the anomaly is gone.
The pair of transactions never changes. Only the level changes, and with it what the engine allows T1 to see. Notice that the stricter levels are not doing anything smarter. They are holding locks for longer or reading from an older snapshot, and both of those cost something.
Why would anyone choose a level that allows wrong answers?

Because for a lot of questions the wrong answer does not matter. A dashboard showing roughly how many orders came in today does not need to be exact, and making it exact might mean blocking the checkout for everybody while it counts. Read committed, which most databases use by default, is the common compromise.

The mistake is not choosing a weak level. The mistake is choosing one without knowing which anomalies it lets through, and then being surprised. Money movement, stock counts and anything with a uniqueness rule usually deserve the strictest setting, which is why Lab 22 asks you to learn the table rather than look it up.

Lab 22 · Fill in the table by experiment
Try this firstGo back to Lab 21 and run each of the four levels against each of the three anomalies, which is twelve runs. Then come here and click each cell to set it to allowed or prevented, and press Check my table.
The table has a staircase in it. Each level prevents everything the level below it prevents, plus one more thing. That is not a coincidence, it is how the levels were defined, and it means the only real question is how far up the staircase your particular query needs to be.
Snapshots: the other way to get isolation

Locks are not the only mechanism. Most modern engines keep several versions of each row, each stamped with the transaction that made it. A reader is given the version that was current when its transaction began, and it keeps reading that version to the end. Writers make new versions rather than overwriting, so a reader never has to wait for a writer.

This is why a report can run for ten minutes on a busy database without freezing anybody, and why old versions have to be cleaned up afterwards by a background job. It changes how the levels are implemented, not what they permit, so the table in Lab 22 still holds.

A report reads the same customer row twice, half a second apart, inside one transaction, and gets two different balances. Which isolation level is the database most likely running?
Read committed. It prevents dirty reads but allows exactly this: another transaction committed in between, and the second read sees the new committed value. Repeatable read is named after the thing it prevents, so it would have returned the same balance twice, and serializable prevents that and phantoms too. This is also the most common default, which is why the surprise happens so often.
Step 12

What "saved" actually promises

Step 8 said a transaction is safe once its commit record is on the disk. That sentence hides a stack of four places the bytes have to travel through, and every one of them can be holding your commit record when the power goes.

The word for a waiting area like that is a buffer: a patch of memory holding data on its way somewhere, so the slow thing at the far end can be dealt with in large batches instead of one item at a time. Buffers are why computers are fast. They are also why "saved" is a slippery word.

The four places your bytes sit on the way down

First the program's own buffer, in its own memory. Then the operating system's buffer, still in memory, because writing to a file usually just hands the bytes to the system and returns immediately. Then the drive's own small buffer, which is on the drive but still electric. Only then the actual storage that survives a power cut.

Each handover has its own instruction. Emptying the program's buffer into the system is a flush. Telling the system to push everything down to the drive and not return until the drive says it is safe is called fsync. A commit that has not run fsync has not really committed, whatever it reported to the user.

Lab 23 · Four buffers and a power cut
Try this firstPress Write, then Pull the plug, and read what survived. Press Start over, then press Write, Flush, fsync and only then pull the plug. Then turn off Drive tells the truth and try that sequence again.
The last switch is not a joke. Cheap drives have shipped for decades that report a write as complete the moment it reaches their own buffer, because it makes them look faster in reviews. A database can do everything right and still lose committed transactions underneath a drive that lies. This is why serious storage has a battery or a capacitor: enough power to empty that buffer after the lights go out.
Why is fsync so slow?

Because it is the one operation that cannot be batched away or answered from memory. It has to wait for physics: for a platter to come round, or for a flash cell to be programmed. On a spinning disk that is several milliseconds, which sounds small until you notice a busy shop wants to commit a thousand transactions a second.

That single number sets the ceiling on how many transactions a database can commit per second, and no amount of processor speed moves it. Which is why the trick in Lab 24 exists, and why it is one of the most valuable ideas in the whole field.

Lab 24 · One fsync, many commits
Try this firstLeave commits per flush at 1 and press Run 200 commits. Read the fsync count and the modelled time. Now drag the slider to 20 and run it again: the same 200 commits, a twentieth of the fsyncs.
Nobody gets a wrong answer, and nobody gets a broken promise. Each transaction still waits until its own record is truly on the disk before it is told it committed. It just waits in company. The cost is that a transaction arriving early in a batch waits a little longer than it strictly needed to, which is a delay traded for throughput, made deliberately.
What if the whole drive dies?

Then durability, as defined here, has nothing to say. Everything in this step is about surviving a power cut on a working drive. A drive that stops existing takes its log and its data file with it, and no amount of fsync helps.

That is a different promise, and it is bought differently: by keeping copies somewhere else. A backup taken nightly, or a second machine receiving the log records as they are written and applying them as they arrive. The second one is a whole course of its own, because the network between the two machines is allowed to drop things. Safe to skip for now.

A database is told to commit, writes its log record with an ordinary file write, gets no error back, and reports success to the user. Two seconds later the power fails. Is the transaction safe?
No. An ordinary write hands the bytes to the operating system and returns straight away, which is exactly why it is fast. Without an fsync the record may still be sitting in memory when the power goes. The two-seconds answer is the tempting one, and it is a gamble rather than a guarantee: the system flushes when it feels like it, and nobody promised you a deadline.
Step 13

How the buffer pool works

A database does not read a page from storage every time a query asks for it. It keeps recently used pages in a reserved area of memory called the buffer pool. A lookup first asks the pool. A hit means the page is already there. A miss means the engine must fetch it from storage.

Each frame in the pool holds one page. A page being used is pinned, so the eviction code may not replace it. A changed page is dirty. It can be evicted only after the matching WAL record is durable and the page has been written back. The pool therefore joins query speed, WAL ordering and checkpoint work.

Why the operating system cache is not enough

The operating system also caches files, but it does not know which database pages are pinned or which ones are expensive to rebuild. A database pool can keep an index root hot, schedule dirty writes and report its own hit rate. Some engines use direct I/O; others cooperate with the operating-system cache.

Two caches can mean the same page occupies memory twice. One cache can mean less control. There is no universal choice. Engineers measure the workload and the storage system before changing it.

Lab 25 · Replay a page-reference trace
Try this firstRun the trace with three frames. Then raise the pool to five frames. Watch misses turn into hits, and inspect which page the LRU policy evicts.
The sequence matters as much as the size. Repeated index-root and leaf pages stay hot. A scan touches many pages once and can push useful pages out. Real engines often protect the working set from a one-time scan instead of using textbook LRU without adjustment.
Lab 26 · Dirty pages meet the WAL rule
Try this firstDirty two pages, then try to evict one before flushing its WAL. The engine must refuse. Flush the WAL and try again.
Write-ahead applies to every dirty-page write. The log sequence number on a page says which WAL record protects its newest change. The page may reach storage only after durable WAL has reached at least that number.
A report scans a table once and the buffer hit rate falls. What should you conclude first?
The scan may have displaced useful pages. A hit-rate change describes memory reuse, not storage health. Check the query and page-reference pattern before buying more memory.
Step 14

Choose the structure for the question

A B-tree is a good default, not the only possible index. Its sorted keys handle equality, ranges and ordering. A hash index is good at equality but discards order. A bitmap or inverted index records which rows contain each value or word. A spatial tree groups nearby regions. A BRIN-style index stores a small summary for a run of neighbouring pages.

Tables also come in different physical shapes. A row store keeps one whole record together and suits transactions that touch a few rows. A column store keeps one column together and suits analytics that scan a few columns across millions of rows. An LSM tree buffers writes and merges sorted runs later, trading cheap writes for background compaction and more complicated reads.

Composite, partial and covering indexes

A composite B-tree on (customer, created_at) is ordered by customer first, then time within one customer. It can jump to one customer's recent orders. Reversing the columns answers a different question. This is the left-prefix rule from Step 6 applied to more than one column.

A partial index stores only rows matching a condition, such as unpaid orders. A covering index includes the columns a query returns, so the engine may answer from the index without visiting the heap. Both save reads, but both add write work and storage.

Lab 27 · Match six questions to a structure
Try this firstChoose a workload. Compare the measured model costs for a row store, column store, B-tree, hash index, LSM tree and page-range summary.
There is no fastest structure. A structure is fast for the questions its layout anticipated. It pays elsewhere through writes, compaction, space or a kind of query it cannot answer.
Lab 28 · Put composite-index columns in order
Try this firstSwitch between two index orders. Then ask for one customer's latest orders and all orders in a time range. Compare the pages scanned.
Column order is part of the design. An index on A then B is not generally a substitute for one on B then A. Write the important query shapes before creating the index.
A warehouse sums two numeric columns across 500 million rows. Which layout is the best first candidate?
The column store. This query scans many rows but needs very little of each row. Keeping each needed column together reduces bytes read and makes compression more effective.
Step 15

Similarity search in 2026

Text, images and audio can be converted by a model into a list of numbers called an embedding. Nearby lists usually represent similar meaning. A vector query asks for the nearest stored vectors, rather than an exact key or a numeric range.

Exact search compares the query with every vector. That is correct and expensive. Approximate nearest neighbour indexes such as HNSW and IVFFlat examine a smaller candidate set. They trade some recall, the share of true nearest items found, for lower latency and memory or build cost.

An embedding is not a fact and distance is not truth

An embedding records patterns learned from training data. Two passages can be nearby and still disagree. A retrieved passage can be outdated or malicious. Similarity search finds candidates; it does not verify their claims.

Production retrieval usually combines vector similarity with ordinary filters, keyword ranking, access control and citations. Keep the original source text and its permissions beside the vector.

Lab 29 · Exact search against an approximate graph
Try this firstMove the search-effort slider from low to high. Compare distance calculations, latency units and recall against the exact top five.
Approximate means measurable, not vague. Keep a held-out query set, compute exact neighbours offline, and measure recall and latency for the settings you may deploy.
Lab 30 · Build a hybrid retrieval plan
Try this firstTry vector search alone. Then add a tenant filter, keyword score and a reranker. Watch relevance improve while cost rises.
Filter early when the index supports it. Permissions applied after retrieval can return too few allowed results and can leak information through timing or counts. Security belongs in the retrieval plan, not in the final display code.
A vector index returns 9 of the true nearest 10 items in 8 ms. What is its recall at 10?
90%. Recall is found relevant items divided by the relevant items in the exact answer: 9 ÷ 10. The 8 ms number is latency and measures a different part of the trade.
Step 16

Several versions of one row

Step 11 introduced snapshots. Now build one. Under multiversion concurrency control, an update creates a new row version and marks the old version as finished. A snapshot contains the transaction ids it is allowed to see. Visibility is a rule over those ids, not simply “pick the newest bytes”.

Old versions let readers and writers continue together. They also occupy space. A vacuum or garbage collector can remove a version only when no active snapshot could still need it. One forgotten transaction can therefore hold back cleanup and make a table and its indexes grow.

Lab 31 · Read one row through three snapshots
Try this firstSelect each snapshot. The physical page does not change, but the visible balance does. Then abort the newest writer and inspect visibility again.
Readers do not need to copy the database. The versions are shared. Each reader applies the same visibility rule using its own snapshot.
Lab 32 · Let a long transaction block cleanup
Try this firstGenerate updates with no old reader. Vacuum can reclaim almost everything. Start the old snapshot, generate more updates and try again.
Watch age, not just runtime. A connection can be idle while holding an open transaction. Engineers monitor the oldest active transaction, dead tuples and vacuum progress together.
Why can an old open transaction make a frequently updated table grow?
Its snapshot may still need them. Cleanup cannot remove a version while any active snapshot is allowed to see it. End or cancel the old transaction, then let cleanup catch up.
Transaction ids also need maintenance

A fixed-size transaction id eventually wraps around. An engine must mark sufficiently old committed versions as permanently visible before that happens. PostgreSQL calls this freezing and performs it as part of vacuum work.

This is why cleanup is part of correctness, not just disk housekeeping. Monitor age limits and vacuum progress before they become an emergency.

Step 17

An invariant no row lock can see

Two doctors are on call. The rule says at least one must remain on call. Each doctor starts a transaction, sees the other is on call, and takes themself off. They update different rows, so no row-level write conflict occurs. Both can commit under snapshot isolation, leaving nobody on call.

This is write skew. The broken invariant spans several rows. Serializable isolation prevents the outcome by tracking read/write dependencies or by locking the predicate that represents “doctors on call”. An explicit constraint or a single locked summary row can also move the invariant into one conflict point.

Lab 33 · Produce write skew
Try this firstRun both transactions under snapshot isolation. Then switch to serializable and run the same schedule. One transaction must retry.
Repeatable reads are not enough. Each transaction sees a stable, sensible snapshot. The combined outcome is still impossible in any serial order.
Lab 34 · Repair the invariant three ways
Try this firstCompare serializable retries, a locked guard row and an unsafe check with no protection. Inspect blocking, aborts and the final invariant.
The database cannot infer every business rule. State the invariant, find every transaction that can change it, and make those transactions meet at a constraint, lock or serializable dependency check.
Two transactions update different rows but together break a rule. What should you inspect?
Inspect the reads and predicate. Write skew is created by a dependency between what each transaction read and what the other wrote. Looking only for two writes to one row misses it.
Prefer a database constraint when one can express the rule

A unique, foreign-key or check constraint is enforced for every writer, including scripts and future services. A check in application code protects only paths that remember to call it.

Cross-row rules are harder to express as simple constraints. For those, document the chosen lock or serializable pattern and test concurrent schedules rather than only single requests.

Step 18

Recovery is three separate jobs

Step 8 replayed a small log from the beginning. A production engine needs a tighter method. It starts at a checkpoint and performs three jobs: analysis finds active transactions and dirty pages; redo repeats updates that may not be present on disk; undo reverses transactions that never committed.

Every log record has a log sequence number, or LSN. Each data page records the LSN of its newest applied change. During redo, a record can be skipped when the page LSN is already as new. This makes replay idempotent: running it twice gives the same result as running it once.

Why undo writes more log records

Recovery itself can crash. When undo reverses a change, it records that action in a compensation log record. A second recovery can see which undo work was already completed and continue safely.

This analysis-redo-undo shape is associated with ARIES. Engines differ in record format and detail, but the questions stay useful: where does recovery start, what is safe to repeat, and how is interrupted recovery resumed?

Lab 35 · Run analysis, redo and undo
Try this firstStep through the three phases. Watch committed T1 get redone, incomplete T2 get undone and an already-current page get skipped.
Redo does not mean “committed only” in every design. A steal/no-force engine may redo history first and then undo losers. The page LSN prevents the same update being applied twice.
Lab 36 · Move the checkpoint
Try this firstMove the checkpoint farther from the crash. The number of records scanned and the modelled restart time rise. Move it too close and foreground writes pay more checkpoint work.
A checkpoint trades work now for work after failure. The useful target comes from a recovery-time objective and measured write bandwidth, not from a fixed interval copied elsewhere.
During redo, a page on disk has pageLSN 240 and the next log record for it has LSN 220. What should recovery do?
Skip it. Page LSN 240 says the page already contains changes through that point. Reapplying record 220 could duplicate a non-idempotent physical action.
Step 19

Detecting damaged pages

A correct WAL order cannot stop a storage device from returning damaged bytes. A page checksum turns the page into a number before it is written. The engine recomputes that number when reading. A mismatch detects accidental change; it does not repair the page or protect against a person deliberately changing both data and checksum.

A power cut can also produce a torn page: some sectors are new and the rest are old. Engines may log a full page image after a checkpoint, use atomic-write storage, or keep a second safe copy. Recovery can restore the complete image before replaying later changes.

Lab 37 · Flip one byte and test the checksum
Try this firstVerify the original page. Flip one byte and verify again. Then restore from a full-page image and replay the later WAL record.
Detection changes the failure from silent to visible. The recovery path still needs a known-good source: WAL image, replica or backup.
Lab 38 · Tear an eight-sector page
Try this firstMove the failure point through the sectors. Compare no protection, a full-page WAL image and atomic page writes.
Test the storage claim end to end. File systems, controllers and drives can all buffer or reorder. The guarantee that matters is what the database observes after power loss.
A page checksum fails during a read. What has the checksum established?
The bytes differ. A checksum detects the mismatch. Diagnosis and repair require logs, hardware evidence and a separately verified copy.
Checksums, encryption and error-correcting memory solve different problems

A checksum detects accidental byte changes. Encryption limits who can understand stored bytes. Authentication detects deliberate modification by someone without the key. ECC memory can correct some bit errors before the database sees them.

A careful system may use all four. Naming the failure first prevents one mechanism being credited with a guarantee it does not provide.

Step 20

A backup is only real after a restore

A backup protects against deletion, corruption and loss of the whole machine. A replica is useful for availability, but it quickly copies an accidental DELETE. A backup kept in another failure domain can recover older state. Keep more than one generation, restrict deletion and test restoration.

Point-in-time recovery combines a base backup with archived WAL. Restore the base, then replay WAL until just before the bad event. The recovery point objective says how much recent data may be lost. The recovery time objective says how long service may remain unavailable.

Lab 39 · Restore to the minute before a mistake
Try this firstSelect a base backup and target time. The simulator checks that every required WAL segment exists, then replays only through the target.
A missing segment breaks the chain. Listing backup files is not a restore test. Start a clean machine, verify checksums, replay and run application-level checks.
Lab 40 · Design an RPO/RTO plan
Try this firstSet the allowed data loss and downtime. Choose full-backup frequency, WAL archive interval and restore bandwidth. Check the resulting plan.
Requirements become arithmetic. If the business allows five minutes of data loss, archiving every hour cannot pass. If restoring 2 TB takes six hours, a one-hour RTO needs a faster restore path or a different recovery design.
Why is a continuously updated replica not a complete backup?
They copy bad changes too. Replication improves availability. Versioned, protected backups and tested point-in-time recovery address a different set of failures.
What a useful restore drill records

Record the backup chosen, every WAL segment used, checksum results, start and finish times, and the application checks run afterwards. Restore into an isolated destination so the drill cannot overwrite the source.

Test operator access too. A backup that only one unavailable person can decrypt or locate does not meet a recovery objective.

Step 21

Change a live database safely

A schema change is a program change applied to stored data. Adding a nullable column is usually cheap. Rewriting every row, checking a new constraint or building an index can run for hours and hold locks. Safe migrations separate compatible steps: expand, backfill in small batches, verify, switch readers, then remove the old shape later.

Diagnosis is also a sequence. Start with the symptom and a time window. Inspect query plans and actual rows, buffer reads, lock waits, WAL rate, checkpoint time, vacuum lag and storage latency. Change one thing, measure again and keep a rollback. An AI assistant can propose hypotheses or explain a plan, but it must not receive secrets or execute unreviewed production changes.

Lab 41 · Plan an online migration
Try this firstOrder the migration steps. Try a table rewrite during peak traffic, then use an expand-and-contract plan with a batched backfill.
Compatibility comes before cleanup. During a rolling deployment, old and new application versions may run together. The intermediate schema must serve both.
Lab 42 · Diagnose from evidence
Try this firstSelect a symptom and request one measurement at a time. Make a diagnosis only when the evidence distinguishes a scan, lock queue, checkpoint stall or vacuum problem.
A graph is not a cause. Correlate a user-visible slowdown with a plan change, wait event or resource limit. Record the before-and-after evidence with the fix.
Use the neighbouring courses instead of stretching this one

Build a Query Engine covers parsing, relational algebra, cardinality estimates and plan search. Distributed Consistency covers histories, replicas and clocks. Raft and Consensus covers an ordered replicated log and membership changes.

Security engineering adds threat models, least privilege, secret handling and incident response. Database permissions, encrypted connections and audit logs should be designed with those tools rather than treated as a final checkbox.

A generated suggestion says “add this index” after seeing one slow query. What should happen next?
Test the hypothesis. The index may help that query and hurt writes or duplicate an existing index. Use representative data, inspect the plan and measure the whole workload.
Step 22

Tune and diagnose it yourself

Everything in this course now exists in one place: a heap of rows in pages, B-trees over them, a log, and a lock table. The last job is the one an engineer is actually paid for. Here is a day of a shop's work. Choose which indexes to keep, and make the total cost fit the budget.

Nothing is hidden. The workload runs against the real structures, every page read and page write is counted as it happens, and the tempting answer of indexing everything will fail the budget for a reason you can read off the bars.

What the budget number actually counts

One unit is one page touched: read into memory, or written back out. It is the standard way engines compare plans, because a page is the unit the hardware works in and because the count is the same on every machine.

Real planners weight the two differently, since a write is dearer than a read, and they add a small charge for the processor work of comparing rows. Those refinements change the numbers by tens of percent and change the answers hardly at all, so this lab counts pages plainly and says so.

What these choices look like typed out in a real database

The language almost every database speaks is called SQL, and the parts this course has been doing by hand are four short commands. Nothing below is needed to finish the lab, but you will meet it within a day of touching a real system.

SELECT id, total FROM orders WHERE customer = 412;
CREATE INDEX orders_customer ON orders (customer);
EXPLAIN SELECT id, total FROM orders WHERE customer = 412;
DROP INDEX orders_customer;

The third one is the interesting command. EXPLAIN does not run the query. It asks the planner to print the plan it would use, including whether it intends to use an index or scan the table, and it is how every one of the problems in Step 6 gets diagnosed in real life.

Lab 43 · Meet the budget
Try this firstPress Run the workload with nothing turned on to see how bad it starts, then press Check my answer to read exactly why it fails. Now switch indexes on and off, running the workload after each change, until you are under budget.
The three indexes are not equally worth having. One of them removes almost all the cost on its own. One helps a query that runs often enough to matter. One helps a query that runs 20 times a day while charging all 1,000 inserts, and it is the one to leave off. If you get stuck, Show me one that works loads a passing set, which you should then take apart.
How does a real planner choose, given it cannot try every plan?

It estimates. For each way of running the query it works out a cost from statistics it keeps about your tables: roughly how many rows there are, how many distinct values a column holds, how the values are spread. Then it picks the cheapest estimate and runs that. It never measures the real thing first, because measuring would cost more than the query.

Which explains the most confusing kind of database problem there is. The statistics go stale, the estimate becomes wrong, the planner picks a bad plan, and a query that ran in a blink for a year starts taking a minute with nothing in your code having changed. The command to refresh those statistics is usually the first thing to reach for.

Lab 44 · The whole engine, no marking
Try this firstType help and press Run to see the words it knows. Then try begin, set amali 400, crash, recover, get amali, one line at a time, and see what the balance is.
Nothing here is marked, and nothing here is faked. The console drives the same engine every earlier lab used. Some things worth trying: commit before crashing and compare; open two transactions and make them fight over one key; and use log to read what recovery would see.
A page that lists a customer's recent orders takes nine seconds. The table has 8 million rows and an index on id only. Where would you look first?
Whether an index on the customer column exists. The query filters on customer, the only index is on id, so the engine has no choice but to read all 8 million rows every time the page loads. Locks and logs are real causes of real problems, but they make a system slow for everybody at once, not one page slow all the time. A single query that is always slow is nearly always a missing index.

What you built

  • A table as bytes in a file, and the arithmetic that jumps to row n.
  • A B-tree over pages, splitting as it grows, with lookups costing its depth.
  • The read and write sides of an index, and the judgement to weigh one against the other.
  • Three join strategies, and the reason the choice between them is really an index question.
  • A write-ahead log, a crash, and a recovery that redoes committed work and discards the rest.
  • A lock table, a lost update it prevents, a deadlock it causes, and the ordering rule that avoids it.
  • The four isolation levels and the anomaly each one still lets through.
  • The four buffers between a commit and a promise you can keep.
  • A buffer pool with hits, misses, eviction and the WAL rule for dirty pages.
  • Row, column and LSM storage, plus composite, covering and vector indexes.
  • MVCC visibility, cleanup, write skew and whole-transaction retries.
  • Analysis, redo and undo recovery, page checksums, backups and point-in-time restore.
  • An expand-and-contract migration and an evidence-led incident diagnosis.

Where this goes

  • Build a Query Engine. You have been choosing plans by hand. Next, parse a written query, work out every plan that would answer it, cost each one, and let the machine choose.
  • Reliable Data Transfer. The log in Step 8 makes one machine survive a crash. Sending it to a second machine over a network that drops things is a harder and different problem.
  • Distributed Consistency. Two copies of this database, on two machines, both accepting writes. Everything in Part 5 comes back, larger, and some of it has no clean answer.
  • Source manuals. PostgreSQL's current documentation explains MVCC, write-ahead logging, index types and point-in-time recovery. Use the manual for the engine and version you are operating.