大屏后端分页不能用传统page/size,须改用游标(如时间戳+id)或限制页码≤20、size≤50;count(*)需独立查询且隔离条件;order by必须含时间戳+主键确保唯一排序;超10万行必须切游标分页。

Limit 和 Offset 能用,但直接套在大屏后端分页接口上大概率出问题——数据跳变、重复、查不到、响应慢,不是 GORM 的锅,是没按场景选对分页模型。
大屏后端分页不能只靠 page + size
可视化大屏的“分页”常被误当作普通列表处理,其实它更接近「滚动加载」或「切片快照」:前端不点页码,而是定时拉最新 N 条;或用户下拉时补数据。此时 page=100 这种请求极可能超时或漏数据。
常见错误现象:page=50&size=50 返回空数组,但数据库明明有 2000+ 条;或者同一记录在两次轮询中出现在不同位置。
- 别让前端传
page,改用时间戳或游标(如last_updated_at、last_id)作为分页锚点 - 如果必须支持页码(比如管理后台嵌入大屏),则限制
page≤ 20,超出直接返回 400 -
size必须硬限制(如min(p.Size, 50)),大屏轮询频次高,size=1000一次查 10 万行会拖垮 DB
Count(*) 查询必须和主查询条件完全隔离
大屏接口常要返回 total 字段供前端显示“共 XX 条”,但 db.Where(...).Limit(n).Offset(m).Count(&total) 算出来永远是 ≤ n,因为 Count 会继承前面的 Limit 和 Offset。
正确做法是新建一个干净的 *gorm.DB 实例做总数统计:
var total int64
db.Session(&gorm.Session{NewDB: true}).Model(&DashboardData{}).Where("status = ? AND updated_at > ?", "active", cutoffTime).Count(&total)
- 不要复用同一个
db链式调用,WHERE 条件残留会导致总数不准 - 如果查询含
Joins或Preload,建议手写子查询,例如db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM dashboard_data WHERE ...) t").Scan(&total) - 大屏对实时性要求高,可考虑缓存
total(如 Redis TTL 30s),避免每次轮询都扫全表
ORDER BY 不是可选项,是数据一致性的底线
没有显式 Order 的分页,在 MySQL/PostgreSQL 中本质是未定义行为。大屏轮询时若前后两次查询排序不一致,轻则数据抖动,重则同一条记录反复出现或消失。
- 必须写
.Order("updated_at DESC, id DESC"),用时间戳 + 主键组合保证唯一排序 - 仅用
.Order("updated_at DESC")危险:并发更新导致多条记录时间相同,数据库返回顺序不可控 - 确保
updated_at和查询条件字段(如dashboard_id)已建联合索引,否则OFFSET性能断崖式下跌
大数据量下 Offset 分页必须切换为游标分页
当单个大屏数据源超过 10 万行,且轮询间隔短(如 5s 一次),OFFSET 50000 LIMIT 50 在 MySQL 上可能耗时 800ms+,且随着偏移增大线性恶化。
游标分页不是“优化项”,是大屏场景的默认选项:
// 前端传:?cursor=2026-08-21T12:00:00Z_12345
// 后端解析后生成:
db.Where("updated_at
- 游标值必须由后端生成并返回(如
"2026-08-21T12:00:00Z_12345"),禁止前端拼接 - 游标字段(
updated_at+id)需有联合索引,且顺序与Order严格一致 - 首次请求无游标时,用
ORDER BY ... LIMIT 50拿首屏,再把最后一条的updated_at和id组成游标返回
Offset 和 Limit,是判断什么时候不该用它们——大屏后端的“分页”本质是流式数据切片,不是页码导航。参数校验、排序稳定性、总数隔离、游标设计,漏掉任何一环,上线后都会变成监控告警里的高频错误。











