分页查询LIMIT 1000000, 20特别慢,怎么优化?
先说结论
LIMIT offset, count的问题在于:MySQL会先扫描前offset+count行,然后丢弃前offset行,只返回count行。offset越大,浪费越多。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
深分页是MySQL性能的经典降坑。前几页飞快,翻到第10万页就卡死了。很多人知道这个问题但说不清解决方案。今天把原因和三种解决方案讲清楚。
为什么慢?用一个例子说明
-- 查第50万页(每页20条)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- MySQL实际做的事:
-- 1. 扫描前 1000020 行
-- 2. 丢弃前 1000000 行
-- 3. 返回第 1000001~1000020 行
-- 前100万行的扫描全是浪费!
方案一:游标分页(最推荐 ★)
核心思想:记住上一页的最后ID,下一页从这个ID开始查。
-- ✖ 慢:传统分页
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✔ 快:游标分页(假设上一页最后ID是1000000)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
为什么快?WHERE id > 1000000可以直接用主键索引定位到第1000000行,不需要扫描前面的数据。时间复杂度从 O(offset+count) 变成 O(count)。
限制: 只能用于“上一页/下一页”的场景,不能直接跳到第N页。但对于移动端的“无限滚动”场景完美适用。
方案二:子查询先定位ID
如果必须支持“跳到第N页”,可以先用子查询找到起始ID:
-- 先用覆盖索引找到起始ID(不需要回表,很快)
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 1000000, 1)
ORDER BY id
LIMIT 20;
子查询只查主键ID,走的是索引覆盖扫描,比查全部字段快很多。
方案三:业务层限制最大页数
这是最简单但最实用的方案。Google搜索也只让你翻到前几十页。大多数业务场景根本不需要翻到第10万页。
我在元宇宙社交App的做法是:后台管理系统限制最多翻100页,超过就引导用户用搜索条件过滤。App端用无限滚动 + 游标分页。
实战踩坑:ORDER BY不稳定
如果ORDER BY的字段有重复值,不同页可能返回重复的数据。解决方案:ORDER BY后面加一个唯一字段兆底(比如id)。
-- ✖ 不稳定:created_at有重复值,不同页可能返回重复数据
SELECT * FROM orders ORDER BY created_at LIMIT 100, 20;
-- ✔ 稳定:加id兆底
SELECT * FROM orders ORDER BY created_at, id LIMIT 100, 20;
总结
| 方案 | 适用场景 | 性能 |
|---|---|---|
| 游标分页(id > X) | 无限滚动、上一页/下一页 | 最快 |
| 子查询定位ID | 需要跳转到第N页 | 快 |
| 限制最大页数 | 所有场景 | 从业务层解决问题 |
面试时能说清楚“为什么LIMIT大偏移慢”的原理,再给出多种方案并分析各自适用场景,就很完整。
下一篇聊一个生产环境经常遇到的场景:给大表加字段加索引,结果库卡死了——Online DDL踩坑经验。
你们生产环境的分页怎么做的?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长