主题
第 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 端口 | 8123 | curl / 浏览器 / clickhouse-connect 走它。 |
| TCP 端口 | 9000 | clickhouse-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-8 | ClickHouse 内部字符串就是 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 Pinot | LinkedIn 出品 / 低延迟 | 索引类型多但运维复杂;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 / Oracle | ClickHouse / 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_time 和 amount,但 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 MB | Decimal 类型同分布数据压缩比 ≈ 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 个值📌 首次术语解释 · SIMD:Single 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 GB | 100% |
| MySQL InnoDB(行存,无压缩) | 1.8 GB | 86% |
| MySQL InnoDB + ROW_FORMAT=COMPRESSED | 1.0 GB | 48% |
| ClickHouse MergeTree(列存 + LZ4 默认) | 310 MB | 15% |
| ClickHouse MergeTree(列存 + ZSTD level 3) | 180 MB | 8.6% |
| ClickHouse + LowCardinality + ZSTD | 95 MB | 4.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) | PostgreSQL | ClickHouse |
|---|---|---|---|
| 定位 | OLTP | OLTP(分析也行) | 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 为什么这么快?请从存储、计算、压缩三个层面回答
考察点:是否能把「列式存储 + 向量化 + 压缩」三件套串成一个完整故事,而不是只会背单点。
标准答案(按层次分点):
- 存储层 —— 列式存储:ClickHouse 把每一列单独存成一个文件(
.bin),同一列的数据在物理磁盘上连续。OLAP 查询通常只关心几列(如SUM(amount)),列存可以只读这几列,跳过表里其它几十列;同时同列数据类型 / 分布相似,压缩比远高于行存(5~20 倍)。 - 计算层 —— 向量化执行:ClickHouse 的所有算子都以 Block(默认 65536 行 × N 列)为最小单位流动,而不是「一行一行」。这样:
- 函数调用次数从 N 降到 N/65536;
- CPU 缓存利用率高(顺序访问,L1/L2 命中率 95%+);
- 数学算子可以走 SIMD 一条指令算 4/8/16 个值。
- 压缩层 —— LZ4 / ZSTD / Delta / DoubleDelta / T64 / LowCardinality 多管齐下:每列可以独立指定 CODEC,时间戳走 DoubleDelta + ZSTD,整数 ID 走 T64 + LZ4,字符串高基数走 ZSTD、低基数走 LowCardinality 字典化。结果是同样数据 ClickHouse 占用空间是 MySQL 的 1/10~1/20。
- 架构层 —— 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 数据库?
- 数据组织方式不同:OLAP 查询每次只用 5~10 列但要扫海量行,列式存储的 IO 优势远高于行存;
- 执行模型不同:OLAP 算子要处理大批量数据,向量化 + SIMD 比逐行执行快 1~2 个数量级;
- 存储成本差异巨大:分析型数据通常归档保留几年,列存压缩可以省 90% 的磁盘成本;
- 写入模式不同: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 灵活度高得多,但实时性略弱 |
| Pinot | LinkedIn 出品,索引类型丰富(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 的代价:
- 每次 INSERT 创建一个 Part:哪怕只插一行,也要生成完整目录结构(几十个文件);
- 后台 Merge 跟不上:默认每个分区最多 300 个未合并的 Part,超过会报
Too many parts拒绝写入; - 元数据开销巨大:1 万次单条 INSERT = 1 万个 Part = 几十万个小文件,文件系统 inode 都吃紧;
- 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)的「合并」思想。
核心机制:
- 写入即追加:每次 INSERT 生成一个新 Part 目录,永远不修改已有 Part(Append-only);
- 后台合并:多个 Part 由后台线程异步合并成更大的 Part,类似 LSM-Tree 的 Compaction;
- 稀疏主键索引:默认每 8192 行(
index_granularity)记录一个 Mark,主键索引文件极小、可常驻内存; - 变种引擎:基于 MergeTree 派生出 ReplacingMergeTree(去重)、SummingMergeTree(汇总)、AggregatingMergeTree(聚合状态)、CollapsingMergeTree(折叠)等,全部共享上述机制;
- 副本与分布式: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()