FDE 落地手记 · 08

在把经营数据接给 AI 的过程中,我越来越重视一个看起来不那么“智能”的问题:模型查到的这个数,到底代表什么,它有没有资格被拿来比较和决策?

接口调用成功,只能说明拿到了响应。订单重复导入、售后归属错位、商品映射缺失、来源没有采全,都可能藏在一个格式正确的结果里。AI 甚至会把这些问题包装成一份很流畅的分析。

围绕 yuce_cube 的实践,我逐渐把工作重心从“把数据放进库里”,转向“让每个结果带着可解释的来源、口径和使用边界”。其中已经有数据清洗,也有分层建模、版本选择和质量状态管理。

这篇复盘依据实际数据库结构、视图定义与状态核验展开。文中会明确区分已经被专题使用的能力、新建的治理结构,以及还需要验收的环节;不把建表等同于上线,也不把有数据等同于数据完整。

1. 数据清洗的目标,是让业务事实保持一致

传统意义上的清洗,常常指去空格、转日期、删重复。这些操作必要,但经营分析里更难的问题是业务语义。

一个订单可能有多个商品、多次发货、多张售后单;同一张报表可能重复下载,也可能因为退款确认而发生修订。如果只用“订单号”去重,真实的商品明细会丢失。如果把所有历史导出直接相加,业务量又会被重复放大。

因此,我会先回答三个问题:一行代表什么事件;什么组合能唯一识别它;同一事实的新版本出现时,旧版本应该如何保留和退出当前统计。

问题容易出现的错误数据层应该承担的职责
同一文件重复导入总量越来越大识别采集批次、文件与业务记录
一笔订单包含多个商品按订单号去重后丢明细按业务粒度定义唯一键
同一天的数据重新导出新旧版本一起参与汇总明确当前有效批次
字段缺失或转换失败被当成真实的零保留 NULL 与异常状态
ERP 与平台都叫“销量”不同口径被直接相加明确来源、单位与日期归属

清洗不是尽可能把表变得整齐,而是避免处理过程改变事实的含义。

2. ODS、DWD、DIM、DWS、ADS,分别解决什么问题

我用下面这组分工理解数据底座。它是一种职责划分,不要求每个系统必须创建五个数据库。

层次核心职责对经营分析的价值
ODS:原始数据层留存来源响应、文件、导入批次及必要元数据能追查“原来拿到了什么”
DWD:明细数据层统一类型、粒度、业务键和事件含义同一种事实可以按同一种方式查询
DIM:维度层管理店铺、商品、渠道、来源契约等解释信息知道记录属于谁、按什么规则归类
DWS:汇总服务层沉淀可复用的渠道、商品、日期等汇总多个应用复用一致的指标
ADS:应用数据层面向经营看板、专题和业务工具组织结果输出适合使用场景的数据产品

数据分层与实际使用边界

DIM 是参与关联的维度,不是 DWD 处理完以后必须顺次经过的一道流水线。DWS 与 ADS 也可能由视图、服务或应用侧计算承担,不能只看表名前缀判断分层是否完善。

在 yuce_cube 中,已经能看到 ODS、DWD、DIM 以及应用相关结构。同时仍有历史业务表和专题视图。这种新旧并存,是渐进治理的真实状态。下一步应该围绕具体链路验收,而不是为了目录整齐而一次性重命名所有表。

3. 原始数据留存,让问题可以被重新解释

以 ERP 出库为例,ods_jst_out_orders_raw 保留接口标识、业务键、同步批次、接收时间、载荷哈希与原始 JSON。相应的 DWD 表再表达店铺、渠道、状态、金额、数量和业务日期等标准字段。

这让“来源给出的值”和“经过业务处理后的值”有机会被分开检查。发现一个渠道分类异常时,可以回到来源载荷核对,而不必只相信应用页面上的最终结果。

这里有一个容易夸大的地方:有 raw_json 和哈希,并不自动意味着已经建立了不可变的全量历史档案。 当前这类原始表还存在按接口与业务键设置的唯一约束。到底是覆盖当前版本,还是另外保存历史版本,需要结合采集程序和留存策略验证。

文件治理也有类似边界。ods_source_file_manifest 已有文件哈希、解析器版本、文件格式、字段块数量和有序字段块摘要,核验中也能看到 verified_fields 状态。这提供了文件与解析结果核对的基础,但“字段验证通过”不等于“所有业务数据已经采全”。

我会把四种验证分开:文件身份是否一致、解析是否完整、业务范围是否齐全、业务汇总是否对账。每一种验证都要有自己的证据。

4. 去重需要同时处理“批次内”和“批次间”

退货专题提供了一个比较具体的例子。

dwd_returns_facts 的唯一约束包含 batch_id + record_key。它能够约束同一批次里的同一个记录键不要重复出现,但不同批次仍然可以保存同一业务事实的不同版本。

如果查询把所有批次直接相加,唯一键并不能阻止重复统计。因此,还需要第二层选择:哪个批次是当前统计应当使用的版本。

dwd_returns_source_partition 按来源、业务日期和范围保存批次引用。专题库中的 v_return_current_facts 会关联有效分区,按来源、日期和批次取得事实明细。其实际定义使用分区中的来源、日期、批次组合进行关联;本次检查到的范围值为单一范围。未来扩展多范围时,还需要重新检验范围隔离,不能直接假定现有视图已支持所有场景。

可以用一个虚构的小例子理解两层机制:

  1. 第一次导入某日售后文件,产生批次 A。
  2. 再次处理 A 时,批次内唯一键约束重复记录。
  3. 来源修订后生成批次 B,保留新的事实版本。
  4. 有效分区指向 B,当前查询读取 B;A 可用于追溯。

其中,“B 通过哪些校验后才能替换 A”,必须由写入流程保证。数据库存在分区表,只能证明具备表达有效版本的结构;并不能证明切换前的质量闸门已经完整实现。

这也解释了为什么我不会把去重简单描述为一条 DISTINCT。去重的核心是记录身份和版本策略。

5. NULL 是一种业务状态,不能为了好看而抹掉

在退货专题的标准化视图中,数值字段会先检查是否符合数值格式,再转换为明确精度的 DECIMAL;不符合条件时保留 NULL。JSON 的空值也被单独处理,而不是直接转换成看似有效的字符串或数字。

这是一项很实用的清洗能力。它防止“未知”“空白”“无法解析”悄悄变成 0。

但仅保留 NULL 仍不够,因为 SUM 等聚合会忽略空值。即使部分记录没有有效数量,查询也可能得到一个看上去很正常的总数。

因此,统计结果至少需要同时保留:总记录数、有效数值记录数、缺失记录数和可用值合计。缺失率的分母也要说明,不能把记录覆盖率冒充金额覆盖率。

下面是使用现有专题字段的检查示例。它只能检查已返回行中的数值缺失,不能单独判断整日数据是否缺失:

SELECT
  business_date,
  COUNT(*) AS observed_rows,
  COUNT(shipped_qty) AS valid_qty_rows,
  SUM(shipped_qty IS NULL) AS missing_qty_rows,
  SUM(shipped_qty) AS observed_shipped_qty
FROM jderp_return_bi.v_return_erp_sales_ship
WHERE business_date >= :start_date
  AND business_date < :end_date_exclusive
GROUP BY business_date;

没有出现的日期,需要用预期日期集合与来源覆盖表再做检查。否则,“没有返回一行”和“返回一行且数量为零”仍然无法区分。

负数也不能一律清掉。财务冲销、退款或更正可能产生合法负值;是否允许负数,应由指标定义决定。清洗的任务是识别含义,而不是把所有“不好看”的值消灭。

6. 来源覆盖和观察窗口,决定结果能不能比较

inventory_bi_source_day_coverage 已经能够按来源和业务日期记录覆盖状态,并区分 data 与 unknown。这比仅保存一个“最后更新时间”更接近分析需要。

不过,data 表示观察到数据,并不自动意味着完整。某个来源的最早日期和最晚日期之间,也不一定每天都有记录。要验证完整性,应该建立“预期来源 × 预期范围 × 日期”的检查集合,再逐项比对。

退货分析还多一层时间问题。发货日期、售后申请日期、退货入库日期、退款到账日期,对应不同事件。把“本月退货数量”除以“本月发货数量”,得到的是期间事件比,不能直接解释成“本月发货订单最终有多少退了”。

如果要比较发货后的退货表现,我会先固定订单或件数口径,再把退货关联回对应发货批次,并设置统一观察窗口。例如按自然日定义“发货后 15 天内”,就需要明确是否包含第 0 天和第 15 天;最近发货、尚未走完窗口的批次,应标为未成熟。

对无法关联原发货记录的来源,仍可以分析期间事件,但需要单列。业务事实无法支持的比较,不应靠提示词补出来。

7. 来源契约,把“这份报表是什么”写进机器可读规则

dim_source_contract 的定义中,已经出现平台、报表、范围、过滤条件、日期区间、时区、日期依据和字段版本等信息。这比只保存文件名更有价值。

同样叫“商品表现”的两份报表,如果一家按成交日期、一家按结算日期;一家覆盖全部店铺、一家只覆盖筛选后的店铺,它们就不应该因为字段相似而被当成同一种数据。

契约的作用,是把这些前提保存下来,并让后续流程能够判断兼容性。不过本次检查到的契约样本中仍有 legacy_unknown 范围和未填写的字段版本。它准确地表达了历史数据尚待确认的部分,不能被包装成一份已经完备的来源规范。

我更愿意保留这种不确定性:先记录已知来源,再补齐范围和版本,然后决定是否允许进入正式指标。只有显式承认未知,后续补证才有位置可放。

8. 已经落地的能力,与需要继续补齐的能力

这次核验最有价值的结论,并不是“已经建立了一套完美数仓”,而是能够准确描述进展。

能力本次实际看到的证据可以得出的结论
批次内记录约束退货明细有批次与记录键的唯一约束已有防止同批次重复记录的结构
当前版本选择退货分区已有数据,专题视图关联有效批次专题已采用有效批次读取机制
类型与空值处理视图包含数值格式检查、DECIMAL 转换、NULL 处理已有明确的标准化逻辑
来源覆盖覆盖表存在 data 与 unknown 状态可以表达有数据和来源未知的区别
统一批次与消费机制通用批次记录处于 shadow,通用分区和消费状态表未见记录属于待进一步验证的治理结构
批次完整性检查到的退货导入批次 complete 均未标为通过不能宣称来源完整性已全部验收

这里的 complete = 0 不能直接翻译成“这一批数据一定错误”。更准确的解释是:完整性尚未被确认。能否用于参考、是否需要阻断正式比较,要由业务规则决定。

下一步我会优先补三件事:来源完整性与控制合计、有效批次切换前的质量闸门、历史重放和失败恢复验收。先让一条完整链路经得住检查,再把方法推广到更多来源。

9. 对 FDE 来说,交付的是一条可解释的数据链路

这项实践让我更清楚地理解了 FDE 工作方式:先进入业务现场,弄明白“销量”“退货”“利润”分别指什么,再把这些认识转成数据结构、检查规则和可复用的服务。

工程上的价值,既包括采集和清洗,也包括发现错误比较、保留缺失状态、解释口径差异,以及让业务知道一个数字适合做什么判断。

当这些基础逐渐稳定,同一份数据才能服务经营看板、专题分析和 AI 工具。MCP 可以让 Agent 更方便地调用它们,但可靠性的来源,仍然是事实、口径和验证过程。

下一篇继续讨论:从“能查数据”到“能信结果”:经营分析 MCP 的可靠性设计。

延伸阅读:企业 AI 的底座:Python、MySQL 与 MCP 的连接思路;MySQL 唯一约束与索引;MySQL 对 NULL 的处理。

代码世界的构建师,现实生活的悠游者。