gorm的limit+offset分页在数据量超10万后性能断崖式下跌,根本原因是数据库需真实扫描并丢弃前n行,而非跳过;应改用游标分页或索引优化,避免count(*)全表统计,慎用可变排序字段。

直接看数据库执行时间,别信 Go 层耗时——GORM 分页慢的根因在 SQL 扫描行数,不是 Go 代码本身。
Offset 分页为什么越翻越慢
MySQL/PostgreSQL 执行 OFFSET 100000 时,不是“跳过”前 10 万行,而是真实读取、比较、再丢弃。百万级表翻到第 5000 页,单次查询可能扫描 50 万行,CPU 和 I/O 压力陡增。
- 典型现象:
db.Offset(99999).Limit(20)在压测中触发连接超时或 CPU 飙升 - 同一页刷新两次返回不同结果,甚至漏掉某条记录——这是数据库语义决定的,不是 GORM bug
- 即使加了索引,只要排序字段没覆盖 WHERE 条件,仍可能回表扫描
游标分页的性能指标怎么测才准
游标分页(WHERE id > ? ORDER BY id LIMIT 20)性能稳定,但测不准就白优化:它依赖 last_id 正确性、索引命中率和前端透传质量。
- 必须确保
id字段有主键或唯一索引,否则WHERE id > ?无法走索引范围扫描 - 首次请求查
ORDER BY id ASC LIMIT 20,后续每页用users[len(users)-1].ID作为下一轮last_id,不能从任意字段拼接 - 前端必须传
last_id,后端不做转换;若传page=5再去算 last_id,等于退回 Offset 模式 - 用
EXPLAIN ANALYZE看执行计划:扫描行数应 ≈ LIMIT 值(如 20),而不是随页数增长
Count 总数查询怎么避免拖垮性能
Count() 不是“顺便带一下”,它是独立 SQL,且极易写错——复用带 Limit/Offset 的 DB 实例会导致总数永远 ≤ 每页条数。
- 简单场景:用
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total)隔离上下文 - 复杂关联(含
Joins或Preload):手写子查询,例如db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE p.active = ?) AS t", true).Scan(&total) - 业务允许时,可只查
size + 1条,返回has_next而非总数——省掉一次全表/全关联扫描
哪些地方最容易被忽略但影响最大
参数校验、排序字段索引、Count 上下文隔离这三件事,不花时间写死逻辑,线上就一定会出问题。
-
page和page_size必须由后端强制截断:page - 没
Order("id ASC")的分页,结果不可重现;只用created_at DESC而不加id DESC作二级排序,高并发下会乱序 - 在事务里连续调
Count+Find,两次查询之间若数据变更,总数和当前页数据对不上——这不是 bug,是设计选择











