01为什么改键等于换 ID

canonical_product_id 是组键的哈希,纯函数、确定性、跨日稳定。这三条是它的优点,也决定了它的脆弱面:键的定义一变,所有组键串都变,哈希出来的 ID 全部换代。

换代不是“新旧 ID 并存”,而是断链:历史分区里的 ID 和新分区里的 ID 指不出同一个商品。于是所有按商品串联的跨日特征——价格指数、榜单动态、跨源价差趋势——在历史与新数据之间无法衔接,只能重放。

有一类特征不受影响:按 (source, source_sku_id, dt) 这种页面级 SKU 跨日比较的价格变动表,它不经过商品身份。所以重放之前先分清楚:哪些下游依赖身份,哪些只依赖页面。这决定了要不要动它们。

02重放的第一步不是跑脚本,是清断点

链路有一套“完成标记”(断点续跑机制),粒度是 日期 × 阶段 二维。它平时是好事:某天某段失败只补那一段。

但在全量重放场景下它会骗人:所有历史日期上早就落着 .done 标记,于是重跑时每一段都被判定“已完成”而跳过,脚本以成功状态结束,实际一行都没重算。

判断标准很简单:**全 SKIP 的重放没有验证价值。**所以正式重放第一步是清掉标记目录。更根本的是,“跳过”必须是可区分的状态——SKIP 和 SUCCESS 混在一起报“成功”,是这类编排脚本最危险的默认行为。

03字段宽度也是一份契约

新键定义引入了新的匹配层级值,而这个值要落到一张已有表的一列里。那一列原本是短 varchar,装不下更长的新值。

Doris 的行为是行级过滤 + 零容忍的过滤比例上限,于是整批装载失败,链路全红。

这次“报错”其实是好事:它比静默截断诚实得多。真正难查的版本是截断之后不报错,你只会在结果里看到某个层级值莫名变成了另一个。

处置不只是 ALTER 加长:在 doris/ddl/migrate/ 下留一个按编号可重放的迁移脚本,并把“改表结构要同时改 DDL 和 ALTER”写进规范。参见 口径只能有一个来源 里那条 DDL 教训。

04旧分区的 schema 残留

重放读历史时撞上另一个问题:早期写出的分区是旧列结构(列数更少),而读取按当前 schema 做整表读,直接冲突。

原因是写入是覆盖式的,但列集合的变化不会自动清理历史文件。最后只能删掉受影响的旧分区再重建。

这条给了一个更普遍的检查项:schema 变更后,历史数据的可读性要单独验证——“新数据能写”不代表“历史能读”。

05补完历史,必须再重载最新一天

有一张维表是全量替换语义(先清空再装载),且没有日期分区。

重放历史时,每个历史日都会把这张表刷成“那天的状态”。全部跑完之后,线上这张表停在的是最后一个被重放的历史日,而不是今天。

所以铁律是:**任何全量替换型装载,重放历史之后必须再对最新日期执行一次。**最后写入者胜——这句话在实时链路里是常识,在批处理回填里很容易被忘掉,因为“回填”这个词给人的直觉是“补旧数据”,而它实际上会动当前线上数据。

06重放期间的假成功

除了 SKIP 掩盖,还有一处很隐蔽:某个阶段用 ... | grep ... || true 取过滤结果。|| true 让管道的真实退出码被吞掉,于是阶段失败也被记成成功。

这与装载层的 label 假成功是同一类问题(见 Doris Stream Load 的假成功):**编排脚本里每一个“忽略错误”的写法,都必须能说清楚它在忽略哪一种失败。**说不清楚的,就改成显式取退出码。

07重放之后:结论要不要重写

好消息是核心论据没塌。用同一份 SQL、相邻日期、只把键定义作为唯一变量做对比,结论在新口径下仍然成立——代价是跨源可配对样本减少,这是精度换召回的预期代价,不是异常。

但有一件事必须诚实标注:新键的验证目前只覆盖了单日样本,全历史重放完成后要把口径刷新成全历史,而不是继续引用旧数字。旧口径的实测值和新口径的实测值不能混用——这句话听起来像废话,但在赶论文进度时是最容易犯的错,因为新旧数字都“来自同一份脚本”。

08相关文章