不能用 offset + limit 做游标分页,因其需扫描丢弃前n行导致性能差,且高并发下会重复或跳过数据;游标分页应基于上一页最后记录的排序字段值续查。

为什么不能用 OFFSET + LIMIT 做游标分页
因为 OFFSET 是数据库真实扫描并丢弃前 N 行,不是跳过——数据量越大,性能越差;高并发写入时,同一条记录可能在两页里重复出现,或直接被跳过。游标分页本质是“基于上一页最后一条的排序字段值继续往后取”,和页码无关。
常见错误现象:
-
database/sql: statement expects 0 arguments, got 1:参数绑定错位,比如把cursor当字符串拼进 SQL 而没用占位符 - 返回空数组但没报错:
cursor解析失败(如时间戳格式不对)后静默 fallback 到全表查询 - 前端翻到第 200 页突然卡住:实际是用了
OFFSET却谎称自己在做游标分页
PostgreSQL 行值比较语法怎么写才安全
PostgreSQL 8.0+ 支持 (created_at, id) > (?, ?) 这种行值比较,语义清晰、索引友好,但必须确保字段顺序和 ORDER BY 完全一致。
实操建议:
- 查询语句写成:
WHERE (created_at, id) > ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3 - 对应索引必须是:
CREATE INDEX idx_created_id ON items (created_at DESC, id DESC),顺序不能反 - 如果用
ASC排序,条件要改成(created_at, id) > (?, ?),且索引也要匹配ASC - SQLite 不支持该语法,得拆成:
WHERE created_at > ? OR (created_at = ? AND id > ?)
Gin 中如何解析和校验 cursor 参数
游标是客户端传来的不可信输入,不能直接 json.Unmarshal 或 strconv.ParseInt,必须分步解码、校验、转换。
推荐做法:
- 用
base64.StdEncoding.DecodeString先解码,避免 URL 特殊字符干扰 - 再按约定格式切分,比如
"2026-08-10T12:34:56Z:12345"→ 用strings.SplitN(s, ":", 2) - 时间部分用
time.Parse(time.RFC3339, tsStr),失败就返回400 Bad Request - ID 部分用
strconv.ParseInt(idStr, 10, 64),拒绝负数、超长数字 - 首次请求允许
cursor为空,但不要默认设为"0"或"1970-01-01"——这会查出全表
用 pgx 还是 sqlx?参数绑定和扫描怎么写
pgx 对 PostgreSQL 更原生,支持 QueryRow 直接扫描结构体,也支持行值比较的参数绑定;sqlx 兼容性更好但对复合游标支持弱一点。
示例(pgx):
rows, err := tx.Query(ctx, `
SELECT id, title, created_at
FROM posts
WHERE status = $1 AND (created_at, id) > ($2, $3)
ORDER BY created_at DESC, id DESC
LIMIT $4`,
"published", cursorTime, cursorID, limit)
if err != nil {
return err
}
defer rows.Close()
var items []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.CreatedAt); err != nil {
return err
}
items = append(items, p)
}
// 最后一条的 created_at 和 id 就是 next_cursor
if len(items) > 0 {
nextCursor = fmt.Sprintf("%s:%d", items[len(items)-1].CreatedAt.Format(time.RFC3339), items[len(items)-1].ID)
}
关键点:
- 别用
db.Find()或 GORM 的链式调用——它不支持行值比较语法 - 扫描时字段顺序必须和 SELECT 一致,否则
Scan会错位 - 游标值建议用
base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(...)))编码后再返回,避免前端 URL 解析失败
DESC,结果就不可靠。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











