返回首页

特殊ClickHouse引擎:何时不需要MergeTree

本文描述了在标准MergeTree不适用时使用的特殊ClickHouse引擎:内存引擎用于临时数据和缓存,缓冲区引擎用于缓冲高频插入(防止Too many parts错误),空引擎用于通过物化视图组织流处理,日志系列引擎用于小型参考表,URL/文件/S3引擎用于外部数据,PostgreSQL引擎用于实时访问。还介绍了Kafka → 缓冲区 → MergeTree架构模式,适用于每秒1万+事件。

特殊ClickHouse引擎:内存、缓冲区、空及其他
Advertisement 728x90

特殊ClickHouse引擎:当MergeTree不适用时

1. Memory引擎——用于临时数据的RAM表

想象一下,你需要快速处理一批投注——分组、计算中间汇总,然后发送到主表。你不想写入磁盘,因为数据是临时的,仅在查询期间需要。

Memory引擎将数据完全存储在RAM中。它是最快的引擎——没有磁盘操作,没有压缩,没有索引(除了主键)。但代价是:当ClickHouse重启时,表会变空。数据不会持久化。

何时使用:

Google AdInline article slot
  • 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;

注意事项:

Google AdInline article slot
  • Memory表不支持合并——如果你执行大量UPDATE(通过插入取消),内存会膨胀。使用TRUNCATE来清除。
  • ClickHouse重启时,数据丢失。不要在这里存储任何关键数据。
  • 表大小受可用RAM限制。如果表增长到50GB,而服务器只有64GB RAM,服务器会崩溃。

类比: Memory引擎就像一块白板。写入快,读取快,但清洁工(重启)之后,白板就空了。

2. Buffer引擎——写入前缓冲INSERT

你每秒有10,000个投注。每个投注都是一个单独的INSERT。如果直接将每个投注写入MergeTree表,ClickHouse会创建成千上万个微小分区,拖慢后台合并,降低性能。

Buffer引擎解决了这个问题:它将插入操作收集到内存缓冲区中,并在满足条件(按行数、大小或时间)时,将数据批量写入目标表。

Google AdInline article slot

带参数的语法:

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,立即刷新

实际工作方式:

  1. 你向bets_buffer插入数据(快速,仅写入内存)。
  2. ClickHouse等待直到累积足够的数据(例如,10万行或30秒过去)。
  3. 然后它异步(在后台)将批次刷新到主bets表(MergeTree)。
  4. 结果,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 DELETEALTER 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;

数据流:

  1. Kafka Connect将消息插入bets_kafka(Null引擎)。
  2. bets_kafka_mv(物化视图)将数据重定向到bets_buffer
  3. bets_buffer在内存中累积批次(例如,10万行或60秒)。
  4. 缓冲区将数据以大分区形式刷新到bets(MergeTree)。
  5. 同时,第二个物化视图为仪表板构建小时级聚合。

为什么这是最优的:

  • 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中的物化视图:增量处理的强大力量

— Editorial Team

Advertisement 728x90

继续阅读