
归档这件事几乎所有做后端的人都听过但真正动手的人不多动手之后没翻车的更少。我给不少团队处理过单表过亿之后的性能问题最后发现最省事的解法往往不是加索引、不是换硬件而是把不该留在这张表里的数据搬走——这就是 MySQL 数据归档策略要解决的核心问题让热表保持在一个可控的体积内让冷数据有地方待着并且随时能取回来同时把归档这个动作变成可定期执行、可回溯、可中断续跑的例行维护而不是某天夜里临时起意的一次性操作。这篇内容适合三类人看一是手上有一张几千万行、开始出现慢查询和 DDL 卡死的表的开发二是被要求数据不许删但库越来越慢的运维三是想提前把归档机制设计进表结构里的架构同学。下面我按为什么要归档—怎么选方案—两种主流方案怎么落地—出问题怎么查这条线把踩过的坑和可直接抄的配置都摊开讲。1. 想清楚再动手数据膨胀到底拖垮了哪些环节1.1 单表变大之后慢的不是查询本身很多人对表大了会慢的理解停留在全表扫描慢实际上数据量上来之后最先崩的是那些看起来跟数据量无关的操作。B 树层级从 3 层涨到 4 层每次回表多一次随机 IO原本 0.5ms 的点查变成 2ms单条看不出来QPS 上到几千就是压垮实例的最后一根稻草。更隐蔽的是buffer pool 命中率一张 80GB 的表热点数据只占 5%但二级索引的随机访问会把冷页不断换进内存把真正的热数据挤出去命中率从 99% 掉到 92%磁盘读 IOPS 直接翻几倍。再往后就是运维层面的痛。加一个索引要走在线 DDL几千万行还能接受几亿行的时候一次 DDL 要跑几个小时期间要拷一份全量数据到临时表磁盘空间得预留一倍。备份也是同理逻辑备份导出时间线性增长物理备份虽然快但恢复验证的窗口越来越长。还有一类容易忽略的统计信息不准。表大了之后采样页数相对变少优化器选错执行计划的概率明显上升同一条 SQL 昨天走索引今天走全扫很多人以为是数据库抽风其实就是统计信息漂移。最后是成本。SSD 存储不是免费的一条三年前的历史订单还躺在高性能盘上占着 buffer pool、占着备份带宽、占着 DDL 的时间窗口却一年被查不到两次。归档的本质是把数据的存储成本和处理成本跟它的访问频率对齐这件事不做所有优化都是在给一个持续膨胀的雪球做表面修补。1.2 归档、删除、备份是三件不同的事这三个概念经常被混着说但混着说就会出事。备份是为了应对灾难是全量快照目的是能恢复到某个时间点删除是数据不再需要物理上消失归档介于两者之间——数据不再参与日常业务但出于合规、审计、对账、客诉追溯等原因必须保留并且要能按条件查回来。我在评审归档方案时一定会先问一句这批数据将来还要不要按什么条件查如果答案是要按用户 ID 查某一年的订单那归档表就必须保留对应的索引不能简单导成 CSV 塞进对象存储如果答案是只在极端情况下按主键调一条那归档表甚至可以只保留主键和少量字段索引全部砍掉体积能压缩一半以上。还有一个边界是在线业务数据的保留期。这个期限不能由技术拍脑袋得业务和合规一起定。常见做法是按业务类型分档交易流水保留 3 年操作日志保留 6 个月消息通知保留 30 天埋点明细保留 7 天。把这个表先定死后面的所有技术选型才有依据。没定清楚就开工最大的风险是删完了业务方说还要查这时候只能从备份里捞那场面非常难看。1.3 先算一笔容量账再决定动不动手动手之前先算账这是我给自己定的规矩。不算账就上手很容易出现迁了三个月发现目标表也满了的情况。算法很简单日增行数 × 平均行长 ×1 索引膨胀系数× 保留天数。指标取值示例说明日均新增行数50 万行从监控里取近 30 天均值平均行长含行头约 300 字节AVG_ROW_LENGTH会偏大手工按字段估更准索引膨胀系数0.6二级索引总量与数据量之比可在information_schema.tables里核对一年总量1.8 亿行 / 约 86GB500000 × 365 × 300B × 1.6目标保留期90 天业务确认后的结果归档后目标体积4500 万行 / 约 21GB缩到四分之一算完之后你会发现两个结论特别有用一是保留期缩短一半体积直接减半谈判空间比技术优化大得多二是日增量决定了归档的执行频率一天 50 万行的量按每天一次、每批 2000 行来跑也就 250 个批次几分钟的事不需要搞得多复杂。真正难的是存量那一亿多行第一次全量搬迁得单独排期。这里有个实操上的建议第一次做存量清理时不要一次删干净。先按历史最久的那批数据做小范围试跑比如先归档 100 万行观察主从延迟、IO、锁等待跑通了再放量。归档任务和业务不同它跑得慢一点没人投诉跑出故障就是全站事故。2. 方案选型几条主流路径怎么挑2.1 分区表 DROP PARTITION最省事的路径但门槛在表结构如果这张表的生命周期从一开始就设计成按时间分片那归档就是最优雅的ALTER TABLE ... DROP PARTITION是元数据操作加文件删除几秒完成不产生逐行 binlog不会锁住大范围数据。这是我一直推的方案前提是你在建表阶段就把分区键设计进去。分区表有几个硬约束不讲清楚会踩坑。第一分区列必须包含在主键和所有唯一索引里MySQL 的分区是本地分区唯一性只能在分区内保证所以PRIMARY KEY (id)的表现在没法按create_time分区必须改成PRIMARY KEY (id, create_time)。这一改动会波及所有依赖单列主键的代码包括那些用INSERT ... ON DUPLICATE KEY UPDATE的地方。第二分区数不是越多越好单表分区建议控制在 50 到 100 个以内分区太多会导致打开表变慢、元数据锁竞争加剧。按月分区的话保留 24 个月就是 24 个分区完全够用。还有一类常见误区以为建了分区查询就自动变快。实际上分区裁剪只在 WHERE 条件里出现分区列时才生效WHERE id 123这种查询会扫描所有分区。所以分区表的代价是非分区列查询的性能可能会退化这一点必须在选型时评估清楚别等上线了才发现主键点查变慢了。2.2 归档表 定时搬迁不改表结构的通用解绝大多数线上表在建设初期都没考虑分区这时候改表结构成本极高通用的做法是建一张结构相同或精简的归档表用定时任务把过期数据搬过去搬完再删原表。这个方案对业务代码零侵入历史数据也还在同一个实例里能直接用 SQL 查对账和客诉场景很友好。它的代价是删除本身是有成本的。即使走索引DELETE也要维护二级索引、写 undo、写 redo、写 binlog物理空间不会立刻归还删完之后表里留一堆空洞还得靠重建表来整理。所以这个方案的成败全在批次控制上一批删多少、两批之间歇多久、怎么断点续跑这些细节决定了它是平稳维护还是灾难现场。后面的第 4 章我会把这个方案拆到可以直接照着写的程度。2.3 逻辑删除 冷热分离读多写少场景的折中有些业务不允许物理删数据比如涉及资金或者需要审计追溯的这时候可以在表里加一个is_archived标记位把过期记录标记掉查询侧统一加上过滤条件同时配合历史库路由应用层按时间判断超过保留期的查询打到另一个库或另一张表。这个方案的优点是删改都是软的随时可回滚缺点是表体积一点没减。标记位只是让数据逻辑上不存在索引照样维护buffer pool 照样占。所以我一般把它当过渡方案先用逻辑删除争取时间把历史库建起来、把查询路由改完再做物理搬迁。直接拿它当终态半年后你会发现表更大了因为标记位本身还要建索引。变种做法是冷热分表 读路由热表只放最近 90 天冷表放全量历史写入双写或者靠异步同步。这个方案对查询性能最友好但架构复杂度最高同步延迟和数据一致性都要额外处理中小规模的数据量不太值得。2.4 导出到外部存储成本最低查询最麻烦数据量大到 MySQL 已经不适合承载的时候就得出库了。常见的落地方式是先导成文本文件压缩后进对象存储需要时按分区拉回来导入临时表查询或者同步进列式存储的分析库专门应对聚合统计类的历史查询。这条路的成本优势非常明显冷数据放在低成本存储上一年能省下不少盘。但它有前提这批数据的查询模式必须是低频、批量、可接受分钟级延迟的。如果业务方随时可能按订单号查一条三年前的记录那出库就是给自己找麻烦每次查询都要拉起一次导入任务。我的经验是出库前先做三个月的访问日志统计看看历史数据的真实查询频次和查询条件用数据说话别用万一要查这种假设绑架架构。2.5 四种路径怎么选一张对照表说清楚维度分区表 DROP归档表 定时搬迁逻辑删除 冷热分离导出外部存储是否需改表结构需要主键含分区列不需要需要加标记字段不需要归档速度秒级分钟到小时级秒级仅打标小时级主库压力低元数据操作中到高取决于批次低中导出扫描空间是否释放是立即否需重建表否是需重建历史数据可查性需查分区或归档表好同库 SQL 直查好差需导入回滚难度高分区删了不好恢复低先写后删极低低适用场景新建表、时间序列类存量表、通用场景合规要求不能删超大规模、低频查询选型的判断顺序我一般是这样新表优先考虑分区一劳永逸存量表走归档表搬迁风险可控有合规硬约束的先用逻辑删除过渡单表超过百 GB 且历史查询极少的认真评估出库。这几种不是互斥的很多团队最后是分区 归档表 出库三级组合热数据在分区表里滚动温数据进归档表冷数据进对象存储。3. 实操一分区表归档的完整落地流程3.1 表结构改造与分区键的选择先看一个可用的建表语句按订单创建时间按月分区CREATE TABLE t_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, order_no VARCHAR(32) NOT NULL, amount DECIMAL(12,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id, create_time), UNIQUE KEY uk_order_no (order_no, create_time), KEY idx_user_time (user_id, create_time), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 PARTITION BY RANGE COLUMNS(create_time) ( PARTITION p202601 VALUES LESS THAN (2026-02-01), PARTITION p202602 VALUES LESS THAN (2026-03-01), PARTITION p202603 VALUES LESS THAN (2026-04-01), PARTITION pmax VALUES LESS THAN (MAXVALUE) );几个细节值得单独说。用RANGE COLUMNS(create_time)而不是RANGE(TO_DAYS(create_time))前者可读性好、边界直观运维手工加分区的出错概率低很多而且TO_DAYS在写边界值时很容易算错一天导致数据落到错误分区。另外一定要留一个pmax兜底分区防止某天忘了提前建分区插入直接报Table has no partition for value那是直接写失败的业务故障。如果这张表已经有存量数据要改成分区表ALTER TABLE ... PARTITION BY会重建整张表几个小时的锁表时间跑不掉必须用在线 DDL 工具做影子表迁移并且提前评估磁盘空间需要预留约 1.2 倍表体积。这个操作我建议放在业务低峰期并且先在从库或者预发环境完整演练一遍把耗时摸清楚。3.2 分区滚动维护与调度方式分区表的日常维护就两件事提前加分区、到期删分区。加分区用REORGANIZE PARTITION把pmax拆开ALTER TABLE t_order REORGANIZE PARTITION pmax INTO ( PARTITION p202604 VALUES LESS THAN (2026-05-01), PARTITION pmax VALUES LESS THAN (MAXVALUE) );删分区就是一行ALTER TABLE t_order DROP PARTITION p202501;注意REORGANIZE会把pmax里已有的数据重新分配到新分区如果pmax里已经堆了大量数据这个操作会很慢。所以提前建分区是关键一般提前 3 到 6 个月建好pmax永远保持空的状态REORGANIZE就是秒级操作。调度方式上我不建议用 MySQL 自带的EVENT SCHEDULER。原因很实际event_scheduler参数在很多托管环境里默认关闭或者不允许改事件执行失败的错误信息藏在information_schema.events里没人会去看等发现的时候往往已经出事了。更稳妥的是外部调度——定时任务平台、调度系统或者简单的 crontab执行一个脚本脚本里做三件事检查下个月分区是否存在、不存在就创建、再检查最老的分区是否超过保留期、超了就归档后删除。脚本里每个 DDL 都要记日志失败要告警这样出问题第一时间能知道。还有一个容易忽略的点DROP PARTITION只记录 DDL 语句到 binlog从库执行同样的 DDL 时也要删文件。如果从库磁盘空间比主库紧张可能出现主库删成功、从库因空间不足或延迟导致复制中断的情况。所以归档操作前先确认所有从库的空间水位这是我踩过的坑后面第 5 章会细说。3.3 用 EXCHANGE PARTITION 做先备份再删DROP PARTITION是不可逆的手一抖删错分区就只能从备份恢复。生产上我一般加一道保险用EXCHANGE PARTITION把待删分区换到一张独立的归档表里导出之后再删。-- 建一张结构完全一致的非分区表 CREATE TABLE t_order_arch_202601 LIKE t_order; ALTER TABLE t_order_arch_202601 REMOVE PARTITIONING; -- 把分区数据换出来秒级完成 ALTER TABLE t_order EXCHANGE PARTITION p202601 WITH TABLE t_order_arch_202601; -- 此时 t_order_arch_202601 里就是那个分区的全部数据 -- 导出、压缩、上传确认无误后再删归档表 DROP TABLE t_order_arch_202601;EXCHANGE PARTITION有个硬性要求两张表的结构、索引、存储引擎必须完全一致LIKE建出来的表带着分区定义必须先REMOVE PARTITIONING否则报错。另外这个操作需要持有表的元数据锁执行前确认没有长事务、没有大查询否则会把整个表的写入堵住。我通常把这一步做成导出到文件而不是保留在库里因为归档表留在同一个实例上磁盘该占还是占起不到降本的作用。导出用SELECT ... INTO OUTFILE或者逻辑导出工具都行关键是导出完做一次行数和校验和比对确认文件完整再删表。这个校验环节别省我见过导出中途磁盘写满导致文件截断删完分区才发现备份是坏的。4. 实操二无分区表的定时搬迁归档4.1 归档表结构与水位线设计存量表不能改分区就走搬迁路线。第一步是建归档表原则是结构够用就好索引能砍就砍CREATE TABLE t_order_archive ( id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, order_no VARCHAR(32) NOT NULL, amount DECIMAL(12,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 ROW_FORMATCOMPRESSED KEY_BLOCK_SIZE8;几个设计取舍要交代清楚。主键用原表的id不带自增保证搬迁过程中重复执行不产生重复行还能直接用INSERT IGNORE实现幂等。归档表砍掉了update_time和部分业务上不需要的字段行更窄。ROW_FORMATCOMPRESSED能省 30% 到 50% 的空间代价是读取时多一次解压 CPU 开销——归档表读得少这个交换划算。如果你的 MySQL 版本支持页压缩也可以考虑COMPRESSIONzlib效果取决于数据特征上线前拿 100 万行实测一下压缩比再决定。然后是水位线表用来记录每张表迁到哪儿了CREATE TABLE archive_watermark ( table_name VARCHAR(64) NOT NULL PRIMARY KEY, last_id BIGINT UNSIGNED NOT NULL DEFAULT 0, cutoff_date DATE NOT NULL, batch_size INT NOT NULL DEFAULT 2000, status TINYINT NOT NULL DEFAULT 0, -- 0 空闲 1 运行中 2 暂停 updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB;水位线的意义在于断点续跑。归档任务随时可能因为主从延迟、磁盘告警、发布重启而中断没有水位线的话第二天要么从头扫一遍要么靠肉眼判断进度。有了last_id任务重启后从上次的位置继续逻辑简单排查也简单。这个表虽小但我认为它是整个方案里最关键的设计比批次大小、索引优化都重要。4.2 分批搬迁的执行逻辑与索引利用先说一个反直觉的结论按create_time分批删除在数据分布均匀时反而更慢。原因是WHERE create_time 2026-01-01 ORDER BY id LIMIT 2000这个写法优化器可能选择idx_create_time扫出一大堆行再回表排序取前 2000 条每次都要重复扫描前面那些已经处理过的行越往后越慢。更稳的做法是按主键区间推进先用一次查询确定范围再按主键游标分批-- 一次性确定起点和终点走主键 SELECT MIN(id), MAX(id) FROM t_order WHERE create_time 2026-01-01; -- 循环执行每次只处理一个主键区间 SELECT id, user_id, order_no, amount, status, create_time FROM t_order WHERE id :last_id AND id :last_id :step AND create_time 2026-01-01 ORDER BY id LIMIT 2000;拿到结果后先批量写归档表再用这批的 id 范围做删除INSERT IGNORE INTO t_order_archive (id, user_id, order_no, amount, status, create_time) VALUES (...), (...), ...; DELETE FROM t_order WHERE id :last_id AND id :last_id :step AND create_time 2026-01-01;这里有两个细节。第一先写后删顺序绝对不能反。写完立刻删、删完才写中间崩了就是数据丢失这在归档场景里是不可接受的。第二删除条件里带上id范围是为了让删除也走主键索引避免优化器选错索引导致大范围扫描加锁。批次大小怎么定我的经验值是从 1000 到 2000 行起步观察三项指标单批耗时、主从延迟、锁等待。单批耗时控制在 200ms 以内比较舒服超过 500ms 就减半从库延迟超过 2 秒就加 sleep。一个可以抄的参数组合是每批 2000 行批间 sleep 0.2 秒每秒吞吐大约 5000 到 8000 行一亿行需要四到六个小时。这个速度不算快但胜在平稳业务几乎感知不到。4.3 任务编排、限流与安全检查把上面的逻辑包成脚本外层做几件事只读时间窗口只在低峰期运行比如凌晨 1 点到 5 点到点无论跑没跑完都主动退出第二天从水位线继续。前置检查启动前检查从库延迟、磁盘使用率、当前是否有大事务任意一项不达标就退出告警不硬跑。动态限流每批结束后读取从库延迟超过阈值就 sleep 加倍恢复后逐渐减小做成自适应。异常熔断连续 3 批失败直接中止并把状态置为暂停等人介入不要无限重试把数据库打垮。交叉校验每天归档完成后对比源表减少行数与归档表增加行数不一致就告警。关于限流还有一个更精确的做法直接监控SHOW REPLICA STATUS里的Seconds_Behind_Source。这个值在有大事务时会跳变单看数值容易被误导更可靠的是看Relay_Log_Pos的推进速度只要还在推进就说明没卡死。如果你们用的是并行复制还要注意WRITESET模式下大事务依然是单线程回放所以批次不能太大是根本任何限流参数都是辅助。最后是校验。搬迁完成后至少做三次核对源表COUNT(*)与归档表行数之和是否等于原始总量、随机抽 100 条按主键在两边比对、对金额字段做SUM()对比。金额类字段的和如果有差异说明有数据被少搬或者重复搬必须停下来查清楚。我见过一次因为INSERT IGNORE遇到主键冲突静默跳过导致少量记录被删掉而在归档表里不存在如果没有做行数对账这批数据就永久丢了。5. 常见问题与排查技巧实录5.1 主从延迟、空间不释放、锁等待三类高频事故主从延迟是归档场景的第一号杀手。原因不复杂批量删除产生大事务从库在WRITESET并行的前提下同一个事务依然只能一个线程回放一个删 10 万行的事务回放要几十秒后面的日志全部堵住。排查方法是对比主库SHOW MASTER STATUS的Position和从库的Exec_Master_Log_Pos差距在拉大就说明在追。解法就一个字拆。批次调小、批间 sleep必要时在从库上临时调大innodb_lock_wait_timeout和并行复制线程数但根本还是控制事务大小。空间不释放是新手最容易困惑的地方。删了 3000 万行df -h一看磁盘还是满的。这是 InnoDB 的表空间管理机制决定的删除只是把页标记为可复用文件不会缩小而且删除产生的空洞会让后续插入出现页分裂索引体积反而可能变大。解决办法是重建表ALTER TABLE ... ENGINEInnoDB或者用在线 DDL 工具做重建期间需要额外一份空间。所以做归档规划时磁盘必须预留归档前体积 归档后体积的总和不能按归档后的体积算。锁等待和死锁主要出现在删除语句走错索引的时候。如果DELETE的 WHERE 条件没命中索引InnoDB 会扫描大量行并给它们加锁一旦和其他事务交叉就死锁。排查靠information_schema.innodb_trx和performance_schema.data_locks看哪个事务持有了大量行锁。预防的办法很直接执行删除前先EXPLAIN一遍确认key是主键或者时间索引rows在几百这个量级不是几十万。还有一类问题比较隐蔽归档任务本身把undo撑爆了。长事务不让 purge 线程回收旧版本history list length持续上涨磁盘上的undo表空间疯长。监控这个指标的方法很简单SHOW ENGINE INNODB STATUS里的History list length超过百万就要警惕。根本解法还是缩短事务别指望调大innodb_purge_threads能救回来。5.2 问题速查表现象大概率原因排查动作处理办法从库延迟持续增大单事务过大回放跟不上对比主从位点、看事务内行数批次降到 1000 以下加 sleep删除后磁盘不降InnoDB 不回收文件查data_free字段低峰期重建表或在线 DDL删除语句长时间不返回走错索引全表扫描加锁EXPLAIN看key和rows补索引或改按主键区间删归档表行数少于预期INSERT IGNORE静默跳过冲突对账 抽主键比对改用REPLACE或先查后插归档任务重复执行插入重复水位线没更新或未提交查水位线updated_at加唯一键写入与水位线同事务undo表空间疯涨长事务阻塞 purge看History list length拆分事务缩短单次运行时长DDL 把写入堵住长事务持有 MDL 等待查metadata_locks表先 kill 长事务再用在线 DDL分区插入报无分区错误pmax被删或未提前建分区查PARTITIONS表立即补建pmax分区时间边界数据算错时区不一致或边界值取整核对time_zone与数据统一 UTC 存储边界写清楚5.3 几个文档里不会写的实操心得第一个心得归档任务要有开关。不要把调度时间写死在代码里做成配置项出问题的时候能一键停掉。我经历过一次归档任务和一次数据修复脚本撞车两边都在删同一批数据行数对不上查了两个小时才发现。现在我的做法是在水位线表里加status字段脚本每批开始前读一次状态不是运行中就立刻退出运维改一个字段就能停。第二个心得首次全量归档单独排期不要放在日常调度里。存量几千万行的第一次搬迁耗时是日常任务的几十倍如果混在每日任务里跑很容易跑到业务早高峰还没结束。做法是首次执行时把时间窗口放宽到整个夜间并且临时调小批次、加大 sleep宁可跑两个晚上也不要在白天占资源。第三个心得归档之前先做索引瘦身。有些表的索引是历史遗留五个索引里三个没人用这些索引在归档搬迁时全都要维护一遍白白增加成本。动手前用performance_schema.table_io_waits_summary_by_index_usage查一下索引使用情况没用的先删掉搬迁速度能快不少归档表的索引也能更精简。第四个心得给归档数据留一条回捞通道。业务方偶尔会要求把某批已归档的数据恢复回主表比如某个客户投诉需要重新核对。如果没有现成通道每次都要手工写 SQL很容易出错。我一般会准备一个反向脚本按主键范围从归档表搬到主表INSERT IGNORE保证幂等跑之前先确认主表没有同 ID 的行。这个脚本平时不用但需要的时候能救命。第五个心得比较朴素归档不是一次性工程是例行维护。做完第一次全量清理很多人就以为任务结束了结果半年后表又涨回去了因为日常归档任务没配、或者配了但没人看告警。我的建议是把归档任务的执行结果接进日常巡检报表每天看一眼昨天归档了多少行、有没有失败这个动作花不了一分钟但能避免半年后再来一次大手术。6. 关于保留期和节奏的一点个人看法做了这么多轮归档我最大的感受是技术方案从来不是难点难的是把保留期这件事说清楚并且落成制度。技术上删一批数据和留一批数据成本差不了多少但业务上一句先都留着吧就能让表在两年内涨十倍。所以我现在的习惯是在设计阶段就把数据分级写进文档哪些表是流水型、保留多久、归档到哪儿、谁能查、查的时候走什么通道。这份东西定下来归档策略才有稳定的输入否则每次都要重新讨论一遍讨论完还是那句话——先留着吧。另外一个体会是关于节奏。归档任务最怕的不是慢是不稳定。一个每天跑五分钟、从不失败的任务比一个偶尔跑两小时、时不时卡住的任务有价值得多。所以参数宁可保守批次宁可小一点sleep 宁可长一点把稳定性放在吞吐量前面。真遇到存量太大跑不完的情况多跑几天就是了数据又不会跑。最后分享一个我常用的检查清单每次上线归档任务前过一遍保留期是否经过业务确认、归档表是否有唯一键保证幂等、水位线是否和写入在同一事务、批次大小是否经过压测、从库空间是否足够、失败是否有告警、有没有一键停止开关、对账脚本是否跑通、回捞脚本是否验证过。这九条里任何一条没过我都会把上线时间往后推。看起来繁琐但每一条背后都是一次真实的故障。