Skip to content
All articles

Databases at Scale

Your database reads pages, not rows

Ask for one 120-byte row and the engine reads an 8 KiB page. The same index can find 200 rows in four page reads or in two hundred, depending on how the rows were stored.

· 5 min read

One customer's 200 orders, found by the same index. A row is 120 bytes and an 8 KiB page holds 55 rows in PostgreSQL. Inserted over three years, the rows are scattered across 200 pages: 200 page reads. Loaded in customer order, they fill 4 pages: 4 page reads. 50 times apart. The index tells the engine where to look; locality decides how many pages it must read.

You ask for one 120-byte row. The storage engine reads an 8 KiB page, because that is the smallest unit the disk and the buffer pool exchange. If your row sits alone on that page, you paid 8 KiB for 120 bytes. If the neighbours you also wanted are on it too, you paid 8 KiB for all of them.

That ratio is locality, and it is the most useful single lens for reasoning about storage performance.

How many rows share a page

With nothing else taking space, an 8 KiB page would hold 68 rows of 120 bytes. A PostgreSQL heap page also spends 24 bytes on its page header and, for every row, 4 bytes on a line pointer and 24 on a tuple header (23 bytes, padded), so it holds 55.

page_bytes, row_bytes = 8 * 1024, 120
ideal = page_bytes // row_bytes                       # 68 rows
postgres = (page_bytes - 24) // (4 + 24 + row_bytes)  # 55 rows

Same index, fifty times apart

Two tables hold the same customer's 200 orders, and the same index finds them in both. In the first, the orders were inserted over three years, so they are scattered across about 200 pages written at different times. The second was bulk-loaded in customer order last night, so the 200 rows sit side by side.

How the rows are storedPage reads for 200 rows
Scattered200
Clustered, 68 rows a page3
Clustered, PostgreSQL's 55 rows a page4

The index tells the engine where to look. It does not reduce the number of pages the engine must fetch; locality does. That is how "the query returns 200 rows and takes 400 ms" happens with a perfectly good index.

Most pages never touch the disk

Pages are usually served from the buffer pool, the memory the engine keeps for hot pages, where a hit costs a memory access instead of an I/O. A hit ratio above 99% is normal on a well-sized database, which makes the ratio a poor alarm: a table scan that evicts the working set can keep it high while ruining every other query's locality. What matters is not how often you hit, but what was evicted to make room.

Think Like an Engineer

A database with 64 GiB of RAM and a 60 GiB working set can be an order of magnitude faster than the same database with a 70 GiB working set. With uniform access there is no gradual decline, because the set either fits or it does not. Skewed access softens the cliff: the hot part of a 70 GiB set may still fit.

When a query is slow despite a good index, count pages, not rows.

Get one diagram a week

A short article built around one engineering diagram, from the same library as these courses.

One diagram-led article a week on AI and systems engineering. We email you once to confirm, and every newsletter has an unsubscribe link. Privacy policy