gorm分页必须手写limit+offset并显式order,count需隔离查询;page/page_size须校验截断;模糊搜索要trim空格;多条件分页必加唯一排序字段;游标分页优于offset大数据场景。

GORM 没有 Paginate 方法,多条件分页必须手写 Limit + Offset,配合显式 Order 和隔离的 Count 查询——这是线上不出错的唯一可靠路径。
参数校验和安全截断必须在 DB 查询前完成
前端传来的 page 和 page_size 是不可信输入,直接透传会触发 panic 或慢查询。例如 page_size=0 在 SQLite 下返回全表,page=-1 会让 Offset 接收负数而崩溃。
-
page小于等于 0 时强制设为 1,不接受“第 0 页”这种语义 -
page_size必须做硬上限(如 100),用min(p.PageSize, 100)截断,别依赖前端约束 - 空字符串、非数字值(如
page=abc)应由c.ShouldBindQuery(&p)拦住并返回400,不 fallback - 模糊搜索字段(如
name、subject)需用Like包裹通配符,且对用户输入做strings.TrimSpace防空格注入
多条件 WHERE + 分页必须显式 Order
没 Order 的分页结果不可靠:并发写入时同一页可能漏数据或重复,这不是 GORM 的问题,而是 SQL 标准行为。尤其多条件组合(如按班级 + 成绩区间 + 姓名模糊)后,数据库更难维持行序稳定。
- 排序字段必须带索引,首选
id ASC,次选created_at DESC, id DESC(避免时间重复) - 禁止只用
Order("score DESC")—— 多人同分时排序不确定,必须补上id等唯一字段兜底 - WHERE 条件顺序不影响性能,但
Order必须在Limit/Offset之前调用,否则部分 GORM v1 版本不生效 - 如果用
Preload("Subjects"),先查出主表 ID 列表再批量加载,别让分页查出 20 个学生却拉取几百条科目记录
总数查询必须和分页查询完全隔离
db.Where(...).Limit(20).Offset(40).Count(&total) 得到的是 20(或更少),不是真实总数——因为 Count 会继承前面的 Limit 和 Offset。这是最常被忽略、导致分页控件错乱的点。
- 用
db.Session(&gorm.Session{NewDB: true}).Model(&Student{}).Where(...).Count(&total)创建干净上下文 - 复杂多表 JOIN 场景下,手写子查询更可控:
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM students s LEFT JOIN classes c ON s.class_id = c.id WHERE ?) t", conditions).Scan(&total) - 不要在同一个事务里反复执行
Count和Find,两次查询之间若数据变更,总数和当前页数据可能不一致(业务能容忍就另说) - 如果总数仅用于显示“共 N 条”,且 N > 10 万,可考虑用近似值(如
SHOW TABLE STATUS)或缓存,避免每次查全表
大数据量时 OFFSET 分页必然变慢,得提前规划游标
当 OFFSET 超过 10 万,MySQL 实际要扫描并丢弃前 N 行,响应时间从毫秒级跳到秒级。这不是 GORM 层能优化的,是查询模型问题。
- 游标分页依赖确定排序字段,首选
id(自增/UUID),次选created_at + id组合 - SQL 写成
WHERE id > ? ORDER BY id LIMIT 20,前端传last_id而非page - 首次请求查
ORDER BY id ASC LIMIT 20,后续每页取users[len(users)-1].ID作为下一轮游标 - 游标分页不能混用
Preload,关联数据必须分两步:先查主表 ID 列表,再用IN批量加载
多条件 + 分页的本质是平衡表达力与稳定性:条件越多,WHERE 越复杂,Order 越关键,Count 越容易被污染。最容易被忽略的不是语法,而是把 Count 和 Find 当成“一次查询”的错觉——它们必须是两条独立、隔离、各自带完整条件的 SQL。











