keyset 分页是什么
深分页慢的根源是 OFFSET:页码越大,数据库仍要先定位并跳过前面所有行,代价随页码线性增长。keyset 分页(游标分页)改用「上一页最后一条的排序键」作为下一页起点,让每次查询都退化成一次索引范围查找。前提是排序键能唯一确定顺序——排序列不唯一时必须拼上主键,写成 (created_at, id) < (值1, 值2) 的行构造器比较,MySQL 与 PostgreSQL 都支持这种写法。
基本信息
| 适用平台 | MySQL 8.0 / PostgreSQL 8.2+ |
|---|---|
| 行构造器比较 | (a, b) < (c, d) 两库都支持,按字典序逐列比较 |
| 前提条件 | 排序键能唯一确定顺序,非唯一列要追加主键 |
| 适用范围 | 只适合顺序翻页;跳页需要额外的键映射或首末页方案 |
| 索引配合 | (created_at, id) 复合索引让下一页定位成为一次范围查找 |
典型使用场景
- 帖子、动态列表的无限滚动加载
- 消息流按时间倒序拉取增量
- 大表数据导出:分批拉取而不越翻越慢
- 榜单类分页:按分数与 id 组合排序后翻页
- 排查线上大 OFFSET 慢查询,用游标方案替换
两种分页写法对比
两种分页写法对比
-- 深分页:OFFSET 100000 需要先跳过 10 万行
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 100000;
-- keyset:从上一页最后一条的 (created_at, id) 继续
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ('2026-06-01 12:00:00', 88888)
ORDER BY created_at DESC, id DESC
LIMIT 20;索引与升序翻页
索引与升序翻页
-- 让下一页的定位变成一次范围扫描
CREATE INDEX idx_posts_created_id ON posts (created_at DESC, id DESC);
-- 升序翻页(下一页取更大的键)
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) > ('2026-06-01 12:00:00', 88888)
ORDER BY created_at ASC, id ASC
LIMIT 20;
-- 客户端只需记住上一页最后一行的两个排序键,无需记录偏移量两种分页方式对比
| 对比项 | OFFSET 分页 | keyset 分页 |
|---|---|---|
| 跳页 | 支持任意页 | 只能顺序前后翻 |
| 深页成本 | 随页码线性增长 | 基本恒定 |
| 并发写入影响 | 新插入的数据会导致重复或漏行 | 基于键定位,结果更稳定 |
| 参数可读性 | page=5000 直观 | 游标参数不直观 |
| 适用场景 | 后台管理、页数有限 | 前台无限滚动、大表导出 |
行构造器比较 (a, b) < (c, d) 等价于 a < c OR (a = c AND b < d),语义是按字典序逐列比较;用行构造器写法更简短,也不容易漏掉相等值分支。
排序键不唯一就直接做 keyset,相等值的行会在页边界被漏读或重复;务必把主键拼进游标,并保证 ORDER BY 的列顺序与方向与索引一致。