OFFSET越大越慢,因数据库仍需扫描并丢弃前N行;应改用游标分页,如基于last_id的WHERE+ORDER BY+LIMIT,并用strconv.ParseInt安全解析cursor。

为什么 offset 越大越慢?
因为数据库执行 OFFSET 100000 时,仍要扫描前 10 万行再丢弃,不是跳过——索引能加速定位,但无法跳过扫描。即使有 id 索引,ORDER BY created_at LIMIT 20 OFFSET 100000 依然可能触发 filesort + 大量行读取。
真实场景下,用户翻到第 5000 页(OFFSET 100000)时,响应常超 2s,且并发一高就拖垮 DB。
关键判断:只要分页深度不可控(比如支持“跳转任意页”或前端无限制滚动),就必须弃用 OFFSET,改用游标(cursor-based pagination)。
怎么用 lastID 实现游标分页?
核心是把“第 N 页”变成“从某条记录之后取 20 条”。依赖有序字段(通常是主键 id 或时间戳 created_at),且该字段必须有索引、无重复、单调递增(或严格唯一)。
- 查询第一页:
SELECT * FROM posts ORDER BY id DESC LIMIT 21(多查 1 条用于判断是否有下一页) - 后续请求带
last_id:SELECT * FROM posts WHERE id (?是上一页最后一条的id) - 前端传参用
?cursor=12345,后端解析为lastID := r.URL.Query().Get("cursor"),注意校验是否为有效整数
示例 Go 片段(忽略错误处理):
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
rows, _ := db.Query("SELECT id, title FROM posts WHERE id <h3>lastID 分页的坑:边界与一致性</h3><p>看似简单,但实际容易出错:</p>
WHERE id 要求排序字段严格单调——如果用created_at,同一秒内多条记录会导致漏数据或重复(因created_at不唯一);必须加二级排序,如ORDER BY created_at DESC, id DESC,对应条件写成WHERE (created_at, id)- 删除中间数据会导致“跳过”:用户看到 ID=1000 后是 ID=998,但 ID=999 被删了——这是正常行为,游标分页不承诺覆盖全集,只保证顺序遍历不重复
- 新插入数据在游标“前方”会不可见:比如按
id DESC分页,用户停留在第 3 页(最后id=500),此时插入id=600的新帖,它不会出现在已加载的页面里——这是设计使然,不是 bug - 不能直接支持“上一页”:需要存上一页的
first_id,或反向查(WHERE id > ? ORDER BY id ASC LIMIT 21),但逻辑更重
Go 里怎么安全解析和传递 cursor?
别直接用 strconv.Atoi 解析 URL 参数,容易 panic;也别用字符串拼接 SQL。
- 用
strconv.ParseInt(r.URL.Query().Get("cursor"), 10, 64),检查 err 是否为nil或strconv.ErrSyntax - cursor 值必须是正整数,负值或 0 应拒绝(
if cursor ) - 返回下一页链接时,不要拼
"?cursor=" + strconv.FormatInt(nextLastID, 10),而是用url.Values{"cursor": []string{strconv.FormatInt(nextLastID, 10)}}防止注入和编码问题 - 如果业务允许,考虑用 base64 编码复合游标(如
created_at+id),避免暴露内部 ID,但增加解码开销
最麻烦的其实不是代码,而是说服前端同事:游标分页没法跳转到任意页,也不能显示总页数——这些得在 API 文档里写清楚,否则联调时容易反复返工。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










