Chapter 05 · Filtering, Sorting, and Limiting Results

LIMIT, OFFSET, FETCH, and Pagination Tradeoffs

Return manageable result windows while understanding syntax portability, performance costs, and pagination drift.

Beginner85–105 minutesPagination design + comparison labLast reviewed: August 2026

Learning outcomes

Result limiting is not merely presentation. It creates a contract about which ordered rows are returned, how clients move through a large result, and how concurrent changes affect repeated requests.

01

Use SQLite/PostgreSQL LIMIT and OFFSET correctly with deterministic ORDER BY.

02

Recognize FETCH FIRST/NEXT and SQL Server TOP portability alternatives.

03

Explain the performance and consistency costs of deep offset pagination.

04

Implement keyset pagination with a stable composite sort key.

LIMIT returns a result window

sqlite · first five ordered sales
SELECT    sale_id,    customer_id,    sold_atFROM saleORDER BY    sold_at DESC,    sale_id DESCLIMIT 5;

The query first defines a deterministic sequence, then returns its first five rows. Without ORDER BY, “first” has no durable meaning.

OFFSET skips rows in the ordered result

sqlite · second page with page size three
SELECT    sale_id,    customer_id,    sold_atFROM saleORDER BY    sold_at DESC,    sale_id DESCLIMIT 3 OFFSET 3;

OFFSET 3 discards the first three rows of the ordered result and returns up to three rows after them.

PageLIMITOFFSET
130
233
336
npage size(n - 1) × page size

Syntax varies across major engines

DialectTypical syntaxNotes
SQLiteLIMIT 10 OFFSET 20Also accepts a comma form, but the keyword form is clearer
PostgreSQLLIMIT 10 OFFSET 20 or FETCH FIRST 10 ROWS ONLYSupports standard-style FETCH
MySQLLIMIT 20, 10 or LIMIT 10 OFFSET 20Both forms are common
SQL ServerTOP (10) or OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLYOFFSET/FETCH requires ORDER BY
OracleOFFSET 20 ROWS FETCH NEXT 10 ROWS ONLYModern row-limiting clause
sql · standard-style row limiting
SELECT sale_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCOFFSET 20 ROWSFETCH NEXT 10 ROWS ONLY;
sql server · top syntax
SELECT TOP (10)    sale_id,    sold_atFROM saleORDER BY sold_at DESC, sale_id DESC;

Deterministic order is mandatory for pagination

sql · incomplete ordering
ORDER BY sold_at DESCLIMIT 3 OFFSET 3;

Rows sharing sold_at can exchange positions. A row may appear on two pages or disappear between them even when the data is unchanged.

sql · stable composite ordering
ORDER BY    sold_at DESC,    sale_id DESCLIMIT 3 OFFSET 3;

Offset pagination becomes expensive

Locate or sort qualifying rows
Walk past OFFSET rows
Discard skipped rows
Return the requested page

A large offset can force the DBMS to process many rows that the client never receives. Exact cost depends on indexes, filters, joins, and the execution plan.

sqlite · a deep page still skips prior rows
SELECT sale_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCLIMIT 25 OFFSET 100000;

Offset pagination is often acceptable for small administrative lists and shallow pages. It is less suitable for very large, frequently changing feeds.

Concurrent changes cause pagination drift

Suppose page 1 is fetched, then a new row is inserted at the beginning of the sort order. When page 2 uses the original offset, one previously seen row may shift onto page 2 and another row may be pushed to page 3.

Change between requestsPossible offset effect
New leading row insertedDuplicate row across page boundaries
Leading row deletedA row can be skipped
Sort-key value updatedRow can move to a different page
Tied rows reorderedPage membership can change without a unique tie-breaker

Keyset pagination continues after the last key

Instead of counting and skipping earlier rows, the client sends the final sort key from the previous page.

sqlite · first keyset page
SELECT sale_id, customer_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCLIMIT 3;

Assume the last row returned has sold_at = '2026-08-05 09:00:00' and sale_id = 107.

sqlite · next keyset page for descending order
SELECT sale_id, customer_id, sold_atFROM saleWHERE sold_at < '2026-08-05 09:00:00'   OR (sold_at = '2026-08-05 09:00:00' AND sale_id < 107)ORDER BY sold_at DESC, sale_id DESCLIMIT 3;

The continuation predicate mirrors the composite ordering. For ascending order, the comparison directions reverse.

Row-value comparison can simplify the cursor

sqlite and postgresql · composite keyset predicate
SELECT sale_id, customer_id, sold_atFROM saleWHERE (sold_at, sale_id)    < ('2026-08-05 09:00:00', 107)ORDER BY sold_at DESC, sale_id DESCLIMIT 3;

SQLite and PostgreSQL support row-value comparisons. Portability and NULL behavior require care; the expanded OR form is more universally understandable.

Offset versus keyset pagination

ConcernOffset paginationKeyset pagination
Jump to arbitrary pageStraightforwardNot naturally supported
Deep-page performanceOften degrades as offset growsUsually continues from an indexed key
Concurrent inserts/deletesCan driftMore stable relative to the cursor
ImplementationSimple page numberRequires encoded last-key state
Ordering requirementDeterministic order still requiredDeterministic, cursor-compatible order required
Best fitSmall lists, shallow navigationLarge feeds, timelines, infinite scroll

Count queries are separate work

sqlite · page data and total count are distinct queries
SELECT sale_id, customer_id, sold_atFROM saleWHERE customer_id = 1ORDER BY sold_at DESC, sale_id DESCLIMIT 10 OFFSET 0;SELECT COUNT(*) AS total_rowsFROM saleWHERE customer_id = 1;

A total count may be expensive for large filtered datasets. Decide whether the interface truly needs an exact total, an estimate, or only a “has more” signal.

Fetch one extra row to detect “has more”

sqlite · request page size plus one
-- UI page size is 3; fetch 4 rows.SELECT sale_id, customer_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCLIMIT 4;

If four rows are returned, display the first three and use the fourth only to conclude that another page exists.

Practice lab

  1. Return the three newest sales deterministically.
  2. Return the second offset-based page with page size three.
  3. Write the equivalent standard-style OFFSET/FETCH form.
  4. Continue after the key ('2026-08-05 09:00:00', 107) with keyset pagination.
  5. Return four rows so a three-row interface can detect another page.
sqlite · possible solutions
SELECT sale_id, customer_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCLIMIT 3;SELECT sale_id, customer_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCLIMIT 3 OFFSET 3;-- Standard-style syntax for engines that support it:SELECT sale_id, customer_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCOFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;SELECT sale_id, customer_id, sold_atFROM saleWHERE sold_at < '2026-08-05 09:00:00'   OR (sold_at = '2026-08-05 09:00:00' AND sale_id < 107)ORDER BY sold_at DESC, sale_id DESCLIMIT 3;SELECT sale_id, customer_id, sold_atFROM saleORDER BY sold_at DESC, sale_id DESCLIMIT 4;

Common failures

Limiting without ORDER BY

The selected subset is not stable.

Ordering by a non-unique key

Ties can migrate between pages.

Using deep offsets for large feeds

The server may repeatedly process and discard a large prefix.

Ignoring concurrent change

Offset pages can duplicate or skip rows as the dataset changes.

Exposing raw cursor data carelessly

Applications should validate and often encode cursor state rather than trusting arbitrary client predicates.

Chapter 5 summary

  • WHERE filters rows through predicates that must evaluate TRUE.
  • AND, OR, and NOT require explicit grouping and NULL-aware reasoning.
  • IN, BETWEEN, and LIKE simplify membership, range, and pattern requirements when edge cases are controlled.
  • ORDER BY defines row sequence; a unique tie-breaker makes it deterministic.
  • LIMIT/OFFSET is simple, while keyset pagination scales and resists drift better for large changing feeds.

Chapter 6 introduces functions, NULL-handling expressions, CASE, and clean derived columns.

References

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.