ClickHouse学习笔记(一):从订单报表理解列式存储与OLAP
ClickHouse学习笔记(一):从订单报表理解列式存储与OLAP
订单系统刚上线时,运营只想知道今天卖了多少钱。一条 SUM 查询就能解决。
后来,报表要按城市、渠道、小时拆分,还要统计支付人数、客单价和最近一年的趋势。订单越积越多,每次打开报表都要扫描大量记录,业务数据库的压力也越来越大。
这时需要先想清楚:创建一笔订单和分析一亿笔订单,对数据库的要求有什么不同?
ClickHouse 就是从这个问题开始学习的。
**系列约定:**以 ClickHouse 25.8 LTS 系列为学习基线,Docker 实操固定使用
clickhouse/clickhouse-server:25.8.1.5101,方便复现;版本可在官方发布记录核对。这是实验基线,不代表最新版本。示例使用单节点、自管理部署,时间统一为Asia/Shanghai。
系列导航:
一、ClickHouse 是什么?
一句话理解:
ClickHouse 是面向分析查询的列式数据库,擅长对大量记录进行筛选、分组和聚合。
“分析查询”可以是每天的订单金额,也可以是接口延迟分位数、用户行为趋势、日志中的错误比例。它的典型输入是不断产生的明细数据,输出是经过统计的结果。官方介绍将它定位为列式 OLAP 数据库。
这里要关注的是工作负载,而不是“哪一种数据库更高级”。
1.1 OLTP 与 OLAP
| 维度 | OLTP:在线事务处理 | OLAP:在线分析处理 |
|---|---|---|
| 典型问题 | 订单 1001 现在是什么状态? | 最近一个月各城市卖了多少钱? |
| 单次访问 | 通常涉及少量记录 | 可能涉及大量记录 |
| 常见操作 | 点查、插入、更新、事务 | 扫描、筛选、分组、聚合 |
| 主要关注 | 业务一致性、并发与响应时间 | 扫描效率、吞吐与聚合成本 |
| 数据组织 | 常按业务实体建模 | 常按分析维度和指标建模 |
MySQL 也能做报表,ClickHouse 也能查询一条记录。这个表格描述的是各自更常见的优化目标,不能理解成 SQL 能力的绝对边界。
1.2 在订单系统中放在哪里?
可以先看一个常见的数据流:
1 | 用户提交订单 |
如果使用 Kafka,Kafka 负责传递事件,ClickHouse 负责保存和查询分析数据。这个数据流是示意,实际接入还需要选择连接器、消费服务和同步策略。
报表的数据新鲜度取决于整条链路。 MySQL 中已经支付成功,ClickHouse 中还没看到这笔支付,可能是同步延迟,需要先查数据到达进度。
二、列式存储:为什么只查几列会有优势?
假设订单明细有这些字段:
1 | order_id | user_id | city | channel | amount | paid_at | address | remark |
现在要统计每个城市的支付金额:
1 | SELECT city, sum(amount) |
查询主要需要 city 和 amount,地址、备注等字段不参与计算。
2.1 行式与列式的直观区别
1 | 行式组织的概念示意: |
列式组织让引擎可以主要读取需要的列。当表很宽、查询只涉及少量列时,能减少无关数据的读取和解码。官方列式存储说明解释了这种设计对分析负载的意义。
这张图只是逻辑示意。ClickHouse 的实际文件组织还与数据 part 的格式有关,不能简单背成“每一列永远对应一个文件”。
2.2 为什么同一列更容易压缩?
同一列的数据类型相同,也更容易出现重复或相近的值:
1 | channel → [app, app, app, web, web, web] |
如果通过排序让相似值靠近,重复模式会更明显。压缩后需要从磁盘读取的数据通常更少,但解压也有 CPU 成本,最终效果仍取决于数据分布、编码和查询模式。
因此,排序键不仅影响过滤,也可能影响压缩。 后面学习 ORDER BY 时,这一点很重要。官方建模指南也讨论了排序与压缩的关系。
三、向量化执行:成批计算有什么意义?
这里的“向量化”指对一批列值进行计算,不是把文本转成 Embedding。
例如,要判断金额是否大于 100:
1 | 输入金额:[120, 80, 200, 50] |
如果每处理一行都重复调用一套判断逻辑,会产生较多的函数调用、分支和对象处理开销。成批处理可以减少这些开销,并给 CPU 的缓存和 SIMD 指令提供利用机会。
官方架构说明介绍了 ClickHouse 按列和数据块执行的方式。不要把向量化理解成“所有算子都一定使用同一种 SIMD 指令”,具体执行方式与函数、类型和平台有关。
把列式存储和批量执行放在一起看:
1 | 只读需要的列 |
少读数据和高效处理数据,是两个相互配合的方向。 如果查询本来只需要读一小部分,却因为建模不当扫描整张表,仅靠更快的计算也无法消除这部分浪费。
四、先认识几个核心名词
| 名词 | 一句话理解 | 本系列中的用途 |
|---|---|---|
| Database | 表的命名空间 | clickhouse_learning |
| Table Engine | 决定表如何存储和处理数据 | MergeTree、AggregatingMergeTree |
| Part | 一批写入形成的数据片段 | 后台合并的对象 |
| Partition | 按表达式划分的数据范围 | 按支付月份组织数据 |
| ORDER BY | 表内数据的排序规则 | 按城市、时间、订单 ID 排序 |
| Primary Index | 帮助排除不相关数据范围的稀疏索引 | 减少需要读取的 granule |
| Materialized View | 将源表的新写入加工后写到目标表 | 按天汇总支付指标 |
这里的物化视图特指本系列使用的增量物化视图。ClickHouse 还提供按调度重新执行查询的可刷新物化视图,两种机制不同。官方视图指南给出了它们的区别。
4.1 表引擎为什么值得单独学习?
同样执行 INSERT,不同引擎对数据的处理方式可能不同:
MergeTree:保存明细,后台合并数据片段。ReplacingMergeTree:合并时按排序键处理重复版本。AggregatingMergeTree:合并相同排序键对应的聚合状态。
这些引擎都属于 MergeTree 家族,但“合并”不等于同一种业务行为。具体语义分别见MergeTree、ReplacingMergeTree和AggregatingMergeTree文档。
五、分析之前,先确定一行代表什么
这是学习数据库时很容易跳过的一步。
下面两种表都能带上 order_id 和 amount,但统计方式完全不同:
| 表的含义 | 一笔订单可能有几行? | 直接求和有什么风险? |
|---|---|---|
| 支付成功事实表 | 本例约定恰好一行 | 重复导入会重复计算 |
| 订单状态事件表 | 创建、支付、取消等多行 | 每个状态都带金额时会多次求和 |
比如订单 1001 的创建和支付事件都带上 120 元:
1 | 1001 | CREATED | 120 |
直接执行 sum(amount) 会得到 240 元。数据库计算正确,业务口径却错了。
本系列的 order_payments 是支付成功事实表:一笔订单只记录一次成功支付,金额为人民币元。 示例不包含拆单支付、退款、取消和重复投递;真实业务需要另外定义这些规则,也不能将这里的支付金额直接当作扣除退款后的净收入。
第二篇会用八条固定记录练习 SQL,第四篇再用独立的小表理解增量聚合。先把统计口径说清楚,后面的查询才有可核对的答案。
六、学习这一篇后,应该能回答什么?
可以用三个问题自测:
- 查询最近一年的城市销售趋势,为什么可能适合放到分析数据库?
- 一张有五十个字段的表,查询只需要三个字段,列式存储的优势在哪里?
- 订单状态事件表中,每条记录都带金额,为什么不能直接求和?
如果能够结合“读取哪些数据、计算多少记录、指标是什么含义”回答,就已经建立了一个有用的起点。
下一篇搭建本地环境,通过建表、插入和查询,把这些概念落到具体 SQL 上。



