📊 第8章 元数据的管理
📄 一句话概括

改表结构最怕的不是报错,是不报错:顺序变了、类型变了,算出来的数看着正常,其实是错的。 演示加一列,老代码当场 ValueError: too many values to unpack;加个视图挡一层,老代码照跑。 沿袭靠作业日志:从报表往上追 4 步到原始表,从原始表往下追 4 张受影响。

📝 本章和前面几章的关系

教材 8.3、8.4 节:数据结构管理、数据沿袭管理。 上一篇讲元数据是什么。这一篇讲它最硬的两个用途:管结构、管来路。 一句话:结构管理管「定义变了怎么办」,沿袭管理管「数不对了找谁」。

一、改结构有两种下场

一种是当场报错。程序崩了,日志里有堆栈,十分钟就能定位。 另一种是不报错。字段顺序变了、类型从整数变成文本,程序照跑,数也算得出来,只是算错了。 第二种贵得多。 因为它不会在当天被发现,而是三周后财务对账对不上,再往回查三周的数据。

所以结构管理要解决的不是「防止改表」,是让改表这件事对下游是可见的。

二、加一列怎么会让老代码崩

演示里写了段老代码:select * from orders,然后用五个变量接住那一行。 跑通了,返回第一行的单号和金额。 然后有人加了一列 channel。 同一句 SQL 再跑:ValueError: too many values to unpack (expected 5)。

五个变量接不住六个值——就这一句话。 这个崩溃其实是好事,它当场就报了。 真正难查的是那种:加的是中间一列,代码按位置取值, 取到的全是错位的——数算出来了,但每个字段都错了一格。

三、用视图挡一层

演示接着建了一个视图 v_orders,只暴露原来那五列。代码改成查视图。 表加了列,视图没变,老代码照跑,返回的还是那两个数。 这就是视图的用处:把「表的结构」和「代码看到的结构」解耦。

代价是多一层。查视图要走一次重写,性能会略降。换来的是改表的时候不用同时改二十个下游。 三种改法各有取舍:直接改表,所有下游都得跟着改,漏一个就崩; 加视图过渡,下游不动,但多一份要维护的视图;新建表并行跑,新老各用各的,代价是两份数据要同步。 小团队通常选第二种。改结构这件事,慢一点比快一点便宜。

四、沿袭:一个数不对,往哪追

演示建了一张五条边的表,记的是「哪个作业把哪张表写成了哪张表」。 然后从报表 rpt_daily 往上追,用一句递归查询,四步追到最原始的 raw_orders: report ← agg ← clean ← load。 每一层都是一个作业,而作业名就是线索。 顺藤摸瓜,摸的是作业。

反过来也能追:改 raw_orders,下游四张表会受影响,要通知四个负责人。 这就是沿袭的全部用途,两条:出问题往上追,改东西往下看。 前者叫根因定位,后者叫影响分析。名字听着高级,实现就是一张两列的表加一句递归。

五、沿袭一断,追到一半就停

演示最后删掉中间那一步的记录,再追一次:只追到两层就断了。 现实里最常见的原因不是删记录,是中间有人手工导了一次 Excel。 那一步没进作业日志,沿袭就断在那儿。 断了之后会怎样?报表数不对,你追到 dwd_orders,再往上就没了。只能去问人:「这张表是谁做的? 」——然后发现那个人上个月离职了。

所以数据团队有条不成文的规矩:能在系统里跑的就别手工导。不是为了省事,是为了留痕。 这条规矩听着像流程洁癖,其实是唯一能让半年后的自己查得动的手段。

六、动手 Demo:加一列,老代码当场崩

先写一段按位置取值的老代码,跑通。 给表加一列,再跑同一段代码,看报什么错。 然后加个视图挡一层,看老代码能不能照跑。

def old_code():
    row = con.execute('select * from orders limit 1').fetchone()
    o, u, sku, q, a = row        # 五个变量,按位置接
    return o, a

old_code()                            # 正常
con.execute('alter table orders add column channel text')
old_code()                            # ValueError: too many values to unpack

# 加一层视图,老代码查视图而不是查表
con.execute('create view v_orders as select o, u, sku, q, a from orders')
row = con.execute('select * from v_orders limit 1').fetchone()
o, u, sku, q, a = row                 # 又是五个,不崩了

实跑输出:

--------------------------------------------------------------------
结构管理:改一列,谁会被绊倒
--------------------------------------------------------------------

  ① 老代码是怎么被绊倒的
  --------------------------------------------------------------------
  老代码:`select * from orders` 然后五个变量接住,跑通了
  返回:(0, 99.0)
  有人加了一列 channel,然后同一句 SQL 再跑:
  ValueError: too many values to unpack (expected 5)
  加一列怎么会让老代码崩? 因为 select * 的列数变了,五个变量接不住六个值。
  这类崩溃还算好的——它当场就报了。
  更麻烦的是不报错的那种:字段顺序变了、类型从整数变成文本,读出来的数看着正常,算出来是错的。

  ② 用视图挡一层
  --------------------------------------------------------------------
  建一个视图 v_orders,只暴露原来那五列。新代码改成查视图:
  返回:(0, 99.0)
  表加了列,视图没变,老代码照跑。这就是视图的作用:把「表的结构」和「代码看到的结构」解耦。
  代价是多一层。查视图要走一次重写,性能略降——换来的是改表的时候不用同时改二十个下游。

| 改法 | 下游要动吗 | 风险 |
|---|---|---|
| 直接改表 | 所有下游都要改 | 漏一个就崩 |
| 加视图过渡 | 下游不动 | 多一层,要维护视图 |
| 新建表并行跑 | 新老各用各的 | 两份数据要同步 |


  ③ 沿袭:这个数是从哪来的
  --------------------------------------------------------------------

| 层数 | 上游表 | 路径 |
|---:|---|---|
| 0 | rpt_daily | rpt_daily |
| 1 | dws_city | rpt_daily <- dws_city |
| 2 | dwd_orders | rpt_daily <- dws_city <- dwd_orders |
| 3 | ods_orders | rpt_daily <- dws_city <- dwd_orders <- ods_orders |
| 4 | raw_orders | rpt_daily <- dws_city <- dwd_orders <- ods_orders <- raw_orders |

  从报表 rpt_daily 往上追,4 步就追到了最原始的 raw_orders。
  这就是数据沿袭(data lineage):每条数据都能说清自己从哪来、经过谁的手。
  它靠的就是第 25 个 Demo 里那张作业日志。

| 层数 | 下游表 | 路径 |
|---:|---|---|
| 0 | raw_orders | raw_orders |
| 1 | ods_orders | raw_orders -> ods_orders |
| 2 | dwd_orders | raw_orders -> ods_orders -> dwd_orders |
| 3 | dws_city | raw_orders -> ods_orders -> dwd_orders -> dws_city |
| 4 | rpt_daily | raw_orders -> ods_orders -> dwd_orders -> dws_city -> rpt_daily |

  反过来追就是影响分析:改了 raw_orders,下游 4 张表会受影响,要通知 4 个负责人。
  这两张表就是沿袭的全部用途:出问题往上追,改东西往下看。

  ④ 沿袭一断,追到一半就停了
  --------------------------------------------------------------------

| 层数 | 上游表 | 路径 |
|---:|---|---|
| 0 | rpt_daily | rpt_daily |
| 1 | dws_city | rpt_daily <- dws_city |
| 2 | dwd_orders | rpt_daily <- dws_city <- dwd_orders |

  把中间那一步的记录删掉,再追一次:只追到 2 层就断了。
  现实里最常见的原因不是删记录,是中间有人手工导了一次 Excel。那一步没进作业日志,沿袭就断在那儿。
  所以数据团队有条不成文的规矩:能在系统里跑的就别手工导。不是为了省事,是为了留痕。

第一段那个崩溃是好事。 真正麻烦的是不崩的那种:加在中间一列,按位置取值就全错位了,程序照跑,数全错。 第二段那个视图只有一行 SQL,但它把「表怎么变」和「代码怎么读」分开了。 这是所有兼容性方案里最便宜的一种。 第三段的两张递归表是同一张 lineage 表查出来的,方向相反而已。 数据库的递归查询写起来就这几行,不用上什么专门工具。 第四段删掉一条边,追溯立刻断在第二层。这就是「手工导了一次 Excel」的后果:数据没错,线索没了。

30 秒实验

  1. 把 select * 改成显式列名 select o, u, sku, q, a,再跑一次加列,看还会不会崩。
  2. 给 lineage 加一条「人工 Excel 中转」的边,但故意写错 src,看追溯会怎么走偏。
  3. 用递归查询算一下:如果改的是中间那张 dwd_orders,下游有几张表要通知。

常见误区

  1. 「加字段是兼容的,不会影响别人。」加在末尾、且下游用显式列名时才兼容。 select * 加按位置取值,一定出事。
  2. 「沿袭要靠专门的工具画图。」工具是锦上添花。核心就是一张「谁写了谁」的表,先把这个记全。
  3. 「这次数据不对,肯定是上游算错了。」先看沿袭,确认你追的是同一条链、同一批次。 追错链比不追更浪费时间。
自测三问(点开对答案) 1. 为什么「加一列」会让老代码崩? —— 因为老代码用了 `select *` 加按位置取值,列数从五变六,五个变量接不住六个值。 用显式列名或视图就不会。 2. 沿袭的两个用途是什么? —— 往上追找根因(这个数从哪来),往下看算影响(改这张表会绊倒谁)。 3. 为什么手工导 Excel 会毁掉沿袭? —— 因为那一步不在作业日志里,链上就少了一个节点。数据本身可能没错,但再也没法从结果追回源头。

下一篇是全书最后一篇:数据新鲜度——这个数到底是几点的。

相关笔记