MergeTree 家族全集

共 17 题
📑 题目列表 17 题
#
★★★

1. MergeTree 主引擎支持主键排序与稀疏索引

请解释 ClickHouse 最基础的 MergeTree 表引擎如何利用主键排序与稀疏索引来加速查询,并说明其索引的组织方式?

  • MergeTree 是 ClickHouse 的基石引擎,理解其主键(ORDER BY)与稀疏索引机制
  • 稀疏索引与稠密索引的区别,以及为何适合列存分析场景
  • 主键排序如何影响数据写入与查询裁剪

MergeTree 是 ClickHouse 最核心的存储引擎,它通过 ORDER BY 指定排序键,数据在写入时按该键排序后存储。主键索引采用「稀疏索引」方式:每 8192 行(默认 index_granularity)生成一个索引标记(mark),索引文件只记录每个 granule 的第一行主键值,而非每一行的索引。查询时根据主键条件通过二分查找确定命中范围,再通过 mark 定位到对应的 part 文件区间进行读取。由于是稀疏索引,合并两个有序数据流后仍保持有序,因此即使频繁插入,每个 part 内部也是有序的。

稀疏索引在分析型工作负载下极具优势:插入时只需维护有序,无需为每一行建立索引,极大降低写放大;而分析查询通常扫描大量数据,稀疏索引配合分区裁剪与列裁剪能过滤掉大量无关数据。相比 MySQL 的 B+ 树稠密索引,稀疏索引更偏向「排序 + 前缀裁剪」的读取模型,牺牲了点查的定位精度,换取了高吞吐顺序扫描。

CREATE TABLE events (
    event_date Date,
    user_id UInt64,
    event_type String
) ENGINE = MergeTree
ORDER BY (event_date, user_id)
SETTINGS index_granularity = 8192;
#
★★★

2. ReplacingMergeTree 支持按 ORDER BY 列去重,final 阶段合并版本

请说明 ReplacingMergeTree 如何实现按 ORDER BY 列去重,以及 final 关键字在查询与合并中的作用?

  • ReplacingMergeTree 的版本合并与去重时机
  • final 查询修饰符的作用与代价
  • 去重是「尽力而为」的最终一致,而非实时保证

ReplacingMergeTree 会在后台合并 part 时,对于具有相同排序键的多行数据,仅保留其中一行(默认保留最新版本)。合并是异步的,因此去重并非实时生效,只保证在合并完成后最终一致。查询时可通过 SELECT ... FROM t FINAL 强制在读取阶段对内存中的行做去重,从而无需等待合并即可看到去重后的结果,但 FINAL 会显著增加查询开销,因为需要在查询期做排序与去重。

ReplacingMergeTree 适合「同一条业务记录被多次更新」的场景,如订单状态、用户资料等。其核心是「以排序键视为业务主键,以版本号或插入顺序决定谁生效」。需要注意:如果两次写入的排序键相同但业务上其实不同,则会被误合并,因此排序键必须能唯一标识业务实体。最终一致性与实时性之间的权衡,决定了实际使用中是否配合 FINAL 或定期 OPTIMIZE。

CREATE TABLE orders (
    order_id UInt64,
    status String,
    updated_at DateTime
) ENGINE = ReplacingMergeTree(updated_at)
ORDER BY order_id;

-- 查询时强制去重
SELECT * FROM orders FINAL;
#
★★★

3. ReplacingMergeTree 的 is_deleted 列、final 查询与合并时机对去重效果的影响

请分析 ReplacingMergeTree 中 is_deleted 列、FINAL 查询以及合并时机对去重效果的具体影响?

  • is_deleted 列如何标记逻辑删除并参与去重
  • 合并时机与异步性对去重可见性的影响
  • FINAL 查询与合并去重的差异

ReplacingMergeTree 在合并时,对同一排序键的多行,若某行带 is_deleted 标记(需要设置 use_is_deleted 参数),则该行会被作为删除标记处理,从而支持逻辑删除。FINAL 查询则是在查询阶段对内存结果做去重合并,不依赖后台合并是否发生,能保证读取时看到最新状态,但代价是更高的 CPU 与内存开销。合并时机(何时、哪些 part 被合并)由系统后台调度决定,具有很强的不确定性,因此去重的可见性本质上是最终一致。

实际工程中,若业务对「删除后立即不可见」有强需求,应使用 FINAL 查询而非依赖后台合并;若可接受延迟,则依赖合并即可。is_deleted 需谨慎使用,因为删除标记本身也是一行数据,若后续又插入新行,需保证版本递增才能正确覆盖删除标记。合并的异步性决定了「最终一致」窗口存在,这是使用 ReplacingMergeTree 必须接受的语义。

#
★★

4. SummingMergeTree 在合并时对数值列求和,适合指标聚合

请说明 SummingMergeTree 在后台合并时对数值列进行求和的机制,以及其适合的典型场景?

  • 合并时对排序键相同行的数值列求和
  • 预聚合思想与实时明细的取舍
  • 非数值列的处理方式

SummingMergeTree 在后台合并 part 时,将排序键相同的多行对其进行数值列求和,从而把多条明细合并为一条聚合结果。适合「累加类指标」场景,如按用户、时间、商品维度累计的访问量、金额、次数等。非数值列(如 String)在合并时会保留排序键相同行中的第一行,因此不能依赖求和语义。它不保证查询时已合并,因此读取时通常配合同样求和语义的 SUM 聚合,或配合 FINAL。

SummingMergeTree 本质是「写时预聚合」:通过合并把数据量压缩,减少存储与扫描成本。但它不保证实时合并,且求和是一种不可逆的压缩,若后续需要撤回或修改历史值则不适合。它与常规聚合表不同,因为它在存储层做预聚合,查询层仍可做更深层聚合,两者叠加可大幅降低分析成本。

CREATE TABLE metrics (
    day Date,
    dim String,
    value UInt64
) ENGINE = SummingMergeTree(value)
ORDER BY (day, dim);
#
★★

5. AggregatingMergeTree 存储 -State 函数预聚合结果

请解释 AggregatingMergeTree 如何存储聚合函数(-State 后缀)的预聚合中间状态,并说明其查询方式?

  • -State 聚合函数生成中间状态对象
  • 查询时用 -Merge 还原聚合结果
  • 与 SummingMergeTree 的差异

AggregatingMergeTree 实际上是聚合的通用版本,它允许在存储层预聚合任意聚合函数(如 sum、avg、count、uniq、quantile 等)的中间状态。写入时使用带 -State 后缀的聚合函数(如 sumState、uniqState)生成状态对象存入列,查询时用对应的 -Merge-MergeState 后缀函数把状态还原为最终结果。它支持任意组合聚合,比 SummingMergeTree 更通用。

典型用法是配合物化视图或聚合中间表:上游把明细数据逐条转换为聚合状态写入 AggregatingMergeTree,合并时状态对象会继续被合并,从而在存储层完成预聚合。查询通过 avgMerge(s) 等函数还原。这种设计把「聚合下推」到存储层,极大减少存储与扫描量,适合需要多种聚合的明细实时汇总场景。

CREATE TABLE agg_table (
    day Date,
    key UInt64,
    state AggregateFunction(uniq, UInt64)
) ENGINE = AggregatingMergeTree
ORDER BY (day, key);

INSERT INTO agg_table
SELECT day, key, uniqState(user_id) FROM raw GROUP BY day, key;

SELECT day, uniqMerge(state) FROM agg_table GROUP BY day;
#
★★

6. CollapsingMergeTree 通过 sign 列标记删除/插入

请说明 CollapsingMergeTree 如何通过 sign 列标记行的插入与删除,以及 sign 配对在合并时的折叠逻辑?

  • sign 列取 1 表示插入、-1 表示删除
  • 合并时对 sign 成对的行折叠消失
  • 查询时需用 SUM(sign) 与 sum 谓词配合

CollapsingMergeTree 通过一个显式的 sign 列(取 1 表示有效行,-1 表示删除标记)来标记行的增删。当业务更新某行时,先插入 sign=-1 的反向行,再插入 sign=1 的新行;后台合并时,若同一排序键下有成对的 sign=1 和 sign=-1 行,则二者被折叠删除。查询时需用 SUM(sign) 过滤已删除行,并配合 sum(x * sign) 把被删行的数值抵消。

该机制适合「状态频繁变更、需要按状态实时查询」的场景,如订单状态、库存。它通过成对反向行实现「伪删除」,避免物理删除带来的重写开销。但折叠依赖合并与成对顺序,若 sign 配平不完整(如两个 1 一个 -1)则折叠可能不彻底,查询时用 SUM(sign) 过滤是必要保障。相比 ReplacingMergeTree,它更适合需要用明确正负号表达增量变化的场景。

CREATE TABLE t (
    id UInt64,
    amount UInt64,
    sign Int8
) ENGINE = CollapsingMergeTree(sign)
ORDER BY id;

-- 查询时按 sign 过滤
SELECT id, sum(amount * sign) FROM t
GROUP BY id HAVING sum(sign) > 0;
#
★★

7. VersionedCollapsingMergeTree 通过 version 列解决乱序 sign 冲突

请说明 VersionedCollapsingMergeTree 如何通过 version 列解决乱序写入导致的 sign 折叠冲突?

  • 在 CollapsingMergeTree 基础上引入 version 列
  • 乱序写入时按 version 正确配对折叠
  • 排序键与 version 的配合

VersionedCollapsingMergeTree 在 CollapsingMergeTree 的基础上增加一个 version 列。折叠时不仅要求 sign 成对,还要求版本相同,从而避免乱序写入(不同批次到达顺序颠倒)时把不同版本的 sign 行错误配对折叠。它要求声明 ORDER BY (key, version),version 参与排序,保证同一 key 的不同版本能正确区分。

CollapsingMergeTree 在乱序场景下,可能把同 key 的 sign=1 与另一批次的 sign=-1 错误折叠,导致数据丢失。引入 version 后,折叠条件更严格,只有当 sign 与 version 都成对时才折叠,从而保证多版本并发写入的正确性。代价是排序键变长、存储与排序开销略增。它适合多源并发、乱序到达的更新场景。

CREATE TABLE t (
    id UInt64,
    version UInt64,
    amount UInt64,
    sign Int8
) ENGINE = VersionedCollapsingMergeTree(sign, version)
ORDER BY (id, version);
#
★★

8. Kafka 引擎通过 Kafka() 表函数消费主题,支持多消费者组

请说明 ClickHouse 的 Kafka 引擎表如何通过 Kafka() 表函数消费 Kafka 主题,以及多消费者组的支持情况?

  • Kafka 引擎表从指定主题消费消息
  • 消费基本单位是 block,配合物化视图落库
  • 多消费者组与并行消费

ClickHouse 的 Kafka 引擎表通过 Kafka() 表函数指定 broker、topic、group 等信息,创建一个只读表结构,用于持续消费 Kafka 消息。消息被拉取后以 block 形式进入 ClickHouse,通常配合物化视图把数据写入目标 MergeTree 表。多消费者组通过不同 group 名称实现,消费的数据会按 group 隔离,支持多个消费者组各自的消费进度。

Kafka 引擎表本身不持久化数据,只作为「消费通道」,真正的数据落在关联的目标表。因为消费是持续的、异步的,消息到达与落库之间存在延迟。多消费者组允许不同业务复用同一 topic 但各自独立消费,互不影响 offset。需要设置合理的 kafka_num_consumers 提升并行度,并关注消费失败时的数据丢失风险。

CREATE TABLE kafka_queue (
    key UInt64,
    value String
) ENGINE = Kafka()
SETTINGS
  kafka_broker_list = 'localhost:9092',
  kafka_topic_list = 'events',
  kafka_group_name = 'ch_group',
  kafka_format = 'JSONEachRow';
#
★★

9. GraphiteMergeTree 直接对接 Graphite 指标

请说明 GraphiteMergeTree 如何作为 Graphite 监控系统的专用存储引擎?

  • 面向 Graphite 时序指标优化
  • 支持 rollup 预聚合与数据保留策略
  • 与 Graphite 的路径/版本格式集成

GraphiteMergeTree 是 ClickHouse 为 Graphite 监控系统设计的专用引擎(基于 MergeTree 的变体)。它通过配置文件定义 rollup 规则,把不同时间粒度的指标数据按规则预聚合(如定时降采样),并支持数据保留策略(TTL)。它直接对接 Graphite 的指标格式,通常由 Graphite 的 carbon 接收器或 ClickHouse 的 carbon 接收器写入,供 Graphite 查询使用。

Graphite 场景的指标通常「多、小、高频」,GraphiteMergeTree 通过按时间与路径聚合,减少存储量并加速查询。它本质上把 Graphite 的 rollup 与保留策略下推到 ClickHouse 存储层,实现监控指标的高效落地。相比通用 MergeTree,它集成了 Graphite 特有的路径结构与版本语义,适合作为 Graphite 的存储后端。

#
★★

10. Materialized View 在 ClickHouse 中通过 SELECT...POPULATE 创建,结果写入目标表

请说明 ClickHouse 物化视图的创建方式,特别是 SELECT...POPULATE 与写入目标表的关系?

  • 物化视图在 INSERT 时触发,把查询结果写入目标表
  • POPULATE 立即回填历史数据
  • 物化视图是「触发器」而非存储表

ClickHouse 的物化视图本质上是一个「消费触发器」:当源表插入数据时,视图把满足查询定义的数据块写入对应的目标表(视图没有独立存储,存储落在目标 MergeTree 表)。创建时使用 SELECT ... POPULATE 会立即把源表历史数据回填到目标表,否则只对新插入数据生效。视图查询结果通过上面的 SELECT 语句定义,落库数据由查询结构决定。

物化视图常用于实时 ETL、预聚合、Topic 拆分等场景。POPULATE 与后续插入是「边写边查」的,若源表在 POPULATE 期间持续写入可能出现数据重复或遗漏,因此生产上通常先建视图再灌历史数据,或接受轻微不一致。物化视图的写入是同步的,会阻塞源表 INSERT,因此要控制目标表写入成本。

CREATE MATERIALIZED VIEW mv_daily
ENGINE = AggregatingMergeTree
ORDER BY day
POPULATE AS
SELECT day, sumState(value) AS s
FROM raw GROUP BY day;
#
★★

11. Kafka 引擎与 Materialized View 协同实现实时 ETL

请说明 Kafka 引擎表与物化视图如何协同实现实时 ETL 管道?

  • Kafka 表作为消费通道,物化视图作为转换处理器
  • 从 Kafka 表读取并写入目标表
  • 实时性与延迟特征

典型做法是:建一张 Kafka 引擎表作为「消费通道」,再建一个物化视图,其 TO 目标表,SELECT 从 Kafka 表读取并进行过滤、清洗、字段映射等转换,把结果写入目标 MergeTree 表。每插入一个 block 到 Kafka 表,物化视图就触发一次转换与落库,从而形成「Kafka → 转换 → 目标表」的实时 ETL 管道。

该架构把 Kafka 消费与业务落库解耦:Kafka 表只负责消费,物化视图负责转换与写库,目标表负责存储与查询。物化视图的同步写入意味着 Kafka 侧慢,目标表写入会受影响,因此目标表需选择合适的 MergeTree 变体并控制 part 生成。为保证实时性,通常需配置合适的 kafka_num_consumers 与消费批次。

CREATE TABLE kafka_raw (...);
CREATE TABLE target (...);

CREATE MATERIALIZED VIEW mv_etl TO target AS
SELECT key, transform(value) AS v FROM kafka_raw;
#
★★

12. Distributed 引擎跨分片查询,要求 local 表与 sharding_key 匹配

请说明 Distributed 引擎如何实现跨分片查询,以及 local 表与 sharding_key 的匹配要求?

  • Distributed 表作为逻辑层,把读写分发到本地表
  • 写入按 sharding_key 哈希分配分片
  • 查询跨分片并行并从各本地表聚合

Distributed 引擎本身不存储数据,它作为逻辑入口,把 INSERT 按 sharding_key 的哈希值分发到各分片的本地表,把 SELECT 广播到所有分片并行执行后聚合结果。因此需要每个分片都存在同结构的本地表,且 sharding_key 需与本地表的分区/排序键匹配,以保证数据在分片间尽可能均匀分布,并保证查询语义正确。

Distributed 表是 ClickHouse 集群水平扩展的核心。写入时 sharding_key 决定某行落到哪个分片,若用随机或均匀分布可避免数据倾斜;查询时后端把任务分发给各节点,再汇总。local 表与 sharding_key 匹配主要是保证分布均匀性与数据亲和性,避免查询时跨分片扫描过多数据。Distributed 表通常不承载存储,只做路由。

CREATE TABLE local_events (...);
CREATE TABLE events ENGINE = Distributed(cluster, default, local_events, rand());
#
★★

13. SummingMergeTree、AggregatingMergeTree 与 CollapsingMergeTree 的选型差异(预聚合 vs 增量 vs 取消)

请对比 SummingMergeTree、AggregatingMergeTree 与 CollapsingMergeTree 三个引擎的选型差异?

  • SummingMergeTree:简单求和预聚合
  • AggregatingMergeTree:任意聚合状态预聚合
  • CollapsingMergeTree:sign 增量更新与取消

SummingMergeTree 只对数值列求和,适合简单累加指标,实现简单但只能支持 sum。AggregatingMergeTree 通过 -State 存储任意聚合函数(uniq、quantile、avg 等)的中间状态,通用性强,但使用需配合 -State/-Merge 较复杂。CollapsingMergeTree 用 sign 列表达增删,适合需要「更新/取消」的场景(如状态变更、错误修正),通过折叠保证读取时反映最新状态。选型核心是:只需要求和用 Summing,需要多种聚合用 Aggregating,需要增量更新/取消用 Collapsing。

三者的本质都是「合并时预聚合」,但语义不同:Summing 是纯累加,Aggregating 是任意聚合状态,Collapsing 是正负抵消。实际中可组合(如 Aggregating 配 Collapsing 处理状态型预聚合)。选型需结合业务更新模式与查询需求,避免为简单求和引入过度复杂的 Aggregating 或 Collapsing。

#

14. MergeTree 的分区键(PARTITION BY 表达式)粒度如何影响 part 合并范围与分区裁剪?

请说明 PARTITION BY 表达式的粒度如何影响 part 合并范围与分区裁剪?

  • 分区粒度决定 part 管理范围
  • 分区裁剪高效过滤
  • 过细 vs 过粗分区的权衡

PARTITION BY 决定数据按什么范围划分到不同 part 目录。分区粒度影响两方面:一是分区裁剪,查询若带分区键等值条件,可直接跳过不相关分区,提高查询效率;二是 part 合并范围,合并只在同一分区内进行,分区越多,part 数量越多,合并调度压力越大。过细(如按小时)会导致大量小分区、part 过多、合并慢;过粗(如按年)则分区裁剪效果差。

分区是「数据组织」手段,不是索引。合理分区(如按天)在时间范围查询与数据生命周期管理(DROP PARTITION)上有优势,同时避免 part 过多。分区粒度需权衡裁剪效率与合并开销,通常按业务查询频率与数据保留粒度选择,如按天、按月。分区键应与常用查询条件匹配以最大化裁剪。

CREATE TABLE t (
  day Date, ...
) ENGINE = MergeTree
PARTITION BY toYYYYMM(day)
ORDER BY day;
#

15. Refreshable Materialized View 在 24.9+(24.10 生产可用)支持定期刷新,与增量物化视图的差异?

请说明 Refreshable Materialized View(24.9+)的定期刷新机制,以及与增量物化视图的差异?

  • Refreshable MV 按固定周期全量刷新
  • 增量 MV 基于 INSERT 触发实时更新
  • 适用场景差异

Refreshable Materialized View(普通物化视图的可刷新变体)在 24.9 引入、24.10 生产可用,它通过周期的 REFRESH 操作定期重新计算视图结果,适合数据源本身是周期更新的场景(如外部表、周期性导入的数据)。相比之下,增量物化视图(普通 MV)是基于源表 INSERT 事件实时触发更新,适合持续流式写入的场景。Refreshable MV 无需常量插入触发,而是定时刷新。

两者互补:增量 MV 实时但依赖插入事件,需要源表持续有数据插入;Refreshable MV 适合源头数据按批更新(如每日 ETL 灌入、外部数据源),用定期刷新确保结果新鲜。Refreshable MV 可配置刷新间隔与并发,适合对实时性要求不极致、但需要稳定周期汇总的场景。它在 24.9 前只能通过 TTL 或手动刷新近似实现。

#

16. MergeTree 的 TTL 与生命周期,数据过期与冷热分层?

请说明 MergeTree 的 TTL 机制如何实现数据过期与冷热分层?

  • TTL 删除过期数据
  • TTL 移动数据到冷存储(冷热分层)
  • TTL 的执行时机与异步性

MergeTree 支持 TTL 表达式,可基于时间列对数据设定过期时间:到期后数据被后台删除(DELETE),或通过 TTL TO VOLUME 将数据移动到冷存储卷(如 HDD、S3),实现冷热分层。TTL 在后台异步执行,由 merge 任务触发。TTL 也可作用于特定列或表级,用于控制数据生命周期。

TTL 是 ClickHouse 数据生命周期管理的关键。热数据(近期)放 SSD 快速访问,冷数据(历史)自动迁移到低成本存储,平衡性能与成本。TTL 删除是异步的,若数据量巨大,需注意后台任务负载与存储空间释放的延迟。TTL 需配合分区与存储策略使用,实现真正的分层。

CREATE TABLE t (
  day Date, v UInt64
) ENGINE = MergeTree
PARTITION BY day
ORDER BY day
TTL day + INTERVAL 90 DAY DELETE,
    day + INTERVAL 30 DAY TO VOLUME 'cold';
#

17. ClickHouse 表引擎家族(MergeTree/Log/Distributed/Buffer)的完整分类与使用场景

请对 ClickHouse 表引擎家族(MergeTree、Log、Distributed、Buffer 等)进行分类并说明使用场景?

  • MergeTree 家族:分析场景主力
  • Log 家族:临时/小数据量
  • Distributed/Buffer:逻辑层与缓冲

ClickHouse 表引擎可分为几类:一是 MergeTree 家族(MergeTree、ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree、ReplicatedMergeTree、Distributed 等),是分析场景的主力,支持主键、分区、TTL、复制;二是 Log 家族(Log、TinyLog、StripeLog),适合临时、小数据量、一次写入多次读取的场景,不支持索引与分区;三是外接引擎(Kafka、MySQL、ODBC、File 等),用于对接外部数据源;四是其他(Buffer、Distributed、Null、Memory 等),Buffer 用于缓冲写入,Distributed 用于逻辑分片,Memory 用于临时表。

选型核心是场景:需要分析、索引、分区、复制、TTL 用 MergeTree 家族;临时小数据或中间结果用 Log;需要对接外部系统用外接引擎;需要缓冲写入减少 part 用 Buffer;需要水平扩展用 Distributed。理解各引擎的存储与索引能力,才能正确选择。