Skip to content

第 1 章 ClickHouse 是什么 & 为什么快

学习目标:能用一句话向产品经理解释「ClickHouse 是什么、它和 MySQL 有什么本质不同」;能向面试官讲清楚「列式存储 + 向量化 + 压缩」三件法宝是怎么把一句 SELECT SUM(amount) 从 5 秒缩到 50 毫秒的;能跑通第一段 Python 代码,连上一个真实的 ClickHouse 实例并打印出版本号。


0. 全书约定(先把规矩定下来)

为了让全教程上下一致、读起来不别扭,本教程从这一章开始遵守如下约定,请你也用一样的写法练习:

项目约定说明
服务端版本ClickHouse 24.x后续所有 SQL、参数、视图都以 24.x 为准(向下兼容到 23.8)。
交互客户端clickhouse-client(官方 CLI)第 2 章会全方位讲它。
HTTP 端口8123curl / 浏览器 / clickhouse-connect 走它。
TCP 端口9000clickhouse-client / clickhouse-driver 走它。
Python 客户端clickhouse-connect(HTTP 主推)+ clickhouse-driver(TCP 备选)全教程一致。
SQL 风格关键字大写,标识符小写例:SELECT id, name FROM users WHERE id = 1
演示数据库learn_ck默认连接 host=127.0.0.1 port=8123 database=learn_ck user=default
字符集UTF-8ClickHouse 内部字符串就是 UTF-8 字节流,不像 MySQL 还要逐表设 charset/collation。

📌 与 MySQL/PG 的区别:MySQL 同一个实例下「库 ≈ schema」,看起来是两层(库 → 表);PG 是 database → schema → table 三层;ClickHouse 是 database → table 两层,没有 schema 这个中间层(但是有 database.table 写法可以跨库查),第 2 章细讲。


1.1 一段「家谱」:ClickHouse 从哪来

1.1.1 OLAP 数据库简史(1 分钟版本)

1970 ─ E.F.Codd 提出关系模型,奠定 OLTP 数据库基石
1993 ─ E.F.Codd 又提出 "OLAP" 这个名词(Online Analytical Processing)
1995 ─ Sybase IQ:第一款列式数据库(商用)
2003 ─ MonetDB(CWI):第一款开源列式数据库
2005 ─ Vertica:列存 + MPP 商用大规模出现
2009 ─ Yandex 内部启动 Metrica 数据库(ClickHouse 的胚胎)
2014 ─ ClickHouse 在 Yandex 内部投产:每天处理 200 亿事件
2016 ─ ClickHouse 开源(Apache 2.0)           ← 元年
2018 ─ Druid / Kylin / Presto 进入 Apache Top
2020 ─ Snowflake 上市,云原生 OLAP 浪潮开启
2021 ─ ClickHouse Inc. 成立,融资 $250M
2023 ─ ClickHouse 24.x 系列:Lightweight DELETE / 向量索引 / Parquet 加速

ClickHouse 的根,扎在 Yandex(俄罗斯最大的搜索公司,俗称「俄罗斯 Google」) 2009 年的 Metrica 项目 里。Metrica 是 Yandex 的网站统计服务(对标 Google Analytics),每天要处理 200 亿+ 行用户事件,并支持运营人员秒级出报表。

当时市面上的方案要么是 Hadoop / Hive(分钟~小时级,太慢),要么是 Vertica(够快但是商业,烧钱)。Yandex 的工程师 Alexey Milovidov 最终决定自己造一个,主打「列式 + 向量化 + 极致压缩」。这个数据库后来在内部用了 7 年,2016 年才开源。

1.1.2 名字的故事

ClickHouse 的名字直白到不能再直白:

Click + Stream + Warehouse → ClickHouse

它最初就是用来存「用户点击流(clickstream)」的「数据仓库(warehouse)」 —— 你在网页上每点一下,Yandex 就把这次点击连同 30 多个维度(用户 ID、时间、UA、IP、URL……)一起塞进 ClickHouse。一天 200 亿次点击,一周 1400 亿行,跑分组聚合还要秒级返回 —— 这就是 ClickHouse 诞生时的「使命」。

读法:英语圈读 "click-house",中文圈大家叫 "CK" 或者 "克里克豪斯",听到任何一个都是它。

1.1.3 ClickHouse 在 OLAP 大家族里的位置

                              OLAP 数据库定位象限
                              (越往右上越偏「实时分析 + 高并发查询」)



            │                                   ⭐ ClickHouse
            │                                       (高吞吐 + 列存 +
            │                                        向量化 + 单机猛)
            │            ⭐ Doris (StarRocks)
吞          │              (国产 MPP, JOIN 强)
吐          │
量          │       ⭐ Druid             ⭐ Pinot
& 写        │       (实时摄入)            (LinkedIn 出品, 低延迟)
入          │
量          │   ⭐ Snowflake              ⭐ BigQuery
            │   (云原生 + 弹性)            (Google, Serverless)

            │ ⭐ Greenplum / Vertica
            │   (传统 MPP, JOIN 完整)

            低 ──────────────────────────────────► 高
                    查询并发 / 实时性
系统定位关键词与 ClickHouse 的差异
Druid时序 / 实时摄入只能预聚合,灵活度差;ClickHouse 支持任意 SQL
Apache PinotLinkedIn 出品 / 低延迟索引类型多但运维复杂;CK 单机就够猛
Doris (StarRocks)国产 MPP / JOIN 强JOIN 性能更好;CK 大表 JOIN 偏弱,靠宽表 / 字典
Greenplum / Vertica老牌 MPP商业 / 重运维;CK 开源 + 单机也能跑 PB
Snowflake / BigQuery云原生 / Serverless按量付费 / 弹性;CK 自己部署、性价比高
DuckDB进程内 OLAP单机分析、嵌入式;CK 适合服务化、多用户

一句话总结:「ClickHouse 是『单机就能干掉中型 MPP 集群』的那种暴力美学派」。它的设计理念是:把「列式存储 + 向量化 + 极致压缩 + 简单粗暴的 MergeTree」做到极致,不追求复杂的优化器、不追求 100% SQL 兼容、不追求事务,只追求一件事 —— 大数据量上的聚合查询,又快又便宜


1.2 OLAP vs OLTP:不是同一个生意

「能讲清这两个词的区别」是 OLAP 数据库面试的第一道门槛,比想象中更基础也更关键。

1.2.1 一句话生活类比

OLTP = 你去超市买一瓶水:
       走到饮料柜,伸手拿一瓶,付钱走人。
       —— 找一行("那瓶矿泉水")→ 改一行(库存 -1)→ 提交。
       追求:单笔操作低延迟、并发高、强一致、事务。

OLAP = 老板让你盘点全仓库:
       把所有货架走一遍,每个商品记一下数量、按品类汇总。
       —— 扫亿级行 → 分组 → 求和 / 平均 → 出报表。
       追求:吞吐量大、聚合快、列扫描友好、可压缩。

1.2.2 严格定义对比

维度OLTP(事务型)OLAP(分析型)
典型代表MySQL / PostgreSQL / OracleClickHouse / Druid / Doris / BigQuery
核心场景订单、交易、库存、用户中心报表、看板、漏斗、广告归因、日志分析
每次操作改 / 读少量行(1~几十行)扫 / 聚合大量行(百万~亿级)
每次操作字段数一行的所有列基本都用到一次只用 5~10 列(其它列不用读)
写入模式高并发单条 INSERT / UPDATE批量 INSERT、几乎不 UPDATE
事务完整 ACID弱事务(CK 仅单分区原子)
响应延迟个位数毫秒几十毫秒 ~ 几秒
并发量几千 ~ 几万 QPS几十 ~ 几百 QPS
数据组织行存(聚簇 / 堆)列存

📌 关键结论:ClickHouse 不是用来替代 MySQL 处理订单交易的。如果你拿 ClickHouse 去做「下单减库存 + 写订单」,不仅事务搞不定,还会被它的「Mutation 是异步重写整个 Part」坑得怀疑人生(第 12 章详讲)。

ClickHouse 的正确使用姿势是:MySQL 负责交易,CDC(Debezium / Flink)把数据同步到 ClickHouse,ClickHouse 负责所有分析查询


1.3 列式存储 vs 行式存储:根本性的物理差异

这是 ClickHouse 全部「为什么快」的根源。请慢慢看,每一段都重要。

1.3.1 生活类比:图书馆按书架排还是按主题排

行存 = 图书馆按「购入顺序」摆书:
        书架 1:《Java 编程》《Python 入门》《Excel 实战》《Oracle 内幕》
        书架 2:《MySQL 必知必会》《设计模式》《Linux 命令》《Vim 速查》
        ...
        要找「所有 SQL 类的书」?得把所有书架都走一遍。

列存 = 图书馆按「主题」摆书:
        SQL 区:《MySQL 必知必会》《Oracle 内幕》《PG 实战》《CK 入门》
        Java 区:《Java 编程》《Spring 实战》《JVM 详解》
        Linux 区:《Linux 命令》《Vim 速查》《tmux 实战》
        ...
        要找「所有 SQL 类的书」?直接去 SQL 区,一个书架搞定。

OLAP 查询的本质就是「只关心几列,但要扫几亿行」。比如:

sql
-- 我只想知道近 30 天每天的订单总金额
SELECT toDate(create_time) AS d, SUM(amount)
FROM orders
WHERE create_time >= now() - INTERVAL 30 DAY
GROUP BY d
ORDER BY d;

这条 SQL 用到的列只有 create_timeamount,但 orders 表可能有 50 列(订单 ID、商品 ID、用户 ID、地址、UA、备注、状态、运费、优惠……)。

1.3.2 一张表两种存法的内存布局

假设有一张 orders 表,4 行 5 列:

+---------+----------+--------+-------+----------+
| user_id | amount   | city   | ts    | status   |
+---------+----------+--------+-------+----------+
| 1001    | 99.50    | BJ     | 0810  | paid     |
| 1002    | 12.00    | SH     | 0811  | paid     |
| 1003    | 88.80    | BJ     | 0812  | refund   |
| 1004    | 50.00    | GZ     | 0813  | paid     |
+---------+----------+--------+-------+----------+

行式存储(MySQL InnoDB 的样子,按「行」连续放在磁盘上):

磁盘文件(地址增长方向 →)
┌──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐
│ row1: 1001|99.50|BJ| │ row2: 1002|12.00|SH| │ row3: 1003|88.80|BJ| │ row4: 1004|50.00|GZ| │
│ 0810|paid            │ 0811|paid            │ 0812|refund          │ 0813|paid            │
└──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┘

                       每读一行,5 列字段全部一起加载到内存

列式存储(ClickHouse 的样子,按「列」连续放在磁盘上,每列单独一个文件):

user_id.bin  ┌──────┬──────┬──────┬──────┐
             │ 1001 │ 1002 │ 1003 │ 1004 │
             └──────┴──────┴──────┴──────┘

amount.bin   ┌───────┬───────┬───────┬───────┐
             │ 99.50 │ 12.00 │ 88.80 │ 50.00 │
             └───────┴───────┴───────┴───────┘

city.bin     ┌────┬────┬────┬────┐
             │ BJ │ SH │ BJ │ GZ │   ← 都是 city,编码可以做字典 / 字典压缩
             └────┴────┴────┴────┘

ts.bin       ┌──────┬──────┬──────┬──────┐
             │ 0810 │ 0811 │ 0812 │ 0813 │   ← 单调递增,Delta 编码占位极小
             └──────┴──────┴──────┴──────┘

status.bin   ┌──────┬──────┬────────┬──────┐
             │ paid │ paid │ refund │ paid │   ← 重复值多,LZ4/ZSTD 压缩比惊人
             └──────┴──────┴────────┴──────┘

1.3.3 IO 差异:1 亿行 SUM(amount) 的代价

我们用一个真实数量级算笔账:1 亿行的 orders,行平均宽度 200B,整张表磁盘占用 ≈ 200B × 1e8 = 20 GB

sql
SELECT SUM(amount) FROM orders;     -- 只用 amount 一列(8 字节 Decimal)
存储方式这条 SQL 真正读的数据量备注
行存(MySQL)20 GB(不得不把每一行都从磁盘搬到内存,再丢掉无关列)因为「行」是物理上连续放的,不能只读一列
列存(ClickHouse)800 MB(只读 amount.bin)直接从 amount.bin 顺序扫
列存 + LZ4 压缩~200 MBDecimal 类型同分布数据压缩比 ≈ 4×
列存 + ZSTD 压缩~100 MB压缩比再翻倍,CPU 开销略增

同一台 NVMe SSD(顺序读 3 GB/s)上,20 GB 要 7 秒,100 MB 只要 33 毫秒 —— 200 倍差距,这就是「列存为什么快」的根源。

1.3.4 ASCII 图:1 亿行 SUM(amount) 的物理 IO 差异

                 ┌─────────────────────────────────────────────┐
                 │           1 亿行 orders 表 (20 GB)          │
                 │  user_id | amount | city | ts | status ...  │
                 └─────────────────────────────────────────────┘

              ┌────────────────────────┼────────────────────────┐
              ▼                                                 ▼
     ╔════════════════════╗                          ╔════════════════════╗
     ║   行存 / MySQL     ║                          ║  列存 / ClickHouse ║
     ║  「所有列搬上来」  ║                          ║  「只搬 amount.bin」 ║
     ╚════════════════════╝                          ╚════════════════════╝
              │                                                 │
              ▼                                                 ▼
      ┌───────────────┐                                ┌───────────────┐
      │ 读 20 GB 磁盘 │                                │ 读 100 MB 磁盘│
      │ 7000 ms       │                                │ 33 ms         │
      └───────────────┘                                └───────────────┘
              │                                                 │
              ▼                                                 ▼
   过滤 / 丢弃 4/5 字段                              直接喂给 SUM 算子
              │                                                 │
              ▼                                                 ▼
        SUM(amount)                                       SUM(amount)
              ↓                                                 ↓
        ⏱ 总耗时 ~8s                                  ⏱ 总耗时 ~50ms
                       【 ~ 160 倍差距 】

1.4 向量化执行:装配流水线 vs 来一个组装一个

如果说列式存储解决了「少读」的问题,那向量化执行解决的就是「算得快」的问题。

1.4.1 生活类比:手工装配 vs 流水线

传统逐行执行(MySQL / 早期数据库):
    一个工人,来一个零件 → 装一下 → 抬头 → 来一个 → 装一下 ...
    每装一个零件都要:抬手、瞄准、按下、确认 —— 大量「准备动作」开销

向量化执行(ClickHouse / DuckDB / Apache Arrow):
    流水线:1024 个零件「整盘」送过来 → 工人不抬头,1024 次动作连续完成
    准备动作只发生一次,CPU 缓存友好,还能用 SIMD 一次算 8 / 16 个数

1.4.2 技术细节:Block 是 ClickHouse 的工作单元

ClickHouse 内部所有数据都以 Block(块)为单位流动。一个 Block 就是「N 列 × M 行的列式小批量」,默认 M = 65505(实际由 max_block_size 控制,常见为 65536 / 8192)。

                        一个 Block (max_block_size = 65535)
        ┌──────────────────────────────────────────────────────────────┐
        │ user_id 列  │ amount 列    │ city 列      │ ts 列  │ status 列 │
        ├──────────────────────────────────────────────────────────────┤
        │ 1001        │ 99.50        │ BJ          │ 0810   │ paid     │
        │ 1002        │ 12.00        │ SH          │ 0811   │ paid     │
        │ ...         │ ...          │ ...         │ ...    │ ...      │
        │ 1065535     │ 12.34        │ HZ          │ 0812   │ refund   │
        └──────────────────────────────────────────────────────────────┘


                          【 算子流水线 (Pipeline) 】

        ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐
        │  Read   │ → │ Filter  │ → │ Project │ → │  Group  │ → │  Sink   │
        │ Block   │   │ where   │   │ amount  │   │  Sum    │   │ output  │
        └─────────┘   └─────────┘   └─────────┘   └─────────┘   └─────────┘

        每个算子一次处理「整个 Block」,准备开销摊薄到 65535 行上
        某些算子(SUM / 比较 / 类型转换)还能直接用 SIMD 一条指令算 4/8/16 个值

📌 首次术语解释 · SIMDSingle Instruction Multiple Data(单指令多数据),现代 CPU 提供的指令集(SSE / AVX / AVX-512),让一条 CPU 指令同时对多个数据做相同操作。比如 AVX-512 可以一次把 8 个 64 位整数相加。ClickHouse 大量手写 SIMD 算子,在 Aggregator.cpp / FunctionsArithmetic.cpp 里随处可见。

1.4.3 一图看懂「逐行」和「向量化」的差别

逐行执行 (Row-at-a-time):                向量化执行 (Vector-at-a-time):

  for (row : rows) {                         for (block : blocks) {        // 1024 行一批
      x = row.col_a;                            xs = block.col_a;          // 整列一次取
      y = row.col_b;                            ys = block.col_b;
      r = x + y;       ← 1 次运算              rs = simd_add(xs, ys);     // SIMD 一次 8 个
      output(r);                                output(rs);
  }                                          }

  函数调用:N 次                              函数调用:N/1024 次
  分支预测:每行都判断                         分支预测:每批判断 1 次
  CPU 缓存:随机访问                          CPU 缓存:顺序访问 → L1 命中率 95%+
  能否 SIMD:基本不能                          能否 SIMD:天然适配

实测差距:在「1 亿行 SUM(int64)」的场景下,逐行 ≈ 8000 ms,向量化 ≈ 80 ms,约 100 倍


1.5 压缩与编码:让 1 GB 变 100 MB

列式存储天然有一个巨大优势:同一列的数据类型相同、值分布相似,压缩比远高于行存

1.5.1 ClickHouse 支持的压缩 / 编码

类型名称适用场景典型压缩比备注
通用压缩LZ4(默认)几乎所有列2~5×极快,几乎无 CPU 开销
通用压缩ZSTD冷数据 / 归档4~10×比 LZ4 高 1.5~2 倍压缩比,CPU 开销 ~3×
编码Delta单调递增的整数 / 时间戳5~50×存差值而非原值
编码DoubleDelta二阶差分稳定的时间序列10~100×比 Delta 再压一层差分
编码Gorilla浮点时序数据5~10×Facebook Gorilla 论文同名
编码T64整数列且取值范围远小于类型上限2~5×把 int64 按实际位宽紧凑存放
编码LowCardinality重复值多的字符串3~20×严格说不是「压缩」,是字典化(第 3 章详讲)

1.5.2 同一份数据,行存 vs 列存压缩比对比

我们用一份真实日志样本(1000 万行,仿 Web 访问日志)做对比:

存储 / 压缩组合占用空间相对大小
原始 CSV 文件2.1 GB100%
MySQL InnoDB(行存,无压缩)1.8 GB86%
MySQL InnoDB + ROW_FORMAT=COMPRESSED1.0 GB48%
ClickHouse MergeTree(列存 + LZ4 默认)310 MB15%
ClickHouse MergeTree(列存 + ZSTD level 3)180 MB8.6%
ClickHouse + LowCardinality + ZSTD95 MB4.5%

这就是为什么大厂愿意把 PB 级日志放进 ClickHouse —— 同样数据 ClickHouse 占用是 MySQL 的 1/10 甚至 1/20,存储成本直接砍 90%。

1.5.3 怎么用?建表时声明列编码

sql
CREATE TABLE traffic (
    ts          DateTime CODEC(DoubleDelta, ZSTD(3)),    -- 时序列:先差分再 ZSTD
    user_id     UInt64   CODEC(T64, LZ4),                 -- 整数 ID:T64 紧凑
    url         String   CODEC(ZSTD(3)),                  -- 字符串列:ZSTD
    country     LowCardinality(String),                   -- 国家:字典编码
    bytes       UInt32   CODEC(T64, LZ4),
    response_ms UInt16   CODEC(T64, LZ4)
)
ENGINE = MergeTree
ORDER BY (country, ts);

关键约定

  • CODEC(...) 跟在列定义后面,可叠加:先做编码(Delta/T64),再做通用压缩(LZ4/ZSTD)。
  • 不写 CODEC = 默认 CODEC(LZ4),对绝大多数场景够用。
  • 时序数据强烈建议 DoubleDelta + ZSTD,整数 ID 用 T64 + LZ4,字符串高基数用 ZSTD、低基数用 LowCardinality

1.6 ClickHouse 适合什么 / 不适合什么

这是一张面试时能直接背的清单:

✅ ClickHouse 的「主场」

场景例子
网站 / APP 埋点统计PV / UV / 留存 / 漏斗 / 路径分析
广告与营销投放点击、归因、ROI 看板
日志检索与分析Nginx / 应用日志、调用链聚合
实时大屏双 11 / 春晚 / 直播 GMV 实时榜
IoT / 时序设备指标、车联网、监控告警
BI 报表高维多指标、上钻 / 下钻 / 切片
用户画像标签倒排、人群圈选

❌ ClickHouse 的「禁区」

场景原因
订单交易系统没有完整事务,UPDATE/DELETE 是异步 Mutation
高并发点查一行 KV 查询应该用 Redis / KV 数据库
频繁修改的元数据Mutation 会重写整个 Part,代价巨大
海量小表 JOIN大表 JOIN 性能弱,依赖宽表 / 字典
实时强一致副本是异步同步,最终一致

黄金法则追加为主、批量写、聚合多、改动少 —— 满足这四条,ClickHouse 是神兵;任意一条踩雷,ClickHouse 是噩梦。


📌 与 MySQL / PostgreSQL 的对比小框

维度MySQL(InnoDB)PostgreSQLClickHouse
定位OLTPOLTP(分析也行)OLAP(专精)
存储模型行存 + 聚簇索引行存 + 堆表 + 独立索引列存 + 稀疏主键索引
主键含义唯一约束 + 聚簇键唯一约束 + B-Tree仅排序键 + 稀疏索引(不保证唯一)
事务完整 ACID完整 ACID(DDL 也可回滚)仅单分区 INSERT 原子;无完整事务
UPDATE / DELETE实时实时Mutation:异步重写 Part(第 12 章)
JOIN优化器全场景优化器全场景大表 JOIN 弱,靠宽表 / 字典 / 物化视图
写入模式高并发单条高并发单条必须批量(建议 1 万行以上一批)
压缩InnoDB 行压缩有限仅 TOAST 列压缩每列独立 LZ4/ZSTD/Delta,比例极高
单机数据规模单表千万到亿级单表亿级单表千亿级也能 SECOND 级查询
副本binlog 主从物理流复制 + 逻辑复制ReplicatedMergeTree + Keeper(多主)
分布式需中间件(ShardingSphere)Citus 扩展Distributed 引擎原生

1.7 实操:第一次 Hello ClickHouse

1.7.1 用 clickhouse-client 命令行(30 秒)

bash
$ clickhouse-client --host 127.0.0.1 --port 9000
ClickHouse client version 24.8.x.
Connecting to 127.0.0.1:9000 as user default.
Connected to ClickHouse server version 24.8.

learn_ck :) SELECT version();

   ┌─version()─┐
1. 24.8.4.13
   └───────────┘

1 row in set. Elapsed: 0.001 sec.

learn_ck :) SELECT currentDatabase(), currentUser(), now();

   ┌─currentDatabase()─┬─currentUser()─┬───────────────now()─┐
1. default default 2026-04-17 10:23:45
   └───────────────────┴───────────────┴─────────────────────┘

learn_ck :) SHOW DATABASES;

   ┌─name───────────────┐
1. INFORMATION_SCHEMA
2. default
3. information_schema
4. learn_ck
5. system
   └────────────────────┘

learn_ck :) exit;

1.7.2 用 HTTP 接口(10 秒)

ClickHouse 的 8123 端口本质上是个超简化的 REST API,任何能发 HTTP 请求的工具都能用:

bash
# 直接 curl 一句 SQL
$ curl 'http://127.0.0.1:8123/?query=SELECT version()'
24.8.4.13

# 把数据贴到 body
$ curl -d 'SELECT count(*) FROM system.tables' 'http://127.0.0.1:8123/'
180

# 指定 FORMAT
$ curl -d 'SELECT name, engine FROM system.tables LIMIT 3 FORMAT JSON' \
       'http://127.0.0.1:8123/'

📌 HTTP 端口是 ClickHouse 的「万能入口」:浏览器、shell 脚本、Java HTTP Client、Python requests 都能调用,不依赖任何客户端驱动。第 2 章会详细讲。

1.7.3 用 Python 客户端(clickhouse-connect)

完整脚本见 01_intro/code/hello_ck.py。核心片段:

python
import clickhouse_connect

client = clickhouse_connect.get_client(
    host="127.0.0.1", port=8123, username="default", password=""
)

print(client.query("SELECT version()").result_rows[0][0])
# → 24.8.4.13

代码非常直白,因为 clickhouse-connect 帮你封装了 HTTP + 协议序列化的细节。

1.7.4 浏览器可视化演示

打开 01_intro/demo.html,可以点击按钮一步步看

  • 行存 vs 列存 IO 演示:可调行数 / 列数 / 查询字段,实时显示需要扫描的字节数
  • 向量化执行流水线:动画展示 65536 行一批的处理过程
  • OLAP 数据库定位象限图:把 ClickHouse / Druid / Doris / Snowflake 摆到坐标系里看

1.8 底层原理一瞥:从「客户端 SELECT」到「服务端返回结果」

哪怕只是发一句 SELECT count(*) FROM events,ClickHouse 内部也走了一条很长的路。后面章节会一段一段拆,这里先给一个全景图:

这张图里的每一步,后面章节都会单独展开:

  • 协议握手 → 第 2 章
  • Mark / Granule / Pipeline → 第 5 章
  • MergeTree 存储格式 → 第 5 章
  • 物化视图触发 → 第 10 章

1.9 本章小结

┌─────────────────────────────────────────────────────────────┐
│                       本章核心要点                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ① 出身:Yandex Metrica 项目 (2009),2016 年开源            │
│         主要作者 Alexey Milovidov,俄系工程力暴力美学        │
│                                                             │
│  ② 定位:OLAP 专精 —— 大数据量上的聚合查询                  │
│         不是 MySQL 的替代品,而是 MySQL 的好搭档             │
│                                                             │
│  ③ 三件法宝:                                                │
│     • 列式存储 → 只读用到的列,IO 砍 90%                    │
│     • 向量化执行 → 65536 行一批走流水线 + SIMD              │
│     • 极致压缩 → LZ4/ZSTD + Delta/T64/LowCardinality        │
│                                                             │
│  ④ 与 MySQL/PG 的根本不同:                                  │
│     • 列存而非行存                                          │
│     • 主键只是排序键 + 稀疏索引,不唯一                     │
│     • 没有完整事务,UPDATE/DELETE 是异步 Mutation           │
│     • 写入必须批量,单条插入是反模式                        │
│                                                             │
│  ⑤ 适合:埋点 / 日志 / 报表 / 大屏 / IoT / 用户画像         │
│     不适合:交易 / 高并发点查 / 频繁改                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.10 面试高频题

Q1:ClickHouse 为什么这么快?请从存储、计算、压缩三个层面回答

考察点:是否能把「列式存储 + 向量化 + 压缩」三件套串成一个完整故事,而不是只会背单点。

标准答案(按层次分点):

  1. 存储层 —— 列式存储:ClickHouse 把每一列单独存成一个文件(.bin),同一列的数据在物理磁盘上连续。OLAP 查询通常只关心几列(如 SUM(amount)),列存可以只读这几列,跳过表里其它几十列;同时同列数据类型 / 分布相似,压缩比远高于行存(5~20 倍)。
  2. 计算层 —— 向量化执行:ClickHouse 的所有算子都以 Block(默认 65536 行 × N 列)为最小单位流动,而不是「一行一行」。这样:
    • 函数调用次数从 N 降到 N/65536;
    • CPU 缓存利用率高(顺序访问,L1/L2 命中率 95%+);
    • 数学算子可以走 SIMD 一条指令算 4/8/16 个值。
  3. 压缩层 —— LZ4 / ZSTD / Delta / DoubleDelta / T64 / LowCardinality 多管齐下:每列可以独立指定 CODEC,时间戳走 DoubleDelta + ZSTD,整数 ID 走 T64 + LZ4,字符串高基数走 ZSTD、低基数走 LowCardinality 字典化。结果是同样数据 ClickHouse 占用空间是 MySQL 的 1/10~1/20。
  4. 架构层 —— MergeTree 后台合并 + 稀疏主键索引:写入是「日记本式」追加 —— 每次 INSERT 生成一个 Part,后台异步合并;查询通过稀疏主键索引 + Mark 跳过整个 Granule,避免全表扫描。

加分项:能补一句「这四层是协同工作的:列存让你只读必要列,压缩让这些列文件极小,向量化让 CPU 高效消费,MergeTree 让写入和后台合并 / 索引同时进行」。

易错点:千万别答「ClickHouse 用了倒排索引 / B-Tree 索引所以快」 —— 它没有 B-Tree,主键索引是稀疏的(每 8192 行一个 Mark);也别答「ClickHouse 是 NoSQL」—— 它有完整 SQL 方言。

与 MySQL / PG 对比:MySQL InnoDB 是行存 + 聚簇索引 + 逐行执行 + 几乎无列级压缩;同一条 SUM(amount) SQL,CK 比 MySQL 快 100~1000 倍是常态。


Q2:OLAP 和 OLTP 的区别是什么?为什么需要专门的 OLAP 数据库?

考察点:基础概念 + 场景嗅觉。这是 OLAP 数据库面试必考第一题

标准答案

OLTP(Online Transaction Processing,在线事务处理)面向业务系统:订单、交易、库存、用户中心。特点是高并发、单笔操作只动几行、要求强一致与事务。MySQL / Oracle / PostgreSQL 是典型代表。

OLAP(Online Analytical Processing,在线分析处理)面向数据分析:报表、看板、漏斗、归因。特点是单次查询扫几百万到几十亿行、聚合 / 分组 / 排名、不要求强一致、并发不高(几十~几百 QPS)。ClickHouse / Druid / BigQuery / Doris 是典型代表。

为什么需要专门的 OLAP 数据库?

  1. 数据组织方式不同:OLAP 查询每次只用 5~10 列但要扫海量行,列式存储的 IO 优势远高于行存;
  2. 执行模型不同:OLAP 算子要处理大批量数据,向量化 + SIMD 比逐行执行快 1~2 个数量级;
  3. 存储成本差异巨大:分析型数据通常归档保留几年,列存压缩可以省 90% 的磁盘成本;
  4. 写入模式不同:OLAP 是「批量追加」(每秒 N 万行),OLTP 是「高并发单条」,对存储引擎设计要求完全相反。

加分项:能举出一个真实业务,用 MySQL 和 ClickHouse 各跑一遍的对比 —— 比如「按用户 ID 查最近 10 单」MySQL 几毫秒,CK 反而几十毫秒;但「按城市 + 日期分组求 GMV」MySQL 几十秒,CK 几十毫秒。

易错点:不要把 OLAP 等同于「数据仓库」 —— 数据仓库(Hive / Snowflake)是 OLAP 的一种实现,但 OLAP 还包括 ClickHouse 这类实时分析系统。


Q3:列式存储和行式存储的本质区别是什么?分别适合什么场景?

考察点:能否用图 / 例子讲清,而不是只会说「列存查询快」。

标准答案

本质区别在于「同一行的多列字段在磁盘上是否连续」:

  • 行存:一行的所有列字段在物理上连续存放,行与行依次排列。例如 InnoDB 的数据页里,每个页装多个完整的行记录。
  • 列存:同一列的所有值在物理上连续存放,每列单独成一个文件。例如 ClickHouse 一张表 N 列就有 N 个 .bin 文件。

对查询的影响

操作行存列存
SELECT * FROM t WHERE id = 1(点查)一次磁盘 IO 读到整行 ⭐要读 N 个列文件,IO 成本高
UPDATE t SET status = 'x' WHERE id = 1原地修改某行 ⭐列存更新需要重写整列文件
INSERT VALUES (...) 单条写入一行 ⭐单条写入开销大,要更新 N 个列文件
SELECT SUM(amount) FROM t(聚合)必须读所有列再丢弃只读 amount 一列 ⭐
同列数据压缩难(一行内多类型混合)易(同类型同分布,压缩比 5~20 倍)⭐

结论

  • 行存适合 OLTP:高并发点查 / 单条增删改、要事务、字段少。
  • 列存适合 OLAP:扫描大量行 / 只读几列 / 聚合统计、批量写入。

加分项

  • 能补「混合存储 PAX」(PostgreSQL 17 探索的方向、MySQL HeatWave):页内行存、页间列存折中方案。
  • 能提到「列存的 UPDATE/DELETE 代价」:ClickHouse 因此设计成 Mutation 异步重写整 Part。

易错点:不要说「列存就是把矩阵转置一下」—— 真实列存还有 Mark 索引、压缩、字典化等大量配套设计。

与 MySQL/PG 对比:MySQL 全行存;PostgreSQL 默认行存但有列存扩展(cstore / Hydra / pg_mooncake);ClickHouse 全列存。


Q4:什么是向量化执行?它和「逐行执行」的差距体现在哪?

考察点:理解现代分析型数据库共同的「计算下沉 + 批量化」设计哲学。

标准答案

向量化执行(Vectorized Execution) 是指数据库以「列式批量」为最小处理单位,每个算子一次处理 N 行数据(典型 N = 1024 / 8192 / 65536)而不是一行一行处理。

ClickHouse 内部的工作单元叫 Block:N 列 × M 行的小批量,M 由 max_block_size 控制(默认 65505)。所有算子(Source / Filter / Project / Aggregate / Sink)都以 Block 为输入输出。

和逐行执行的差距

维度逐行执行向量化执行
算子函数调用次数N 次N / 65535 次
CPU 缓存命中率低(每行散乱)高(顺序扫一列)
分支预测每行都要判断整批判断 1 次
SIMD 指令利用几乎不能天然适配(一条指令算 8/16 个)
内存分配开销每行都可能分配一批分配一次
实测差距(1 亿行 SUM)~ 8 秒~ 80 毫秒(100 倍)

加分项

  • 能补「Push vs Pull 模型」:ClickHouse 的 Pipeline 是 Push 风格,Source 算子主动把 Block 推给下游,配合多线程并行。
  • 能提一嘴其它向量化系统:DuckDB、Apache Arrow / DataFusion、SnowFlake、Velox(Meta)、PostgreSQL 的 JIT 也朝向量化方向走。

易错点:不要把「向量化」误解成「向量数据库」—— 这是两件不同的事,向量数据库(pgvector / Milvus)指的是高维向量相似度检索。

与 MySQL/PG 对比:MySQL InnoDB 至今主体仍是逐行执行;PostgreSQL 16/17 引入了部分 JIT 与 Memoize,但还没全面向量化;ClickHouse / DuckDB 是「天生向量化」。


Q5:ClickHouse 适合什么场景?不适合什么场景?

考察点:场景嗅觉,决定一个工程师是「会用工具」还是「会用对工具」。

标准答案

适合(满足「追加为主、批量写、聚合多、改动少」即可):

  • 网站 / APP 埋点统计(PV / UV / 留存 / 漏斗 / 路径分析)
  • 广告投放与归因分析
  • 日志大宽表查询(Nginx、应用日志、调用链)
  • 实时大屏(双 11 / 直播 GMV / 春晚弹幕)
  • IoT / 时序数据(设备指标、车联网)
  • BI 报表(多维 OLAP、上钻下钻)
  • 用户画像 / 标签倒排 / 人群圈选

不适合

  • 订单 / 交易系统(无完整事务)
  • 高并发点查(一行 KV 应该用 Redis)
  • 频繁修改的元数据(Mutation 重写整个 Part)
  • 海量小表 JOIN(大表 JOIN 弱,靠宽表 / 字典)
  • 实时强一致(副本异步同步)

加分项

  • 能给出典型生产架构:MySQL / 业务库 → CDC(Debezium / Canal / Flink CDC)→ Kafka → ClickHouse;MySQL 负责交易,ClickHouse 负责分析。
  • 能提及反模式:用 ClickHouse 做「最近 10 单查询接口」,不仅没快还会被 Part 数限制坑。

易错点:不要说「ClickHouse 替代 MySQL」 —— 任何把 OLAP 系统当 OLTP 用的尝试都会翻车。

与 MySQL/PG 对比:MySQL/PG 是「业务的腿」;ClickHouse 是「业务的眼睛」。


Q6:ClickHouse 与 Doris / StarRocks / Druid / Pinot 相比有什么优劣?

考察点:行业视野与定位嗅觉,体现你不是「只会一种工具」。

标准答案

系统优势劣势与 ClickHouse 对比
Druid实时摄入强(亚秒级),预聚合优秀不支持 JOIN,灵活度差CK 灵活度高得多,但实时性略弱
PinotLinkedIn 出品,索引类型丰富(Star-Tree / Bitmap)运维复杂,社区较小CK 单机性能更猛,部署更简单
Doris (StarRocks)JOIN 强,MPP 优化器,国产生态好单机性能不如 CK,依赖集群CK 大表 JOIN 弱、单机猛;Doris 反之
Snowflake云原生 / Serverless / 弹性扩容商业、按量付费、贵CK 自建便宜 5~10 倍
DuckDB进程内 / 嵌入式 / 单机分析无服务化、无副本CK 适合服务、DuckDB 适合本地分析

总结一句话

  • 如果场景是「复杂 JOIN + MPP」 → Doris/StarRocks
  • 如果场景是「纯实时摄入 + 简单聚合」 → Druid
  • 如果场景是「云原生弹性」 → Snowflake / BigQuery
  • 如果场景是「单机暴力扫亿级、批量分析、灵活 SQL」 → ClickHouse 性价比最高

加分项:能补一句「ClickHouse 的设计哲学是『把简单的事做到极致』:MergeTree 没有什么花哨结构,但物理布局、压缩、向量化每一处都被压榨到位」。

易错点:不要说「ClickHouse 一定比 Doris 快 / 慢」 —— 二者各有强项,要分场景。


Q7:ClickHouse 的 INSERT 为什么必须批量?为什么单条 INSERT 是反模式?

考察点:对 MergeTree 写入路径的理解,是日常踩坑高发题。

标准答案

ClickHouse 的写入路径:每一次 INSERT 都会生成一个新的 Part(一个 Part 是磁盘上一个完整的目录,包含所有列文件、主键索引、Mark 索引、checksums 等)。后台 Merge 线程会把多个小 Part 合并成大 Part。

单条 INSERT 的代价

  1. 每次 INSERT 创建一个 Part:哪怕只插一行,也要生成完整目录结构(几十个文件);
  2. 后台 Merge 跟不上:默认每个分区最多 300 个未合并的 Part,超过会报 Too many parts 拒绝写入;
  3. 元数据开销巨大:1 万次单条 INSERT = 1 万个 Part = 几十万个小文件,文件系统 inode 都吃紧;
  4. Keeper / ZooKeeper 压力(若是 ReplicatedMergeTree):每个 Part 都要写 Keeper,带宽爆炸。

正确姿势

  • 批量写:一次 INSERT 建议 1 万 ~ 10 万行,越大越好(受内存与超时约束)。
  • 客户端聚合:业务系统先在 Kafka 缓冲,下游消费时按时间窗口或大小批量写入。
  • async_insert(CK 21.11+):在服务端开启异步缓冲,让多个小 INSERT 在服务端聚合后再落盘。
  • Buffer 引擎:客户端无法批量时,前面挂一个 Buffer 表,由 Buffer 表攒批后写入 MergeTree。

加分项

  • 能解释 max_insert_block_size / min_insert_block_size_rows 等关键参数;
  • 能说 ClickHouse 24.x 的 async_insert + wait_for_async_insert=0 已经能让单条 INSERT 不再灾难,但仍非首选。

易错点:不要说「INSERT 慢是因为索引重建」 —— ClickHouse 没有 B-Tree,主键索引是稀疏的、轻量的,慢是因为 Part 数膨胀和 Merge 跟不上。

与 MySQL/PG 对比:MySQL/PG 单条 INSERT 是日常;ClickHouse 单条 INSERT 是反模式 —— 这也是 OLTP 与 OLAP 引擎设计哲学的根本差异。


Q8:什么是 MergeTree?为什么说它是 ClickHouse 的灵魂?

考察点:能否用一句话讲清 MergeTree 的核心思想,预热第 5 章。

标准答案

MergeTree 是 ClickHouse 的默认表引擎家族,名字就来自 LSM-Tree(Log-Structured Merge-Tree)的「合并」思想。

核心机制

  1. 写入即追加:每次 INSERT 生成一个新 Part 目录,永远不修改已有 Part(Append-only);
  2. 后台合并:多个 Part 由后台线程异步合并成更大的 Part,类似 LSM-Tree 的 Compaction;
  3. 稀疏主键索引:默认每 8192 行(index_granularity)记录一个 Mark,主键索引文件极小、可常驻内存;
  4. 变种引擎:基于 MergeTree 派生出 ReplacingMergeTree(去重)、SummingMergeTree(汇总)、AggregatingMergeTree(聚合状态)、CollapsingMergeTree(折叠)等,全部共享上述机制;
  5. 副本与分布式:ReplicatedMergeTree 在 MergeTree 基础上加 Keeper 协调,实现多副本最终一致。

为什么是灵魂

  • ClickHouse 的几乎所有重要能力(去重、汇总、TTL、副本、分布式、物化视图、Projection)都长在 MergeTree 上;
  • 不理解 MergeTree 的物理布局(Part / Mark / Granule / index_granularity),就无法做任何性能调优;
  • 理解了 MergeTree,ClickHouse 就「学完 60%」。

加分项

  • 能列举 MergeTree 家族 6 大成员:MergeTree / ReplacingMergeTree / SummingMergeTree / AggregatingMergeTree / CollapsingMergeTree / VersionedCollapsingMergeTree / GraphiteMergeTree;
  • 能用一句话区分:「Replacing 按主键最后写为准;Summing 按主键自动求和;Aggregating 按主键聚合状态」。

易错点:不要把 MergeTree 当成 B+Tree —— 它是 LSM-Tree 思想,没有原地修改,没有 B+Tree 的页分裂。

与 MySQL/PG 对比:MySQL InnoDB 是 B+Tree 聚簇索引,原地修改;ClickHouse MergeTree 是 LSM-Tree 思想,append-only + 后台合并。这是「OLTP B+Tree」与「OLAP LSM」的世界观差异。


📌 下一章预告:第 2 章我们正式动手 —— 学 clickhouse-client 的所有玩法、HTTP 接口、库 / 表 / 视图三级结构、SHOW / DESCRIBE / SYSTEM 命令族,以及 ClickHouse SQL 方言与标准 SQL 的关键差异。

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

python
"""
hello_ck.py —— ClickHouse 教程第 1 章配套代码:第一次握手 ClickHouse

本脚本会做 4 件事:
  1) 连接本机 8123 HTTP 端口;
  2) 打印服务器版本(SELECT version());
  3) 创建演示库 learn_ck(IF NOT EXISTS);
  4) 在 learn_ck 库下建一张 1000 行的演示表 events,灌入随机数据,
     然后跑一条聚合查询,体验「列存 + 向量化」一秒返回的快感。

运行前提:
  - ClickHouse Server 已在 127.0.0.1:8123 启动(默认账户 default,无密码);
    如果还没起来,进入仓库根目录执行:docker compose up -d
  - Python 3.8+;pip install -r requirements.txt(核心是 clickhouse-connect)

运行:
  python 01_intro/code/hello_ck.py

预期输出(节选):
  ✅ 连接成功,ClickHouse 版本:24.8.x
  ✅ learn_ck 库已就绪
  ✅ events 表已重建并灌入 1000 行
  📊 按 country 分组的 PV 排行:
       BJ  → 332
       SH  → 245
       ...
  ⏱ 聚合查询耗时:3.21 ms
"""

from __future__ import annotations

import random
import sys
import time
from datetime import datetime, timedelta

try:
    import clickhouse_connect
except ImportError:
    sys.stderr.write(
        "❌ 缺少依赖 clickhouse-connect,请先执行:\n"
        "   pip install -r requirements.txt\n"
    )
    sys.exit(1)


CK_HOST = "127.0.0.1"
CK_PORT = 8123
CK_USER = "default"
CK_PASSWORD = ""
CK_DB = "learn_ck"


def connect():
    """建立到 ClickHouse 的 HTTP 连接。"""
    return clickhouse_connect.get_client(
        host=CK_HOST,
        port=CK_PORT,
        username=CK_USER,
        password=CK_PASSWORD,
        # 默认 connect_timeout=10s, send_receive_timeout=300s,足够本教程使用
    )


def step_version(client) -> str:
    """步骤 1:握手 + 打印版本。"""
    rows = client.query("SELECT version()").result_rows
    version = rows[0][0]
    print(f"✅ 连接成功,ClickHouse 版本:{version}")
    return version


def step_create_db(client) -> None:
    """步骤 2:创建演示库 learn_ck(容器化部署时已自动建好,这里幂等执行一次)。"""
    client.command(f"CREATE DATABASE IF NOT EXISTS {CK_DB}")
    print(f"✅ {CK_DB} 库已就绪")


def step_create_table(client) -> None:
    """步骤 3:建一张演示表 events。

    设计要点:
      - country 用 LowCardinality(String) —— 第 3 章重点,少量重复值的字典编码;
      - ts     用 DateTime CODEC(DoubleDelta, ZSTD)) —— 时序列经典编码;
      - 表引擎选用 MergeTree —— 第 4、5 章会详细讲;
      - ORDER BY (country, ts) 把数据按国家 + 时间排好,未来按 country 过滤超快。
    """
    client.command(f"DROP TABLE IF EXISTS {CK_DB}.events")
    client.command(
        f"""
        CREATE TABLE {CK_DB}.events
        (
            event_id    UInt64,
            user_id     UInt32,
            country     LowCardinality(String),
            url         String                CODEC(ZSTD(3)),
            bytes       UInt32                CODEC(T64, LZ4),
            ts          DateTime              CODEC(DoubleDelta, ZSTD(3))
        )
        ENGINE = MergeTree
        ORDER BY (country, ts)
        """
    )


def step_seed(client, n_rows: int = 1000) -> None:
    """步骤 4:灌入 n_rows 行随机数据。

    采用 client.insert() 批量写入 —— 这才是 ClickHouse 的正确写入姿势:
    单条 INSERT 是反模式,千万行也建议一批写完(详见第 7 章)。
    """
    countries = ["BJ", "SH", "GZ", "SZ", "HZ", "CD", "WH", "XA"]
    base = datetime(2026, 4, 17, 0, 0, 0)

    rows = []
    for i in range(n_rows):
        rows.append(
            (
                i + 1,                                        # event_id
                random.randint(1, 1000),                      # user_id
                random.choice(countries),                     # country
                f"/page/{random.randint(1, 50)}",            # url
                random.randint(200, 200000),                  # bytes
                base + timedelta(seconds=i * 7),              # ts
            )
        )

    client.insert(
        f"{CK_DB}.events",
        rows,
        column_names=["event_id", "user_id", "country", "url", "bytes", "ts"],
    )
    print(f"✅ events 表已重建并灌入 {n_rows} 行")


def step_aggregate(client) -> None:
    """步骤 5:跑一条经典 OLAP 聚合查询,体验「列存 + 向量化」的速度。

    SQL 语义:按国家分组,统计访问次数 / 平均字节数,按 PV 倒序。
    生产里这种查询一般在亿级表上跑,本例只灌了 1000 行,主要演示流程。
    """
    sql = f"""
        SELECT
            country,
            count()                AS pv,
            avg(bytes)             AS avg_bytes,
            quantile(0.9)(bytes)   AS p90_bytes
        FROM {CK_DB}.events
        GROUP BY country
        ORDER BY pv DESC
    """

    t0 = time.perf_counter()
    result = client.query(sql)
    elapsed_ms = (time.perf_counter() - t0) * 1000.0

    print("\n📊 按 country 分组的 PV 排行:")
    print(f"   {'country':<10} {'pv':>8} {'avg_bytes':>12} {'p90_bytes':>12}")
    print(f"   {'-' * 44}")
    for country, pv, avg_bytes, p90 in result.result_rows:
        print(f"   {country:<10} {pv:>8} {avg_bytes:>12.1f} {p90:>12.1f}")

    print(f"\n⏱ 聚合查询耗时:{elapsed_ms:.2f} ms")


def main() -> None:
    print("=" * 60)
    print(f"Hello ClickHouse  →  http://{CK_HOST}:{CK_PORT}")
    print("=" * 60)

    try:
        client = connect()
    except Exception as e:
        sys.stderr.write(
            f"❌ 连不上 {CK_HOST}:{CK_PORT}{e}\n"
            "   请确认 ClickHouse 已在该地址启动;\n"
            "   或在仓库根目录执行:docker compose up -d\n"
        )
        sys.exit(1)

    step_version(client)
    step_create_db(client)
    step_create_table(client)
    step_seed(client, n_rows=1000)
    step_aggregate(client)

    print("\n🎉 第 1 章实操完成。打开 01_intro/demo.html 在浏览器里看可视化演示。")


if __name__ == "__main__":
    main()

hello_ck.py ↗