SQL Query for Pagination — LIMIT, OFFSET, and Keyset Pagination

Almost every list view in an application — search results, product catalogs, order history — needs pagination. SQL makes basic pagination easy with LIMIT and OFFSET, but that same simplicity becomes a performance trap once a table grows into the millions of rows. This guide covers the standard LIMIT/OFFSET syntax, explains exactly why OFFSET slows down at scale, and shows the keyset pagination pattern that fixes it.

Basic Pagination with LIMIT and OFFSET

LIMIT caps the number of rows returned, and OFFSET skips a number of rows before starting to return results. Together they implement "page N of the results":

-- MySQL / PostgreSQL: page 1, 20 rows per page
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

-- Page 2 (skip the first 20 rows)
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;

-- Page 3 (skip the first 40 rows)
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;

The general formula for page p with pageSize rows is:

LIMIT pageSize OFFSET (p - 1) * pageSize

An ORDER BY clause is required for reliable pagination — without it, the database gives no guarantee that rows come back in the same order across requests, and pages could show duplicate or missing rows.

Why OFFSET Gets Slow on Large Tables

OFFSET is deceptively expensive. The database cannot jump straight to row 500,000 — it has to walk through and discard every row before that position, every single time you request that page:

  • Page 1 (OFFSET 0) — fast, no rows to skip
  • Page 1,000 (OFFSET 20000) — scans and discards 20,000 rows before returning 20
  • Page 25,000 (OFFSET 500000) — scans and discards 500,000 rows before returning 20

The deeper the page, the more wasted work — query time grows roughly linearly with the offset value, even though the result set size never changes. On a table with millions of rows, deep pages can take seconds instead of milliseconds, and the cost lands on every single page request as users click "next."

Keyset (Cursor-Based) Pagination

Keyset pagination replaces OFFSET with a WHERE condition based on the last row the client already saw. Instead of counting rows, it uses an indexed column to jump directly to the right position:

-- First page
SELECT id, title, created_at
FROM articles
ORDER BY id ASC
LIMIT 20;

-- Client remembers the last id from the previous page, e.g. 20
-- Next page: no OFFSET needed
SELECT id, title, created_at
FROM articles
WHERE id > 20
ORDER BY id ASC
LIMIT 20;

-- Next page after that, last id was 40
SELECT id, title, created_at
FROM articles
WHERE id > 40
ORDER BY id ASC
LIMIT 20;

Because id is indexed (typically the primary key), the database seeks directly to WHERE id > 40 using the index instead of scanning from the start. Page 25,000 costs the same as page 1 — performance stays flat regardless of how deep you page.

For pagination ordered by a non-unique column like created_at, add the primary key as a tiebreaker to keep the cursor stable when timestamps repeat:

-- Cursor is (last_created_at, last_id) from the previous page
SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < ('2026-07-01 10:15:00', 583)
ORDER BY created_at DESC, id DESC
LIMIT 20;

LIMIT/OFFSET vs Keyset Pagination — When to Use Each

  1. Use LIMIT/OFFSET when the table is small to medium sized, or users need to jump to an arbitrary page number (like "go to page 47")
  2. Use keyset pagination for large tables, infinite scroll, or API feeds where users only move forward/backward sequentially
  3. Keyset limitation — you cannot easily jump to an arbitrary page number, since there is no concept of "skip N rows"
  4. Hybrid approach — some products show page numbers using an approximate/cached count, but fetch actual rows with keyset pagination underneath

Practical Tips for Reliable Pagination

  • Always index the column(s) used in ORDER BY for pagination — without an index, both LIMIT/OFFSET and keyset queries force a full sort
  • Avoid SELECT COUNT(*) on every page load for large tables — cache the total count or estimate it periodically
  • Use a stable, unique tiebreaker column (usually the primary key) so rows never get skipped or duplicated across pages
  • MySQL and PostgreSQL both use LIMIT ... OFFSET ...; older SQL Server uses OFFSET ... ROWS FETCH NEXT ... ROWS ONLY instead

Frequently Asked Questions

How do I paginate SQL query results?

Use LIMIT to control how many rows to return and OFFSET to skip a number of rows before returning results, for example LIMIT 20 OFFSET 40 to get page 3 of 20 rows per page. For large tables, keyset pagination using WHERE id > last_seen_id is faster than OFFSET.

Why is OFFSET slow on large tables?

OFFSET does not skip rows for free — the database still has to scan and discard every row before the offset position. On page 10,000 with 50 rows per page, the database scans and throws away 500,000 rows before returning the 50 you actually want, which gets slower as the offset grows.

What is keyset pagination?

Keyset pagination, also called cursor-based pagination, fetches the next page using a WHERE condition on the last row seen, such as WHERE id > last_seen_id ORDER BY id LIMIT 20, instead of counting through OFFSET rows. It uses an index to jump directly to the right spot, so performance stays constant regardless of page number.

Try the Free AI SQL Query Builder

Describe the paginated query you need in plain English and get a ready-to-run query instantly.

Related articles