主题
第 4 章 表引擎全景图
学习目标:理解「表引擎」在 ClickHouse 里到底是什么、它和 MySQL 的「存储引擎」相比为什么权力大得多;能背出主流引擎的家族关系;遇到一个新需求(百亿日志、Kafka 实时摄入、关联 MySQL 维表、跨集群查询、临时缓存表……)能在 30 秒内说出"应该用哪个引擎"。
4.0 一句话总览
「在 ClickHouse 里,建表先选引擎,比建表更早的还是选引擎。」
MySQL: CREATE TABLE t (...) ENGINE=InnoDB; -- 引擎只决定「数据怎么落盘」
ClickHouse:CREATE TABLE t (...) ENGINE=MergeTree -- 引擎决定「存哪儿 / 索引 / 复制 /
PARTITION BY ... -- 是否支持更新 / 合并策略 /
ORDER BY ...; -- 读哪儿 / 怎么删」| 维度 | MySQL InnoDB / MyISAM | ClickHouse 引擎 |
|---|---|---|
| 存储位置 | 永远在本地磁盘 | 可以本地、可以远端 MySQL/PG/S3、可以纯内存、可以是别人 |
| 索引 | B+Tree 聚簇 + 二级 | 稀疏主键 + 跳数索引(仅 MergeTree 系才有) |
| 复制 | 引擎外的 binlog 主从 | 引擎自身决定(Replicated* 才支持) |
| 更新 | 任意 UPDATE/DELETE | 多数引擎根本不支持,少数靠 Mutation 异步重写 |
| 数据合并 | 无此概念 | MergeTree 系核心特性:写小 Part → 后台合并 |
| 同库异引擎 | 罕见 | 常态:一个库里 MergeTree、Kafka、MySQL 引擎共存 |
📌 生活类比 · 引擎 = 车型 同一个车牌「Tesla」在不同型号下完全不同:Model S 是轿车(高速)、Model X 是 SUV(载货)、Cyber Truck 是卡车(拉砖)。 ClickHouse 的引擎就像车型 ——
MergeTree是高速轿车(亿级查询),Memory是电瓶车(快但跑不远),Kafka是公交车(只接客不存人),Distributed是空中调度的飞机(只调度,不载货)。 写错引擎 = 拿卡车去跑 F1,能跑,但跑得很丑。
4.1 三大族 + 一个特种部队
ClickHouse 官方把所有引擎分成 4 个族(family),所有引擎都是这 4 个族的成员:
ClickHouse Engines
│
┌─────────────────────┼─────────────────────┬────────────────┐
▼ ▼ ▼ ▼
MergeTree 家族 Log 家族 Integration 家族 Special 家族
─────────────── ─────────── ──────────────── ─────────────
生产首选 99% 走它 小数据玩具 连接外部系统 奇技淫巧
(已不推荐)
┌────────────────┐ ┌────────────┐ ┌─────────────┐ ┌───────────┐
│ MergeTree │ │ TinyLog │ │ MySQL │ │ Memory │
│ ReplacingMT │ │ StripeLog │ │ PostgreSQL │ │ File │
│ SummingMT │ │ Log │ │ Kafka │ │ URL │
│ AggregatingMT │ └────────────┘ │ S3 │ │ Null │
│ CollapsingMT │ │ HDFS │ │ Set │
│ VersionedColl* │ │ ODBC/JDBC │ │ Join │
│ GraphiteMT │ │ MongoDB │ │ Buffer │
│ Replicated*MT │ ← 上面任何一个都可 │ MaterializedPG │ Merge │
└────────────────┘ 在前面加 Replicated │ S3Queue │ │ Distributed│
实现 Keeper 协调副本│ EmbeddedRocksDB │ Dictionary │
└─────────────┘ │ View / MV │
│ Generate* │
└───────────┘4.1.1 MergeTree 家族 ——「Click 之魂」
一句话:你 99% 的生产表都该用它。第 5、6 章会专门展开。这里只先认门牌。
| 引擎 | 一句话定位 | 典型场景 |
|---|---|---|
MergeTree | 基础列存表,分区 + 排序 + 稀疏索引 | 万物起点,所有原始数据表 |
ReplacingMergeTree | 主键去重(最终一致) | 维度表、用户最新画像、CDC 落地 |
SummingMergeTree | 数值列自动求和合并 | 报表预聚合(PV、订单金额合计) |
AggregatingMergeTree | 任意聚合函数的「中间状态」合并 | 物化视图 + uniq/quantile 实时大屏 |
CollapsingMergeTree | +1/-1 行折叠 | 订单状态翻转、可撤销事件 |
VersionedCollapsingMergeTree | 带版本号的折叠,乱序写入也能折对 | 多客户端乱序写入的 CDC |
GraphiteMergeTree | 时序 rollup(按时间粗化) | 给 Graphite/Prometheus 做存储 |
Replicated* | 上述任何一个 + Keeper 多副本 | 生产环境标配,单节点也建议用 |
4.1.2 Log 家族 ——「玩具」
已经基本被 MergeTree 取代,了解即可。生产禁用。
Log 家族特征:
✗ 无主键 / 无索引(每次查询全表扫)
✗ 不支持并发读写(写时上排他锁)
✗ 不支持复制
✓ 实现极简,启动快、占用小
✓ 适合:< 100 万行的临时小表、单测| 引擎 | 区别 |
|---|---|
TinyLog | 最简:每列一个文件,不支持并发读 |
StripeLog | 所有列写到一个数据文件 + 一个索引文件 |
Log | 介于两者之间,每列单文件 + 共享元数据 |
4.1.3 Integration 家族 ——「数据库的 USB 接口」
把外部系统当成 ClickHouse 的"虚拟表"使用,数据不在 CH 这边落盘(除非你显式 INSERT ... SELECT 拉一份)。
ClickHouse ┌──────────┐
┌──────────┐ │ MySQL │
│ Engine= │ ◀──── 实时 ───▶│ PostgreSQL │
│ MySQL │ │ Kafka │
│ Kafka │ │ S3 │
│ S3 │ │ HDFS │
└──────────┘ └──────────┘
「占位表」 真实数据所在| 引擎 | 干啥 | 备注 |
|---|---|---|
MySQL / PostgreSQL | 把对端的某张表当成 CH 表查 | 每次查都走网络,适合维度表 |
MaterializedPostgreSQL | 用逻辑复制实时同步 PG 整库 | 本地落 ReplacingMT |
Kafka | 把 Kafka topic 当成可消费的"队列表" | 通常配物化视图把数据搬到 MergeTree |
S3 / HDFS / URL | 直接 SELECT 远程 Parquet/CSV/JSON | 数据湖 + Lakehouse 入口 |
MongoDB / ODBC / JDBC | 桥接其他数据源 | JDBC 走 clickhouse-jdbc-bridge |
EmbeddedRocksDB | 嵌入式 KV,单点查询毫秒级 | 拿来做小型字典或临时维表 |
S3Queue | 监听 S3 新文件并自动入库 | 数据湖文件触发器 |
4.1.4 Special 家族 ——「奇技淫巧 + 全村希望」
| 引擎 | 一句话 | 类比 |
|---|---|---|
Memory | 数据全在内存,掉电就没 | Redis 但只是个表 |
File(Format) | 表 = 一个本地文件(CSV/JSONEachRow/Parquet…) | cat file.csv | query |
URL(Format) | 表 = 一个 HTTP URL 上的远程文件 | curl 出来当表查 |
Null | 写进去就丢,但能触发 MV | 「数据黑洞」+ 派发器 |
Set / Join | 专为 IN / JOIN 操作预先放右表 | 内存里的临时索引 |
Buffer | 在内存里攒小批,定时 flush 到目标表 | 写入合并器(已渐被 async_insert 取代) |
Merge | 多张同结构表的"虚拟联合视图"(不是物理 union) | 老板把多张子公司的表"叠"在一起看 |
Distributed | 跨分片的元表,自己不存数据 | 调度员,把 SQL fan-out 到所有 shard |
Dictionary | 把字典数据当成只读表查 | 字典表(详见第 9 章) |
View | 普通视图,只是 SQL 改写 | 完全不存东西 |
MaterializedView | 第 10 章重头戏:增量预聚合 | "INSERT 触发器" |
GenerateRandom | 给我生成 N 行随机数据 | 造数神器(seed.py 的好基友) |
4.2 一张「全引擎对照表」(背下这一张就够了)
记忆口诀:MergeTree 全能、Log 玩具、Integration 借用、Special 凑数。
| 引擎 | 持久化 | 主键索引 | 跳数索引 | 复制 | UPDATE/DELETE | 典型用途 | 典型坑 |
|---|---|---|---|---|---|---|---|
MergeTree | ✅ | 稀疏 | ✅ | ❌(用 Replicated) | Mutation 异步 | 主表 | 写小批 → Part 爆炸 |
ReplacingMergeTree | ✅ | 稀疏 | ✅ | ❌ | Mutation | 主键去重 | 不是实时去重,要 FINAL 才看到 |
SummingMergeTree | ✅ | 稀疏 | ✅ | ❌ | Mutation | 求和预聚合 | 非数值列保留主键第一行 |
AggregatingMergeTree | ✅ | 稀疏 | ✅ | ❌ | Mutation | MV 预聚合 | 必须配 *State/*Merge 函数 |
CollapsingMergeTree | ✅ | 稀疏 | ✅ | ❌ | Mutation | 行折叠 | 必须保证 +1/-1 同主键 |
Replicated* | ✅ | 稀疏 | ✅ | ✅ Keeper | Mutation | 生产标配 | Keeper 故障 → 写入挂 |
TinyLog / Log / StripeLog | ✅ | ❌ | ❌ | ❌ | ❌ | 玩具 / 单测 | 不支持并发,已不推荐 |
Memory | ❌ | ❌ | ❌ | ❌ | ❌ | 临时缓存、子查询右表 | 重启即没 |
File | ✅(本地文件) | ❌ | ❌ | ❌ | ❌ | 本地文件查询 | 同时只能一个进程读写 |
URL | ❌(远端) | ❌ | ❌ | ❌ | ❌ | 远程文件查询 | 网络抖动直接失败 |
MySQL | ❌(在 MySQL) | 由 MySQL 决定 | ❌ | ❌ | ✅(透传) | 维度表 / 增量同步 | 大表 SELECT 拖死对端 |
PostgreSQL | ❌(在 PG) | 由 PG 决定 | ❌ | ❌ | ✅(透传) | 同上 | 同上 |
Kafka | ❌(在 Kafka) | ❌ | ❌ | N/A | ❌ | 消息消费表 | 只能消费一次,必须配 MV |
S3 | ❌(在 S3) | ❌ | minmax | ❌ | ❌ | 数据湖入口 | 网络 IO 是瓶颈 |
Distributed | ❌(在各 shard) | 看下层 | 看下层 | 看下层 | 看下层 | 跨分片读写 | 写入要会 internal_replication |
Merge(db, regex) | ❌(在底层表) | 看下层 | 看下层 | N/A | 看下层 | 跨表联合查询 | 不能写,只能读 |
Dictionary | ✅(被字典 own) | 字典自己 | ❌ | N/A | ❌ | 把字典当表 | 内存敏感 |
Buffer | ❌(内存) | ❌ | ❌ | ❌ | ❌ | 写入缓冲 | 服务重启 → 内存数据丢 |
Null | ❌(黑洞) | ❌ | ❌ | ❌ | ❌ | 触发 MV,自身不存 | 不知道 MV 链 → 以为没写进去 |
4.3 重点小节:把那些「常用但容易写错」的引擎挨个打开
4.3.1 Memory —— 内存里的全表缓存
sql
CREATE TABLE learn_ck.tmp_users
(
id UInt32,
name String
) ENGINE = Memory;
INSERT INTO learn_ck.tmp_users VALUES (1, 'a'), (2, 'b');
SELECT * FROM learn_ck.tmp_users;
-- ┌─id─┬─name─┐
-- │ 1 │ a │
-- │ 2 │ b │
-- └────┴──────┘特性:
- 全表都在内存,重启就没。
- 没有任何索引,扫描就是 for 循环。
- 多线程读写安全(内部加锁),但写多了会争锁。
- 唯一价值:子查询右表 / 临时维度 / 单元测试。
📌 想做"复杂子查询右表",更专业的做法是
Set/Join引擎,专为 IN / JOIN 优化过。
4.3.2 File(Format[, path]) —— 把表当成文件,把文件当成表
sql
-- ① 读:把 /var/lib/clickhouse/user_files/orders.csv 当成表
CREATE TABLE learn_ck.orders_csv
(
order_id UInt64,
user_id UInt32,
amount Decimal(18, 2)
) ENGINE = File(CSV, '/var/lib/clickhouse/user_files/orders.csv');
SELECT count() FROM learn_ck.orders_csv;bash
# ② 写:INSERT 进去会直接写文件
echo "1,1001,99.50" >> /var/lib/clickhouse/user_files/orders.csv支持的 Format:CSV / TSV / JSONEachRow / Parquet / ORC / Arrow / Avro …全家桶超过 80 种。
坑:
- 路径必须在
<user_files_path>下,否则拒绝。 - 同一时刻只能一个进程读写(文件锁)。
- 大文件查询性能远不如 MergeTree(无索引 + 没列压缩切片)。
4.3.3 URL(url, Format) —— 远程文件即表
sql
CREATE TABLE learn_ck.gh_events
(
type String,
actor Tuple(login String),
repo Tuple(name String),
public UInt8,
created_at DateTime
) ENGINE = URL('https://data.gharchive.org/2024-01-01-0.json.gz', JSONEachRow);
SELECT type, count() FROM learn_ck.gh_events GROUP BY type ORDER BY 2 DESC LIMIT 5;用途:临时探查远程开源数据集、做 demo、对接 RESTful 接口。
坑:网络抖动 → 整条 SQL 直接失败;不要拿来跑生产分析。
4.3.4 MySQL(host:port, db, table, user, pwd) —— 把 MySQL 表当成 CH 的虚拟表
sql
CREATE TABLE learn_ck.users_ext
(
id UInt32,
name String,
email String
) ENGINE = MySQL('mysql-host:3306', 'biz', 'users', 'reader', 'pwd');
-- 像查本地表一样
SELECT count(), max(id) FROM learn_ck.users_ext;特性:
- 数据始终在 MySQL 那边,CH 只做 SQL 改写 + 网络回拉。
- 可以
INSERT INTO learn_ck.users_ext VALUES …透传到 MySQL(少用)。 - 适合维度表关联、一次性增量同步(
INSERT … SELECT FROM mysql_table)。
坑:
SELECT *会把 MySQL 整表拉过来 → 第一行就把对方 DBA 拉黑名单。- WHERE 谓词大部分能下推(
int = 1、str LIKE 'a%'),但复杂表达式不能下推,自己EXPLAIN验证。 - 无 CH 索引,不能享受跳数 / minmax 优化。
4.3.5 PostgreSQL —— 同 MySQL,连 PG
sql
CREATE TABLE learn_ck.products_ext
(
id UInt32,
name String
) ENGINE = PostgreSQL('pg-host:5432', 'shop', 'products', 'reader', 'pwd', 'public');PG 这边支持 schema(CH 的 MySQL 引擎没有 schema 参数,因为 MySQL 没有 schema 概念)。
还有进阶版
MaterializedPostgreSQL:基于 PG 的逻辑复制把整库实时同步到 CH 的 ReplacingMergeTree 上,适合 CDC。
4.3.6 Kafka —— 消息消费引擎(仅介绍,第 14 章详讲)
sql
CREATE TABLE learn_ck.kafka_src
(
user_id UInt64,
event String,
ts DateTime
) ENGINE = Kafka()
SETTINGS kafka_broker_list = 'broker:9092',
kafka_topic_list = 'events',
kafka_group_name = 'ck_consumer',
kafka_format = 'JSONEachRow',
kafka_num_consumers= 4;核心心智:
- Kafka 引擎表是一次性消费的——
SELECT * FROM kafka_src会消费 offset,再查就没了! - 真实用法:Kafka 引擎 + 物化视图 + MergeTree 落地表,三件套配合。
┌────────┐ 消息 ┌───────────────┐ INSERT 触发 ┌──────────────────┐ 存储 ┌─────────────┐
│ Kafka │ ─────▶ │ Engine=Kafka │ ───────────▶ │ MaterializedView │ ─────▶ │ MergeTree │
│ Broker │ │ kafka_src │ │ (SELECT … ) │ │ events │
└────────┘ └───────────────┘ └──────────────────┘ └─────────────┘详细讲解放第 14 章「集成生态」。
4.3.7 S3('url', 'AKID', 'SK', 'Format')
sql
CREATE TABLE learn_ck.logs_s3
(
ts DateTime,
msg String
) ENGINE = S3('https://bucket.s3.amazonaws.com/2024/*/*.parquet', 'AKID', 'SK', Parquet);- 直接当数据湖入口;通配符
*?{a,b}都支持。 - 写也支持(
INSERT … SELECT输出 Parquet 到 S3)。 - 跳数索引仅
minmax可用,主要靠分区路径裁剪。
4.3.8 Distributed(cluster, db, table[, sharding_key]) —— 不存数据的"调度员"
sql
CREATE TABLE learn_ck.events_local ON CLUSTER prod (...) ENGINE = ReplicatedMergeTree(...);
CREATE TABLE learn_ck.events ON CLUSTER prod AS learn_ck.events_local
ENGINE = Distributed(prod, learn_ck, events_local, cityHash64(user_id));events(Distributed 表)自身不落盘;- 查询时 fan-out 到所有 shard,结果 fan-in 合并;
- 写入时按
sharding_key路由到对应 shard。 - 详见第 13 章「副本与分布式」。
4.3.9 Merge(db_regex, table_regex) —— 多表"叠"成一张表
sql
-- 把 logs_2024_01、logs_2024_02、… 全部叠成一张可查询的虚拟表
CREATE TABLE learn_ck.logs_all AS learn_ck.logs_2024_01
ENGINE = Merge(learn_ck, '^logs_2024_');
SELECT count() FROM learn_ck.logs_all WHERE level = 'ERROR';
-- ↑ 实际会 SELECT … UNION ALL 各匹配表关键点:
- 只能读、不能写(
INSERT直接报错)。 - 各底层表的列名 / 类型必须一致。
- 自动新增的表(如新建一个
logs_2024_03)下次查询自动包含,无需 ALTER。
📌 不要和"分布式表"混淆:
Merge是单机内多表合并,Distributed是多机间分片调度。
4.3.10 Dictionary —— 把字典当成表
字典是 CH 加速 JOIN 的杀手锏(第 9 章详讲)。
Dictionary引擎是把已注册的字典直接暴露成可 SELECT 的表,便于排查。
sql
CREATE DICTIONARY dict_users ( id UInt32, name String )
PRIMARY KEY id
SOURCE(MYSQL(...))
LIFETIME(MIN 60 MAX 300)
LAYOUT(HASHED());
CREATE TABLE learn_ck.dict_users_view
(
id UInt32,
name String
) ENGINE = Dictionary(dict_users);
SELECT * FROM learn_ck.dict_users_view LIMIT 5; -- 仅用于排查字典内容4.3.11 Buffer(db, target, num_layers, min_t, max_t, min_rows, max_rows, min_b, max_b)
sql
CREATE TABLE learn_ck.events_buf AS learn_ck.events_local
ENGINE = Buffer(learn_ck, events_local,
16, -- 内部分桶
10, 60, -- min/max 秒
10000, 1000000, -- min/max 行数
1048576, 104857600); -- min/max 字节- 在内存里攒数据,达到阈值之一就 flush 到底层
events_local。 - 解决"客户端写得太碎,导致 MergeTree Part 爆炸"。
- 现代替代方案:
async_insert = 1(服务端攒批),见第 7 章。Buffer 引擎已被官方"温和地不推荐"——一旦服务重启,内存里的数据直接消失。
4.4 决策树:「我这个需求该选哪个引擎?」
快速记忆:「主存储 = MergeTree 全家桶;接外部 = Integration 全家桶;玩巧 = Special 全家桶。」
4.5 📌 与 MySQL / PG 对比小框
| 维度 | MySQL InnoDB | MySQL MyISAM | PG Heap | ClickHouse 引擎 |
|---|---|---|---|---|
| 引擎数量 | 5+,但默认 InnoDB | 已淘汰 | 仅一种 + TableAM 接口 | 20+,必须每张表显式指定 |
| 一个库多种引擎 | 罕见 | 罕见 | 不可能 | 常态 |
| 索引模型 | B+ 聚簇 + 二级 B+ | B+ 非聚簇 | 堆 + 独立 B-Tree/GIN/GiST/BRIN | 稀疏主键 + 跳数索引 |
| 复制 | 引擎外 binlog | 引擎外 binlog | 引擎外 WAL 流复制 | 引擎自带(Replicated) + Keeper* |
| 跨库表 | 跨实例靠 FEDERATED | 跨实例靠 FEDERATED | 靠 postgres_fdw 扩展 | MySQL/PG/Kafka/S3 引擎原生 |
| 内存表 | MEMORY 引擎(已不推荐) | 同 | 不支持 | Memory / Set / Join / Buffer |
| 多表合并视图 | UNION ALL | UNION ALL | UNION ALL | Merge 引擎自动维护 |
| 分布式 | NDB Cluster(少用) | 无 | Citus 扩展 | Distributed 引擎是一等公民 |
最大反差:在 MySQL/PG,"用什么引擎"是 DBA 关心的事;在 ClickHouse,"用什么引擎"是业务建模的第一选择,写错引擎 → 整张表的查询 / 写入 / 复制都跟着错。
4.6 实操:一气呵成把 5 种引擎建一遍
sql
CREATE DATABASE IF NOT EXISTS learn_ck;
-- ① 内存表
CREATE TABLE IF NOT EXISTS learn_ck.demo_memory
( id UInt32, val String
) ENGINE = Memory;
-- ② File 引擎(CSV)—— 假设已有 /var/lib/clickhouse/user_files/cities.csv
CREATE TABLE IF NOT EXISTS learn_ck.demo_file
( city String, pop UInt32
) ENGINE = File(CSV);
-- ③ URL 引擎(远程 JSON)
CREATE TABLE IF NOT EXISTS learn_ck.demo_url
( id UInt32, name String
) ENGINE = URL('https://jsonplaceholder.typicode.com/users', JSONEachRow);
-- ④ MergeTree 主表
CREATE TABLE IF NOT EXISTS learn_ck.demo_mt
(
event_date Date,
user_id UInt64,
event_name LowCardinality(String),
revenue Decimal(18, 2)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_date);
-- ⑤ Merge 引擎(多表联合)
CREATE TABLE IF NOT EXISTS learn_ck.demo_mt_2024_01
AS learn_ck.demo_mt
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_date);
CREATE TABLE IF NOT EXISTS learn_ck.demo_mt_2024_02
AS learn_ck.demo_mt
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_date);
CREATE TABLE IF NOT EXISTS learn_ck.demo_merge
AS learn_ck.demo_mt
ENGINE = Merge(learn_ck, '^demo_mt_2024_');执行后 system.tables 里就能看到 6 张表,引擎一栏五花八门——这就是 ClickHouse 的"动物园"。
4.7 本章小结
┌─────────────────────────────────────────────────────────┐
│ 本章核心要点 │
├─────────────────────────────────────────────────────────┤
│ ① 表引擎 = 表的灵魂,决定存储 / 索引 / 复制 / 更新 │
│ ② 4 大族:MergeTree(主力)/ Log(玩具)/ │
│ Integration(USB 接口)/ Special(巧器) │
│ ③ 99% 生产表 = MergeTree 家族 + Replicated 前缀 │
│ ④ Memory/File/URL 是"轻量探查"三件套 │
│ ⑤ MySQL / PG / Kafka / S3 引擎的数据"不在自己家" │
│ ⑥ Kafka 引擎必须配 MV + MergeTree 三件套 │
│ ⑦ Distributed = 调度员,Merge = 单机多表叠合,二者别混 │
│ ⑧ Buffer 已被 async_insert 取代,Log 家族已被 MT 取代 │
└─────────────────────────────────────────────────────────┘4.8 面试高频题
Q1:ClickHouse 表引擎和 MySQL 存储引擎有什么本质区别?
考察点:是否真的理解 CH 的"引擎权力"远大于 MySQL。
标准答案:
- 决策权差异:MySQL 引擎只决定"数据怎么落盘";CH 引擎额外决定是否落盘、是否支持索引、是否支持复制、是否支持更新、合并策略、查询路由。
- 粒度差异:MySQL 同一个库通常都是 InnoDB;CH 一个库内 MergeTree、Kafka、MySQL、Distributed 共存是常态。
- 存储位置差异:MySQL 引擎数据永远在本机;CH 的
MySQL/PostgreSQL/Kafka/S3/URL等引擎数据根本不在 CH 这边。 - 复制实现:MySQL 复制由 binlog 在引擎外实现;CH 复制由
Replicated*引擎本身 + Keeper 协调实现。
加分项:能补一句"CH 没有'存储引擎'这种说法,所有引擎对等,且都是表声明的一部分;这是 CH 把 OLAP 多场景统一抽象成"引擎多态"的设计哲学。"
易错点:千万别说"CH 引擎只是 MySQL 引擎换名"——本质区别是 CH 引擎参与查询路由与生命周期管理。
Q2:Memory / Buffer / Log / Null 的区别是什么?
考察点:4 个看起来都"轻量"的引擎到底干啥用,是否能区分。
标准答案:
| 引擎 | 落盘 | 触发 MV | 主要用途 |
|---|---|---|---|
Memory | ❌ | ❌ | 临时缓存表、子查询右表 |
Buffer | ❌(内存攒,flush 到目标) | ✅ flush 时 | 写入合并器(已被 async_insert 取代) |
Log 系 | ✅ | ❌ | 玩具,已淘汰 |
Null | ❌ | ✅ INSERT 时 | "数据黑洞 + MV 派发器" |
易错点:
- 把
Memory当 Redis 用 → 重启数据丢光; - 把
Null引擎当垃圾桶 → 忘了它会触发挂在它上面的 MV(这是 CH 一种常见的"事件分发"模式); - 用
Buffer但不知道服务重启会丢数据。
Q3:什么是 Distributed 引擎?它和 Merge 引擎的区别?
考察点:单机多表 vs 多机分片,必考。
标准答案:
Distributed(cluster, db, table[, sharding_key]):跨多台机器的分片代理表,自身不存数据;查询时 fan-out 到 cluster 中各 shard,结果 fan-in 合并;写入按sharding_key路由。Merge(db_regex, table_regex):单机内把多张同结构的表"叠"成一张虚拟表,只能读不能写;自动包含新匹配上的表。
对比表:
| 维度 | Distributed | Merge |
|---|---|---|
| 跨机 | ✅ | ❌ |
| 跨表 | 通过下层表 | ✅ |
| 写入 | ✅(路由) | ❌ |
| 自身存数据 | ❌ | ❌ |
| 必备依赖 | 集群配置 + Keeper(如带副本) | 仅同库下表名正则 |
加分项:能讲"Distributed 上层套 Merge / Merge 上层套 Distributed"的高级模式。
Q4:为什么"ClickHouse 接 Kafka"必须要用 Kafka 引擎 + MV + MergeTree 三件套?
考察点:理解 Kafka 引擎"消费一次"的语义。
标准答案:
Engine = Kafka表本质上是个消费指针,不存数据;SELECT一次就把对应 offset 的数据消费走了——再 SELECT 就拿不到。- 所以业务查询永远不能直接打到 Kafka 引擎表。
- 标准做法:在 Kafka 引擎表上挂
MaterializedView,MV 把每条新数据"另存一份"到下游MergeTree落地表;业务永远查 MergeTree。
Kafka(broker) → Engine=Kafka(指针表) ─触发─▶ MaterializedView ─INSERT─▶ MergeTree(永久表) ← 业务查这里加分项:
- 能讲到
kafka_num_consumers与kafka_thread_per_consumer的关系; - 能讲到一个 Kafka 表上可以挂多个 MV实现"同份数据多种落地"。
易错点:以为 Kafka 引擎表能像普通表一样反复 SELECT。
Q5:生产环境为什么单节点也建议用 ReplicatedMergeTree?
考察点:Replicated* 不止"为副本",更是"为可靠性"的表。
标准答案:
ReplicatedMergeTree通过 ClickHouse Keeper(或 ZooKeeper)实现写入幂等——重复 INSERT 同一批数据会被去重(基于块哈希),这是普通 MergeTree 不具备的。OPTIMIZE/ALTER/MUTATION在 Replicated 引擎下都是分布式执行,行为一致、可恢复。- 单节点先用 Replicated,未来扩成多副本就是改一个 ON CLUSTER,不必重建表。
- Keeper 即便单节点也能跑(standalone 模式),开销可忽略。
加分项:能讲块哈希去重窗口(replicated_deduplication_window,默认 100),即"最近 100 个 block 内的重复才被去重"。
易错点:以为单节点就只能用普通 MergeTree。
📌 下一章预告:第 5 章我们把
MergeTree这台"发动机"拆开 —— Part 目录长什么样、primary.idx怎么稀疏、Mark 怎么跳 Granule、后台 Merge 怎么合并。这是整本教程最重要的一章。
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
python
"""
第 4 章 表引擎全景图 · Python 演示
依赖:
pip install clickhouse-connect
运行:
python engine_play.py
它会:
1. 连接本地 ClickHouse(127.0.0.1:8123, default 用户,无密码)
2. 在 learn_ck 库里依次建/插/查 5 种引擎的样表
3. 演示 Memory / File / URL / MergeTree / Merge 五种引擎的差异
如需重置环境:
DROP DATABASE IF EXISTS learn_ck;
"""
from __future__ import annotations
import sys
import textwrap
from typing import Iterable
import clickhouse_connect
HOST = "127.0.0.1"
PORT = 8123
USER = "default"
PASSWORD = ""
DATABASE = "learn_ck"
def banner(title: str) -> None:
print()
print("=" * 70)
print(f" {title}")
print("=" * 70)
def show(client, sql: str) -> None:
print(textwrap.dedent(f">>> {sql}").strip())
try:
result = client.query(sql)
rows = result.result_rows
cols = result.column_names
if rows:
widths = [
max(len(str(c)), max((len(str(r[i])) for r in rows), default=0))
for i, c in enumerate(cols)
]
sep = "+".join("-" * (w + 2) for w in widths)
print(sep)
print("|" + "|".join(f" {c:<{w}} " for c, w in zip(cols, widths)) + "|")
print(sep)
for r in rows:
print(
"|"
+ "|".join(f" {str(v):<{w}} " for v, w in zip(r, widths))
+ "|"
)
print(sep)
else:
print("(no rows)")
except Exception as e:
print(f"!! error: {e}")
def execute(client, sqls: Iterable[str]) -> None:
for sql in sqls:
sql = sql.strip()
if not sql:
continue
try:
client.command(sql)
print(f"OK {sql.splitlines()[0][:80]} ...")
except Exception as e:
print(f"!! {sql.splitlines()[0][:80]} -> {e}")
def main() -> int:
client = clickhouse_connect.get_client(
host=HOST, port=PORT, username=USER, password=PASSWORD
)
print(f"Connected to ClickHouse at {HOST}:{PORT} as {USER}")
banner("0. 准备数据库 learn_ck")
execute(client, [f"CREATE DATABASE IF NOT EXISTS {DATABASE}"])
client.database = DATABASE
banner("1. Memory 引擎:内存里的临时表")
execute(
client,
[
"DROP TABLE IF EXISTS demo_memory",
"""
CREATE TABLE demo_memory
(
id UInt32,
name String,
ts DateTime DEFAULT now()
) ENGINE = Memory
""",
"INSERT INTO demo_memory(id, name) VALUES "
"(1, 'Alice'), (2, 'Bob'), (3, 'Carol')",
],
)
show(client, "SELECT engine FROM system.tables WHERE name = 'demo_memory'")
show(client, "SELECT * FROM demo_memory ORDER BY id")
banner("2. URL 引擎:远程 JSON 文件即表(需要外网)")
execute(
client,
[
"DROP TABLE IF EXISTS demo_url",
"""
CREATE TABLE demo_url
(
id UInt32,
name String,
email String
) ENGINE = URL('https://jsonplaceholder.typicode.com/users',
JSONEachRow)
""",
],
)
show(client, "SELECT id, name, email FROM demo_url ORDER BY id LIMIT 5")
banner("3. MergeTree 主表 + 两张子表")
execute(
client,
[
"DROP TABLE IF EXISTS demo_mt",
"DROP TABLE IF EXISTS demo_mt_2024_01",
"DROP TABLE IF EXISTS demo_mt_2024_02",
"""
CREATE TABLE demo_mt
(
event_date Date,
user_id UInt64,
event_name LowCardinality(String),
revenue Decimal(18, 2)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_date)
""",
"CREATE TABLE demo_mt_2024_01 AS demo_mt ENGINE = MergeTree "
"PARTITION BY toYYYYMM(event_date) ORDER BY (user_id, event_date)",
"CREATE TABLE demo_mt_2024_02 AS demo_mt ENGINE = MergeTree "
"PARTITION BY toYYYYMM(event_date) ORDER BY (user_id, event_date)",
"INSERT INTO demo_mt_2024_01 VALUES "
"('2024-01-01',1001,'click',1.50),"
"('2024-01-02',1002,'purchase',99.00),"
"('2024-01-03',1001,'click',0.80)",
"INSERT INTO demo_mt_2024_02 VALUES "
"('2024-02-01',1003,'click',2.10),"
"('2024-02-02',1001,'purchase',49.99)",
],
)
show(client, "SELECT name, engine FROM system.tables "
"WHERE name LIKE 'demo_mt%' ORDER BY name")
banner("4. Merge 引擎:把两张子表「叠」成一张虚拟表")
execute(
client,
[
"DROP TABLE IF EXISTS demo_merge",
"CREATE TABLE demo_merge AS demo_mt "
"ENGINE = Merge(learn_ck, '^demo_mt_2024_')",
],
)
show(client, "SELECT count() AS rows FROM demo_merge")
show(
client,
"SELECT _table, count() FROM demo_merge "
"GROUP BY _table ORDER BY _table",
)
banner("5. Null 引擎:黑洞表(写进去就丢,可触发 MV)")
execute(
client,
[
"DROP TABLE IF EXISTS demo_null",
"""
CREATE TABLE demo_null
(
user_id UInt64,
event String,
ts DateTime
) ENGINE = Null
""",
"INSERT INTO demo_null VALUES (1, 'foo', now()), (2, 'bar', now())",
],
)
show(client, "SELECT count() AS still_zero FROM demo_null")
banner("6. 全表汇总:看看本库都有哪些引擎")
show(
client,
"SELECT name, engine FROM system.tables "
f"WHERE database = '{DATABASE}' ORDER BY name",
)
print("\nAll done. Try `clickhouse-client --query \"SHOW TABLES FROM learn_ck\"`.")
return 0
if __name__ == "__main__":
sys.exit(main())