游标分页需用严格单调递增的唯一字段(如id或(created_at,id)复合索引)作锚点,避免created_at等易重复/回退字段;查询须用WHERE+ORDER BY+LIMIT配合预处理参数,索引须覆盖排序与过滤条件。
游标字段必须是严格单调递增的索引列
深分页(比如 offset 1000000 limit 20)慢,本质是数据库要扫描并跳过前面一百万行。游标分页绕开这个问题,靠的是“记住上一页最后一条的锚点值”,下一页直接从那里往后查。但这个锚点必须可靠——它得是唯一、非空、不会重复、不会回退的。
常见错误是拿 created_at 当游标字段:时区不一致、批量插入导致时间相同、业务逻辑允许时间回拨,都会让游标失效或漏数据。
- 首选
id(自增整型或 UUIDv7),前提是它全局有序且写入不乱序 - 若用时间字段,必须搭配唯一约束(如
(created_at, id)复合索引),且应用层确保created_at不回退 - 绝对不要用
updated_at—— 它天生可变、可重复、可倒流 - MySQL 中
TIMESTAMP受时区影响,比DATETIME更容易出错
查询语句必须用 WHERE + ORDER BY + LIMIT,不能带 OFFSET
游标分页不是“换种方式写 OFFSET”,而是彻底抛弃偏移量。典型错误是写成 WHERE created_at > ? ORDER BY created_at ASC LIMIT 20 却忘了加 id > ? 去重,结果同秒内多条记录就漏掉或重复。
正确写法取决于游标字段组合:
- 单字段游标(如
id):WHERE id > ? ORDER BY id ASC LIMIT 20 - 复合游标(如
(created_at, id)):WHERE (created_at, id) > (?, ?) ORDER BY created_at ASC, id ASC LIMIT 20 - 方向翻转时(上一页),要把
>换成,且 <code>ORDER BY方向也要反(否则索引失效) - PostgreSQL 支持行值比较,MySQL 8.0+ 才支持;老版本 MySQL 得拆成
WHERE created_at > ? OR (created_at = ? AND id > ?)
客户端传入的游标值必须校验合法性
用户可能篡改 URL 中的 cursor=12345,填个负数、超长字符串、SQL 注入片段。不校验会导致查询失败、全表扫描甚至安全漏洞。
- 对整型游标(如
id),先用parseInt()或后端强转,拒绝非数字输入 - 对复合游标(如 Base64 编码的
created_at|id),解码后逐字段校验格式、范围、是否为空 - 永远不要把原始游标值拼进 SQL —— 必须用预处理参数绑定(
WHERE id > ?) - 首次请求无游标时,应明确走默认排序起点(如
WHERE id > 0),而不是留空条件导致全表扫
索引必须覆盖 ORDER BY 和 WHERE 条件
游标查询快不快,最终卡在索引是否能“一气呵成”完成定位 + 排序 + 限行。常见坑是只给 created_at 加了索引,但查询里用了 WHERE created_at > ? ORDER BY created_at, id —— 这时 MySQL 可能放弃索引做 filesort。
- 最优索引是联合索引,顺序必须匹配查询:如查询用
ORDER BY created_at ASC, id ASC,索引就得建为INDEX idx_cursor (created_at, id) - 如果查询还涉及其他过滤字段(如
status = 'active'),需评估是否加入索引前缀,但注意索引宽度和维护成本 - 用
EXPLAIN看key和Extra字段:出现Using filesort或Using temporary就说明索引没生效 - PostgreSQL 对复合索引更友好;MySQL 在
WHERE + ORDER BY字段顺序不一致时容易退化
游标分页真正难的不是写那几行 SQL,而是整个链路要对齐:字段选型、索引设计、查询写法、客户端编码、服务端校验——漏掉任何一环,都可能在十万级数据后突然慢下来,而且问题还不易复现。










