📊 第7章 大数据的活用
📄 一句话概括

即席分析是用户临时提问、系统当场作答。矛盾在于:问题越自由,算得越慢。 同一个问题两种写法:现场 join 三张表 0.469 秒,查宽表 0.309 秒,只差 1.5 倍。 差距小是因为只有一台机器。 分布式下 join 要按关联键重发数据,那才是数量级差别——这一段本沙箱测不出,只能讲机制。

📝 本章和前面几章的关系

上一篇讲数据怎么摆出来给人找。这一篇讲找到之后怎么问。 教材 7.2、7.3 节:即席分析、构筑即席分析环境。 一句话:难点不在 SQL 写不出来,在「自由」和「快」不能同时要。

一、什么叫即席

即席就是事先没准备过的提问。「上个月华东区新客的复购率是多少」,这种问题没法提前排进固定报表。 传统报表是「事先定好问题」,一次算好、很多人看。即席分析是「问题现场来」,一次算一次、一个人看。 两者的成本结构完全不同。所以即席分析天生贵——贵在重复计算。

理解这一点,后面所有的优化手段都能自己推出来:要么少算(预计算),要么算得快(并行、索引), 要么让人排队(资源管理)。

二、同一个问题,两种写法

演示建了二十万订单、五千用户、二百商品,外加一张拼好的宽表,建库约 1.7 秒。 要问的是:按城市、用户分层、商品类目分组,成交额最高的五个组合是什么。 写法一,现场 join 三张表:0.469 秒。

写法二,直接查宽表:0.309 秒。 两条结果完全一样,耗时差 1.5 倍。 这里必须说实话:1.5 倍不惊人。 单机上 join 只是多查了两次索引, 二十万行的小表撑不起数量级的差距。

要是这里写「宽表快了五十倍」,那就是编的。真实差距就这么大。

三、为什么单机差得少,分布式差得多

数一下碰了多少行:join 要扫二十万行订单,再按 key 查二十万次用户、二十万次商品; 宽表只扫二十万行,一次按 key 查都没有。 碰的行数是三倍,但每次查的都是内存里的索引,所以只差 1.5 倍。 分水岭在分布式。那时候 join 意味着把两张表按关联键重新分发到网络上,数据要在机器之间搬一遍。

搬多少、搬多远,才是瓶颈。而这一项在本沙箱里根本不存在——一台机器、一个 sqlite 文件。 所以上面那个 1.5 倍不能当成分布式的结论。这条界限不说清,就是拿单机数据骗人。

四、宽表并不省空间

演示还量了一个数:宽表二十万行占 7.91MB,每行 41 字节;订单表同样二十万行占 4.12MB,每行 22 字节。 差 1.9 倍。 因为宽表把订单的五个字段和用户、商品的信息全拼在一起,行数没变、每行变宽。 所以宽表省的是计算,不是存储。每建一张,磁盘就再存一份,源表一改还得重建。

这句话值得抄下来:预计算是把「算的时间」换成「存的空间和重建的麻烦」。 它是一笔交易,不是白拿的好处。想清楚这笔交易划不划算,才叫选型。

五、维度钉死了,那就看日志

宽表最大的限制是维度被钉死。这张有城市、分层、类目,用户要「按月份看」就得重建。 所以真实做法通常是两层:宽表 + 明细。常见问题走宽表,特殊问题走明细,允许慢一点。 那该建哪些宽表?看查询日志,不靠猜。

演示给了三档示例分界:每周五十次以上建宽表,十到五十次建索引或物化视图,十次以下现场算。 分界是示例,你们的要从自己的日志里量。 教材 7.3 节「构筑即席分析环境」讲的就是两个手段: 资源管理(别让一个人的大查询拖垮所有人)+ 预计算(把常用的先算好)。

六、动手 Demo:同一个问题:join 三张表,还是查宽表

建二十万订单、五千用户、二百商品,另拼一张宽表。 同一个分组问题,两种写法各跑一次,比耗时。 再量一下宽表到底占多少空间。

SQL_JOIN = ('select w.city, w.seg, g.cat, sum(o.q * o.a) as amt '
            'from orders o '
            'join users w on o.u = w.u '
            'join goods g on o.sku = g.sku '
            'group by w.city, w.seg, g.cat order by amt desc limit 5')

SQL_WIDE = ('select city, seg, cat, sum(q * a) as amt '
            'from wide group by city, seg, cat order by amt desc limit 5')

t0 = time.perf_counter(); r1 = con.execute(SQL_JOIN).fetchall()
t_join = time.perf_counter() - t0

t0 = time.perf_counter(); r2 = con.execute(SQL_WIDE).fetchall()
t_wide = time.perf_counter() - t0

# 宽表到底占多少:问 sqlite 每一页的实际字节数
for name, sz in con.execute(
        'select name, sum(pgsize) from dbstat group by name'):
    print(name, sz)

实跑输出:

--------------------------------------------------------------------
即席分析:现场 join 三张表,还是查一张宽表
--------------------------------------------------------------------
  建好 20 万订单 + 5000 用户 + 200 商品,外加一张宽表
  建库耗时 1.66 秒

  ① 同一个问题,两种问法
  --------------------------------------------------------------------
  问题:按城市、用户分层、商品类目分组,算成交额最高的五个组合。
  写法一:现场 join 三张表。
  写法二:直接查预先拼好的宽表。

| 写法 | 耗时 | 第一条结果 |
|---|---:|---|
| 现场 join 三张表 | 0.469 秒 | 城市19 / S3 / 类目7 / 6158486 |
| 查宽表 | 0.309 秒 | 城市19 / S3 / 类目7 / 6158486 |

  两条结果完全一样,耗时差 1.5 倍。
  注意这个倍数并不惊人。 单机上 join 只是多查了两次索引,20 万行的小表撑不起「数量级」的差距。
  要是这里写「宽表快了 50 倍」,那就是编的。真实差距就这么大。

  ② 那为什么还要建宽表:数一数碰了多少行
  --------------------------------------------------------------------

| 写法 | 扫多少行 | 按 key 查多少次 |
|---|---|---|
| join 三张表 | 20 万行订单 | 用户 20 万次 + 商品 20 万次 |
| 查宽表 | 20 万行宽表 | 0 次 |

  碰的行数是三倍,但每次查的都是内存里的索引,所以只差 1.5 倍。
  真正的分水岭在分布式环境。
  那时候 join 意味着把两张表按关联键重新分发到网络上——数据要在机器之间搬一遍,
  搬的量和网络带宽才是瓶颈。
  本沙箱只有一台机器、一个 sqlite 文件,这段开销测不出来。
  所以上面这个 1.5 倍不能当成分布式的结论。

  ③ 宽表并不省空间
  --------------------------------------------------------------------

| 表 | 行数 | 占用 | 每行 |
|---|---:|---:|---:|
| goods | 200 | 4 KB | 20 字节 |
| orders | 200000 | 4.12 MB | 22 字节 |
| users | 5000 | 108 KB | 22 字节 |
| wide | 200000 | 7.91 MB | 41 字节 |

  宽表每行 41 字节,订单表每行 22 字节,差 1.9 倍。
  原因是宽表把订单的五个字段和用户、商品的信息全拼在一起,行数没变、每行变宽。
  所以宽表省的是计算,不是存储。每建一张宽表,磁盘就再存一份,而且源表一改就得重建。
  上面这个「每行」是 sqlite 的实际页占用除以行数,含索引和空闲页,
  和列宽不完全成正比——但量级关系是真的。

  ④ 提前算的代价:维度被钉死了
  --------------------------------------------------------------------

| 做法 | 问得快不快 | 能问的问题 |
|---|---|---|
| 不预计算 | 慢,每次都要 join | 什么都行 |
| 建宽表 | 快 | 只能问宽表里有的维度 |
| 建很多宽表 | 都快 | 存储翻几倍,维护很累 |

  这张宽表有城市、分层、类目,但用户要是问「按月份看」,就得重建。
  所以真实做法通常是「宽表 + 明细」两层:常见问题走宽表,特殊问题走明细,允许慢一点。
  教材 7.3 节讲的「构筑即席分析环境」,核心就是这两个手段:
  资源管理(别让一个人的大查询拖垮所有人)+ 预计算(把常用的先算好)。

  ⑤ 谁来决定建哪些宽表
  --------------------------------------------------------------------
  看查询日志:哪些组合被问得最多,就给哪些建宽表。

| 每周被问次数 | 处理方式 |
|---|---|
| 50 次以上 | 建宽表,专门优化 |
| 10 到 50 次 | 建索引或物化视图 |
| 10 次以下 | 现场算,接受慢 |

  这是数据团队最实用的一条工作方法:别猜用户要什么,看日志。
  顺带一句:表里的次数分界是示例,你们自己的分界要从日志里量出来。

第一段那个 1.5 倍是本篇最容易被忽略的地方。它不够响亮,但它是真的。 任何号称「宽表快几十倍」的说法,都得先说清是在什么规模、什么部署下量的。 第二段那张对照表说明差距的来源:不是「SQL 写得好不好」,是碰了多少行。 碰三倍的行,只慢一点五倍,说明索引在顶着。 第三段是诚实交代。分布式下 join 的开销主要是网络搬运,本沙箱一台机器,这一项恒为零,所以测不出。 第四段那个 41 字节对 22 字节,打破一个常见错觉:宽表看起来「更整齐」,但它比明细表更占地方。

30 秒实验

  1. 把 orders 从二十万加到一百万,看 1.5 倍会不会变(大概率不变,因为两边都是随行数线性增长)。
  2. 给 orders 的 u、sku 建索引,再跑一次 join,看差距缩小多少。
  3. 想想你们团队最近被问最多的三个问题,它们能不能提前算成一张表。

常见误区

  1. 「宽表一定快几十倍。」单机上不是。倍数取决于 join 要搬多少数据、搬多远。
  2. 「预计算没有代价。」占空间、要重建、维度锁死。它是交易,不是白拿。
  3. 「即席分析就是让用户随便查。」随便查会把集群拖垮。真实环境一定要有资源限制和排队。
自测三问(点开对答案) 1. 即席分析为什么天生比固定报表贵? —— 因为每次提问都要重新算一遍,不能复用别人的计算结果。 报表是一次算好很多人看,即席是一次算一次一个人看。 2. 为什么单机上 join 和宽表只差一点几倍? —— 因为两边扫的行数在同一个量级,而且 join 查的是内存里的索引。 真正拉开差距的是分布式下的网络搬运,单机没有这一项。 3. 决定建宽表之前,第一件事该看什么? —— 看查询日志:哪些维度组合被问得最频繁。猜用户要什么,几乎一定猜错。

下一篇讲最后一公里:数据怎么变成人能看懂的图和能用的应用。

相关笔记