AI 更适合数据治理:从乱线穿过闸门的一条直路

AI 更适合数据治理,而不是做分析

AI 靠不靠得住,取决于这件事的对错能不能被机器检查。一家航空公司的机务数据,从几十段脚本整理成一份能读的 SQL、一张能点开的流程图;文中给出三个判断条件、六个习惯,以及为什么同一套办法拿去做分析会失效。

一家航空公司的机务数据,原本散落在几十段脚本里。我们把它翻成一份能从头读到尾的 SQL,再做出一张点开就能核对的流程图。
本文把这两个案例的细节摊开讲:做了什么、卡在哪、怎么验收。

一、先说结论

AI 靠不靠得住,不取决于它多聪明,而取决于这件事的「对错」能不能被机器检查。

  • 有标准答案的活:把一段脚本换成另一种写法、把“哪个字段对应哪个字段”整理成表、检查两处逻辑是否一致。这类活能跑、能报错、能对比,AI 可以自己检查自己,越做越准。
  • 没有标准答案的活:哪条航线延误最严重、这个业务问题该怎么看。答案的对错在人的脑子里,机器无法验证,AI 只能交一份看起来合理的初稿。

所以这两个案例里,AI 全部用在“整理”这一侧:不产出结论,只把结论赖以成立的那份数据与规则整理清楚。

左边是没有标准答案的分析,右边是有标准答案的整理
同一个问题,左边只能猜,右边可以验证。

二、判断标准:三个条件

不是所有活都值得交给 AI。我的判断只有三条,三条都满足才投产:

  1. 目标能写成一句话。 不是“帮我分析一下”,而是“把这段 Python 翻成等价的 SQL,字段一个都不能少”。
  2. 对错能自动检查。 能不能跑通、字段在不在、前后引用顺序对不对——由程序说了算,不靠人反复通读。
  3. 结果能重复生成。 产物落到文件里,改一次重跑一次,两次能对比;机器生成的东西不靠手改。

这三条正好是写代码的三个特征。反过来说,分析方法失效的根本原因不是模型不强,而是没有信号:没有信号,就没法靠迭代把错误压下去。

三道关:目标写清楚、对错自动查、结果能重跑
三道关全过,产物才敢交给人看;卡在任何一道,都只能是初稿。

三、案例一:把散落的脚本,翻成一份能读的 SQL

3.1 先把任务定义成“能验收”的样子

原来的数据处理逻辑写在一个可视化编程平台里,几十个处理环节分散在多个脚本文件。想知道“机型这一列怎么来的”,得同时翻三四个文件,再问当初写它的人。

我没有直接让它“重写”,而是先把任务写成可验收的形式:

  • 唯一可信来源:平台导出的那份 Python 工程源码,几十个环节,一环不缺;
  • 目标:判断这套流程能不能用纯 SQL 表达,并把表达写出来;
  • 产物:一个 SQL 文件,航班、事件各一条 WITH 链,共 348 行;
  • 验收标准:每段引用必须写在定义之前、字段必须存在、语义必须和平台里的节点说明一致;
  • 结论:能。20 个处理阶段(航班侧 11 段、事件侧 9 段)即可覆盖 53 个原子操作。

这一步是整个案例的地基。任务定义得模糊,后面所有检查都无处落地。

3.2 三组真实对照

下面三组是原文与译文的一一对应。为保护客户,表名、编号与机队信息已做替换,逻辑结构保持原样。

① 去重:平台函数 → 窗口函数

# 原文:平台自带的一个函数,按更新时间每个航班只留最新一行
flights = max_row(flights, order_by=['change_time'], keys=['flight_id'])
-- 译文:同一件事,用窗口函数写清楚;顺带把两步过滤合并进来
flights_dedup AS (
  SELECT * FROM (
    SELECT f.*, ROW_NUMBER() OVER (
             PARTITION BY flight_id ORDER BY change_time DESC) AS rn
    FROM src_flights f
  ) t
  WHERE rn = 1 AND delete_sign <> 'D' AND aircraft_registration IS NOT NULL
)

平台函数最大的问题是“看不出它做了什么”。max_row 这个名字读三遍,也不敢确定它是“取最新”还是“取最大”。换成窗口函数之后,排序、去重、过滤三件事一眼可见。

产品里就是这样对照着看的——点开“航班去重”这一步,右栏上面是平台函数,下面是展开后的完整 SQL:

航班去重节:左栏说明,右栏平台函数与完整 SQL 上下并排
点开一个节点:上面一句原文,下面一段展开的 SQL,黄线标出两者的对应关系。

② 维度注入:藏在代码里的对照表 → 摊开的 CASE

# 原文:机型映射藏在工具函数里,要跳两个文件才看得到对照关系
flights = flights.join(fleet.select('aircraft_registration', 'ac_type_short'),
                       'aircraft_registration', 'left')
flights = flights.withColumn('aircraft_type',
    map_fleet_type(F.col('aircraft_type')))
-- 译文:映射被摊开成一条条可读、可核对的规则(编号已隐去)
CASE WHEN ac.ac_type_short IN ('AC-001','AC-002','AC-003','AC-004') THEN 'ERJ'
     WHEN ac.ac_type_short IN ('AC-011','AC-012') THEN 'B737'
     WHEN ac.ac_type_short IN ('AC-021','AC-022') THEN 'A319'
     ELSE ac.ac_type_short END AS aircraft_type

这就是治理的实质:不是多写几行 SQL,而是把“藏在工具函数里的约定”变成“摆在明面上、可评审、可对比的文本”。原来要问三个人,现在是一处能读的规则。

点开“补机型”这一步,左右两栏就把这件事摆平了:左栏一句话说明“关联机队表与改写机型是同一件事”,右栏上半是 Python,下半是摊开的规则清单。

补机型节点:左栏一句说明,右栏 Python 与展开后的 CASE 规则并排
同一个节点里,右栏上半是原文、下半是译文,中间不需要任何转述。

③ 拆分视图:一个维度拆成出发、到达两套

# 原文:同一张机场维度表,拆成两套列名前缀
arrival_airports = airports.select(
    F.col('airport_iata').alias('arrival_airport_iata'),
    F.col('airport_icao').alias('arrival_airport_icao'), ...)
-- 译文:原本是两条 CREATE VIEW,改成两个中间结果
dep_airports AS (SELECT airport_iata AS departure_airport_iata, ... FROM dim_airport),
arr_airports AS (SELECT airport_iata AS arrival_airport_iata, ... FROM dim_airport),

这种“一表两用”的写法,在平台里表现为两个视图节点、两条输出线;翻成 SQL 后变成两段可并列阅读的代码。节点多、输出多的地方,正是最容易出错的地方——因为没人记得住哪条线接到了谁。

拆起降视图节点:左侧两条视图定义,右侧两套列名前缀改写并排
一个中间表节点,产出两套视图;画布上它拆成两个节点,分别被下游的两步消费。

三组放在一起看,就是同一件事的三种典型形态:

三组对照:去重、映射、拆分,各有一条原文与一条译文
去重靠窗口函数、映射靠展开的规则、拆分靠两条并列的视图。

3.3 最值钱的一步:把缺口变成“明确的错误”

这是整个案例里最有说服力的一段。源数据本身是不完整的:有的字段只写了注释没有实现,有的地方“先引用、后定义”。

这类问题放在分析场景里,表现只是“结果有点怪”;放在整理场景里,它是明确的错误,机器能一个一个指出来。最终我们列出 8 类缺口、9 处修正标记,每条都带着判断依据:

-- ⚠️ 修正:flight_uid 原本在后面才生成,但前一步的 LAG/LEAD 已经引用 → 生成逻辑提前
-- ⚠️ 修正:维修方案字段原为注释占位 → 按节点说明补一条关联,待确认
-- ⚠️ 修正:累计航班数、实际时刻等字段原数据未定义 → 用近似写法,并标注待核对
-- ⚠️ 修正:原数据的一处产出没有命名 → 按下游引用补上名字

注意第一条:“先引用后定义”和代码里的“使用未声明变量”是同一类问题——判断标准明确,可以自动检出。而像“按节点说明推断”这类判断,AI 只给候选、就地标注,人只做确认。

这就是治理场景里人机最舒服的分工:AI 负责穷举和标注,人负责拍板。

3.4 维修侧:259 条清洗动作 → SQL 候选

另一条业务线(维修工程)的清洗层里,有 259 条“字段 → 解析器”的动作,实现集中在 11 个解析器函数里。把它们翻成 SQL 是典型的确定性翻译,但时区与函数语义必须核对,所以每一行都写清核对点:

解析器动作

原文写法

SQL 候选(Spark SQL / Trino 方言)

核对点

去空格

F.trim(col)

TRIM(col)

直译

转小数

col.cast(DecimalType())

CAST(col AS DECIMAL(p,s))

精度与小数位要显式写

时间戳转毫秒

to_long(col) * 1000

CAST(EXTRACT(EPOCH FROM col) * 1000 AS BIGINT)

to_long 是否已是秒级

毫秒转时间

long_to_timestamp(col - 8*60*60*1000)

to_timestamp((col - 28800000) / 1000)

8 小时补偿的时区归属

转 UTC

F.to_utc_timestamp(col, 'PRC')

col AT TIME ZONE 'Asia/Shanghai' AT TIME ZONE 'UTC'

方言差异、夏令时

字符串转时间

regexp_replace('T',' ') + to_timestamp(fmt)

TO_TIMESTAMP(REPLACE(col,'T',' '),'yyyy-MM-dd HH:mm:ss')

格式串写法不同

右边这些是候选,不是已验证产物。核对方式也很“coding”:同一份样本数据,两边各跑一遍,对比结果——而不是靠人读代码争论。

四、案例二:把 SQL 变成一张能点开的图

翻译只是第一步。让人看懂,才是治理的交付形态——但“看懂”很容易滑成不可判定的审美问题。我们的做法是:把“理解”结构化成一张有规矩的图。

4.1 两个案例的规模

指标

案例一(航班侧)

案例二(维修侧)

图形形态

双页签切换

左侧对象列表 + 右侧画布

处理阶段 → 泳道列

20 个阶段 → 20 列

25 个对象流,按最长路径分层

节点 / 连线

46 / 46 与 27 / 29

合计 107 个转换步骤

输入关系

11 + 7 张输入表

117 个“原始 → 清洗”输入对

输出

33 字段与 24 字段

25 张表、615 个输出字段

案例一的图长这样。 按处理阶段分成泳道,从左到右是数据流;节点按动作类型上色——去重、过滤、关联、补数、计算各一种颜色。四十多个节点、四十多条连线,全部由脚本摆位,不靠手拖。

案例一流程图:按阶段分泳道,节点按动作类型上色,点开任意节点看对照
泳道是阶段,方块是动作,颜色是动作类型;一眼能看出数据在哪一层被加工。

案例二是另一种形态。 左边是一份业务对象清单(机队、工作包、飞行记录本、部件拆装等),右边是选中对象自己的管线:原始表 → 清洗 → 计算 → 输出。25 个对象、107 个转换步骤,都能点开。

案例二数据管线:左侧 25 个业务对象,右侧每个对象从原始表经清洗、计算到输出
同一个页面里,既可以按对象看全貌,也可以按步骤看细节。

每个方块都能点开。 点开之后,左边一句人话说明这一步在干什么,右边上下两栏是原始脚本与等价 SQL,一一对应。于是“这段逻辑为什么这么写”,从口口相传变成可以点开的证据。

4.2 节点怎么切,是有规则的

节点不是随手画的,一句话就能决定切分方式,比如“LEFT JOIN 不算过滤”:

  • 只有 INNER JOIN,或者 LEFT JOIN 的关联条件里带了主表过滤,才构成一个“过滤”节点;
  • 而 LEFT JOIN 和它带进来的字段计算,属于同一件事,必须合并在一个节点里——反例就是把“关联机队表”和“补机型”拆成两个节点,看图的人会以为中间还有一步。

这类规则写进文档,就变成了可回归的判据:下次换个人做,照这条规则切,图就长得一样。

4.3 图能不能用,由脚本判

图合不合格,不靠“看起来还行”,而靠程序算:

  • 节点不重叠;
  • 连线不穿越节点;
  • 只从左往右,不允许回头边;
  • 每个节点入边不超过 2 条(合并类节点除外);
  • 没有空的泳道列。

两个案例各有一套校验脚本,加起来 5 + 4 条,跑一遍只要几秒;一键对齐之后再跑同一套检查。布局算法本身是确定性的(分层 + 就近 + 行约束),改数据就重画,重画就重验——这才是写代码的节奏,而不是设计排版的节奏。

4.4 反面:别做一张“没人敢改的图”

如果只做一张漂亮的流程图,它两周后就会变成没人敢动的图片:坐标是手拖的、语义是口口相传的、数据改了图不会变。

我们的做法相反:图是数据的函数。图的数据文件由脚本生成,图不对,说明数据不对——去修数据,不修图。

五、六个习惯

方法不难,难的是每次都做到。我们把要求写成六条,每条都配一项机器检查——没有检查的纪律,过两周就会失效:

  • 一个来源。 每段内容都注明来自哪个文件、哪一处,随时能追回原文。
  • 不手改生成物。 生成的图表和 SQL 开头写“请勿手改”,要改就改源头再重跑。
  • 拿不准就标注。 每处推断都标出来,写清跟谁确认,不含糊写过去。
  • 检查自动跑。 排版、格式、数字都交给脚本核对,不靠人眼过一遍。
  • 内外分开。 对外发布前自动检查,确保没混进内部页面。
  • 数字能重算。 文中每个数字都能用脚本重新算出来。

六、反过来看:同一套办法去做分析会怎样

拿这批数据问 AI 一句:“哪条航线延误最严重?”听着简单,实际上有四个开关要先定:

  • 延误按起飞算,还是按落地算?
  • 取消、备降、返航算不算延误?
  • 时间窗取多长——一个月,还是一年?
  • 样本少于多少班的航线,不参与排名?

四个开关两两组合,就是 16 个都说得通的答案。更麻烦的是,没有一个程序能判定哪个对,只能等业务方说“感觉不对”。

四个开关组合出十六个都合理的答案,定下一个口径才得到一个结论
答案不是算出来的,是口径定出来的。

正确的顺序不是“让 AI 更努力地分析”,而是先让它把口径整理成产物:延误怎么算、算哪些状态、时间窗多长,写成能评审的字段与规则;然后分析才站在这份规则上跑。

一句话:AI 不负责给结论,AI 负责把结论赖以成立的口径做对。

七、如果你也想这么用

  • 动手前先问一句:这件事的对错,能不能自动检查?不能,就别指望 AI 收敛。
  • 把目标写成一句话(翻译、抽取、核对),不要写成“帮我分析一下”。
  • 找到唯一的出处,并让产物能追回原文。
  • 让产物落地、能重跑;机器生成的东西不手改。
  • 人只做确认,不做通读——把时间花在确认那 9 处标注上,而不是通读 348 行 SQL。

结语

这两个案例之后,我对 AI 的期待变了一个方向:不再指望它替我想清楚业务问题,而是把它当成一个极有耐心、不会疲倦的整理工——它负责把散落的东西摆整齐、把说不清的地方标出来,判断留给懂业务的人。

要说门槛,其实只有一个:你能不能把“什么算对”说清楚。 说得清,AI 就能帮你把这件事做到几百次都不走样;说不清,它给你的只是一份看着挺像的初稿。

边界与说明:本文已隐去客户名称、机队编号、内部表名与系统代号,代码中的字段为通用写法,逻辑结构保持原样。文中所有产物都是“AI 起草、脚本判定、人工确认”的结果——机械翻译与结构生成交给 AI,口径判断与验收留给人。

No comments yet