Menu
Try in Playground

SQLite LIMIT and OFFSET: Paginate and Slice Result Sets

How LIMIT and OFFSET work in SQLite — capping rows, skipping rows, paginating safely, and the performance trap to avoid on large tables.

This page includes runnable editors — edit, run, and see output instantly.

LIMIT Caps the Number of Rows

LIMIT is the simplest knob in SQL: it tells SQLite "give me at most this many rows." Stick it at the end of a SELECT and you'll get back up to that many results — no more, possibly fewer if the table doesn't have enough rows to satisfy it.

You get the first three rows back. Which three, exactly? That's the catch — without an ORDER BY, SQLite picks whatever order it finds convenient. Today it might be insertion order; tomorrow, after an update or an index change, it might not. LIMIT on its own is fine for "show me a sample," but the moment the order matters, you need to make it explicit.

OFFSET Skips Rows From the Front

Pair LIMIT with OFFSET and you can ask for a slice from the middle of the result set. OFFSET k discards the first k rows; LIMIT n then returns up to n rows from what's left.

That's "skip two rows, return the next two" — rows 3 and 4 of the ordered result. The mental model: WHERE filters, ORDER BY sorts, OFFSET skips, LIMIT caps. They run in that order, and they all matter.

Pagination Always Needs ORDER BY

The most common use of LIMIT and OFFSET is pagination — splitting a long list into pages of, say, 20 rows each. Page 1 is LIMIT 20 OFFSET 0, page 2 is LIMIT 20 OFFSET 20, and so on.

Two things to notice. First, the ORDER BY is non-negotiable — without it, "page 2" has no defined meaning and rows can shuffle between page loads. Second, the sort key includes id as a tiebreaker. If two posts share a created_at, you need a unique column to give them a deterministic order, otherwise their positions can swap and a row can leak across pages.

Rule of thumb: order by something unique, or order by your sort column plus a unique tiebreaker.

A Shorthand: LIMIT n, m

SQLite supports an older comma syntax for backwards compatibility with MySQL: LIMIT offset, count. It means the same as LIMIT count OFFSET offset, but the order is reversed and easy to misread.

-- These two are equivalent:
SELECT * FROM books LIMIT 10 OFFSET 20;
SELECT * FROM books LIMIT 20, 10;     -- offset first, then count

The second form is concise, but it bites people who expect the first number to be the count. Stick with LIMIT n OFFSET k — it's explicit and reads left to right.

OFFSET Without LIMIT: the LIMIT -1 Trick

OFFSET isn't valid on its own — SQLite's grammar requires it to follow a LIMIT. So how do you say "skip the first 10 rows and give me everything after"? The convention is LIMIT -1, which SQLite reads as "no upper bound."

Any negative LIMIT does the same thing, but -1 is the established idiom. You'll mostly see this in scripts that page through a result and want a "give me the rest" query for the final batch.

The OFFSET Performance Trap

Here's the thing nobody mentions until you hit it: OFFSET doesn't make SQLite skip work, it makes SQLite skip output. To return rows 10,001 through 10,020, the engine still walks past the first ten thousand rows internally before it starts emitting results. Small offsets are free; offsets in the tens or hundreds of thousands get noticeably slow.

For deep pagination, the standard fix is keyset pagination: instead of "skip N rows," remember the last row's sort key and ask for "rows after this one."

Each page does an indexed lookup instead of scanning past everything that came before. The trade-off: you can't jump to "page 47" — only forward through the data. For infinite-scroll feeds and API cursors, that's exactly what you want.

OFFSET-based pagination is fine for admin tables and small result sets. For anything that grows without bound, reach for keyset pagination.

A Worked Example

Putting it all together — a paginated query with filtering, sorting, and a deterministic tiebreaker:

Filter down to office products, sort by price ascending with name as the tiebreaker, take the first two. Change OFFSET 0 to OFFSET 2 for page 2. The query is short, but every clause earns its keep.

Next: DISTINCT

LIMIT controls how many rows come back; DISTINCT controls whether duplicates come back at all. It's the next clause in the SELECT toolkit, and it's deceptively easy to misuse — that's the next page.

Frequently Asked Questions

What does LIMIT do in SQLite?

LIMIT n caps the number of rows a SELECT returns to at most n. It runs after WHERE, GROUP BY, and ORDER BY, so you're limiting the final result set, not the rows the query scans. SELECT * FROM users LIMIT 10 returns up to ten rows.

How does OFFSET work with LIMIT in SQLite?

OFFSET k skips the first k rows of the result before LIMIT starts counting. So LIMIT 10 OFFSET 20 returns rows 21 through 30. SQLite still has to walk through those skipped rows internally, which is why large offsets get slow.

Can you use OFFSET without LIMIT in SQLite?

Not directly — OFFSET is only valid as part of a LIMIT clause. The workaround is LIMIT -1 OFFSET k, where -1 means 'no upper bound,' so SQLite skips k rows and returns everything after. It's a quirk worth remembering.

Why do paginated queries need ORDER BY?

Without ORDER BY, SQLite is free to return rows in any order it likes, and that order can change between queries. Pagination then breaks — the same row might appear on page 1 and page 3, or vanish entirely. Always pair LIMIT/OFFSET with an ORDER BY on a stable, unique column.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED