How do LIMIT and OFFSET work together, and what goes wrong when you use them for pagination?
LIMIT caps the number of rows returned. OFFSET skips that many rows before the cap is applied. Together they give you a page: LIMIT 2 OFFSET 2 is the third and fourth row of the sorted result. The ORDER BY is not optional in practice — without it the engine is free to return rows in any order, so the pages are not stable.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL,
city VARCHAR(80) NOT NULL
);
INSERT INTO customers (id, name, city) VALUES
(1, 'Ada Lovelace', 'London'),
(2, 'Grace Hopper', 'New York'),
(3, 'Alan Turing', 'London'),
(4, 'Joan Clarke', 'London'),
(5, 'Katherine Johnson', 'Hampton');
-- Page 2 with a page size of 2: rows 3 and 4.
SELECT id, name
FROM customers
ORDER BY id
LIMIT 2 OFFSET 2;
-- Page 3: row 5.
SELECT id, name
FROM customers
ORDER BY id
LIMIT 2 OFFSET 4;The first query returns Alan Turing and Joan Clarke, the second returns Katherine Johnson. Note the ordering of the two keywords: LIMIT comes first, OFFSET second, in SQLite, PostgreSQL and MySQL. SQL Server spells it OFFSET n ROWS FETCH NEXT m ROWS ONLY and insists on an ORDER BY.
Now the problem. OFFSET makes the engine produce and discard the first n rows, so page 100 has to walk through a hundred rows before it can start — the cost grows linearly with the page number, and a deep page can be slow enough to time out. Worse, it is wrong under concurrent writes. If a customer is deleted between two requests, every row after the deletion shifts up by one, and one row is skipped entirely; if a row is inserted at the top, one row is repeated. The user sees duplicates and gaps while paging.
The other problem is that OFFSET cannot be pushed into an index when the sort key is not the same as the offset key, so even a moderately deep page on a large table can be expensive.
The fix is keyset pagination: instead of saying "skip 100 rows", you say "give me rows after the last id I saw". The next query continues exactly where the previous one stopped, costs the same regardless of page number, and is immune to insertions and deletions above the cursor. The cost is that you need a stable, indexed sort key and that you cannot jump to an arbitrary page number.