gorm 默认分页对ulid主键不友好,因其为时间有序但非自增字符串,limit offset 在大数据量下无法利用索引高效跳转,且字符串比较开销大、易全表扫描;应改用游标分页,依赖 order by id desc + where id
为什么
GORM默认分页对ULID主键不友好因为
ULID是时间有序但非自增的字符串(如"01HR8Z4YXGQZQF2V7K9TJZQW5D"),GORM的Paginate或LIMIT OFFSET分页在大数据量下会严重拖慢查询——每次都要跳过前 N 条,而数据库无法用索引高效定位“第 50000 页”。更糟的是,字符串比较开销比整型大,且排序依赖完整值而非前缀,容易触发全表扫描。
ULID场景下该用游标分页而非偏移分页游标分页靠上一页最后一条记录的
id值做条件筛选,只查下一页数据,不依赖OFFSET。对ULID尤其合适:它天然带时间戳,按字典序排序即等效于时间倒序。
- 必须确保查询带
ORDER BY id DESC,且id字段有索引(GORM默认不会为string类型主键建索引,需显式声明)- 游标值应取上一页结果中最后一个
id,作为下一页WHERE id 的条件(注意是 <code>,不是 <code>)- 不要拼接
id做范围查询(如BETWEEN),ULID不保证严格连续,中间可能有空缺- 示例片段:
db.Where("id
GORM中实现安全游标分页的关键配置直接用
Limit/Where手写容易漏掉边界或并发问题,建议封装成可复用方法,并处理空游标、越界等场景。
- 主键字段定义必须加
gorm:primarykey,index标签:type User struct { ID string `gorm:"primaryKey;index;size:26"` }- 游标参数应校验长度(
ULID固定 26 字符),避免 SQL 注入或无效查询- 首次请求无游标时,不加
WHERE条件,仅靠ORDER BY ... LIMIT拿首屏- 返回结果需附带下一页游标(即最后一条的
ID),前端无需解析或拼接- 注意时区:若 ULID 由客户端生成,需统一使用 UTC,否则跨服务排序错乱
别忽略
ULID生成时机对分页一致性的影响如果
ULID在应用层生成(如用github.com/oklog/ulid),且多实例并发插入,同一毫秒内生成多个 ULID,它们的字典序与真实插入顺序可能不一致——导致分页漏数据或重复。这不是分页逻辑的问题,而是数据写入层的隐性风险。游标分页本身不难,难的是让
- 生产环境建议在数据库层生成(PostgreSQL 可用
ulid_generate()扩展,MySQL 需自定义函数)- 若必须应用层生成,请确保单调递增:例如加锁 + 时间戳回退机制,或改用
ksuid等带序列号的变体- 分页查询前,务必确认表中
id字段已按字典序严格单调(可用SELECT id FROM t ORDER BY id DESC LIMIT 10快速抽检)ULID的“时间有序性”真正落地到每一行数据的物理插入顺序和索引行为上。很多人卡在测试阶段没问题,上线后流量一上来就漏数据——往往不是代码写错了,而是没意识到生成、写入、索引三者之间存在微妙的时间差和排序假设。












