数据池存原样,数据仓库存洗过的。 前者为了能重来,后者为了能算准。 同一个问题两边各查一次:相差 601140 元,池子那边漏了缺金额的、重复的算了两次。 活用是在仓库上面问问题;池子只负责别丢东西。
上一篇讲三段怎么拼。这一篇拆开中间那段。 教材 2.3、2.4 两节:数据的积累、数据的活用。 教材把池和仓并列讲,容易看成「两种存法」。 本篇用一批真脏数据证明:它们不是两种存法,是两个阶段。 后面第 6 章会把仓库这块展开:分析型数据库、列式存储、DWH 制品。
一、先看一批真脏数据
演示造了 20500 条订单,故意弄脏:6150 条金额是字符串、2050 条压根没有金额、2050 条没有用户号、 还有 500 条重复单号。 这四种毛病在真实数据里天天见。 上游改了个类型(金额从数字变成字符串)、某个字段偶尔不传、重跑任务灌了两遍。 它们不是「数据质量差」,是「数据本来的样子」。 业务系统忙着处理交易,没空帮你对齐类型。
所以第二段必须分成两步:先把原样留住,再洗干净。这两步就是池和仓。
二、池子:为什么不洗
池子只干一件事:收到的原样留住,不改、不删、不猜。 演示里 20500 条一条不少地写进池子,文件 1123348 字节,里面照样有字符串金额、有缺字段、有重复。 为什么不顺手洗了?因为清洗规则会变。
今天你决定「缺金额就补 0」,明天业务说「缺金额的应该排除」,后天又说「应该用上一次的价格补」。 规则改三次,脏数据被洗错三次,没有原始数据就再也回不去了。 池子的价值是「可重放」:规则改了,拿原始数据重跑一遍就行。 这一点在出问题的时候值多少钱,做过数据的人都知道。 顺带一句:池子一般不做索引、不做约束、不管口径。它就是个能装很多东西的桶。
三、仓库:洗完再进去
仓库跟池子相反:进去之前必须洗干净。 演示里做了三件事:去重(扔掉 500 条)、类型统一(字符串金额转成浮点)、缺字段补默认值(2000 条)。 洗完剩 20000 条,进了 SQLite,建了主键。 库文件 618496 字节——比池子小得多,因为重复和冗余都被消掉了。
补默认值这里是 2000 条,比上面那个 2050 少几条,因为重复的那 500 条里也有缺金额的,被一起去掉了。 这类活有个名字叫 ETL:抽取、转换、加载。第 5.3 篇会专门讲。 仓库的核心不是「存」,是「口径」。 什么叫「有效订单」、什么叫「本月」、 退款算不算——这些定义必须固定在仓库这一层,否则每个人算出来都不一样。
四、同一个问题,两个答案
演示第四段最有意思:对两边问同一个问题「总额是多少」。 池子那边现洗现算,得到 27480480.00;仓库直接查,得到 26879340.00。相差 601140 元。 差在两处:池子那边跳过了 2050 条没有金额的记录(算不了),重复的 500 条又被多算了一遍。
这不是池子算错了。池子的职责是「别丢东西」,不是「算得准」。口径统一是仓库的活。 速度也差 31 倍:池子 0.0990 秒,仓库 0.0032 秒。因为仓库里类型已经对了,查的时候不用再判断。 这个差异说明一件很重要的事:「数据在哪」和「数据能不能直接用来算」是两件事。前者是池子, 后者是仓库。
五、活用:在仓库上面问问题
第三段(活用)几乎总是接在仓库后面,不是接在池子后面。原因就是上面那 601140 元。 活用这件事本身也有层次:先做报表(上个月卖了多少),再做诊断(为什么这个店差), 最后做预测(下个月该备多少货)。 每上一层,对仓库的要求就更严:数据要更细、更新要更快、口径要更稳。
第 7 章讲的就是这一段的展开:数据市场把常用口径加工成半成品,即席分析让人自己查, 可视化让人一眼看懂。 但这一切的前提都是仓库那一层把口径定住了。 口径不定,上面盖什么都会歪。
六、动手 Demo:同一批脏数据,池子和仓库各查一次
造 20500 条脏订单:类型不一致、缺字段、有重复。 一份原样进池子,一份洗净进仓库。 然后对两边问同一个问题,比答案、比速度。
raw = load_dirty(20000) # 类型混、缺字段、有重复
# 池子:原样写,一条不改
with open('pool.jsonl', 'w') as f:
for r in raw:
f.write(json.dumps(r) + '\n')
# 仓库:洗一遍再进
clean, seen = [], set()
for r in raw:
if r['order'] in seen: # 去重
continue
seen.add(r['order'])
amt = r.get('amt', 0.0)
if isinstance(amt, str): # 统一类型
amt = float(amt)
clean.append((r['order'], r.get('user', -1), r['qty'], amt))
con.executemany('insert into orders values(?,?,?,?)', clean)
实跑输出:
--------------------------------------------------------------------
数据池存原样,数据仓库存洗过的
--------------------------------------------------------------------
① 一批脏数据长什么样
--------------------------------------------------------------------
条数:20500
amt 是字符串的:6150 条
根本没有 amt 的:2050 条
没有 user 的:2050 条
重复单号:500 条
这四种毛病在真实数据里天天见:上游改了类型、某个字段偶尔不传、重跑了一次灌了两遍。
② 数据池:原样存进去
--------------------------------------------------------------------
写入 20500 条,耗时 0.1318 秒
文件 1123348 字节
池子只干一件事:把收到的原样留住。不改、不删、不猜。
它的价值是「以后能重来」——清洗规则改了,可以拿原始数据重跑一遍。
③ 数据仓库:洗干净再进去
--------------------------------------------------------------------
去重扔掉 500 条,补默认值 2000 条
入库 20000 条,耗时 0.3046 秒
库文件 618496 字节
补默认值是 2000 条,比上面那个 2050 少几条——因为重复的那 500 条里也有缺金额的,被一起去掉了。
④ 同一个问题,两边各查一次
--------------------------------------------------------------------
| 从哪查 | 结果 | 耗时 |
|---|---:|---:|
| 数据池(现场洗) | 27480480.00 | 0.0935 秒 |
| 数据仓库(直接查) | 26879340.00 | 0.0032 秒 |
两个数差 601140.00 元
差在两处:池子那边跳过了 2050 条没有金额的记录,重复的那 500 条又被多算了一遍。
同一个问题,从池子查和从仓库查,答案不一样。
这不是池子的错。池子本来就该原样存——它的职责是「别丢东西」,不是「算得准」。口径统一是仓库的活。
速度也不一样:仓库快 29 倍。因为它已经洗过了,查的时候不用再判断类型。
第一段那四个数字值得记:百分之三十的金额是字符串。 这不是数据脏,是上游系统的接口没定死类型,属于常态。 池子那段代码只有三行,因为它真的什么都不做。「什么都不做」在这里是优点,不是偷懒。 仓库那段补默认值的条数(2000)比前面统计的缺金额条数(2050)少,因为去重先发生, 重复的那 500 条里有 50 条也缺金额。这类差几十条的地方最容易被忽略。 最后那张表里两个数差了六十多万。 这个差额不是 bug,是口径差异——池子算的是「洗得动的部分」,仓库算的是「定义好的部分」。
30 秒实验
- 把去重那一步去掉,重跑,看仓库的总额变成多少——应该比池子的还大。
- 把「缺金额补 0」改成「缺金额直接跳过」,看仓库的总额变不变。 (不变,因为补 0 对求和不影响结果,但会影响「平均单价」这类指标。)
- 把池子的读取改成先全读进内存再算,看速度差多少。
常见误区
- 「池子就是备份。」不是。备份是为了恢复,池子是为了重放——用新规则把历史数据重算一遍。
- 「仓库里数据越全越好。」仓库的价值在「口径统一」,不在「什么都有」。 塞进来越多没洗的东西,口径越容易乱。
- 「两边数不一样,肯定有一个错了。」两个都没错,是定义不同。 搞清楚「跳过缺值」还是「补零」,比争论谁对更有用。
自测三问(点开对答案)
1. 为什么池子不顺手把数据洗干净? —— 因为清洗规则会变。洗过之后原始信息就没了,规则一改没法重来。池子存在的意义就是「可重放」。 2. 同一批数据,池子算出 27480480、仓库算出 26879340,哪个对? —— 看你要回答什么问题。要「所有记录的总金额」两个都不完整; 要「有效订单总额」,仓库那个才有定义。数字对错取决于口径,不取决于计算。 3. 仓库比池子小了一半,为什么? —— 因为重复被去掉了、字段被压成定长、没有冗余的字段名。池子保留全部原始信息,本来就会更大。下一篇进第 3 章:一台机器撑不住的时候,先看哪儿满了。
- ☐ (本篇完,下一篇 3.1 先找瓶颈)