Skip to content
Charles Shao
Go back

OLAP(开篇)· 列式存储原理与 ClickHouse/Doris

views

广告实时链路把曝光、点击、转化事件搬进 Kafka、用 Flink 算完之后,钱和效果最终要变成可交互的报表:广告主按 campaign × 媒体 × 地域 × 设备切一刀,要在秒级看到花费、点击、UV、CTR。这件事落在 OLAP(Online Analytical Processing)——而现代 OLAP 的地基,几乎全是列式存储。这个系列三篇,从列存原理讲到实时数仓分层,再到广告多维报表实战;开篇这一篇先把「为什么列存、怎么加速、ClickHouse/Doris 怎么选」讲透。

本文是实时数仓与 OLAP 系列的第 1 篇(开篇 · 列式存储)。 全系列 3 篇:

  1. OLAP(开篇)· 列式存储原理与 ClickHouse/Doris(本篇)
  2. 实时数仓 · 分层设计与 Lambda/Kappa 架构
  3. 广告多维报表与漏斗分析实战

一句话定位:OLAP 要用列存,是因为分析查询通常「读很少几列、却扫很多行」;编码压缩、向量化、MPP 把这条路径从分钟级压到秒级,ClickHouse 与 Doris 是这条路上最常见的两块工程积木。

TL;DR

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 列存如何对症下药

列存把同一列的值连续存放。好处立竿见影:

  1. 只读相关列:上面的 SQL 只碰 campaign_id / media_id / cost / clk / event_date,其余 70 列完全不进 I/O。
  2. 同列相邻 → 压缩极好:同一列类型一致、取值空间相近,字典/RLE/Delta 压缩比远高于行存块。
  3. 向量化友好:连续同类型数组天然适合 SIMD 批量运算。

行式存储与列式存储对比示意图。左侧行存面板展示一张广告效果表(列:imp_id、campaign、ad_id、media、cost、clk)的三行数据,每一整行用琥珀色高亮,说明磁盘一次 I/O 扫过整行全部列,点查友好但宽表扫表浪费。右侧列存面板把同样六列竖着排列,仅将查询相关的 campaign、cost 两列蓝色高亮,说明 SQL 只需这两列时只扫相关列,同列相邻利于编码压缩与向量化。底部注解指出广告效果表往往几十到上百列,报表却常 GROUP BY 少数维度再 SUM,列存使只读相关列成为默认路径。

同一份广告效果数据:行存按行连续(适合点查),列存按列连续(适合宽表聚合)。OLAP 默认站在右边。

心智切换:把「取一行对象」换成「取几列数组再聚合」。一旦接受这条,后面的编码、向量化、MPP 都是在这条路上继续加油。

2. 编码、压缩与跳过:少扫一字节是一字节

列存不是「竖着存」就完事。工业实现里,一列通常切成数据块(granule / page / segment),块内再编码,块级挂跳过信息:

技术适用作用
Dictionary低基数字符/枚举(媒体、OS)存字典 ID,体积骤降
RLE大量连续重复值存「值 × 出现次数」
Delta / DoubleDelta有序时间戳、自增 ID存差分,整数更小更好压
LZ4 / ZSTD通用块压缩进一步砍传输与缓存占用
MinMax / 稀疏索引任意可比较列谓词下推时整块跳过

广告场景里 media_idoscountry 几乎总适合字典;event_time 适合有序 + 跳过索引;imp_id 高基数则靠分区裁剪而不是指望字典。分区键 + 排序键(ORDER BY / 主键序)选型决定了「一次报表能跳过多少块」——这是比调压缩算法更先做的功课。

2.1 分区键与排序键:比压缩算法更早定

以 ClickHouse MergeTree 为例(Doris 的分区分桶同理):

一条经验:先保证「时间分区 + 业务过滤前缀在排序键最左」,再谈换压缩编解码器。 许多人一上来调 ZSTD 等级,却让查询永远扫全分区——是典型的优化顺序颠倒。

2.2 宽表要不要拆列族?

列存已经按列独立存储,不必像早期宽表那样强行垂直拆很多物理表。更常见的是:

拆表的收益往往来自生命周期与权限(原始 debug 字段保留 7 天、指标列保留 400 天),而不是「列存还要再竖着拆才快」。

3. 向量化执行:一批一批算,而不是一行一算

传统执行器多是 Volcano 迭代器next() 一次吐一行,虚函数调用、分支、缓存不友好。向量化引擎改为:

sum(cost) 这类广告指标,向量化意味着「对一段 float/int 数组做紧密循环累加」,吞吐比逐行解释高一个数量级并不夸张。ClickHouse、Doris、DuckDB、Spark Adaptive 后期优化都走这条路。

3.1 谓词下推与延迟物化

向量化之外,分析引擎还会做两件与列存强绑定的优化:

广告宽表尤其吃这一套:先靠 advertiser_id / campaign_id 缩到一小撮 granule,再解压指标列,比一上来解码 80 列高效得多。

4. MPP:一查询打满多台机器

单机再快也扛不住「半年明细 × 全站广告主」的扫表。MPP(Massively Parallel Processing)的分工很朴素:

  1. Coordinator 解析 SQL、生成计划、把扫描任务分给 Worker;
  2. 各 Worker 只扫自己负责的分片(shard / tablet),本地过滤、局部聚合;
  3. Shuffle / 二次聚合GROUP BY key 归约,结果回客户端。

列存加速三件套示意图。自上而下三层面板。① 列内编码与压缩:Dictionary、RLE/Delta、LZ4/ZSTD、Skip Index。② 向量化执行:列块 Batch → Filter/Project → Aggregate SIMD。③ MPP:Coordinator 分发 → Worker 本地扫列 → Shuffle 汇总 → 回客户端。

编码压缩减 I/O → 向量化抬单核 → MPP 扩集群:OLAP 性能的三层叠塔。

注意:数据局部性决定 MPP 效率——排序键与分片键能对齐高频过滤条件,本地聚合就能消掉大部分 shuffle。分片乱切、查询却总按广告主过滤,会把集群打成「全员 shuffle」的惨状。

4.1 分片策略与广告主热点

广告流量天然倾斜:头部广告主贡献大部分曝光。若分片键是 cityHash(advertiser_id),头部会砸在少数分片上,出现「集群整体空闲、个别节点冒烟」——与 Flink 热 key 是同一类物理事实。

缓解手段:

MPP 不是「加机器就线性变快」的魔法——分片与查询模式不对齐时,加机器只会让 shuffle 网络更热闹。

5. ClickHouse vs Doris:两套架构心智

两者都是列存 + 向量化 + MPP,但「集群怎么组织、表怎么建模」差别明显。

5.1 ClickHouse:MergeTree + 本地表优先

5.2 Apache Doris:FE / BE 分离

ClickHouse 与 Apache Doris 架构对比。左侧 ClickHouse:客户端 → 各节点对等(本地表 + Distributed)→ MergeTree 分片副本 → 极致扫表。右侧 Doris:FE 解析调度 → 多个 BE 列存与 Tablet → MySQL 协议与物化视图友好。

CH 偏「强大引擎 + 本地表心智」;Doris 偏「FE/BE 产品化 + 报表友好」。

5.3 选型对照

维度ClickHouseApache Doris
架构对等节点 + DistributedFE / BE 分离
协议HTTP / nativeMySQL 协议友好
建模宽表 + 排序键为主明细 + 物化视图/聚合表常见
导入批量/Kafka 引擎多Stream/Routine Load 产品化
扫表明细极强
运维心智更「引擎可控」更「数仓易用」

AdTech 实践:明细实时写入 + 秒级多维看板两边都能做;若团队已有深 ClickHouse 经验就继续挖投影/物化,若希望 BI/数据同学用 MySQL 工具自助、少碰 Distributed 表,Doris 更顺。别在「谁更 OLAP」上空辩——差在工程生态与人。

6. 写入、合并与「实时」的边界

列存为了扫描优化,写入通常是追加小文件/小批次,再后台合并(CH 的 merge、Doris 的 compaction)。这带来两个现实约束:

实时数仓里常见链路是 Flink Exactly-Once 写出 → OLAP 近实时可见——「近」通常是秒到数十秒,不是交易库的毫秒事务。

6.1 重复数据与 Replacing / 主键模型

Kafka/Flink 至少一次语义或重放,会把同一 imp_id 写进 OLAP 多次。常见对策:

选型要和 SLA 绑定:广告主看板能容忍「合并前 UV 略高」吗?账单能不能?能容忍短暂不精确,就能省大量在线去重算力。

6.2 副本与高可用

列存集群的副本不只是备份:查询可走副本分散负载,写入则要考虑 quorum/异步复制的可见性。生产上至少:多副本跨机架、备份到对象存储、演练过「单节点挂掉后重建」。OLAP 挂掉时广告主后台应有降级文案,而不是白屏。

7. 什么时候不该上列存 OLAP

不是所有「要分析」的需求都值得上 CH/Doris:

列存的甜蜜点是:追加型、宽表、扫很多行、聚很少列、延迟秒级可接受。 广告效果明细几乎完美命中;竞价决策日志里的「在线特征点查」则未必。

参考


views
Share this post on:

Previous Post
实时数仓 · 分层设计与 Lambda/Kappa 架构
Next Post
MySQL 内核 · 主从复制与高可用