资产折旧历史不能裸用offset分页,因时间密集写入导致depreciation_date不唯一,仅按该字段排序会跳页或重复;mysql扫描大偏移量性能骤降,且未加id等二级排序和联合索引将引发数据错乱。

资产折旧历史这类数据具备「时间密集、写入频繁、查询靠后页多」的特点,用 Limit + Offset 直接分页极易漏记录、查得慢,甚至翻到第 50 页就超时。必须显式排序 + 独立总数查询 + 校验参数,大偏移量场景下应直接切游标分页。
为什么资产折旧历史不能裸用 Offset 分页
折旧记录通常按 asset_id 和 depreciation_date 组合插入,高频写入下,同一秒可能产生多条记录;若只 Order("depreciation_date DESC"),数据库无法稳定排序,导致同一页重复或跳过某期折旧。MySQL 对 OFFSET 10000 的处理是真实扫描并丢弃前 10000 行——而折旧表动辄几十万行,第 200 页响应可能从 20ms 拉到 2s+。
- 常见错误现象:
page=200&page_size=20返回空切片,但实际数据存在;刷新两次,id=12345这条记录有时在第 199 页,有时在第 201 页 - 根本原因:没加二级排序字段(如
id DESC),且depreciation_date无唯一性约束 - 性能临界点:MySQL 下
OFFSET > 5000就该警惕,PostgreSQL 下建议> 10000强制切游标
正确实现传统分页的三步硬要求
适用于后台管理类接口(如财务人员查某资产全部折旧明细),且页码基本不超前 30 页的场景。
- 参数校验必须前置:
page小于 1 时设为 1;page_size超过 100 就截断(如limit := min(p.PageSize, 100)),避免恶意传page_size=10000 - 排序必须复合:
Order("depreciation_date DESC").Order("id DESC"),确保depreciation_date相同时靠id定序;对应字段需建联合索引:INDEX idx_asset_date_id (asset_id, depreciation_date, id) - 总数查询必须隔离:
db.Session(&gorm.Session{NewDB: true}).Model(&Depreciation{}).Where("asset_id = ?", assetID).Count(&total),否则复用主查询链会把Limit/Offset带进Count,返回永远 ≤page_size
游标分页怎么写才不丢折旧期
面向前端“加载更多”或导出全量折旧历史时,必须用游标。核心是传递上一页最后一条的确定性字段值,绕过 OFFSET 扫描缺陷。
- 首次请求不带游标:
db.Where("asset_id = ?", assetID).Order("depreciation_date DESC, id DESC").Limit(20).Find(&deps) - 后续请求传
last_cursor(格式:"2024-06-30T00:00:00Z,12345"):db.Where("asset_id = ? AND (depreciation_date - 关键细节:前端必须原样透传
last_cursor,后端不做任何解析转换;若date或id解析失败(如非时间格式、负数id),直接返回400 Bad Request,不 fallback - 索引必须覆盖:
INDEX idx_asset_cursor (asset_id, depreciation_date, id),否则 WHERE 条件无法走索引,游标分页也变慢
Count 总数查询容易被忽略的坑
资产折旧历史常需精确总期数(比如确认是否已提足折旧),但 Count 写错会导致财务对账偏差。
- 别写:
db.Where("asset_id = ?", assetID).Limit(20).Offset(40).Count(&total)—— 它返回的是 20,不是总期数 - 简单关联场景(如要统计已审核的折旧期):
db.Session(&gorm.Session{NewDB: true}).Model(&Depreciation{}).Where("asset_id = ? AND status = ?", assetID, "approved").Count(&total) - 复杂条件(如含
Joins("JOIN assets...")):db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM depreciations d JOIN assets a ON d.asset_id = a.id WHERE a.category = ? AND d.status = ?) t", category, "approved").Scan(&total) - 如果业务允许模糊感知(如“还有下一页”即可),可查
page_size + 1条,省掉Count—— 折旧历史这种强一致性场景慎用
最易被忽略的是排序字段索引缺失和游标解析逻辑松散:没索引,游标分页比 Offset 还慢;解析时容忍空字符串或非法时间,会导致 WHERE 条件恒假,整页数据消失。这两点线上一出问题,财务系统直接误报折旧差额。











