轨迹回放禁用limit+offset分页,因高偏移量导致扫描开销大、结果重复/丢失;必须用created_at+id组合游标分页,并显式order、独立count、参数校验。

车联网平台查轨迹回放,用 GORM 做分页不能只写 Limit + Offset —— 轨迹点数据量大、时间密集、查询频次高,直接 offset 翻到第 500 页就可能超时或丢数据。
为什么轨迹回放不能用 page=500&size=50 这种传统分页
车辆轨迹点通常每秒 1–10 条,一天就是百万级。MySQL 执行 OFFSET 24999(第 500 页 × 每页 50)时,会真实扫描并丢弃前 24999 行,不是跳指针。实测在 200 万轨迹表上,第 1000 页平均耗时 1.7s,CPU 占用飙升,且结果顺序不稳定(尤其主从延迟时)。
- 前端传
page=0或size=0,GORM 不报错但Offset(-50)直接 panic - 没加
Order("created_at ASC, id ASC"),同一页刷新两次,可能漏掉某条轨迹(因 MVCC 或索引扫描顺序变化) - 用同一个
*gorm.DB实例先Count()再Find(),Count会继承前面的Limit/Offset,总数永远是 50
游标分页必须用 created_at + id 组合,不能只用 created_at
单纯按 created_at DESC 分页,在高并发轨迹上报场景下,多个点可能落在同一毫秒(尤其使用 DATETIME(3) 时),导致 where created_at 漏掉部分同时间戳的点,或重复返回。
- 正确排序:
Order("created_at DESC, id DESC")——id是主键自增,天然唯一且有索引 - 首次请求不带 where,只用
db.Order("created_at DESC, id DESC").Limit(20).Find(&points) - 后续请求传
last_created_at和last_id,条件写成Where("created_at - 数据库字段必须建联合索引:
INDEX idx_created_id (created_at, id),否则游标失效
GORM 中 Count 总数怎么查才准又不拖慢查询
轨迹回放接口一般不需要精确总条数(用户不会翻到底),但前端常要显示“共 XXX 条”。硬扫全表 count 会锁表、拖慢写入。折中方案是用覆盖索引 + 估算,或缓存 + 异步更新。
- 简单准确法:用干净 DB 实例查
db.Session(&gorm.Session{NewDB: true}).Model(&Trajectory{}).Where("vehicle_id = ? AND created_at BETWEEN ? AND ?", vid, start, end).Count(&total) - 高性能法(推荐):提前建物化视图或定时任务把每日轨迹数写入
daily_trajectory_count表,查时用db.Raw("SELECT SUM(count) FROM daily_trajectory_count WHERE ...").Scan(&total) - 绝对避免:
db.Where(...).Count()复用主查询链——它会把Limit/Offset/Order全带进去,count 结果不可信
Preload 关联设备信息时,游标分页必须分两步走
轨迹回放常要连查车辆型号、司机姓名等,但 GORM 的 Preload 会破坏游标逻辑:它会在子查询里加 JOIN,导致 WHERE created_at 条件失效或重复计数。
- 第一步:用游标分页查出
[]uint64的id列表(纯 ID 查询最快) - 第二步:用
db.Where("id IN ?", ids).Find(&devices)批量加载关联数据 - 别用
Preload("Vehicle")直接套在游标查询上——GORM v2.6+ 在复杂 Preload 下会静默忽略游标条件 - 如果必须单次查完,改用
Joins("JOIN vehicles ON trajectories.vehicle_id = vehicles.id")+ 手写Select("trajectories.*", "vehicles.name"),但需确保ORDER BY字段仍来自trajectories表
真正卡住车联网轨迹分页的,从来不是语法会不会写,而是游标字段选错、索引没建对、Count 复用了 DB 实例这三处——它们不会报错,但会让第 100 页开始缓慢漂移、漏数据、前端反复刷新看到不同结果。











