归档表必须与原表结构完全一致,使用InnoDB引擎并建立必要索引,通过archive_progress表记录断点、专用模型绕开全局作用域,避免在请求中实时归档。

归档表结构怎么设计才不踩坑
冷热数据分离的前提是归档表和原表结构一致,否则 INSERT ... SELECT 或模型批量写入会报字段错。别想着“少存几个字段省空间”——时间戳、软删除字段(delete_time)、业务状态字段都得保留,否则查历史数据时逻辑对不上。
常见错误:归档表没加索引,归档后查某用户历史订单巨慢;或者归档表用了 MyISAM 引擎,导致事务中归档失败时主表也回滚不了。
- 归档表必须用 InnoDB,且主键、查询常用字段(如
user_id、create_time)建好索引 - 不要删原表的触发器或事件调度器依赖的字段,比如有定时任务靠
update_time判定是否归档,你把这字段去掉了,任务就停摆 - ThinkPHP 6+ 建议用迁移命令生成归档表:
php think migrate:create create_archive_order_table,避免手动建表漏掉默认值或字符集
用 Db::query() 还是模型 select() 做归档
直接执行 SQL 归档快,但绕过模型事件和自动处理;用模型归档安全,但大数据量下内存溢出风险高。关键看数据规模和一致性要求。
场景举例:每天归档 10 万条订单,要求不能丢数据、要记录归档批次号、失败要重试——这时候必须走模型,靠 chunk() 分批 + 事务控制。
立即学习“PHP免费学习笔记(深入)”;
- 小批量(Db::execute('INSERT INTO archive_order SELECT * FROM order WHERE create_time 最省事
- 大批量或需审计:用
OrderModel::where('create_time', 'chunk(1000, function ($orders) { ArchiveOrderModel::insertAll($orders->toArray()); }) - 注意
chunk()默认不包含软删除数据,如果原表有delete_time IS NOT NULL的记录也要归档,得显式加上withTrashed()
归档时机选 Cron 还是请求中触发
绝对不要在用户下单、编辑等接口里实时归档。哪怕只归 100 条,遇到高峰也可能拖慢整个响应,还可能因超时导致部分归档成功、部分失败。
ThinkPHP 自带的命令行调度足够用,配合 Linux cron 每日凌晨跑一次最稳。难点在于怎么判断“哪些该归”,而不是“要不要归”。
- 用独立字段标记归档状态,比如加个
archivedtinyint(1) 默认 0,归档成功再更新为 1,避免重复归档 - 别用
create_time 这种动态条件——万一某天任务卡住没跑,第二天补跑就会漏掉中间三天的新数据 - 推荐方案:维护一张
archive_progress表,记录上次归档到的id或时间点,每次从断点继续
scope 和 globalScope 对归档的影响
如果你在模型里写了全局作用域(globalScope),比如自动过滤 status = 1,那归档查询时也会被套上这个条件——结果就是连已关闭的订单都查不到,归档数据永远缺一块。
ThinkPHP 6.1+ 支持临时取消全局作用域,但很多人不知道怎么关干净。
- 归档前必须调用
withoutGlobalScope(),且要传具体作用域名,比如withoutGlobalScope('statusFilter'),不能只写withoutGlobalScope()(它不生效) - 如果用了闭包定义的作用域,得在定义时给个名字:
protected $scope = ['active' => function ($query) { ... }],否则无法精准关闭 - 更稳妥的做法:归档专用模型继承原模型,然后在新模型里重写
baseQuery()方法,彻底绕开所有作用域
归档不是一劳永逸的事,真正麻烦的是后续的数据一致性校验和跨表关联查询。比如用户中心要同时展示近三个月订单和历史归档订单,JOIN 两个库的表时,字段类型稍有差异(比如一个用 datetime 一个用 timestamp),MySQL 就可能拒绝关联。这种细节,上线前不压测根本发现不了。



















