物联网分页必须用游标分页,排序字段需为带索引的时间戳+主键组合,禁用Limit/Offset;首次查50条,后续基于上页末条created_at和id条件查询,并建复合索引idx_created_id。

Limit 和 Offset 在物联网场景下基本不可用,高写入、时间敏感、数据量大——这三点直接否定了传统分页的可行性。必须用游标分页,且排序字段得是带索引的时间戳 + 主键组合。
为什么物联网数据不能用 page=100&page_size=20
设备上报数据是持续写入流,每秒可能新增数千条记录。此时 Offset(1980) 不代表“第 100 页”,而代表“跳过前 1980 条物理行”,但这些行在你查询间隙可能已被新数据挤出结果集,导致漏数据或重复;更关键的是,MySQL 执行 OFFSET 100000 时真会扫描并丢弃前 10 万行,IO 和 CPU 压力陡增,响应从毫秒级升到秒级。
- 常见错误现象:
page=500返回空 slice,但err == nil;同一批设备数据在刷新后顺序错乱甚至消失 - 设备端常按
created_at查询最近 N 条,但只用Order("created_at DESC")不够稳——毫秒级重复多,必须加二级排序,例如Order("created_at DESC, id DESC") - 前端传
last_id或last_time比传page更合理:避免服务端做乘法计算,也绕开偏移膨胀问题
游标分页怎么写才不丢设备数据
核心是“基于上一页最后一条的排序字段值继续往后取”,不是“翻到第 N 页”。物联网数据天然适合按时间推进,但必须防住精度和并发陷阱。
- 首次请求不带游标:
db.Order("created_at DESC, id DESC").Limit(50).Find(&records) - 后续请求必须用上一页最后一条的
created_at和id:db.Where("created_at < ? OR (created_at = ? AND id < ?)", lastTime, lastTime, lastID).Order("created_at DESC, id DESC").Limit(50).Find(&records) -
created_at字段必须是DATETIME(3)或BIGINT时间戳,避免 MySQL 默认秒级精度导致大量重复 - 复合索引必须匹配排序顺序:
INDEX idx_created_id (created_at DESC, id DESC),否则WHERE + ORDER无法走索引
总数查询在 IoT 场景里要不要做
绝大多数物联网接口根本不需要精确总条数。设备数据是滚动窗口,用户关心的是“有没有下一批”,不是“一共多少条”。硬查 COUNT(*) 在百万级设备表上可能比主查询还慢。
- 推荐做法:查
size + 1条,比如要返回 50 条,就Limit(51),如果结果长度是 51,说明还有下一页,返回has_next: true;否则has_next: false - 如果业务强依赖总数(如后台报表),必须隔离会话:
db.Session(&gorm.Session{NewDB: true}).Model(&DeviceData{}).Where("device_id = ?", deviceID).Count(&total),否则Limit/Offset条件会污染 COUNT - 避免在 JOIN 多张设备关联表时用
Count()—— 它不会继承Joins,容易虚高,此时手写子查询更准:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM device_data d JOIN devices dev ON d.device_id = dev.id WHERE dev.status = ?) AS t", "online").Scan(&total)
Preload 关联设备元信息会翻车
物联网分页常需附带设备名称、型号等信息,但直接 Preload("Device") 会导致 N+1 查询或数据膨胀——GORM 先查 50 条上报记录,再为每条发一次 SELECT * FROM devices WHERE id = ?,50 次 round-trip 比单次 JOIN 还慢。
- 正确做法:用
Joins一次性 JOIN,再用Select控制字段:db.Joins("JOIN devices ON device_data.device_id = devices.id").Select("device_data.*, devices.name, devices.model").Where(...).Order(...).Limit(50).Find(&records) - 别让
Preload出现在分页链路里,它和Limit天然冲突;若必须用,先分页取 ID 列表,再批量查关联数据 - 设备元信息变化少,可考虑 Redis 缓存
device:id → {name,model},查完主数据后并行 Get,降低 DB 压力
游标分页不是“优化选项”,而是物联网数据分页的底线。最容易被忽略的是排序字段的索引覆盖和时间精度——没建对索引,游标就退化成全表扫;用错时间类型,游标就会在毫秒级重复里反复横跳。


















