SQL LIMIT & OFFSET: How to Paginate Query Results Step by Step

Module 13

SQL LIMIT & OFFSET: How to Paginate Query Results Step by Step

Master row pagination, predictable sorting with ORDER BY, handling tied values, and building efficient page queries in MySQL.

SELECT product_name, price FROM products ORDER BY price DESC LIMIT 10 OFFSET 20;

Imagine loading an online store with 50,000 items. Fetching all 50,000 products over the network in a single query would overwhelm the browser, waste memory, and slow down your application. To solve this, web applications break large datasets into manageable chunks—a process known as pagination.

In MySQL, pagination relies on two critical mechanisms: LIMIT (which controls how many rows to return) and OFFSET (which controls how many rows to skip). However, pagination only works reliably when paired with explicit sorting via ORDER BY. Without deterministic row ordering, your paginated results can skip rows or present duplicate items across pages.

1. Try This First

Before diving into pagination formulas, run these two basic queries against a sample table to see how row ordering behaves.

First, execute a plain SELECT without an explicit order:

SELECT product_name, price
FROM products;

Next, add an explicit ORDER BY clause:

SELECT product_name, price
FROM products
ORDER BY price ASC;

What to compare: In the first query, MySQL returns rows in whatever physical sequence it retrieved them during table scanning. In the second query, MySQL guarantees that the cheapest products appear at the top, sorted strictly by price in ascending order.

What I noticed while testing this

When two products have the exact same price, sorting only by price leaves the relative order of those tied rows undefined. MySQL may return them in different sequence on repeated query runs or after table maintenance.

2. Row Order Without ORDER BY

When a SELECT query does not contain an ORDER BY clause, you should never assume rows will return in the order they were inserted or by primary key.

Under the hood, the relational database engine selects an execution plan based on available indexes, cached memory, and table statistics. If MySQL decides to perform a parallel scan, read from an index, or merge join operations, the resulting row sequence can change unpredictably.

If output sequence matters to your user interface, report generation, or paginated API response, you must make that requirement explicit by writing an ORDER BY clause.

3. Ascending and Descending Sort

SQL provides two directional keywords for ordering:

  • ASC: Ascending order (lowest to highest, A to Z, oldest to newest). This is the default.
  • DESC: Descending order (highest to lowest, Z to A, newest to oldest).

To display products from highest price to lowest price, use DESC:

SELECT product_name, price
FROM products
ORDER BY price DESC;

Although ASC is implicit when no direction is specified, writing ASC explicitly in production code makes your code intent clearer to other developers.

4. Multiple-Column Sorting

You can sort query results by multiple expressions separated by commas. MySQL evaluates these sorting expressions sequentially from left to right.

SELECT product_name, category, price
FROM products
ORDER BY category ASC, price DESC;

Here is how MySQL processes this multi-column sort:

  • category ASC is the primary sorting expression. All rows are grouped alphabetically by category.
  • price DESC is the secondary sorting expression. It is evaluated only when category values in adjacent rows are identical.

5. Deterministic Tie-Breaking

Understanding tie-breaking is critical when implementing pagination with LIMIT and OFFSET.

Consider this query:

SELECT product_name, price
FROM products
ORDER BY price DESC;

If five products share the exact price of $99.99, MySQL has no rule for ordering those five tied items. If Page 1 ends in the middle of those tied items and Page 2 requests the next set of rows, tied rows might shift across pages, causing users to see duplicate items or miss items entirely.

To guarantee deterministic ordering across paginated requests, add a unique column (such as the primary key id) as a tie-breaker:

SELECT product_name, price, id
FROM products
ORDER BY price DESC, id ASC;
What I noticed while testing this

When you add id ASC as a second sort expression, every row in the result set gets a distinct location. Tied rows maintain a predictable, stable sequence across different pages.

6. Paginating with LIMIT & OFFSET

In MySQL, LIMIT specifies the maximum number of rows to return, while OFFSET specifies how many rows to skip before returning data.

LIMIT count

Restricts the query output to a maximum of count rows.

OFFSET skip

Skips the first skip rows in the sorted result set.

To calculate OFFSET in application code for a specific page number, use this formula:

OFFSET = (page_number - 1) * page_size

Page 1 (First 5 Products)

SELECT product_name, price
FROM products
ORDER BY price DESC, id ASC
LIMIT 5 OFFSET 0;

Page 2 (Next 5 Products)

SELECT product_name, price
FROM products
ORDER BY price DESC, id ASC
LIMIT 5 OFFSET 5;

Combining Filtering with Pagination

When combining filtering (WHERE), sorting (ORDER BY), and pagination (LIMIT / OFFSET), SQL requires a specific clause sequence:

SELECT product_name, price
FROM products
WHERE category = 'Laptop'
ORDER BY price DESC, id ASC
LIMIT 5 OFFSET 0;

In this execution flow, WHERE filters the table rows first, ORDER BY sorts the matching rows, and LIMIT / OFFSET extracts the requested page window.

7. NULL Values Ordering

How missing or NULL data is ordered depends on the database engine:

  • In MySQL: NULL values are treated as lower than any non-NULL value. Thus, in ascending sort (ASC), NULL values appear first. In descending sort (DESC), NULL values appear last.
  • In PostgreSQL: PostgreSQL provides explicit control using NULLS FIRST and NULLS LAST.

Here is how you explicitly control NULL placement in PostgreSQL:

-- PostgreSQL Syntax Example
SELECT product_name, price
FROM products
ORDER BY price ASC NULLS LAST;

If you need to force NULL values to the end in MySQL, you can sort using a conditional expression such as ORDER BY (price IS NULL) ASC, price ASC.

Author's Note

Always verify NULL handling when switching database engines. Assuming MySQL and PostgreSQL handle missing values identically is a common source of unexpected UI sorting bugs.

8. Inspecting Queries with EXPLAIN

You can inspect how MySQL executes your sorting and pagination queries by prepending EXPLAIN to your query:

EXPLAIN
SELECT product_name, price
FROM products
ORDER BY price DESC;

Depending on the query, indexes, filters, and table volume, MySQL may need an additional sorting phase. EXPLAIN helps you inspect what the database optimizer actually chose.

If the Extra column in MySQL displays Using filesort, it indicates that MySQL could not fulfill the requested sort order directly from an index and had to perform an extra sorting step in memory or on disk.

What I noticed while testing this

When MySQL cannot satisfy the requested order using a suitable index, EXPLAIN may show Using filesort. Seeing Using filesort does not automatically mean a query is broken, but on large tables, creating a composite index matching your ORDER BY columns can eliminate this extra sorting phase.

9. My Practical Rules for ORDER BY & Pagination

  • Write explicit ORDER BY clauses: If row order matters to your application, always write ORDER BY. Never rely on default insertion sequence.
  • Include a unique tie-breaker: Add a unique column (such as primary key id) to your sort expression for stable, leak-free pagination.
  • Be explicit with ASC / DESC: Write sort directions clearly to avoid ambiguity.
  • Inspect execution plans before indexing: Run EXPLAIN to see if your query triggers expensive sorting passes before creating new indexes.
  • Remember index overhead: Indexes take disk space and slow down INSERT, UPDATE, and DELETE statements. Avoid adding indexes solely because an ORDER BY clause exists.

10. Step-by-Step Practical Setup

Follow these practical steps in your MySQL environment (such as MySQL Workbench, phpMyAdmin, or MySQL CLI) to build and paginate a standard products table.

1
Create the test table: Define a table with primary key, category, and price columns.
CREATE TABLE products (
  id INT PRIMARY KEY AUTO_INCREMENT,
  product_name VARCHAR(100) NOT NULL,
  category VARCHAR(50) NOT NULL,
  price DECIMAL(10, 2) NULL
);
2
Insert sample data: Add rows including tied prices and NULL entries to observe real sorting mechanics.
INSERT INTO products (product_name, category, price) VALUES
('Pro Laptop', 'Electronics', 1200.00),
('Wireless Mouse', 'Electronics', 25.00),
('Mechanical Keyboard', 'Electronics', 85.00),
('HD Monitor', 'Electronics', 220.00),
('USB-C Cable', 'Electronics', 12.50),
('Desk Chair', 'Furniture', 150.00),
('Standing Desk', 'Furniture', 450.00),
('Desk Lamp', 'Furniture', 25.00),
('Prototype Item', 'Electronics', NULL);
3
Run Page 1 Query (3 rows per page): Fetch the first 3 products ordered by price descending.
SELECT id, product_name, price
FROM products
ORDER BY price DESC, id ASC
LIMIT 3 OFFSET 0;
4
Run Page 2 Query: Skip 3 rows and fetch the next 3 rows.
SELECT id, product_name, price
FROM products
ORDER BY price DESC, id ASC
LIMIT 3 OFFSET 3;
5
Run Page 3 Query: Fetch the remaining items. Note how NULL handles at the end of DESC sort.
SELECT id, product_name, price
FROM products
ORDER BY price DESC, id ASC
LIMIT 3 OFFSET 6;

11. Common Pagination Problems & Fixes

Problem 1: Duplicate rows appear on page transitions

Users report seeing the same product on both Page 1 and Page 2 when clicking through search results.

Solution

Your query sorts by a non-unique column (like price or created_at) without a tie-breaker. Append a unique column like primary key id ASC to guarantee unique deterministic ordering: ORDER BY price DESC, id ASC.

Problem 2: Performance degrades on high OFFSET values (Deep Pagination)

Query response time becomes slow when navigating to higher page numbers (e.g., OFFSET 100000).

Solution

With high offsets, MySQL must still scan and sort all preceding rows before discarding them. For large datasets, consider keyset pagination (seeking by last seen ID): WHERE id > 100000 ORDER BY id ASC LIMIT 20.

12. Homework Exercises

Practice Exercises

  1. Write a query to retrieve the top 3 most expensive items in the Electronics category.
  2. Calculate the correct OFFSET value required to view Page 4 when each page displays 15 items.
  3. Write a query that sorts products by category ascending, then by price ascending, using id ASC as the tie-breaker, returning rows 6 through 10.
  4. Run EXPLAIN on a multi-column sorting query in MySQL and check whether Using filesort appears in the execution plan.

13. Live SQL Playground

Test pagination and sorting directly in your browser using SQLite WebAssembly sandbox.

Browser SQL Sandbox

Runs client-side in SQLite syntax.

Click "Run Query" to execute SQL...

14. Module 13 Quiz

Test your knowledge of SQL LIMIT, OFFSET, and ORDER BY sorting mechanics.

1. What occurs if you run a SELECT query without an ORDER BY clause?

2. In MySQL, what is the default sort order if neither ASC nor DESC is specified in ORDER BY?

3. Which SQL clause limits the maximum count of returned records in MySQL?

4. How do you skip the first 20 records and return the next 10 records in MySQL?

5. Why is deterministic tie-breaking critical when paginating query results?

6. How does MySQL treat NULL values during ascending order sorting (ORDER BY col ASC)?

7. Which formula calculates correct OFFSET for page_number (1-indexed) and page_size?

8. In MySQL EXPLAIN output, what does "Using filesort" indicate?

9. Which PostgreSQL clause explicitly places NULL values at the end during ascending sorts?

10. What is the correct syntax order for SQL queries combining filtering, sorting, and pagination?

15. Frequently Asked Questions

1. Is LIMIT OFFSET part of standard ANSI SQL?

LIMIT and OFFSET are standard in MySQL, PostgreSQL, and SQLite. ANSI SQL standard defines FETCH FIRST n ROWS ONLY and OFFSET n ROWS, which SQL Server and Oracle support.

2. What happens if OFFSET exceeds the total number of rows in the table?

MySQL returns an empty result set (0 rows) without raising an error.

3. Can I use OFFSET without LIMIT in MySQL?

In MySQL, OFFSET requires a LIMIT clause. To skip 10 rows and return all remaining rows, write a large limit: LIMIT 18446744073709551615 OFFSET 10.

4. Why does deep pagination with high OFFSET get slow?

For OFFSET 100000, MySQL must read and sort 100,000 rows first before returning rows 100,001 to 100,010. To optimize, use keyset pagination (seeking by indexed cursor).

5. Does ORDER BY execute before or after LIMIT?

ORDER BY processes before LIMIT. MySQL sorts all matching rows first, then applies the limit and offset boundary.

6. Should I add an index to every column used in ORDER BY?

No. Indexes take storage and add maintenance cost on writes. Only add indexes for high-traffic queries where EXPLAIN shows performance bottlenecks.

16. Official References

Related SQL Lessons

Related Practice

Reinforce your query pagination and sorting skills with these practical database tasks:

  • Build a paginated SQL query for a blog system returning 10 articles per page, sorted by publish date descending with primary key tie-breaker.
  • Experiment with `EXPLAIN` output on a query before and after adding a composite index on `(category, price)`.
  • Write a pagination query handling missing (`NULL`) discount codes without dropping valid rows.
  • Convert a `LIMIT OFFSET` query into a keyset query using `WHERE id > last_seen_id` to evaluate performance differences.

Post a Comment

0 Comments