特殊ClickHouse引擎:当MergeTree不适用时
1. Memory引擎——用于临时数据的RAM表
想象一下,你需要快速处理一批投注——分组、计算中间汇总,然后发送到主表。你不想写入磁盘,因为数据是临时的,仅在查询期间需要。
Memory引擎将数据完全存储在RAM中。它是最快的引擎——没有磁盘操作,没有压缩,没有索引(除了主键)。但代价是:当ClickHouse重启时,表会变空。数据不会持久化。
何时使用:
- ETL过程的临时表。例如,从Kafka加载了一百万条投注,去重后,再插入到主MergeTree表中。
- 实时赔率的缓存——赔率每秒都在变化,无需存储历史,只需要当前快照。
- 小型查找表(最多1000-1500万行),每次脚本运行时重新创建。
实时赔率缓存示例:
-- 当前赔率表(存在于RAM中)
CREATE TABLE live_odds_cache
(
event_id UInt64, -- 赛事ID
market_id UInt32, -- 市场ID
selection_id UInt32, -- 选项ID
odds Decimal(10,3), -- 赔率
updated_at DateTime
)
ENGINE = Memory()
ORDER BY (event_id, market_id, selection_id); -- ORDER BY是必需的,但索引效率低
插入数据(例如,从流中):
-- 新赔率到达,插入
INSERT INTO live_odds_cache VALUES (100500, 10, 200, 1.85, now());
-- 读取投注的当前赔率
SELECT odds FROM live_odds_cache
WHERE event_id = 100500 AND market_id = 10 AND selection_id = 200;
注意事项:
- Memory表不支持合并——如果你执行大量UPDATE(通过插入取消),内存会膨胀。使用
TRUNCATE来清除。 - ClickHouse重启时,数据丢失。不要在这里存储任何关键数据。
- 表大小受可用RAM限制。如果表增长到50GB,而服务器只有64GB RAM,服务器会崩溃。
类比: Memory引擎就像一块白板。写入快,读取快,但清洁工(重启)之后,白板就空了。
2. Buffer引擎——写入前缓冲INSERT
你每秒有10,000个投注。每个投注都是一个单独的INSERT。如果直接将每个投注写入MergeTree表,ClickHouse会创建成千上万个微小分区,拖慢后台合并,降低性能。
Buffer引擎解决了这个问题:它将插入操作收集到内存缓冲区中,并在满足条件(按行数、大小或时间)时,将数据批量写入目标表。
带参数的语法:
CREATE TABLE bets_buffer AS bets -- 复制bets表的结构
ENGINE = Buffer(
'default', -- 目标表数据库名
'bets', -- 目标表名(数据将刷新到这里)
16, -- 并行刷新线程数
10, -- 最小延迟(秒)(min_time)
100, -- 最大延迟(秒)(max_time)
10000, -- 刷新最小行数
1000000, -- 刷新最大行数
10000000, -- 刷新最小字节数
100000000 -- 刷新最大字节数
);
刷新参数:
| 参数 | 值 | 含义 |
|---|---|---|
| min_time | 10秒 | 不早于10秒刷新 |
| max_time | 100秒 | 不晚于100秒刷新 |
| min_rows | 10,000 | 如果累积了1万行,可以刷新 |
| max_rows | 1,000,000 | 如果累积了100万行,立即刷新 |
| min_bytes | 10 MB | 如果累积了10 MB,可以刷新 |
| max_bytes | 100 MB | 如果累积了100 MB,立即刷新 |
实际工作方式:
- 你向
bets_buffer插入数据(快速,仅写入内存)。 - ClickHouse等待直到累积足够的数据(例如,10万行或30秒过去)。
- 然后它异步(在后台)将批次刷新到主
bets表(MergeTree)。 - 结果,
bets接收到大的分区(10万行),加速后台合并。
为什么不直接插入MergeTree? 每次向MergeTree插入都会创建一个微分区。如果你每秒执行10,000次INSERT,一分钟后就会有60万个分区。后台合并跟不上。ClickHouse会抱怨Too many parts,插入会变慢。
注意事项: ClickHouse重启时,缓冲区丢失。尚未刷新到bets的数据会消失。因此,只有在可以接受丢失几秒数据的情况下(例如,用于分析,而不是余额)才使用Buffer引擎。
3. Null引擎——数据黑洞
Null引擎只是吸收数据。它不写入任何地方,不存储,不索引。但有一个技巧:如果带有Null引擎的表有物化视图,这些视图会接收数据并进行处理。
模式:Kafka → Null + 物化视图 → MergeTree
这是高负载从Kafka插入的经典架构。
-- 步骤1:接收表(黑洞)
CREATE TABLE bets_null
(
user_id UInt64,
amount Decimal(18,2),
created_at DateTime
)
ENGINE = Null; -- 不存储任何东西
-- 步骤2:目标表(实际保存数据)
CREATE TABLE bets
(
user_id UInt64,
amount Decimal(18,2),
created_at DateTime
)
ENGINE = MergeTree()
ORDER BY (created_at, user_id);
-- 步骤3:物化视图(桥梁)
CREATE MATERIALIZED VIEW bets_mv TO bets AS
SELECT * FROM bets_null; -- 任何进入bets_null的数据最终都会进入bets
现在发生了什么:
-- 客户端(或Kafka消费者)向bets_null插入数据
INSERT INTO bets_null VALUES (123, 100.00, now()); -- 即时
-- 数据通过物化视图并保存在bets中
-- 它不会保存在bets_null本身中
为什么这样做?
bets_null是一个非常轻量级的表;它不会在磁盘上创建文件。- 所有订阅者(物化视图)同时接收数据。
- 你可以将一个Null表附加多个物化视图:一个用于MergeTree中的原始数据,另一个用于AggregatingMergeTree中的聚合,另一个用于ReplacingMergeTree中的去重。
类比: Null引擎就像一个底部有洞的邮箱。信件掉进去但不会停留。但你所有的秘书(物化视图)都能读到它们并将它们复制到各自的文件夹中。
4. Log/TinyLog/StripeLog——用于小数据的简单引擎
这类引擎适用于小表(最多100-200万行),不需要高性能和索引。
| 引擎 | 特性 | 何时使用 |
|---|---|---|
TinyLog |
每列一个文件 | 非常小的表(<10万行),临时用途 |
Log |
每列一个单独文件,有并行读取标记 | 最多100万行的表,需要快速读取 |
StripeLog |
所有列在一个文件中(紧凑) | 节省空间,不频繁读取 |
联赛查找表示例(200行):
-- 足球联赛查找表(每月更改一次)
CREATE TABLE leagues_ref
(
league_id UInt32,
name String,
country String,
updated_at Date
)
ENGINE = TinyLog(); -- 尽可能简单,没有ORDER BY
为什么不用MergeTree? MergeTree会创建索引、分区、压缩——对于200行来说过于复杂。TinyLog占用更少空间,维护更简单。
注意事项: 这些引擎不支持ALTER DELETE和ALTER UPDATE。如果需要修改数据,必须重建表。
5. URL引擎——表作为HTTP端点
URL引擎允许直接从HTTP源(API)读取数据,甚至可以通过PUT插入数据。
CREATE TABLE currency_rates_url
(
base String,
rate Decimal(10,4),
date Date
)
ENGINE = URL('https://api.exchangerate.com/latest?base=USD', CSV)
SETTINGS
method = 'GET',
format = 'CSV',
headers = 'Authorization: Bearer token123';
用法:
-- 直接从API读取当前汇率
SELECT * FROM currency_rates_url;
实际场景: 一个小的分析任务,你不想设置ETL。例如,每小时从免费API读取汇率,与投注关联,重新计算金额。
注意事项:
- 没有索引;每个查询都会对源进行全扫描。
- 如果API返回错误,查询失败。
- 不适合高负载查询(假设数据缓存在ClickHouse内部,而不是每次都从API读取)。
6. File引擎——表作为磁盘上的文件
允许读取和写入ClickHouse服务器本地文件系统中的文件。支持CSV、TSV、JSONEachRow、Parquet等格式。
-- 读取CSV文件的表
CREATE TABLE imported_players
(
user_id UInt64,
username String
)
ENGINE = File(CSV, '/var/lib/clickhouse/user_files/players.csv');
何时使用:
- 从文件加载数据(管理员放置了包含新用户的CSV文件)。
- 通过
INSERT INTO ... SELECT将查询结果导出到文件。
注意事项: ClickHouse必须有权访问该文件夹(为了安全,通常是/var/lib/clickhouse/user_files/)。
7. S3引擎——直接查询S3
直接从Amazon S3存储桶(或MinIO、Yandex Object Storage)读取数据。不将数据复制到ClickHouse中。
CREATE TABLE logs_s3
(
timestamp DateTime,
message String
)
ENGINE = S3(
'https://mybucket.s3.amazonaws.com/logs/*.parquet',
'AWS_ACCESS_KEY', 'AWS_SECRET_KEY',
'Parquet'
);
何时使用:
- 你在S3中有数TB的日志,想要偶尔运行分析查询,而不复制到ClickHouse中。
- 冷数据(S3比ClickHouse磁盘便宜)。
注意事项: 每个查询都会从S3下载数据,可能很慢且昂贵(对于出站流量)。仅适用于不频繁的查询。
8. PostgreSQL引擎——来自PostgreSQL的实时数据
PostgreSQL引擎允许像操作ClickHouse表一样读取和写入PostgreSQL表。
CREATE TABLE pg_players
(
user_id UInt64,
balance Decimal(18,2)
)
ENGINE = PostgreSQL(
'postgres-host:5432', -- 主机和端口
'betting', -- 数据库
'players', -- PostgreSQL中的表
'clickhouse_user', -- 用户
'password' -- 密码
);
用法:
-- 从PostgreSQL读取当前余额
SELECT * FROM pg_players WHERE user_id = 123;
-- 甚至可以与ClickHouse表进行JOIN
SELECT b.user_id, b.amount, p.balance
FROM bets b
JOIN pg_players p ON b.user_id = p.user_id;
何时使用:
- 你正在从PostgreSQL逐步迁移到ClickHouse,一些数据仍然留在旧数据库中。
- 你需要由外部应用程序更新的实时数据,并且不想设置ETL。
注意事项:
- 每个查询都会访问PostgreSQL,对于大量数据可能很慢。
- ClickHouse无法构建涉及此类表的高效执行计划(没有下推)。
9. 投注场景的架构模式:Kafka → Buffer → MergeTree
现在让我们把所有内容整合起来。想象一下,你从Kafka每秒接收10,000条以上的投注事件消息。你需要以最小延迟将它们保存到ClickHouse,同时不创建成千上万个微小分区。
现成架构:
-- 1. Kafka接收表(Null)
CREATE TABLE bets_kafka
(
user_id UInt64,
event_id UInt64,
amount Decimal(18,2),
bet_time DateTime
)
ENGINE = Null;
-- 2. 目标MergeTree表
CREATE TABLE bets
(
user_id UInt64,
event_id UInt64,
amount Decimal(18,2),
bet_time DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(bet_time)
ORDER BY (bet_time, user_id);
-- 3. 缓冲表以平滑插入
CREATE TABLE bets_buffer AS bets
ENGINE = Buffer('default', 'bets', 16, 5, 60, 10000, 1000000, 10000000, 100000000);
-- 4. 物化视图:Kafka → Null → Buffer(通过视图)
CREATE MATERIALIZED VIEW bets_kafka_mv TO bets_buffer AS
SELECT * FROM bets_kafka;
-- 5. 另一个用于实时聚合的视图(可选)
CREATE MATERIALIZED VIEW bets_stats_mv TO bets_hourly_agg AS
SELECT
toStartOfHour(bet_time) AS hour,
countState() AS bet_count,
sumState(amount) AS total_amount
FROM bets_kafka
GROUP BY hour;
数据流:
- Kafka Connect将消息插入
bets_kafka(Null引擎)。 bets_kafka_mv(物化视图)将数据重定向到bets_buffer。bets_buffer在内存中累积批次(例如,10万行或60秒)。- 缓冲区将数据以大分区形式刷新到
bets(MergeTree)。 - 同时,第二个物化视图为仪表板构建小时级聚合。
为什么这是最优的:
- Kafka写入Null(即时,无开销)。
- Buffer防止成千上万个微小分区激增。
- MergeTree接收大分区,合并工作高效。
- 通过第二个视图实时构建聚合。
如果直接从Kafka写入MergeTree会发生什么: 每秒1万事件,一分钟内创建60万个分区。ClickHouse因Too many parts崩溃。Buffer引擎可以避免这种情况。
何时选择哪个引擎——速查表
| 任务 | 引擎 | 原因 |
|---|---|---|
| 持久存储,分析 | MergeTree(或*MergeTree) | ClickHouse的基础,索引,压缩 |
| 缓冲高负载 | Buffer | 将小INSERT粘合成大分区 |
| 临时数据(会话,临时) | Memory | 最大速度,数据不关键 |
| Kafka消费者无需存储 | Null + MV | 数据仅用于视图 |
| 小型静态查找表 | TinyLog / Log | 简单,元数据少 |
| 连接PostgreSQL | PostgreSQL ENGINE | 无需ETL的实时数据 |
| 对S3的罕见查询 | S3 ENGINE | 廉价冷存储 |
| 从文件导入 | File ENGINE | 一次性复制 |
下一步
你已经了解了解决常规MergeTree无法解决的问题的特殊引擎。现在你知道:
- Memory用于缓存和临时数据,
- Buffer用于防止过于频繁的INSERT,
- Null用于通过MV将数据“分支”到多个流,
- URL / File / S3 / PostgreSQL用于外部数据。
接下来可以深入的主题:
- 如何设置Kafka引擎——无需单独连接器的内置Kafka连接器。
- 高级物化视图——用于复杂ETL的视图链。
- 分布式表——如何在服务器间分片数据。
总结: 并非所有ClickHouse任务都通过MergeTree解决。有时你需要Buffer来避免用插入压垮服务器,有时需要Null + MV来将数据分发到不同的聚合,有时需要Memory进行临时哈希。主要规则:先设计数据流,再选择引擎,而不是相反。
← 上一篇: ClickHouse中的字典:无需JOIN的快速查找
→ 下一篇: ClickHouse中的物化视图:增量处理的强大力量
— Editorial Team
暂无评论。