广告实时链路把曝光、点击、转化事件搬进 Kafka、用 Flink 算完之后,钱和效果最终要变成可交互的报表:广告主按 campaign × 媒体 × 地域 × 设备切一刀,要在秒级看到花费、点击、UV、CTR。这件事落在 OLAP(Online Analytical Processing)——而现代 OLAP 的地基,几乎全是列式存储。这个系列三篇,从列存原理讲到实时数仓分层,再到广告多维报表实战;开篇这一篇先把「为什么列存、怎么加速、ClickHouse/Doris 怎么选」讲透。
本文是实时数仓与 OLAP 系列的第 1 篇(开篇 · 列式存储)。 全系列 3 篇:
- OLAP(开篇)· 列式存储原理与 ClickHouse/Doris(本篇)
- 实时数仓 · 分层设计与 Lambda/Kappa 架构
- 广告多维报表与漏斗分析实战
一句话定位:OLAP 要用列存,是因为分析查询通常「读很少几列、却扫很多行」;编码压缩、向量化、MPP 把这条路径从分钟级压到秒级,ClickHouse 与 Doris 是这条路上最常见的两块工程积木。
TL;DR
- 行存 vs 列存:行存按行连续落盘,点查/更新友好;列存按列连续,只读相关列、同列易压缩、向量化友好——宽表聚合的默认形态。
- 广告效果表是列存的教科书场景:几十上百列维度,报表却常只
GROUP BY少数列再SUM(cost)——行存会白读大半行,列存只扫命中列。 - 编码与压缩是加速的第一层:Dictionary(低基数字典)、RLE/Delta(连续/有序)、LZ4/ZSTD(块压缩)、稀疏/跳过索引(Skip/minmax)一起把扫描字节砍掉一个数量级。
- 向量化执行:按「列块 / Batch」一批一批算,吃满 SIMD 与 CPU 缓存,远高于「一行一算」的 volcano 迭代器。
- MPP:Coordinator 拆查询,Worker 本地扫各自分片再 shuffle/汇总——吞吐随节点近线性扩展。
- ClickHouse:MergeTree 家族、节点对等 + Distributed 表,扫表极强、引擎丰富,运维心智偏「本地表优先」。
- Doris:FE/BE 分离、MySQL 协议、物化视图与导入体验友好,报表语义和传统数仓更接近。
- 选型:要极致扫描与可控运维 → CH;要 MySQL 生态 + FE/BE + 物化视图省心 → Doris。AdTech 都能做实时 ADS。
- 分区/排序键先于压缩调优;头部广告主会造成 MPP 分片倾斜,需盐值或租户隔离。
Table of contents
Open Table of contents
1. 为什么 OLAP 绕不开列存
1.1 OLTP 与 OLAP 的访问模式完全相反
缓存、交易库、订单系统关心的是:按主键取一行、改几列、事务提交——这是 OLTP。行式存储(InnoDB 一类)把一行的所有列连续放在一起,一次 I/O 拿完整行,点查最快。
广告报表、漏斗、归因看板关心的是另一种问法:扫过去 N 天、按若干维分组、聚合几个指标。典型 SQL 像:
SELECT campaign_id, media_id, sum(cost), countIf(clk = 1)
FROM ad_effect
WHERE event_date BETWEEN today() - 7 AND today()
GROUP BY campaign_id, media_id;
一张效果明细表可能有 80+ 列(创意、出价、设备、OS、地域、流量类型…),但这条查询真正用到的列只有少数几个。若按行存扫表,磁盘和内存缓存被无关列填满——这就是 OLAP 里人人吐槽的「宽表扫不动」。
1.2 列存如何对症下药
列存把同一列的值连续存放。好处立竿见影:
- 只读相关列:上面的 SQL 只碰
campaign_id / media_id / cost / clk / event_date,其余 70 列完全不进 I/O。 - 同列相邻 → 压缩极好:同一列类型一致、取值空间相近,字典/RLE/Delta 压缩比远高于行存块。
- 向量化友好:连续同类型数组天然适合 SIMD 批量运算。
同一份广告效果数据:行存按行连续(适合点查),列存按列连续(适合宽表聚合)。OLAP 默认站在右边。
心智切换:把「取一行对象」换成「取几列数组再聚合」。一旦接受这条,后面的编码、向量化、MPP 都是在这条路上继续加油。
2. 编码、压缩与跳过:少扫一字节是一字节
列存不是「竖着存」就完事。工业实现里,一列通常切成数据块(granule / page / segment),块内再编码,块级挂跳过信息:
| 技术 | 适用 | 作用 |
|---|---|---|
| Dictionary | 低基数字符/枚举(媒体、OS) | 存字典 ID,体积骤降 |
| RLE | 大量连续重复值 | 存「值 × 出现次数」 |
| Delta / DoubleDelta | 有序时间戳、自增 ID | 存差分,整数更小更好压 |
| LZ4 / ZSTD | 通用块压缩 | 进一步砍传输与缓存占用 |
| MinMax / 稀疏索引 | 任意可比较列 | 谓词下推时整块跳过 |
广告场景里 media_id、os、country 几乎总适合字典;event_time 适合有序 + 跳过索引;imp_id 高基数则靠分区裁剪而不是指望字典。分区键 + 排序键(ORDER BY / 主键序)选型决定了「一次报表能跳过多少块」——这是比调压缩算法更先做的功课。
2.1 分区键与排序键:比压缩算法更早定
以 ClickHouse MergeTree 为例(Doris 的分区分桶同理):
- 分区键通常取
event_date(或小时):报表几乎总带时间范围,分区裁剪能直接扔掉无关天; - 排序键 / 主键序应匹配最高频的等值/范围过滤与 GROUP BY 前缀。广告看板若 80% 查询都是「某广告主下的计划 × 媒体」,排序键写成
(advertiser_id, campaign_id, media_id, event_time)比把高基imp_id放最前有价值得多; - 稀疏索引粒度(index_granularity)过细会抬元数据,过粗会少跳过——先按默认,再用慢查询的「读取粒度标记数」反推。
一条经验:先保证「时间分区 + 业务过滤前缀在排序键最左」,再谈换压缩编解码器。 许多人一上来调 ZSTD 等级,却让查询永远扫全分区——是典型的优化顺序颠倒。
2.2 宽表要不要拆列族?
列存已经按列独立存储,不必像早期宽表那样强行垂直拆很多物理表。更常见的是:
- 一张明细宽表 + 投影/物化覆盖高频聚合;
- 把「极少访问的 debug 列」拆到旁路表,避免拖累主表的 part 合并与缓存。
拆表的收益往往来自生命周期与权限(原始 debug 字段保留 7 天、指标列保留 400 天),而不是「列存还要再竖着拆才快」。
3. 向量化执行:一批一批算,而不是一行一算
传统执行器多是 Volcano 迭代器:next() 一次吐一行,虚函数调用、分支、缓存不友好。向量化引擎改为:
- 一次拿一个 Batch(常见数千行的列式数组);
- Filter / Project / Aggregate 整列推进;
- 热路径可用 SIMD;减少解释开销。
对 sum(cost) 这类广告指标,向量化意味着「对一段 float/int 数组做紧密循环累加」,吞吐比逐行解释高一个数量级并不夸张。ClickHouse、Doris、DuckDB、Spark Adaptive 后期优化都走这条路。
3.1 谓词下推与延迟物化
向量化之外,分析引擎还会做两件与列存强绑定的优化:
- 谓词下推:
WHERE event_date = today() AND campaign_id = 42尽量在扫描阶段就砍掉行,少进上层算子; - 延迟物化(late materialization):先用排序键/索引列过滤出「存活行号」,再去拉取
cost等宽大的指标列——避免「先解压所有列再过滤」。
广告宽表尤其吃这一套:先靠 advertiser_id / campaign_id 缩到一小撮 granule,再解压指标列,比一上来解码 80 列高效得多。
4. MPP:一查询打满多台机器
单机再快也扛不住「半年明细 × 全站广告主」的扫表。MPP(Massively Parallel Processing)的分工很朴素:
- Coordinator 解析 SQL、生成计划、把扫描任务分给 Worker;
- 各 Worker 只扫自己负责的分片(shard / tablet),本地过滤、局部聚合;
- Shuffle / 二次聚合 按
GROUP BYkey 归约,结果回客户端。
编码压缩减 I/O → 向量化抬单核 → MPP 扩集群:OLAP 性能的三层叠塔。
注意:数据局部性决定 MPP 效率——排序键与分片键能对齐高频过滤条件,本地聚合就能消掉大部分 shuffle。分片乱切、查询却总按广告主过滤,会把集群打成「全员 shuffle」的惨状。
4.1 分片策略与广告主热点
广告流量天然倾斜:头部广告主贡献大部分曝光。若分片键是 cityHash(advertiser_id),头部会砸在少数分片上,出现「集群整体空闲、个别节点冒烟」——与 Flink 热 key 是同一类物理事实。
缓解手段:
- 分片键掺入更均衡的字段(如
impression盐),查询仍按广告主过滤时接受一定额外扫描; - 或按业务把超大广告主拆到独立表/独立集群(租户隔离);
- 预聚合到 DWS,让查询别总扫最细明细。
MPP 不是「加机器就线性变快」的魔法——分片与查询模式不对齐时,加机器只会让 shuffle 网络更热闹。
5. ClickHouse vs Doris:两套架构心智
两者都是列存 + 向量化 + MPP,但「集群怎么组织、表怎么建模」差别明显。
5.1 ClickHouse:MergeTree + 本地表优先
- 存储引擎核心是 MergeTree 家族(含 Replacing / Aggregating / Collapsing 等):数据按主键序落盘,带稀疏索引,后台 merge 合并 parts。
- 节点大体对等:查询可打到任一节点;跨节点靠 Distributed 表 路由到本地表。没有单独的「FE 元数据大脑」这一层(复制与分片由配置/Keeper 等协同)。
- 强项:扫表吞吐、SQL 函数生态、物化视图与投影(Projection)、宽表明细直接扛查询。
- 心智:工程师感强——排序键、分区、副本、Distributed 都要自己设计清楚。
5.2 Apache Doris:FE / BE 分离
- FE(Frontend):SQL 解析、优化器、元数据、查询调度(MySQL 协议入口极友好)。
- BE(Backend):列存、向量化执行、Tablet 分片与副本。
- 强项:导入(Stream Load / Routine Load)、物化视图、与传统数仓/BI 工具对接顺滑。
- 心智:更接近「数仓产品」——表模型、分桶、副本由 FE 统筹,报表团队上手成本往往更低。
CH 偏「强大引擎 + 本地表心智」;Doris 偏「FE/BE 产品化 + 报表友好」。
5.3 选型对照
| 维度 | ClickHouse | Apache Doris |
|---|---|---|
| 架构 | 对等节点 + Distributed | FE / BE 分离 |
| 协议 | HTTP / native | MySQL 协议友好 |
| 建模 | 宽表 + 排序键为主 | 明细 + 物化视图/聚合表常见 |
| 导入 | 批量/Kafka 引擎多 | Stream/Routine Load 产品化 |
| 扫表明细 | 极强 | 强 |
| 运维心智 | 更「引擎可控」 | 更「数仓易用」 |
AdTech 实践:明细实时写入 + 秒级多维看板两边都能做;若团队已有深 ClickHouse 经验就继续挖投影/物化,若希望 BI/数据同学用 MySQL 工具自助、少碰 Distributed 表,Doris 更顺。别在「谁更 OLAP」上空辩——差在工程生态与人。
6. 写入、合并与「实时」的边界
列存为了扫描优化,写入通常是追加小文件/小批次,再后台合并(CH 的 merge、Doris 的 compaction)。这带来两个现实约束:
- 写入吞吐与查询延迟要折中:批次太小 → merge 风暴;太大 → 可见延迟升。
- 更新/删除是二等公民:多靠标记删除、版本列、专用引擎,而不是 OLTP 式原地改行。广告明细应以 append + 去重/替换语义设计(见系列末篇的 upsert 与精确去重)。
实时数仓里常见链路是 Flink Exactly-Once 写出 → OLAP 近实时可见——「近」通常是秒到数十秒,不是交易库的毫秒事务。
6.1 重复数据与 Replacing / 主键模型
Kafka/Flink 至少一次语义或重放,会把同一 imp_id 写进 OLAP 多次。常见对策:
- ReplacingMergeTree / 主键去重模型:查询侧
FINAL或按版本取最新(有代价,别滥用); - 写入前幂等:Flink sink 侧按业务键去重,或用引擎的去重表引擎;
- 对账接受短暂重复:报表读聚合表,明细重复在合并后消失。
选型要和 SLA 绑定:广告主看板能容忍「合并前 UV 略高」吗?账单能不能?能容忍短暂不精确,就能省大量在线去重算力。
6.2 副本与高可用
列存集群的副本不只是备份:查询可走副本分散负载,写入则要考虑 quorum/异步复制的可见性。生产上至少:多副本跨机架、备份到对象存储、演练过「单节点挂掉后重建」。OLAP 挂掉时广告主后台应有降级文案,而不是白屏。
7. 什么时候不该上列存 OLAP
不是所有「要分析」的需求都值得上 CH/Doris:
- 强事务、按行频繁更新的预算账户状态 → 仍是 OLTP / Redis;
- 低延迟点查单个
imp_id详情 → 行存或 KV 更合适; - 团队无人会运维列存、日增只有百万行 → 可能 Postgres + 合适索引就够。
列存的甜蜜点是:追加型、宽表、扫很多行、聚很少列、延迟秒级可接受。 广告效果明细几乎完美命中;竞价决策日志里的「在线特征点查」则未必。
参考
- ClickHouse Docs. What is ClickHouse?:MergeTree、稀疏索引与列存执行的官方入口。
- Apache Doris Docs. Architecture:FE/BE、Tablet 与物化视图概念。
- D. Abadi et al. Column-Stores vs. Row-Stores:列存优势的经典论文视角。
- ClickHouse. Compression in ClickHouse:编码与编解码器实践。