GORM默认Limit/Offset分页在物流追踪等高频深分页场景下性能断崖式下跌,必须改用游标分页或覆盖索引优化;因数据库执行OFFSET时需真实扫描并丢弃前N行,而非跳过,即使有索引,第1000页响应也常超2秒。

直接说结论:GORM 默认的 Limit/Offset 分页在物流追踪这种高频、深分页(如第1000页 × 每页20条)场景下会严重拖慢查询,必须改用游标分页(WHERE id > ? ORDER BY id LIMIT ?)或覆盖索引优化。
为什么 Offset 越大越慢?
MySQL/PostgreSQL 执行 OFFSET 20000 时,并非跳过前2万行再取数据,而是先扫描并丢弃前2万行——每页都重做全量偏移计算。物流节点表通常有千万级记录、高写入频次,OFFSET 在第500页后响应常超2秒。
- 真实错误现象:
SELECT * FROM tracking_events ORDER BY created_at DESC LIMIT 20 OFFSET 19980执行时间从 12ms(第1页)飙升至 1840ms(第1000页) - 根本原因:数据库无法利用索引跳过已跳过的行,必须逐行计数
- 即使加了
created_at索引,OFFSET仍需回表或扫描大量索引项
GORM 游标分页实操(推荐)
用上一页最后一条记录的排序字段值作为下一页起点,避免 OFFSET。GORM 本身不内置游标支持,需手动拼接条件。
- 假设按
id降序分页,上一页最后一条是id = 123456,则下一页查:db.Where("id - 必须确保排序字段(如
id或created_at)有唯一性或配合其他字段去重(例如ORDER BY created_at DESC, id DESC),否则可能漏数据或重复 - 首次请求无游标值,用
ORDER BY id DESC LIMIT 20获取第1页;后续所有请求带last_id参数 - 不要用
Find+Count做总页数——物流场景用户根本不需要“共多少页”,删掉Count可省掉一次全表扫描
索引必须覆盖排序+查询字段
游标分页性能高度依赖索引设计。若查的是 status 和 created_at,但只对 created_at 建索引,仍会回表。
- 正确做法:建复合索引,把排序字段放最左,查询字段放
INCLUDE(PostgreSQL)或直接加入索引列(MySQL):CREATE INDEX idx_tracking_cursor ON tracking_events (created_at DESC, id DESC) INCLUDE (status, node_name);(PostgreSQL) - MySQL 替代方案:
CREATE INDEX idx_tracking_cursor ON tracking_events (created_at DESC, id DESC, status, node_name); - 验证是否走索引:用
EXPLAIN看type是否为range,Extra是否含Using index - 别忽略
WHERE条件字段——如果前端总带WHERE carrier_id = ?,索引必须以carrier_id开头,再接排序字段
游标分页不是“换种写法就变快”,它要求排序字段稳定、不可为空、最好单调递增(如自增ID),且索引必须精确匹配查询模式。物流系统里 created_at 可能重复,务必补上 id 做二级排序,否则分页错乱很难排查。


















