Efficient Pagination notes

Efficient Pagination notes

Offset pagination and its problems

You may have seen requests looking like https://api.example.com/posts?limit=10&offset=50. This is offset pagination. We are saying skip 50 rows and give me the next 10.

SELECT * FROM posts
LIMIT 10 OFFSET 50  -- "skip 50, give me next 10"

Problems with this approach:

  • Database must scan through all 50 rows to skip them

  • Gets slower as offset increases (page 1000 means scanning 1000 rows!)

  • Can miss/duplicate records if new posts are added while paginating

  • Still has to sort everything first

PS. It's still useful when you want to show page results and items wouldn't update too often e.g. booking system search results. Because the experience of having pages here is more natural and desired to users.

Cursor pagination and its benefits

Cursor pagination is when you provide a cursor parameter to the API. This cursor is the last item seen in the previous page (id of the last item). The API then uses this cursor to figure out where to start the next page from. Stripe for example uses this e.g. with starting_after parameter.

Looks like this: https://api.example.com/posts?starting_after=1000, 1000 being the id of the last item in the previous page.

SELECT * FROM posts
WHERE id < 1000    -- Last ID user saw
ORDER BY id DESC   -- Newest first (5,4,3,2,1)
LIMIT 10;

Why it's better:

  • Uses PRIMARY KEY index (comes free with table)

  • Can jump directly to ID 1000 in index

  • Consistent performance regardless of page depth

  • No missed/duplicated records

  • Works great with AUTO_INCREMENT ids since they're:

    • Monotonic (always increasing)

    • Map to creation order

    • Already indexed

Response example:

// Request
GET /api/posts?limit=10  // First page
GET /api/posts?cursor=1000&limit=10  // Next pages (1000 = last seen id)

// Response
{
  "posts": [...],
  "hasMore": true,
  "nextCursor": "990"  // ID of last item in this page
}

990 because sorted DESC. Larger ids would mean newer posts.

Ordering

  • DESC = highest to lowest (5,4,3,2,1) = newest first

  • ASC = lowest to highest (1,2,3,4,5) = oldest first

How is sorting efficient?

The key is that we're using the PRIMARY KEY index, which is already sorted!

Think of an index like a pre-sorted phone book:

PRIMARY KEY index is already stored like:
id: 1 -> data
id: 2 -> data
id: 3 -> data
id: 4 -> data
id: 5 -> data

When we say:

WHERE id < 1000
ORDER BY id DESC

The database:

  1. Goes to index (already sorted!)

  2. Finds position of id 1000

  3. Walks backwards through the index (for DESC)

  4. Takes 10 items