自增id游标分页天然更高效,因其严格单调递增,b+树索引可直接定位where id > ?起始位置,无需扫描跳过任何行,mysql/postgresql对此类主键范围查询深度优化,响应恒定且结果可预测。

用自增 id 做游标分页,比用雪花 ID 快且稳;但若必须用雪花 ID,得确保它按时间单调递增、索引覆盖完整,否则游标失效、数据错乱、性能反不如自增 ID。
为什么自增 id 游标分页天然更高效
自增 id 是严格单调递增的整数,B+ 树索引能直接定位 WHERE id > ? 的起始位置,无需扫描跳过任何行。MySQL/PostgreSQL 对这种主键范围查询做了深度优化,响应时间基本恒定(毫秒级),且结果绝对可预测。
- 首次请求:
db.Order("id ASC").Limit(21).Find(&users),取 21 条是为了判断是否有下一页 - 下一页游标直接取
users[len(users)-1].ID,拼进WHERE id > ? - 复合排序如
ORDER BY status, id时,索引必须是INDEX(status, id),否则仍会回表或全索引扫描 - 避免用
ORDER BY id DESC+WHERE id :某些 MySQL 版本对反向范围扫描优化不足,实测延迟略高
雪花 ID 游标分页的三个硬性前提
雪花 ID 虽然也带时间戳,但不等于天然适合游标分页——它只有在满足以下全部条件时,才能接近自增 ID 的分页性能:
- 数据库字段类型必须是
BIGINT UNSIGNED,否则高位为 1 的 ID(如0x8000000000000000)会被 GORM 或 MySQL driver 当作负数截断或报错 - 必须用
ORDER BY id ASC(不能用created_at替代),因为不同节点生成的 ID 在同一毫秒内可能无序,仅靠时间戳无法保证全局顺序 - 索引必须是单列
INDEX(id),不能被复合索引“覆盖”后失效;若加了WHERE status = ?,需建联合索引INDEX(status, id),且查询中status必须是等值条件 - 雪花生成器必须禁用时钟回拨 panic,改用缓存上一毫秒戳 + 日志告警,否则某次回拨会导致后续一批 ID 时间戳倒流,破坏游标单调性
GORM 中用雪花 ID 分页时最容易漏掉的坑
很多人以为只要 ID 唯一、有索引,就能照搬自增 ID 的游标写法,结果线上翻车:
- 前端传
last_id=1234567890123456789,后端没做strconv.ParseInt校验就直传给Where("id > ?", lastID),遇到非法字符串会静默转成 0,查出全表前 N 条 - 用
sony/sonyflake但没设StartTime,导致容器重启后生成的 ID 时间戳小于上次,WHERE id > ?漏掉大量记录 - 误以为
ORDER BY created_at DESC, id DESC更安全,结果因created_at精度只到秒(或数据库未启用datetime(3)),同一秒内多条记录排序不稳定,游标值重复或跳变 - 在事务里生成雪花 ID 后立即用于分页查询,但事务未提交,其他连接查不到该 ID,造成“下一页为空”的假象
真正关键的不是 ID 怎么生成,而是游标值是否能稳定锚定一条唯一、可索引、不可变的记录——自增 id 天然满足,雪花 ID 需要你亲手把每处时钟、索引、类型、解析逻辑都拧紧。











