大表分页 LIMIT 优化

大表分页 LIMIT 优化

深度分页问题

-- 传统分页写法
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 100000, 20;  -- 第 5000 页

问题LIMIT 100000, 20 不是”跳过 100000 行取 20 行”,而是取 100020 行然后丢弃前 100000 行

LIMIT 100000, 20 的执行过程:
① 扫描前 100020 行
② 丢弃前 100000 行
③ 返回最后 20 行

随着页码增大,需要扫描的行数越来越多。
第 1 页:扫描 20 行
第 100 页:扫描 2000 行
第 5000 页:扫描 100020 行

深度分页的性能损耗

-- 数据量 1000 万行
-- 越往后查询越慢

EXPLAIN SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 0, 20;
-- rows: 20(很快)

EXPLAIN SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 100000, 20;
-- rows: 100020(越来越慢)

EXPLAIN SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 5000000, 20;
-- rows: 5000020(极慢!)

优化方案一:子查询延迟关联

核心思路:先走索引快速定位偏移位置的 ID,再关联出完整数据

-- ❌ 慢
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 100000, 20;

-- ✅ 快:子查询延迟关联
SELECT o.*
FROM orders o
INNER JOIN (
    SELECT id
    FROM orders
    ORDER BY created_at DESC
    LIMIT 100000, 20
) tmp ON o.id = tmp.id
ORDER BY o.created_at DESC;

为什么更快

子查询部分:
→ 只查 ID(覆盖索引)
→ 在索引中完成排序 + 分页
→ 不需要回表
→ 快

外层 JOIN:
→ 根据 20 个 ID 回表
→ 只取 20 行
→ 几乎是常数时间

优化方案二:游标分页(推荐)

利用有序字段的特性,记录上一页的最后位置

-- 第一页
SELECT * FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 获取最后一行的 created_at 和 id
-- last_created_at = '2024-01-15 10:30:00'
-- last_id = 100

-- 第二页(基于游标)
SELECT * FROM orders
WHERE created_at < '2024-01-15 10:30:00'
   OR (created_at = '2024-01-15 10:30:00' AND id < 100)
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 第三页(继续基于游标)

游标分页的性能

EXPLAIN SELECT * FROM orders
WHERE created_at < '2024-01-15 10:30:00'
   OR (created_at = '2024-01-15 10:30:00' AND id < 100)
ORDER BY created_at DESC, id DESC
LIMIT 20;
type: range
rows: 20(始终只扫 20 行!)
Extra: Using index condition

无论翻到多少页,性能始终和第一页一样好!

游标分页的实现要求

1. 排序字段必须唯一
   - 只有 created_at 可能重复 → 必须加 id 作为第二排序
   - ORDER BY created_at DESC, id DESC

2. 不能跳页
   - 只有"下一页""上一页"按钮
   - 没有页码输入框

3. 适合场景
   - 社交信息流(刷不完的 Feed)
   - 搜索结果(无限滚动)
   - 日志列表

优化方案三:覆盖索引

-- 如果不需要全部列,用覆盖索引
-- ❌ 慢(需回表)
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 100000, 20;

-- ✅ 快(覆盖索引)
SELECT id, order_no, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 100000, 20;
-- Extra: Using index(所有数据都在索引中)

优化方案四:限制最大页数

-- 业务层面限制
-- 前端最多显示 100 页
-- 超过 100 页提示"数据太多,请筛选"

-- 或强制加条件
-- 用户不能翻超过半年以前的数据
WHERE created_at > DATE_SUB(NOW(), INTERVAL 6 MONTH)

不同分页方案对比

方案 翻页方式 性能问题 适用场景
传统 LIMIT OFFSET 页数 越往后越慢 ❌ 不推荐大表使用
子查询延迟关联 页数 较好 ✅ 需要页码的翻页
游标分页 下一页 始终优秀 ✅ 无限滚动
覆盖索引 页数 较好(少返回列时)✅ 列表页不需要全字段

面试要点

  • LIMIT 大偏移量的本质问题:MySQL 要扫描 OFFSET + LIMIT 行的数据再丢弃
  • 延迟关联:先在索引中做分页(只查 ID),再关联回表
  • 游标分页是最优方案:无论翻多少页,性能恒定
  • 覆盖索引:如果列表不需要全部字段,能大幅提升分页性能
  • 业务约束:不让用户无限制翻页(限制页数或时间范围)

一句话总结:大表 LIMIT 深度分页的核心问题是 MySQL 要扫描并丢弃大量行;最优解是游标分页(基于排序字段定位下一页),其次是用子查询做延迟关联减少回表代价。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容