B-tree indexes
How a B+tree finds a row
The idea in 30 seconds
A B-tree index is a few levels of 8 kB pages. Leaf pages hold every key in sorted order with a pointer to its row; the pages above only route the search. A lookup reads one page per level, a range scan walks sideways along the leaves, and the tree grows taller only when its root splits.
Helps to know first: How a table is stored on disk (coming soon)
On this page
Three kinds of page
A PostgreSQL B-tree index is a file of 8 kB pages, like a table. The pages play three roles:
| Page | What it holds | How many |
|---|---|---|
| Metapage | Where the root is, and how tall the tree is | Exactly one, always the first page |
| Internal pages | Separator keys, each with a pointer one level down | A few, including the root at the top |
| Leaf pages | Every key, in sorted order, each with the CTID of its row | Almost all of them |
The email index from the previous page has 4,961 pages: 1 metapage, 33 internal pages and 4,927 leaves. Over 99% of an index is leaves, which is typical. The tree above them is a small routing layer that stays hot in memory.
Every leaf is at the same depth. That is what makes the tree balanced: no key is ever further away than any other.
Following a lookup down the tree
To find user777777@example.com, PostgreSQL starts at the root page. There it compares the key with the separators and follows the one pointer whose range contains it. It repeats that on each level until it reaches a leaf. The leaf holds the key and its CTID, and one more read fetches the row.
That is the 4 pages you saw in the EXPLAIN output: root, internal page, leaf, table page.
Range scans walk sideways
Each level of the tree is also a linked list: every page knows its left and right neighbors. So once a search reaches the first matching leaf, a range query does not go back up the tree. It walks along the leaves in key order and stops when keys pass the end of the range.
This is why a B-tree serves BETWEEN, <, > and ORDER BY. It is the only PostgreSQL index type that returns rows in sorted order. With ORDER BY created_at DESC LIMIT 50, PostgreSQL can read the first 50 entries from the end of the index and stop, without sorting the whole table.
Inserts and page splits
A new key goes into the leaf where it belongs in sorted order. If that leaf is full, it splits: a portion of its keys, roughly half, moves to a new page, and a new separator goes up into the parent. If the parent is full too, it splits in turn, and splits can cascade all the way up.
When the root splits, PostgreSQL creates a new root above it and the tree gains one level. That is the only way a B-tree grows taller, so every leaf always moves down together.
Step through Load 24 keys in order, then Same keys, random order. Watch the message under the tree at each split, and compare the “leaves % full” figure at the end.
PredictSame keys, different order
Two tables each get the same one million bigint keys, inserted into an index that already exists. One table receives them in increasing order; the other receives them shuffled. When both are done, how do the two indexes compare in size?
The shuffled one is about 36% bigger. We measured it on PostgreSQL 18.3:
| Insert order | Leaf pages | Leaves full | Index size |
|---|---|---|---|
| Increasing | 2,733 | 90.0% | 21 MB |
| Random | 3,720 | 66.2% | 29 MB |
When a key lands in the middle of the tree, a full leaf splits roughly in half and both halves are left with room to spare. When each new key is the largest so far, PostgreSQL treats it as extending the index at the right edge: it keeps the old leaf filled to the index’s fillfactor, 90% by default, and starts a new page. The visualizer does the same, except that it fills pages completely, because its pages hold only a few keys.
Order only matters while the index is being built up by inserts. When we built an index on the shuffled table after loading it, it came out at 2,733 leaf pages and 90% full, identical to the in-order one. CREATE INDEX builds the whole index from one scan of the table and fills each leaf to the fillfactor as it goes.
Repeated keys are stored once
Many indexes contain the same key many times, such as a status or a foreign key column. PostgreSQL B-trees deduplicate them by default: a group of identical keys becomes one entry that stores the key once, followed by a sorted list of row pointers. Indexes on repetitive columns shrink a lot as a result, but every row still needs its pointer, so the index never becomes free.
Check yourself
What do internal pages and leaf pages each store, and which kind makes up almost all of the index?
Show answer
Internal pages store separator keys and pointers to the next level down; they only route searches. Leaf pages store every key in sorted order with the CTID of its row. Leaves are typically over 99% of the pages.
How does a B-tree answer
WHERE created_at BETWEEN a AND bwithout visiting the tree once per row?Show answer
It descends once to the first leaf containing
a, then walks right along the linked leaf pages, reading keys in order until they passb.How does a B-tree become taller, and why does that keep every leaf at the same depth?
Show answer
Only by splitting the root: a new root is created above the old one. Every leaf moves down one level at the same moment, so the tree stays balanced.
Why does inserting keys in increasing order produce a smaller index than inserting the same keys at random?
Show answer
Appending to the right edge keeps the old leaf filled to the fillfactor (90% by default) before starting a new page. Random inserts split full leaves in the middle, leaving pages around two thirds full, so the same keys need more pages.
Finished reading?
Saved in this browser only.
Sources
Where each claim on this page comes from.
- 1.PostgreSQL documentation: 65.1 B-Tree IndexesPrimary· §65.1.4.1 B-Tree Structure
The metapage, leaf and internal pages; each level is a doubly-linked list of pages; typically over 99% of pages are leaves; splits cascade upward and a root split adds a level.
- 2.PostgreSQL documentation: 65.1 B-Tree IndexesPrimary· §65.1.4.3 Deduplication
Duplicate keys are merged into one posting list tuple that stores the key once with a sorted array of TIDs.
- 3.PostgreSQL documentation: CREATE INDEXPrimary· Storage parameters: fillfactor, deduplicate_items
B-tree leaves are filled to fillfactor (default 90) during an index build and when adding new largest keys; deduplication is on by default.
- 4.PostgreSQL documentation: 11.4 Indexes and ORDER BYPrimary· §11.4 Indexes and ORDER BY
Only B-tree indexes return rows in sorted order; with ORDER BY … LIMIT n, the first n rows can be read straight from the index.
- 5.PostgreSQL documentation: F.33 pgstattuplePrimary· F.33 pgstattuple: pgstatindex
tree_level, leaf_pages and avg_leaf_density used in the measurements on this page.
Go further
What to read next, and why it is worth the time.
- 1.Use The Index, Luke!: Anatomy of an SQL IndexReferenceMarkus Winand· Chapter 1: The Leaf Nodes, and The B-Tree
Two short pages that draw the same structure with different examples. Good to read straight after this one.
- 2.PostgreSQL source: src/backend/access/nbtree/READMEPrimary
How PostgreSQL lets many sessions read and split the tree at once, using the Lehman and Yao design. Dense, but it is the real implementation.
- 3.Database InternalsReferenceAlex Petrov· Part I, Chapters 2 and 4: B-Tree Basics, Implementing B-Trees· location not yet verified
Splits, merges, and page layout from the point of view of someone building a storage engine.
Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.