前段时间,我们准备清理一批线上历史数据。

事情一开始看着并不复杂。表越来越大,存储成本在涨,查询和后续治理也越来越别扭,按常识说,这类过期数据早晚都要清。真正让我开始紧张的,是我发现这次要动的表,存的是操作行为数据,里面还挂着体检信息这类业务字段。到这一步,问题已经变了:这些东西现在到底算不算真的可以删。

那次之后,我重新理解了线上删除。线上删除最难的,往往不是执行动作本身,而是确定性:

  • 删的是不是业务上真的可以删的数据。
  • 影响面是不是已经看全了。
  • 真删错了,还有没有低成本收回来的路。

卡人的不是 SQL,是这批数据到底算什么

如果只站在数据库视角看,很多历史数据都像“年龄够大,可以清”。不过一旦数据本身还带业务语义,这个判断就没那么简单了。

像这次这批数据,虽然从时间上看已经很老了,但它记录的是操作行为,里面还有体检信息。只要这些字段还可能被业务解释、被排查链路回看、被统计口径引用,它就不只是“旧数据”,而还是业务对象的一部分。

我那次最直接的感受:线上删除很多时候是业务语义问题,不是数据操作问题。数据库能告诉你有多少行、分布在哪些表、索引怎么走,但它回答不了另一件更关键的事:

这批数据在当前业务阶段,到底是不是已经完成了自己的生命周期。

问题就卡在这儿。

这件事最后只能业务回答。所以像这种有业务语义的删除,技术侧自己拍板其实不够,最后还是得把 BG、产品和技术拉到一起确认:这批操作行为数据现在还承不承载业务含义,体检信息是不是已经彻底失效,哪些字段能删,哪些字段只能先下线不可见,哪些关联链路还要一起处理。

也是从那以后,我倾向于一个判断:有业务语义的删除,主路径应该优先收敛到业务入口,而不是直接从底层表开刀。因为只有业务入口最清楚几件事:

  • 删除条件到底是什么。
  • 关联对象有哪些。
  • 该做逻辑删除还是物理删除。
  • 下游要不要同步通知。
  • 出问题以后怎么补偿、怎么回滚。

我们先对齐的,不是脚本,而是删除边界

以前我也会下意识把删除理解成一个执行题:分批、限流、错峰、加监控,尽量把风险压下去。后来我发现,这些动作当然重要,但它们处理的是“怎么删得稳”。至于“这批东西到底能不能删”,得先对齐边界才行。

像这次这种数据,最该先对齐的其实是边界:

  • 哪些记录已经没有业务意义了。
  • 哪些字段虽然老了,但还会被统计、排查或补偿链路回头使用。
  • 哪些对象必须一起删,不然就会留下半套数据。
  • 哪些内容不适合一步物理删除,只能先做业务下线。

这些问题如果不先问清楚,后面的删除脚本写得再漂亮,也只是把一个判断错误执行得更稳定。

所以那次我后面最强烈的一个感受,不是“线上删除要谨慎”这种正确废话。更具体地说:删除这件事要先建立的,是边界确定性。先把边界收住,再谈执行策略。

我为什么不再把硬删当成默认答案

这次历史数据清理之后,我不再把线上删除默认理解成“一步删完”。

原因不复杂。很多删除场景里,可怕的不是删不掉,是删完以后才发现:

  • 还有链路在依赖。
  • 还有副本没处理。
  • 还有查询会命中。
  • 还有历史功能会回溯使用。

如果这时候走的是一步到位的硬删,系统几乎只能把全部压力留给备份恢复。可现实里更多遇到的是局部误删,比整库事故还难受:

  • 某一批删多了。
  • 某一类对象删错了。
  • 主数据删了,但关联数据没清完整。
  • 库里删了,缓存和索引还留着旧状态。

这类问题最麻烦的地方就在这里:它往往卡在恢复代价上——能恢复,但代价特别重。能恢复,不代表恢复成本低;有备份,也不代表业务真能扛得住恢复窗口。

我后来更愿意把成熟系统里的删除能力理解成一个过程能力,而不是一个最终动作。更稳妥的默认做法,通常应该是两阶段:

  1. 业务上先不可见、不可用。
  2. 经过观察期后,再做物理清理。

这个方案最有价值的地方,在于它把删除最现实的风险收回来了一点:后悔成本。如果删除过程本身是可控、可观察、可恢复的,很多线上风险其实是能被消化的;如果默认就是一步硬删,那系统就只能把确定性不足的问题,一次性推到最后。

备份和血缘都重要,但它们都不能替代删除判断

删除这类话题里,大家很容易先想到两个兜底词:备份,和血缘。

我对这两件事的边界感比以前强。

先说备份。备份当然重要,但它更多解决的是“理论可恢复”,并不天然等于“低成本、快速、细粒度恢复”。所以它更像删除安全的最后一道防线,而不是删除能力本身。更靠前的能力,应该是:

  • 删除前能评估影响面。
  • 删除中能分批、限流、观测。
  • 删除后能保留恢复窗口。
  • 真出问题了,备份再做最终兜底。

再说数据血缘、SQL 访问关系这类分析能力。它们也有价值,因为它们能帮你把依赖关系看得更全,降低“以为没关系,实际上还有链路在用”的风险。

但这类能力解决的是“看见关系”,解决不了“定义语义”。

它可以辅助你发现谁还在访问这批数据,却很难直接替你回答:

  • 这条数据现在到底该不该删。
  • 这个对象的生命周期是不是已经结束。
  • 哪些数据应该一起删,哪些数据应该保留。

这些问题最后还是业务定义,而不是访问关系自动推导出来的。所以我现在更愿意把备份和血缘都当成辅助能力,而不是主判断依据。它们都重要,但都不该替代“删除边界是谁定、怎么定、定完怎么执行”这件更靠前的事。

线上删除最后考验的,其实是系统成熟度

这次历史数据清理之后,我对删除这件事最大的变化是:我开始把它看成系统成熟度的一部分。

一个系统删除能力不成熟时,每一次历史数据清理都会让人紧张。删除语句谁都会写,让人紧张的是你不确定自己删的是不是完整,不确定影响面是不是已经收敛,不确定删错之后有没有低成本恢复路径。

反过来,一个成熟系统对删除的处理,至少应该具备几件事:

  • 删除语义是清晰的。
  • 删除入口是收敛的。
  • 删除过程是可控的。
  • 删除结果是可追溯的。
  • 删除错误是可恢复的。

做到这一步,删除才从“高风险人工操作”变成“系统内建能力”。

这也是我这次之后留下最深的一个判断:线上删除难的地方,在 delete 跑起来之前就定了——“删什么、谁来定、怎么删、删错了怎么办”这些问题,得在真正动手之前工程化地收住。