Pagination
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Offset vs keyset pagination: the 60-second story

    Offset vs keyset pagination

    Page 1 takes 0.013 ms. Page 50,001 takes 592 ms.

    Same query, same 20 rows returned. To find them, the database first produced the million rows before them.

    OFFSET counts from the top. Keyset opens at a bookmark.

    A B-tree stores keys, not positions, so OFFSET 100,000 walks 100,020 entries. “After this row” is one seek plus 20.

    OFFSET only costs speed?

    Bad: Only deep pages hurt

    A new order slides every row down, so page 2 repeats O7.

    Good: Positions move

    Keyset’s cursor is a value, not a position. Inserts above it change nothing.

    The keyset query, line by line

    1. Remember the last row’s (created_at, id)
    2. WHERE (created_at, id) < (last_ts, last_id)
    3. ORDER BY created_at DESC, id DESC
    4. LIMIT 21: a 21st row means has_more

    Three orders share created_at 19:30:00, and the page boundary falls between them.

    The trap

    With < you skip two; with <= you repeat one. End the sort with a unique column, usually the id.

    Check EXPLAIN. Each engine seeks on a different spelling.

    PostgreSQL seeks on the row value, MySQL on the expanded OR. The OR form with a leading created_at bound seeks on both.

    501×

    more rows shipped by OFFSET across 16 shards

    No shard knows its rows’ global position, so for page 501 each sends its top 10,020: 160,320 rows. Keyset ships 320.

    What keyset gives up

    1. No page 37: jump to a date instead
    2. No cheap total: show a capped “1,000+”
    3. NULL sort keys vanish: make the column NOT NULL
    4. Mutable keys move: page exports by id

    Remember

    OFFSET reads every row it skips. Keyset seeks with a unique tie-breaker and a matching index, 20 rows at any depth.