Files

4.3 KiB
Raw Permalink Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week
id title type domain stack tags level prerequisites est_hours related week
backend.mysql.scenario.deep-pagination 大表深分页和历史数据:游标分页、延迟关联、归档 scenario backend
mysql
深分页
游标分页
延迟关联
归档
大表
intermediate
backend.mysql.index-explain
2
backend.mysql.ops-scale
backend.mysql.interview
2
14

大表深分页和历史数据:游标分页、延迟关联、归档

业务背景与问题

一个 SaaS 后台的“操作记录 / 订单列表”页面,单表已积累数千万行。用户反馈:前几页很快,翻到很后面或者导出时接口超时;同时这张表的慢查询日志里大部分是 LIMIT 大数, 20。

约束

  • 列表需要按时间倒序展示,支持按用户、状态筛选。
  • 产品希望保留“跳到第 N 页”,但能接受在超过一定页数后改成“加载更多”。
  • 导出需要导出全部符合条件的数据。
  • 不希望马上引入分库分表或搜索引擎。

方案设计

  1. 找原因:LIMIT 199000, 20 需要先按索引顺序找到 199020 行(并回表),再丢掉前 199000 行,越往后越慢。
  2. 游标分页(主方案):前端传上一页最后一条的 (created_at, id),查询 WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT 20,配合 (user_id, created_at, id) 联合索引,每页都只扫 20 行。适合“加载更多”、无限滚动、App 列表。
  3. 延迟关联(保留跳页时用):子查询只在覆盖索引上找出这一页的 id,再回表取整行,减少大量回表。仍然要扫描 offset 行,只是更便宜;同时限制最大页数(如 100 页)。
  4. 导出:用游标分页按批读取,后台异步生成文件,不在请求里同步完成。
  5. 归档:超过一定时间(如 1 年)的数据移到历史表或对象存储,主表只保留热数据;历史查询走单独的入口。

关键代码

完整可运行对比见 examples/deep-pagination/paginate.py:

-- 游标分页:时间相同的行用 id 区分,保证不重不漏
SELECT id, created_at, note FROM orders
WHERE user_id = 1 AND (created_at < ? OR (created_at = ? AND id < ?))
ORDER BY created_at DESC, id DESC LIMIT 20;

-- 延迟关联:先在覆盖索引上取 id,再回表
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders WHERE user_id = 1 ORDER BY created_at DESC, id DESC LIMIT 20 OFFSET 199000) t
  ON o.id = t.id
ORDER BY o.created_at DESC, o.id DESC;

在本地 10 万~20 万行的测试数据上,深页的游标分页比 OFFSET 快一到两个数量级(具体数字与机器有关,以你自己运行的结果为准)。

取舍

方案 优点 缺点
游标分页 每页耗时稳定,适合海量数据 不能直接跳到第 N 页;排序字段必须唯一或加 id 兜底
延迟关联 保留跳页,改动小 仍随 offset 变慢,只是常数更小
限制最大页数 简单直接 需要产品接受
搜索引擎(ES 等) 复杂筛选、全文检索 多一套系统,要做数据同步(见跨栈场景“数据同步”)
归档 主表变小,所有查询受益 历史数据查询入口要单独设计

易错点

  • 只按 created_at 做游标:时间相同的多行会被跳过或重复,必须加 id 作为第二排序键。
  • 游标条件和 ORDER BY、索引列顺序不一致:用不上索引。
  • SELECT COUNT(*) 统计总页数在大表上也很慢:可以显示“约 xx 条”,或缓存计数。
  • 导出在 HTTP 请求里同步完成:超时、占满连接。改为异步任务。
  • 归档用一条大 DELETE:长事务、锁、主从延迟。按主键范围分批删除。

面试怎么讲

“列表接口深分页超时,慢查询日志里都是大 offset。原因是 LIMIT offset 要扫描并丢弃前面所有行。我把 App 和导出改成游标分页:传上一页最后一条的时间和 id,配合联合索引,每页耗时稳定;后台管理保留跳页,用延迟关联并限制最大页数;同时按时间归档一年前的数据,主表变小。”

追问准备:为什么要加 id?总数怎么显示?如果按多个字段排序怎么做游标(把所有排序字段都放进游标)?