必须物理隔离冷表并设计游标分页:因混合表中OFFSET拖慢热查询且无法索引剪枝;冷数据需用created_at+id双条件游标、禁用COUNT(*)而查archive_stats或information_schema;API须路由解耦,响应头标注归档表。

冷热数据混在同一张表时,用 OFFSET 分页查归档数据(比如 3 年前的订单)会严重拖慢热数据查询,且无法利用时间分区或索引剪枝——必须物理隔离冷表,再针对性设计分页逻辑。
为什么不能在混合表里直接加 WHERE created_at
即使加了 created_at 条件,MySQL 仍可能全表扫描后过滤,尤其当该字段没建联合索引、或存在大量热数据干扰时;OFFSET 越大,跳过的行越多,性能越不可控;GORM 的 Offset((page-1)*size) 不会自动识别“归档区间”,它只按最终结果集算偏移,导致查冷数据时也得扫完所有热记录。
- 常见错误现象:
db.Where("created_at 在千万级混合表上执行超 3 秒,且 CPU 持续飙升 - 根本原因:没有物理隔离,数据库优化器无法跳过热数据块,索引失效风险高
- 正确做法:归档数据必须迁出到独立表(如
orders_archive_2022),并确保该表有PRIMARY KEY(id)或INDEX(created_at, id)
冷表分页必须用游标,且游标字段要带时间精度
冷数据写入已停止,但排序仍需稳定——只靠 id 可能不够,因为归档表往往按时间批量导入,id 不连续;推荐用 created_at + id 组合游标,且 created_at 字段类型必须是 DATETIME(3) 或 BIGINT 时间戳,避免秒级精度下多条记录时间相同导致漏数据。
- 首次请求:
db.Table("orders_archive_2022").Order("created_at ASC, id ASC").Limit(20).Find(&orders) - 后续请求:
db.Table("orders_archive_2022").Where("created_at > ? OR (created_at = ? AND id > ?)", lastCreatedAt, lastCreatedAt, lastID).Order("created_at ASC, id ASC").Limit(20).Find(&orders) - 别用
WHERE created_at >= ?——等于号会重复返回同时间点的第一条 - 前端必须透传
last_created_at和last_id两个值,后端不做默认 fallback
总数统计不能走 COUNT(*),改用预估或元数据
冷表虽不写入,但单次 COUNT(*) 在亿级归档表上仍可能耗时数秒;更糟的是,如果用了分区表(如按月分区),COUNT(*) 会跨所有分区扫描,哪怕你只查其中一张子表。
- 可靠方案:建一张
archive_stats表,每次归档完成时写入对应表的精确总行数,分页时直接查该表:db.Table("archive_stats").Where("table_name = ?", "orders_archive_2022").Select("row_count").Scan(&total) - 轻量方案:用 MySQL 的
information_schema.TABLES查近似值(误差 ≤ 5%):db.Raw("SELECT table_rows FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?", "mydb", "orders_archive_2022").Scan(&approxTotal) - 绝对不要:
db.Table("orders_archive_2022").Count(&total)——它会触发全表扫描,且 GORM v2 默认不走覆盖索引
GIN 接口层要显式路由冷热路径,禁止参数混用
同一个 /api/orders 接口若靠 query 参数(如 ?archive=true)分流,容易导致缓存污染、中间件误判、审计日志难追踪;应拆成明确路由,让冷热查询彻底解耦。
- 热数据接口:
GET /api/orders→ 走主表 +OFFSET分页(配合强索引和 size 限流) - 冷数据接口:
GET /api/orders/archive/2022→ 强制绑定归档年份,校验该表是否存在,再走游标分页 - 禁止在冷接口里支持
page参数——游标分页下page=100无意义,也防恶意构造超大 offset - 冷接口响应头加
X-Archive-Table: orders_archive_2022,便于网关和监控识别流量归属
真正难的不是写对 SQL,而是让冷表结构、索引设计、API 路由、运维统计全部对齐——少一个环节,游标就可能断链,预估总数就会漂移,归档收益就归零。











