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

元数据就是关于数据的数据。 静态记结构(有哪些表、哪些列、什么类型),动态记变化(谁在什么时候动了它)。 演示里六张表二十五列,一句 pragma table_info 就全读出来; 手抄的那份加了一列就过期了,还没人发现。 四十六张表里找「哪张有 u 列」:逐张问要问 46 次,查元数据表只要 1 次。

📝 本章和前面几章的关系

第 7 章讲数据怎么用出去。这一章讲怎么保证用出去的时候不出错。 教材 8.1、8.2 节:整体概念和静态元数据、动态元数据和元数据管理的实现方法。 一句话:元数据不是文档,是「让数据能被信任」的那部分信息。

一、两层意思,别混在一起

静态元数据记「长什么样」:有哪些表、每张表哪些列、什么类型、谁负责、多久更新一次。 动态元数据记「发生了什么」:哪个作业、什么时候、读了哪张表、写了哪张表、进了多少行、出了多少行。 静态回答「有什么」,动态回答「怎么变成现在这样的」。

这两层缺一层,元数据就是半残的。 只有静态,你知道表在哪,但数不对的时候无从下手; 只有动态,你知道跑过什么,但不知道跑的是什么东西。

二、静态那层,系统自己就记着

演示建了六张业务表,一共二十五列。 这些信息不用问任何人:一句 pragma table_info(表名) 就全读出来,列名、类型、能不能为空, 一个不缺。 静态元数据的第一来源是系统本身,不是人的记性。

这句话听着像废话,但大多数团队的第一版元数据就是一份 Excel——有人花两天整理出来,然后就没有然后了。

三、手抄的那份,一定会过期

演示接着做了一个对照:先把当前的列数抄下来(orders 五列),然后加一列 channel。 实际变成六列,文档还写着五列。差一列,谁都没发现。 这不是谁偷懒。改表的人只想着「加个字段」,不会想到还有一份文档在别处。 没人会在改表的时候顺手改文档,这是人性,不是态度问题。

所以结论很硬:静态元数据要从系统里读,不是往文档里抄。 抄一次就多一个会过期的副本,而且你不知道它什么时候过期的。

四、规模一大,差别就出来了

演示第三段问了一个很实际的问题:哪几张表里有 u 这个字段。 办法一,逐张表问结构:四十六张表要问四十六次,0.0022 秒。 办法二,先把结构灌进一张元数据表,再查:一次,0.0002 秒。

差别不在这一次的零点零零二秒,在问的次数。 四十六张表差四十六倍,一千张表差一千倍。而真实的数仓,一千张表只是起步。 这就是元数据管理的价值:它不是让单次操作变快,是让「问结构」这件事的成本不随规模涨。

五、动态那层才是排查问题的抓手

演示第四段列了一张作业日志,五行:作业名、输入表、输出表、读入行、写出行、耗时。 看那串数字:load 读进十万行、写出九万九千八百四十二行; clean 又把九万九千八百四十二洗成九万七千三百一十。 每一步都少一点。少的那部分去哪了?

没有这张日志,你只知道最后的结果;有了它,你能回答「哪一步丢的、丢了多少」。 这就是动态元数据的用处:它不是给领导看的报表,是出事时唯一能拿出来的证据。

六、落地只有三种做法

手工文档:上手最快,三个月就过期。 配置文件:能进版本库,但它和实际结构仍然是两份,照样会分叉。 系统内置目录:建表的时候顺手记,不记就建不了。Hive Metastore 那类就是。

第三种的关键不是技术先进,是把「记录」变成「操作的副产品」。 整章记住这一句就够了:任何需要额外动作去维护的元数据,最后都会烂掉。

七、动手 Demo:六张表二十五列:手抄的和活的

建六张业务表,从库里把结构读出来。 再对照一份「手抄的清单」,看加一列之后谁对谁错。 最后在四十六张表里找某个字段,比「逐张问」和「查元数据表」。

# 静态元数据:系统自己记着
for name, _ in DDL:
    info = con.execute('pragma table_info(%s)' % name).fetchall()
    print(name, len(info), [c[1] for c in info])

# 加一列,手抄的文档立刻过期
doc_cols = 5
con.execute('alter table orders add column channel text')
print(doc_cols, len(con.execute('pragma table_info(orders)').fetchall()))

# 把结构灌进一张表,以后查它就够了
con.execute('create table meta_col(tbl text, col text, typ text)')
for t in tables:
    for c in con.execute('pragma table_info(%s)' % t):
        con.execute('insert into meta_col values(?,?,?)', (t, c[1], c[2]))

hit = con.execute("select tbl from meta_col where col='u'").fetchall()

实跑输出:

--------------------------------------------------------------------
元数据:关于数据的数据
--------------------------------------------------------------------

  ① 结构这件事,数据库自己就知道
  --------------------------------------------------------------------

| 表 | 列数 | 前三个列名 | 类型 |
|---:|---|---|---|
| orders | 5 | o、u、sku | INTEGER、REAL |
| users | 4 | u、city、seg | INTEGER、TEXT |
| goods | 3 | sku、cat、price | INTEGER、REAL、TEXT |
| pay | 5 | pid、o、amt | INTEGER、REAL、TEXT |
| refund | 4 | rid、o、amt | INTEGER、REAL、TEXT |
| channel | 4 | dt、channel、cost | INTEGER、REAL、TEXT |

  这 25 列、6 张表的信息,是数据库自己记着的。
  你写一句 pragma table_info(表名) 就能读出来——静态元数据的第一来源,是系统本身,不是人的记性。

  ② 手抄的文档一定会过期
  --------------------------------------------------------------------
  假设半年前有人手抄了一份字段清单:orders 有 5 列。
  三个月前业务加了一列 channel,实际是 6 列。
  文档还写着 5 列 —— 差 1 列,谁都没发现。
  这就是文档漂移:不是谁偷懒,是没人会在改表的时候顺手改文档。
  所以静态元数据要从系统里读,不是往文档里抄。抄一次就多一个会过期的副本。

  ③ 元数据多了以后,查一次和查一百次不一样
  --------------------------------------------------------------------

| 做法 | 要问几次 | 耗时 | 命中表数 |
|---|---:|---:|---:|
| 逐张表问结构 | 46 次 | 0.0021 秒 | 2 |
| 查元数据表 | 1 次 | 0.0002 秒 | 2 |

  差别不在这一次的 0.002 秒,在问的次数。
  表有 46 张的时候,差 46 倍;表有一千张的时候,差一千倍。元数据的价值就是随规模放大。

  ④ 动态元数据:记「发生了什么」
  --------------------------------------------------------------------

| 作业 | 输入 | 输出 | 读入行 | 写出行 | 耗时 |
|---|---|---|---:|---:|---:|
| j01_load_orders | raw_orders | ods_orders | 100000 | 99842 | 12.4 秒 |
| j02_load_users | raw_users | ods_users | 5000 | 5000 | 0.8 秒 |
| j03_clean | ods_orders | dwd_orders | 99842 | 97310 | 21.7 秒 |
| j04_agg_city | dwd_orders | dws_city | 97310 | 960 | 6.2 秒 |
| j05_report | dws_city | rpt_daily | 960 | 24 | 1.1 秒 |

  这张表叫作业日志。它记的不是「表长什么样」,是「谁在什么时候动了哪张表」。
  静态元数据回答「有哪些数据」,动态元数据回答「这些数据是怎么变成现在这样的」。
  看出门道没有:load_orders 读进 100000 行、写出 99842 行,clean 又把 99842 洗成 97310。
  每一步都少了一点,少的那部分去哪了?
  没有这张日志,你只知道结果;有了它,你能回答「哪一步丢的、丢了多少」。

  ⑤ 元数据管理怎么落地
  --------------------------------------------------------------------

| 做法 | 优点 | 会怎么坏 |
|---|---|---|
| 手工文档 | 上手快 | 三个月就过期 |
| 配置文件 | 能进版本库 | 和实际结构还是两份 |
| 系统内置目录 | 永远和实际一致 | 要一开始就用对工具 |

  第三种是 Hive Metastore 那类东西:建表的时候顺手记,不记就建不了。
  把「记录」变成「操作的副产品」,才不会过期。
  这是整章的核心判断:任何需要额外动作去维护的元数据,最后都会烂掉。

第一段那张表就是元数据最朴素的样子。 注意它的来源:不是有人整理的,是数据库自己记的,所以永远不会和实际不符。 第二段那个「五列对六列」很朴素,但它是所有数据事故里最常见的一类:不是算错,是拿了一份过期的说明。 第三段那个四十六次对一次,是本篇唯一一个「规模」相关的数。表越多,这个差距越离谱。 第四段那张作业日志看着枯燥,但它是下一篇文章的基础——沿袭就是靠这些「谁读了谁、 谁写了谁」的记录连起来的。

30 秒实验

  1. 把 dim_00 到 dim_39 加到两百张,看「逐张问」和「查元数据表」的倍数变成多少。
  2. 给 meta_col 加一个「最后修改时间」列,想想它能不能回答「这张表多久没动了」。
  3. 翻一翻你们公司有没有一份「数据字典」文档,看它最后一次更新是什么时候。

常见误区

  1. 「元数据就是数据字典文档。」文档是元数据的一种载体,不是元数据本身。手抄的文档一定会过期。
  2. 「元数据管理是运维的事,和写 SQL 的人无关。 」改表不加记录,沿袭就断了,最后追不到源头的是分析师。
  3. 「先把业务跑起来,元数据以后再说。」以后就是永远。 等到出事那天才补,补不出来——因为历史已经丢了。
自测三问(点开对答案) 1. 静态元数据和动态元数据各记什么? —— 静态记结构:表、列、类型、负责方。 动态记变化:哪个作业什么时候读了什么、写了什么、进出多少行。 前者回答「有什么」,后者回答「怎么变成这样的」。 2. 为什么手抄的数据字典一定会过期? —— 因为改表和改文档是两个动作,没人会把它们绑在一起。 只有让记录成为操作的副产品,才不会有第二份会分叉的副本。 3. 元数据管理的价值体现在哪里? —— 体现在规模上:它让「问结构」的成本不随表的数量增长。四十六张表差四十六倍,一千张差一千倍。

下一篇讲结构管理和数据沿袭:改一列会绊倒谁,一个数不对该往哪追。

相关笔记