Skip to main content
Indexinglesson 2 of 4 · 3 min read

B-Trees, the Default

Why it has been the default for fifty years

Type CREATE INDEX without saying anything more and you get a B-tree. It has been the default for fifty years for a reason, and once you do the arithmetic you can see why.

Your database reads the disk in fixed-size pages, 8 kilobytes of it at a time in Postgres. A B-tree fills each page with hundreds of sorted keys and pointers down to child pages.

That width is the whole trick. An ordinary binary tree splits two ways at each step, so finding one of your 50 million entries takes about 26 steps. A tree splitting 400 ways takes 3.

The multiplication does the rest. One page at the top covers 400 children, 160,000 grandchildren, and 64 million entries at the third level down. Depth is how many page reads a lookup costs, and the top levels are used so heavily they are always sitting in memory. Finding one key among tens of millions costs you one or two real disk reads.

Ranges and ordering come free

Ranges and ordering come free, because your keys are stored sorted, which a hash index cannot offer at all.

Your query for everything between two dates drops to the first match and then walks along the leaves sideways. Ask for the first twenty rows in order and the database reads twenty entries off the leaves and stops, with no sorting step at all.

That covers four shapes: exact matches, ranges, prefixes, and sorting. One structure serving the four shapes that dominate real work is why the exotic index types stay niche.

It stays balanced by splitting. Insert into a full page and the page splits in two, occasionally pushing a whole new level on top. Every one of your leaves ends up at the same depth, so the worst case equals the typical case, and that predictability is what the rest of your latency budget quietly leans on.

the shape of it
Root pagealways in RAMInternal pagenarrows the rangeLeaf pagekey found hereHeap rowthe actual dataread 1read 2row pointer
step 1 of 3
High fan-out means a handful of page reads reach one row among millions, and the top of the tree stays cached.

Worked example

Nina's analytics table at a logistics company holds 200 million shipment events, and a product manager assumes querying one shipment's history must be slow at that size. She does the math on a whiteboard instead of guessing. The index on shipment_id is a B-tree with 8 KB pages holding roughly 350 entries each: depth works out to four levels for 200 million keys. The root and second level, a few hundred pages total, live permanently in the buffer cache, so a lookup costs about two real SSD reads at 100 microseconds each. She runs it: EXPLAIN ANALYZE shows 0.6 ms. The range query for one shipment's 30 events is barely slower, since all 30 sit adjacent on one or two leaf pages. The PM's proposed "archive table for speed" project dies on the spot, which was the point of the whiteboard.