Databases 9 min read

Why Large‑Offset LIMIT Is Slow in MySQL and How to Speed It Up

The article explains why a MySQL query with a large LIMIT offset becomes extremely slow, demonstrates the underlying I/O behavior, and shows how rewriting the query with a sub‑query that fetches primary keys first can cut execution time from seconds to milliseconds while also reducing buffer‑pool pollution.

dbaplus Community
dbaplus Community
dbaplus Community
Why Large‑Offset LIMIT Is Slow in MySQL and How to Speed It Up

When querying a non‑sharded MySQL table of about 9.5 million rows, a plain SELECT … LIMIT 0,10 took 16 seconds, but after moving the filter into a sub‑query that first selects only the primary‑key IDs and then joins the remaining columns, the same request finished in 347 ms.

Operation and Principle

The optimization moves the filtering condition into a sub‑query that returns only the primary‑key values, then joins those IDs to the main table. This reduces the number of row‑lookups ("回表"), leveraging delayed joins or sub‑queries to handle many pagination scenarios efficiently.

SQL Before and After

-- Original SQL
SELECT <em>various fields</em>
FROM `table_name`
WHERE <em>conditions</em>
LIMIT 0,10;
-- Optimized SQL
SELECT <em>various fields</em>
FROM `table_name` main_table
RIGHT JOIN (
    SELECT <em>primary key only</em>
    FROM `table_name`
    WHERE <em>conditions</em>
    LIMIT 0,10
) temp_table ON temp_table.id = main_table.id;

Test Environment

MySQL version 5.7.17. Table test schema:

+--------+---------------------+------+-----+---------+----------------+
| Field  | Type                | Null | Key | Default | Extra          |
+--------+---------------------+------+-----+---------+----------------+
| id     | bigint(20) unsigned| NO   | PRI | NULL    | auto_increment |
| val    | int(10) unsigned   | NO   | MUL | 0       |                |
| source | int(10) unsigned   | NO   |     | 0       |                |
+--------+---------------------+------+-----+---------+----------------+

The table contains 5,242,882 rows.

Performance Comparison

Query with a large offset: SELECT * FROM test WHERE val=4 LIMIT 300000,5; Execution time: ~16 seconds.

Rewritten using an inner join on the IDs:

SELECT * FROM test a
INNER JOIN (
    SELECT id FROM test WHERE val=4 LIMIT 300000,5
) b ON a.id = b.id;

Execution time: ~0.38 seconds, a dramatic improvement.

Why the Original Query Is Slow

MySQL first scans the index leaf nodes to locate the matching rows, then for each leaf node it fetches the full row from the clustered index. With an offset of 300,000, it must read 300,005 index entries and the same number of clustered rows, only to discard the first 300,000 rows. This results in massive random I/O.

Even if the index is used, the engine still performs 300,005 lookups on the clustered index, loading many pages that never appear in the final result set.

Buffer‑Pool Evidence

By checking INNODB_BUFFER_PAGE, the slow query loaded 4,098 data pages and 208 index pages into the buffer pool, whereas the optimized query loaded only 5 data pages and 390 index pages, confirming far fewer I/O operations.

-- After slow query
SELECT index_name, COUNT(*) FROM information_schema.INNODB_BUFFER_PAGE
WHERE INDEX_NAME IN ('val','primary') AND TABLE_NAME LIKE '%test%'
GROUP BY index_name;
+----------+----------+
| index_name | count(*) |
+----------+----------+
| PRIMARY   | 4098    |
| val       | 208     |
+----------+----------+

-- After fast query
SELECT index_name, COUNT(*) FROM information_schema.INNODB_BUFFER_PAGE
WHERE INDEX_NAME IN ('val','primary') AND TABLE_NAME LIKE '%test%'
GROUP BY index_name;
+----------+----------+
| index_name | count(*) |
+----------+----------+
| PRIMARY   | 5        |
| val       | 390      |
+----------+----------+

To ensure a clean test, the buffer pool was cleared by shutting down MySQL and restarting it.

Conclusion

Using a large OFFSET with LIMIT forces MySQL to read and discard a huge number of rows, causing heavy random I/O and polluting the buffer pool. Rewriting the query to first fetch the primary‑key IDs and then join the remaining columns dramatically reduces I/O, execution time, and buffer‑pool usage.

References

https://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/

https://dev.mysql.com/doc/refman/5.7/en/innodb-information-schema-buffer-pool-tables.html

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

query optimizationInnoDBmysqlLIMITbuffer poolSubquery
dbaplus Community
Written by

dbaplus Community

Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.