必须校验page和size参数:page≤1时设为1,size硬限制1–100并截断;必须显式order排序;count需独立执行;大数据量应改用游标分页。

page 和 size 参数必须显式校验,不能依赖前端传值
用户请求里 page=0、page=-1、size=abc、size=999999 是常态,不是异常。GORM 不会帮你拦截,Offset(-10) 或 Limit(0) 会直接发给数据库,MySQL 可能报 ERROR 1235,PostgreSQL 会返回空或 panic。
-
Page小于等于 1 时,统一设为 1(不返回 400,用户点错页码不是服务端错误) -
Size必须硬限制在 1–100 区间,超出就截断为 20(防慢查询和连接池耗尽) - 用
c.ShouldBindQuery(&p)绑定结构体比c.Query("page")更安全,能结合binding:"gte=1,lte=100"做字段级校验 - 若
ShouldBindQuery返回 error(如page=abc),应立即c.JSON(400, ...),别默默 fallback
GORM 分页必须显式调用 Order,否则结果不可靠
没 Order 的分页是“伪分页”:数据库不保证无序查询的行顺序,同一页可能漏数据、重复、甚至前后页颠倒。这不是概率问题,是确定性风险。
- 必须显式指定排序字段和方向,例如
Order("id ASC")或Order("created_at DESC") - 避免用
ORDER BY RAND()或函数表达式(如Order("updated_at + ?")),会导致索引失效、性能骤降 - 复合排序更稳妥,比如
Order("status ASC, created_at DESC"),可消除主键重复时的不确定性 - 不要把排序逻辑藏在 GORM Scope 里——容易被链式调用覆盖,应在每次分页查询中明确写出
Count 查询必须独立执行,不能复用同一 DB 实例
COUNT(*) 和主查询是两个语义完全不同的 SQL,共用一个 *gorm.DB 实例会导致 WHERE 条件污染、总数不准,尤其在多租户或带软删除场景下极易越权或漏计。
- 总数查询应新建 DB 实例或用
Session(&gorm.Session{NewDB: true})隔离上下文 - 写法示例:
db.Session(&gorm.Session{NewDB: true}).Where("tenant_id = ?", tenantID).Model(&User{}).Count(&total) - 别用
db.Count(&total)后再db.Limit().Offset().Find(),前者会把 Limit/Offset 也带进 COUNT - 大数据量时,考虑用估算值(如
EXPLAIN SELECT ...的 rows)替代精确 COUNT,避免全表扫描
Offset + Limit 不适合深分页,10 万条后性能断崖式下降
当 Offset 超过几万,MySQL/PostgreSQL 都要先扫描并丢弃前面所有行,CPU 和 I/O 开销剧增,接口响应从毫秒级变成秒级甚至超时。
- 超过 1000 条记录建议切换游标分页(cursor-based pagination),用上一页最后一条的
id或created_at作为下一页起点 - 游标分页必须配合唯一、有序、非空的字段(如主键或带索引的时间戳),且禁止跳页
- 如果业务强依赖跳页(如页码输入框),可在后台对
page * size > 10000的请求自动降级为游标逻辑 + 缓存总页数 - 别指望加索引能救深分页——
OFFSET 100000 LIMIT 20仍要定位到第 100001 行,索引只加速查找,不跳过扫描
实际项目里最容易被忽略的是:游标分页和传统分页不能混用同一套参数结构。用 page/size 的接口一旦开始支持游标,就必须新增 cursor 字段,并在逻辑层彻底隔离两套路径——否则前端传了 cursor 还去算 Offset,结果必然错乱。











