Skip to content

第 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 / MyISAMClickHouse 引擎
存储位置永远在本地磁盘可以本地、可以远端 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稀疏MutationMV 预聚合必须配 *State/*Merge 函数
CollapsingMergeTree稀疏Mutation行折叠必须保证 +1/-1 同主键
Replicated*稀疏✅ KeeperMutation生产标配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

支持的 FormatCSV / 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 = 1str 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 InnoDBMySQL MyISAMPG HeapClickHouse 引擎
引擎数量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 ALLUNION ALLUNION ALLMerge 引擎自动维护
分布式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。

标准答案

  1. 决策权差异:MySQL 引擎只决定"数据怎么落盘";CH 引擎额外决定是否落盘、是否支持索引、是否支持复制、是否支持更新、合并策略、查询路由
  2. 粒度差异:MySQL 同一个库通常都是 InnoDB;CH 一个库内 MergeTree、Kafka、MySQL、Distributed 共存是常态
  3. 存储位置差异:MySQL 引擎数据永远在本机;CH 的 MySQL/PostgreSQL/Kafka/S3/URL 等引擎数据根本不在 CH 这边
  4. 复制实现: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)单机内把多张同结构的表"叠"成一张虚拟表,只能读不能写;自动包含新匹配上的表。

对比表

维度DistributedMerge
跨机
跨表通过下层表
写入✅(路由)
自身存数据
必备依赖集群配置 + Keeper(如带副本)仅同库下表名正则

加分项:能讲"Distributed 上层套 Merge / Merge 上层套 Distributed"的高级模式。


Q4:为什么"ClickHouse 接 Kafka"必须要用 Kafka 引擎 + MV + MergeTree 三件套?

考察点:理解 Kafka 引擎"消费一次"的语义。

标准答案

  1. Engine = Kafka 表本质上是个消费指针,不存数据;SELECT 一次就把对应 offset 的数据消费走了——再 SELECT 就拿不到。
  2. 所以业务查询永远不能直接打到 Kafka 引擎表。
  3. 标准做法:在 Kafka 引擎表上挂 MaterializedView,MV 把每条新数据"另存一份"到下游 MergeTree 落地表;业务永远查 MergeTree。
Kafka(broker) → Engine=Kafka(指针表) ─触发─▶ MaterializedView ─INSERT─▶ MergeTree(永久表) ← 业务查这里

加分项

  • 能讲到 kafka_num_consumerskafka_thread_per_consumer 的关系;
  • 能讲到一个 Kafka 表上可以挂多个 MV实现"同份数据多种落地"。

易错点:以为 Kafka 引擎表能像普通表一样反复 SELECT。


Q5:生产环境为什么单节点也建议用 ReplicatedMergeTree

考察点Replicated* 不止"为副本",更是"为可靠性"的表。

标准答案

  1. ReplicatedMergeTree 通过 ClickHouse Keeper(或 ZooKeeper)实现写入幂等——重复 INSERT 同一批数据会被去重(基于块哈希),这是普通 MergeTree 不具备的。
  2. OPTIMIZE / ALTER / MUTATION 在 Replicated 引擎下都是分布式执行,行为一致、可恢复。
  3. 单节点先用 Replicated,未来扩成多副本就是改一个 ON CLUSTER,不必重建表。
  4. 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())

engine_play.py ↗