workshop private

← all creations

Seek

viz · created 2026-09-27

The same query box dropped on three layouts of one grid — row-major, Z-order, Hilbert — so the number everybody quotes comes out a tie (8.00 runs against 8.00) and the number that actually decides what the query costs, the distance between those runs, comes out 56 against 4.

algorithmsinterview-prepcanvas

Every store you can actually buy is one-dimensional. A B-tree, an LSM, a key-value API, a file: each of them hands you intervals — start here, read forward. A map is two-dimensional, and so is every interesting question you ask it: everything inside this rectangle. Somewhere between those two facts you have to choose an order, and the order is the whole performance story, because a rectangle has to be broken into intervals before the store will accept it.

Three panels, one grid, one box. Each panel colours every cell by where it sits along that layout’s curve — cool early, warm late — so two cells that look alike are close together in the store. Underneath each panel is the store itself, drawn as one line, with the box’s cells marked on it. Drag the box anywhere; all three panels are always answering for the same rectangle.

The tie nobody expects

The obvious metric is the number of runs: how many separate seeks does this rectangle become? Over 3,000 random 8×8 boxes on a 64×64 grid:

layoutmean runsworst boxmedian gap between runs
row-major8.00856
Z-order (Morton)14.17222
Hilbert8.00144

Read that again, because it is the opposite of the folklore. Hilbert — the curve people reach for precisely because of its locality — ties the naive layout, and Z-order, the one that actually ships inside geohashes and S2 and half the spatial indexes in the world, is nearly twice as bad. Row-major’s 8.00 is not an average at all: a box of height h is exactly h runs, always, one per row. It cannot do better and it cannot do worse.

So if you count seeks, the fancy curve buys you nothing.

The metric that decides it

No real store seeks between two runs three cells apart. It reads straight through and throws the middle away, because one read of 67 cells costs less than two reads of 32. The slider at the bottom is that policy: read through gaps of g cells or fewer, and watch what each layout does with the permission.

grow-majorZ-orderHilbert
08.0014.178.00
48.006.124.48
168.003.702.84
561.00 (+392 wasted cells)2.20 (+74)2.02 (+58)

Row-major does not move. It cannot move: consecutive rows of the box are exactly one row stride apart — 56 cells for an 8-wide box on a 64-wide grid — so nothing merges until the tolerance can span an entire row of the map, and at that point the single read it finally issues drags in 392 cells nobody asked for, six unwanted for every one wanted. Z-order’s median gap is 2 and Hilbert’s is 4, so both of them are already collapsing at g = 4 while row-major sits at 8.

That is the real result, and the chart at the bottom of the demo is the whole argument in one picture: three curves of reads against tolerance, with the row-major line flat across the entire chart and then falling off a cliff at exactly one row stride. The count was a tie. The distances were not.

The same property from the other side

Switch the question to one sequential read — what part of the map is it? and the demo runs the argument backwards: take one interval of the store and draw the area it covers. Over every possible start of a 64-cell read:

layoutmean of the touched area actually deliveredworst
row-major50.8%50.0%
Z-order47.6%8.3%
Hilbert69.9%53.3%

A 64-cell row-major read is a 64×1 sliver — everything one cell above it is another seek. An aligned Hilbert read is an 8×8 block, and even a badly aligned one never falls below 53%. Z-order’s worst case is the honest smudge here: a read straddling the seam between two top-level quads sprawls over a 16×10 area and delivers 8% of it.

One caveat the piece does not hide: crank the read up to 256 cells and row-major’s mean fill (80.3%) beats Hilbert’s (69.6%), because 256 is four whole rows. Fill is not the same as usefulness — a 64×5 strip is a high-fill read of a shape almost no query wants. The area matters, but so does whether it is square.

What it costs Hilbert

Press find Hilbert’s worst box. On a 64×64 grid the worst 8×8 box sits at (25, 3) and costs 14 runs, against row-major’s guaranteed 8. It straddles the split between two top-level quadrants: the curve leaves the box, crosses the map, and comes back. Hilbert’s average is the best of the three and its tail is worse than the layout that never varies — and nothing in the query tells you which one you drew. That trade is why “use a Hilbert curve” is a design decision and not a free win, and it is why Z-order ships more often despite losing on every locality number here: the Morton index is a bit interleave (a few instructions, invertible, and a prefix of the key is a quadtree cell), while a Hilbert index is a loop over the bits with a rotation at every level and no equally clean prefix story.

Reuse

src/curves.js is framework-free and DOM-free:

Gotchas