即席分析是用户临时提问、系统当场作答。矛盾在于:问题越自由,算得越慢。 同一个问题两种写法:现场 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 秒实验
- 把 orders 从二十万加到一百万,看 1.5 倍会不会变(大概率不变,因为两边都是随行数线性增长)。
- 给 orders 的 u、sku 建索引,再跑一次 join,看差距缩小多少。
- 想想你们团队最近被问最多的三个问题,它们能不能提前算成一张表。
常见误区
- 「宽表一定快几十倍。」单机上不是。倍数取决于 join 要搬多少数据、搬多远。
- 「预计算没有代价。」占空间、要重建、维度锁死。它是交易,不是白拿。
- 「即席分析就是让用户随便查。」随便查会把集群拖垮。真实环境一定要有资源限制和排队。
自测三问(点开对答案)
1. 即席分析为什么天生比固定报表贵? —— 因为每次提问都要重新算一遍,不能复用别人的计算结果。 报表是一次算好很多人看,即席是一次算一次一个人看。 2. 为什么单机上 join 和宽表只差一点几倍? —— 因为两边扫的行数在同一个量级,而且 join 查的是内存里的索引。 真正拉开差距的是分布式下的网络搬运,单机没有这一项。 3. 决定建宽表之前,第一件事该看什么? —— 看查询日志:哪些维度组合被问得最频繁。猜用户要什么,几乎一定猜错。下一篇讲最后一公里:数据怎么变成人能看懂的图和能用的应用。
- ☐ (本篇完,下一篇 7.3 可视化与数据应用)