订阅更新

分页查询LIMIT 1000000, 20特别慢,怎么优化?

MySQL手册 第 11 题 约 3 分钟读完

本文目录
  1. 先说结论
  2. 为什么慢?用一个例子说明
  3. 方案一:游标分页(最推荐 ★)
  4. 方案二:子查询先定位ID
  5. 方案三:业务层限制最大页数
  6. 实战踩坑:ORDER BY不稳定
  7. 总结

先说结论

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年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器