mysql中分页查询优化与LIMIT子句的使用

来源:这里教程网 时间:2026-02-28 20:39:14 作者:

为什么 OFFSET 越大,LIMIT 查询越慢

MySQL 的

LIMIT offset, size
offset
很大时(比如
LIMIT 1000000, 20
),实际仍会扫描前 1000020 行,再丢弃前 1000000 行——这不是跳过,而是「读取后过滤」。InnoDB 没有跳转到第 N 行的物理索引能力,尤其当排序字段无索引或使用
ORDER BY RAND()
时,性能断崖式下跌。

全表扫描 + 文件排序(
Using filesort
)会频繁触发磁盘 I/O
如果
WHERE
条件未命中索引,MySQL 可能先回表查主键,再排序,再跳 offset,三重开销
EXPLAIN
rows
值远大于
limit
,就是危险信号

用游标分页(Cursor-based Pagination)替代 OFFSET

适用于按时间、ID 等单调递增字段排序的场景,核心是「记住上一页最后一条的值」,下一页直接

WHERE id > last_id LIMIT 20
。它不依赖行号,避免深度偏移。

必须确保排序字段有索引(如
INDEX (created_at, id)
),且查询条件能命中该索引最左前缀
不能跳页(比如从第 1 页直接跳第 100 页),但对 Feed 流、日志列表等滚动加载足够高效 注意边界:若排序字段存在重复值(如多个记录
created_at
相同),需加入二级排序字段(如
id
)保证结果稳定
SELECT * FROM orders 
WHERE created_at > '2024-01-01 12:00:00' 
  AND (created_at, id) > ('2024-01-01 12:00:00', 55678)
ORDER BY created_at, id 
LIMIT 20;

LIMIT 配合延迟关联(Deferred Join)减少回表

当需要分页返回宽表字段(如含 TEXT、BLOB),但排序只依赖少量字段时,先用

LIMIT
查出主键,再用主键二次 JOIN 获取完整数据,可大幅减少临时表和回表量。

原始写法:
SELECT * FROM users WHERE status=1 ORDER BY created_at DESC LIMIT 100000, 20
—— 回表 100020 次
优化后:先查 ID,再 JOIN,只回表 20 次 要求
status
created_at
组成联合索引,否则子查询仍慢
SELECT u.* FROM users u
INNER JOIN (
    SELECT id FROM users 
    WHERE status = 1 
    ORDER BY created_at DESC 
    LIMIT 100000, 20
) AS t ON u.id = t.id;

覆盖索引 + FORCE INDEX 防止执行计划漂移

即使写了

ORDER BY indexed_col
,MySQL 优化器也可能因统计信息过期或数据倾斜,放弃走索引而选全表扫描。强制指定索引并确保
SELECT
字段都在索引中(覆盖索引),能锁死高效路径。

创建覆盖索引:
CREATE INDEX idx_status_ctime_id ON users(status, created_at, id)
查询时显式
FORCE INDEX
,避免优化器误判
注意:覆盖索引字段越多,写入开销越大,权衡读写比
SELECT id, name, email FROM users 
FORCE INDEX (idx_status_ctime_id)
WHERE status = 1 
ORDER BY created_at DESC 
LIMIT 20;

真正卡住分页性能的,往往不是

LIMIT
本身,而是它暴露了索引缺失、排序字段无序、或查询未收敛到索引范围的问题。游标分页和延迟关联不是银弹,但它们把「跳过多少行」这个不可伸缩的操作,转化成了「找到下一个位置」这个可索引的操作——这才是数据库擅长的事。

相关推荐