游标分页更适合高并发场景,因其不依赖OFFSET,而是以排序字段值(如id)为查询起点,利用索引范围扫描,性能恒定;但不支持随机跳页,仅适用于线性翻页。

游标分页为什么比 offset/limit 更适合高并发场景
因为游标分页不依赖 OFFSET,它用上一页最后一条记录的排序字段值(比如 id 或 created_at)作为下一次查询的起点,避免了数据库扫描跳过大量行。MySQL 和 PostgreSQL 在执行 SELECT ... WHERE id > ? ORDER BY id LIMIT 100 时,只要 id 有索引,就能走索引范围扫描,rows 扫描量恒定,不会随数据量增长而恶化。
但注意:游标分页天然不支持“跳转到第 N 页”,只适合“下一页/上一页”这类线性翻页;如果业务强依赖随机跳页(如后台管理查第 872 页),得换方案或加兜底逻辑。
在 GORM 中手写游标分页的三个关键点
GORM 没有开箱即用的 CursorPaginate 方法,必须手动拼条件。核心是三件事:
- 确保排序字段唯一且有索引(推荐用主键
id,或组合(created_at, id)防止时间重复) - 查询时用
WHERE sort_field > ?(升序)或WHERE sort_field (降序),不能用 <code>BETWEEN或IN - 每次返回结果后,显式提取最后一条的排序字段值,传给下一次请求 —— 别从客户端传整个对象,只传原始值(如
"12345")
示例代码片段:
// 假设按 id 升序分页
var users []User
lastID := uint(0) // 上一页最后的 id,首次为 0
db.Where("id > ?", lastID).Order("id ASC").Limit(100).Find(&users)
if len(users) > 0 {
nextCursor = fmt.Sprintf("%d", users[len(users)-1].ID)
}
用 GORM 的 FindInBatches 替代游标分页?别混淆
FindInBatches 不是分页,而是批量流式处理:它按主键范围自动切片(内部用 WHERE id BETWEEN x AND y),目标是遍历全表并逐批处理,不返回“下一页 token”。它解决的是“导出百万数据”类任务,不是“用户翻页浏览”场景。
常见误用:
- 把
FindInBatches返回的batch当作一页数据直接返回给前端 —— 缺少游标值,无法做下一页请求 - 在 HTTP 接口里调用
FindInBatches处理单次请求 —— 它会锁住连接直到整批完成,阻塞其他请求 - 没设
batchSize导致单次拉取过多内存(默认 1000,大数据量建议 100~500)
PostgreSQL 下游标分页要防的两个坑
PostgreSQL 对 NULL 值和排序稳定性更敏感,容易翻出重复或漏数据:
- 如果排序字段允许
NULL,ORDER BY created_at ASC会让所有NULL排最前,导致游标值比较失效 —— 务必加IS NOT NULL条件或设默认值 - 用非唯一字段(如仅
created_at)排序时,多条记录时间相同时,数据库返回顺序不确定,可能某次翻页漏掉其中一条 —— 必须补一个唯一字段,例如ORDER BY created_at ASC, id ASC
验证方法:对同一批数据连续翻两次,对比第二页首条是否等于第一次翻页的最后一条 —— 不等就说明排序不稳定。


















