✏️ Explanatory Question

Deep pagination is killing your API — LIMIT 1000000, 20 takes 30 seconds. Why, and how do you fix it?

👁 8 Views
📘 Detailed Answer
🟢 Easy
💡

Answer with Explanation

91

Deep pagination is killing your API — why is LIMIT 1000000, 20 so slow, and how do you fix it?

Level: Very Hard — Every large paginated system hits this wall; keyset pagination is the senior-level answer.

Scenario: Your API paginates a 10-million-row table. Page 1 (LIMIT 0, 20) is instant. But a user jumping to page 50,000 (LIMIT 1000000, 20) makes the API take 30 seconds and spike CPU. The index is there and used. Why is a "deep" page so slow?

The Query & Table

-- posts table: 10 million rows, indexed on (created_at, id)
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 1000000, 20;   -- page 50,001

The cause: LIMIT 1000000, 20 does not skip straight to row 1,000,000. MySQL must read, sort, and discard the first 1,000,000 rows, then return the next 20. The deeper the page, the more rows are scanned and thrown away. The work grows linearly with the offset.

$$ \text{Rows processed} = OFFSET + LIMIT = 1{,}000{,}000 + 20 $$

Why interviewers ask this: OFFSET pagination is the intuitive-but-wrong default. They want to see if you know keyset (seek) pagination — the technique used by Facebook, Twitter, and every "infinite scroll" feed.

OFFSET vs Keyset — Cost by Page Depth

PageOFFSET rows scannedKeyset rows scanned
Page 12020
Page 1,00020,02020
Page 50,0001,000,02020

Keyset pagination reads a constant ~20 rows regardless of depth — it's O(1) vs OFFSET's O(n).

The Fix — Keyset (Seek) Pagination

-- SLOW: OFFSET scans and discards 1M rows
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 1000000, 20;

-- FAST: remember the LAST row of the previous page, then seek past it
-- Suppose the last row shown was: created_at='2026-03-15 10:00:00', id=8423122
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ('2026-03-15 10:00:00', 8423122)  -- seek condition
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- Uses the index to jump directly; reads only 20 rows
How it works: Instead of "skip N rows," keyset pagination says "give me rows after this specific value." Because the WHERE uses the indexed sort columns, MySQL seeks directly to the position — no scanning of skipped rows. The tuple comparison (created_at, id) < (...) handles ties correctly.

OFFSET vs Keyset Trade-offs

OFFSET Pagination

  • Can jump to any page number
  • Gets slower the deeper you go
  • Rows can shift if data changes mid-paging

Keyset Pagination

  • Constant speed at any depth
  • Stable — no duplicate/skipped rows
  • Only "next/prev", no random page jump

Alternative: Deferred Join (when you need OFFSET-style)

-- Fetch only IDs via the covering index (cheap), then join for full rows
SELECT p.id, p.title, p.created_at
FROM posts p
JOIN (
    SELECT id FROM posts
    ORDER BY created_at DESC, id DESC
    LIMIT 1000000, 20
) AS keys ON p.id = keys.id
ORDER BY p.created_at DESC, p.id DESC;
-- The inner query scans a narrow index; the outer fetches just 20 full rows
Interviewer follow-up: "The UI needs 'Go to page 50,000' — keyset only does next/prev. What now?" → Options: (1) cap deep pagination (Google stops at ~page 100), (2) use the deferred join trick above, or (3) redesign the UX to search/filter instead of deep page-jumping — nobody realistically browses to page 50,000.