围绕ClickHouse MergeTree系列展开深入探讨,从基本概念出发逐步深入到高级应用。内容涵盖数据库与存储系统、数据仓库与数据湖中的ClickHouse MergeTree系列原理、关键算法、性能特性及优化策略,结合实际案例分析常见问题与解决方案,为工程实践提供系统性指导,适合希望深入理解该领域的技术人员学习参考。

学习提示

需要学习ClickHouse MergeTree系列,涉及ClickHouse、MergeTree等核心概念。从基本概念出发逐步深入到高级应用。这一知识点在数据仓库与数据湖 > 数据仓库建模中承上启下,连接基础理论与工程实践。

目录
  1. 可视化演示
  2. AI 应用场景
  3. Python 演示
  4. 常见误区
  5. 面试题

可视化演示

状态—

围绕ClickHouse MergeTree系列展开深入探讨,从基本概念出发逐步深入到高级应用。内容涵盖数据库与存储系统、数据仓库与数据湖中的ClickHouse MergeTree系列原理、关键算法、性能特性及优化策略...

存储引擎基础

图书馆开门营业前,要先定规矩:新书一律送到导流台,由工作人员按分类捆成小包,再交给管理员上架。ClickHouse 的世界里,这个「导流台规矩」就是 MergeTree 表引擎——它规定了数据进门后如何被对待:不覆盖旧书、不原地改,而是把新来的数据打包成一个个不可变的「小捆」,也就是 data part。

每个 data part 是磁盘上一个自包含的目录,里面有列数据文件、索引文件和元数据。你可以把它理解为一捆书:里面所有书都按同一套规则排好序,并且这捆书一旦捆好就不再拆开。新增数据永远产生新的小 part,旧 part 原封不动。这一设计的核心奥妙在于:写入变成了「只追加」,省去了随机写和行级锁的开销,天然适合海量日志、指标这类持续涌入的数据。

在存储层面,MergeTree 表的物理布局大致为:表目录下先按分区键拆成多个分区目录,每个分区目录下再挂多个 part 子目录。每次 INSERT 至少生成一个 part,后台再慢慢把这些 part 合并更大、更有序的 part。

一句话理解:MergeTree 表就是一堆不可变数据块(part)的集合;新数据只追加,合并交给后台。

MergeTree table=⋃i=1nparti,parti∩partj=∅ (物理文件上)\displaystyle \text{MergeTree table} = \bigcup_{i=1}^{n} \text{part}_i, \quad \text{part}_i \cap \text{part}_j = \emptyset \ (\text{物理文件上})
  1. MergeTree table(表整体)一张 MergeTree 表由若干部分(part)组成,像图书馆由多个书架构成
    MergeTree table={part1,part2,…,partn} \text{MergeTree table} = \{ \text{part}_1, \text{part}_2, \dots, \text{part}_n \}
  2. 并集符号 ∪物理空间上所有 part 合起来承载表的所有数据,不重复、不折叠
    ⋃i=1nparti \bigcup_{i=1}^{n} \text{part}_i
  3. 不可变交集为空任意两个 part 在物理文件层面互不重叠,保证元数据与数据一致性 回到图书馆:正因为每捆书都独立编号、独立存在,管理员才能轻松地在夜里把几十捆小书合并成一捆大书,而读者白天永远看到的是整洁有序的大书架。理解 MergeTree 的存储结构,是后面所有优化技巧的基石——没有「不可变 part」这个前提,后台合并、稀疏索引、副本同步都无从谈起。
    parti∩partj=∅ \text{part}_i \cap \text{part}_j = \emptyset

排序键与主键

图书馆最怕的事,是读者要一本「数据库入门」,管理员却要从一楼找到顶楼。MergeTree 的解法很朴素:在把数据捆进 part 时,就按读者最常找的字段排好序。这个「最常找的字段」就是排序键(ORDER BY)。一本书在书架上的位置由它决定,数据行在 part 内部的物理顺序也由它决定。

但数据量动辄上亿行,就算有序,也不能逐行扫描。MergeTree 的做法是画一张「目录卡」:每 8192 行分成一个 granule(数据颗粒),索引文件只记录每个 granule 第一行的排序键值。这就是稀疏索引——它不精确到每一行,而是像一个「每排书架只记第一本书书名」的目录。查询时先在目录卡上二分查找,确定目标大概率落在哪几个 granule,再读取这几个数据块。

主键(PRIMARY KEY)这里要特别注意:它不等于 MySQL 里的主键,不保证唯一性。MergeTree 中主键只是排序键的一个前缀,专门用来构建稀疏索引。实际建表时,ORDER BY 是必备的,主键字段可以省略(默认沿用 ORDER BY)。选对了排序键,等值查询、范围查询都能走索引快速跳过大量数据。

数据在 part 内按排序键有序排列,每 index_granularity 行生成一条稀疏索引记录,指向对应数据块的起始行。

granule_index=⌊row_numberindex_granularity⌋\displaystyle \text{granule\_index} = \left\lfloor \frac{\text{row\_number}}{\text{index\_granularity}} \right\rfloor
  1. row_number(行号)数据在 part 内的物理序号,从 0 开始连续计数,表示「这本书在第几个位置」
    row_number∈{0,1,2,…,N−1} \text{row\_number} \in \{0, 1, 2, \dots, N-1\}
  2. index_granularity(索引粒度)每多少行生成一条索引,默认 8192,决定目录卡「每排书架记几本书」
    index_granularity=8192 \text{index\_granularity} = 8192
  3. 向下取整整除取整数,行号映射到它所属的数据颗粒编号,得到「这行属于第几排书架」 回到图书馆:目录卡只记每排第一本书,读者先在目录上锁定某排,再在那一排里快速找到目标——这就是稀疏索引的全部精髓。选排序键的智慧在于:把查询条件中最常用的等值字段放在最前面,让目录卡能最大程度缩小查找范围,千万别把主键当成唯一约束的保险箱。
    ⌊row_number8192⌋=该行所在 granule 的编号 \left\lfloor \frac{\text{row\_number}}{8192} \right\rfloor = \text{该行所在 granule 的编号}

分区机制

大图书馆还有一个聪明的设计:按楼层分区。一楼是文学,二楼是科技,三楼是历史。新书进来先看类别送到对应楼层,读者要找计算机书时直奔二楼,绝不去一楼瞎逛。MergeTree 的分区(PARTITION BY)就是给数据盖「楼层标签」,按分区键的值将数据拆进不同目录。

在磁盘上,每个分区对应一个形如 202410_1_3_0 的目录名,由四个部分组成:分区 ID、最小块编号、最大块编号和合并层级。这个命名规则像图书馆的「楼层-区域-批次」编码,让人一眼看出这堆数据属于哪个时间范围、经历了多少次合并。分区键可以是日期、整数或任意表达式,最常用的是 toYYYYMM(date) 按月份分区。

分区最大的价值是分区裁剪:查询 WHERE 条件带分区键时,ClickHouse 直接从元数据层跳过无关分区目录,只扫描命中的分区。这相当于读者在图书馆门口先刷楼层门禁,根本不用进那些不相关的楼层。注意:分区和排序键是两个维度——分区决定「数据落在哪层楼」,排序键决定「楼里的书怎么排」。

part 目录名携带分区 ID 与块编号范围,ClickHouse 借此快速定位数据所属分区,并推断合并历史。

part_name=partition_id_min_block_no_max_block_no_merge_level\displaystyle \text{part\_name} = \text{partition\_id}\_\text{min\_block\_no}\_\text{max\_block\_no}\_\text{merge\_level}
  1. partition_id(分区 ID)由分区键值哈希或格式化后得到的字符串,是楼层编号
    partition_id=format(partition_key_value) \text{partition\_id} = \text{format(partition\_key\_value)}
  2. min_block_no 与 max_block_no(块编号范围)记录这个 part 覆盖的数据块编号区间,像楼层里从第 1 排到第 3 排
    min_block_no≤block_no≤max_block_no \text{min\_block\_no} \leq \text{block\_no} \leq \text{max\_block\_no}
  3. merge_level(合并层级)part 被合并的次数,层级越高说明这个 part 经历过越多轮整理 分区是把双刃剑:按天数分区,查询当天数据极快,但如果每天写入的数据不足一个 part 的量,反而会造成大量碎片 part 拖垮合并线程。聪明的馆长会选「月」做楼层,而不是「小时」。分区粒度要结合数据量、TTL 策略和查询模式综合权衡,记住:分区让数据管理有边界,但真正的查询加速引擎还是排序键。
    merge_level∈{0,1,2,… } \text{merge\_level} \in \{0, 1, 2, \dots\}

合并与突变

图书馆每天晚上闭馆后,管理员会做一件重要的事:把白天积攒的几十捆小书拆开,按统一排序规则合并成几捆大书,再插回书架。这个动作在 MergeTree 里叫后台合并。合并线程会定期扫描各分区下的 part,把多个小 part 读出、按排序键归并排序、写成一个更大的新 part,旧 part 随后被标记删除。

ClickHouse 的合并策略是分层的:新写入的小 part 属于 L0 层,合并后升到 L1、L2……层级越高 part 越大、数量越少。这有点像图书馆管理层级:先合并到「书籍区」再合并到「书架」。合并的好处显而易见:part 数量减少,查询要打开的文件少了,稀疏索引的覆盖范围更整齐,还能顺带压缩数据。

但「改动现有数据」就麻烦了。MergeTree 的设计哲学是数据不可变,所以 UPDATE 和 DELETE 不是同步逐行改,而是通过 Mutation(突变) 实现:后台把包含目标行的整个 part 重写一份,在重写过程中完成修改,再切换文件引用。这相当于管理员要把某本书的内容改了,必须把那捆书整个重新抄一遍,再换上新书捆。因此突变操作成本很高,尤其当目标行分散在大量 part 中时,写放大可能非常惊人。

合并是把多个小 part 归并排序成一个有序大 part;突变则是重写包含目标行的 part,二者都由后台异步完成。

merged_part=sort(parta∪partb∪⋯ ),new_rows=rewrite(old_part,mutation)\displaystyle \text{merged\_part} = \text{sort}\left( \text{part}_a \cup \text{part}_b \cup \cdots \right), \quad \text{new\_rows} = \text{rewrite}(\text{old\_part}, \text{mutation})
  1. sort(归并排序)合并不是简单堆叠,而是把多个 part 的行按排序键重新归并,保证新 part 内部依然有序
    sort(A∪B) \text{sort}(A \cup B)
  2. rewrite(重写)突变操作读取旧 part 的所有行,应用修改后写出一份全新 part,替代旧文件
    rewrite(old_part)→new_part \text{rewrite}(\text{old\_part}) \to \text{new\_part}
  3. mutation(突变标记)每次 ALTER UPDATE/DELETE 会生成一个 mutation 版本,各 part 逐步在该版本下异步完成重写 图书馆管理员的智慧在于:白天不动书架,晚上统一整理,读者体验永远平滑。理解「合并是好,突变是贵」的权衡,你就能解释为什么 ClickHouse 适合海量分析型负载,却不适合高频行级更新。实际工程中,宁可把数据按批次重刷,也不要依赖频繁突变来修正历史数据。
    mutation_version=global_version+1 \text{mutation\_version} = \text{global\_version} + 1

性能优化

图书馆经营久了,总有一些「过期书」:去年的旧杂志、三年前的促销传单。馆长会在夜里安排管理员把这些书下架销毁——这就是 MergeTree 的 TTL(生存时间) 机制。建表时可以对某列或整表声明 TTL 表达式,到期后数据会被自动删除或移动到冷存储,既节省空间又让热数据查询更快。

除了 TTL,MergeTree 还提供了丰富的压缩编码:列式存储天然能让同类数据聚在一起,配合 LZ4、ZSTD 等压缩算法,以及 Delta、Gorilla 等专用编码,数值型数据的压缩比可以做到 5~10 倍甚至更高。这相当于把旧书换成了更薄的纸张,同样的书架能装下更多书。物化视图(Materialized View) 则是另一个省心工具:数据写入时自动触发聚合计算,把预聚合结果存成一张隐式表,查询高并发报表时不必现算。

最后一个关键调优点在索引:index_granularity 默认 8192,调大会减少索引体积但增加扫描粒度,调小则反之;还可以通过 INDEX 声明跳过索引(Skip Index),对高基数列做布隆过滤。优化的核心思想是:让「目录卡」更聪明、让「楼层」更合理、让「书页」更薄,三者配合才能榨干硬件性能。

TTL 为数据设定自动过期时间;压缩比衡量列存储的瘦身效果。两者共同目标是降低成本、提升热查询速度。

expire_at=now()−ttl_interval,compression_ratio=raw_sizecompressed_size\displaystyle \text{expire\_at} = \text{now()} - \text{ttl\_interval}, \qquad \text{compression\_ratio} = \frac{\text{raw\_size}}{\text{compressed\_size}}
  1. now()(当前时间)TTL 计算以服务器当前时间为基准,数据插入时间与 TTL 时长相加即得过期点
    expire_at=insert_time+INTERVAL \text{expire\_at} = \text{insert\_time} + \text{INTERVAL}
  2. ttl_interval(TTL 间隔)用户在表定义中声明如 INTERVAL 30 DAY,表示 30 天后数据过期
    INTERVAL 30 DAY \text{INTERVAL 30 DAY}
  3. compression_ratio(压缩比)原始大小与压缩后大小的比值,数值越大说明列式压缩效果越好 馆长视角看优化:TTL 让书架只保留读者需要的书,压缩编码让同样空间装更多内容,物化视图让常用答案提前算好。性能优化的本质不是某一招必胜,而是「少存取、少计算、少空间」的系统工程。每加一层优化,都要用真实查询和数据量做基准验证,避免为了炫技引入过度设计。
    raw_sizecompressed_size \frac{\text{raw\_size}}{\text{compressed\_size}}

副本与分布式

当图书馆规模大到一座楼放不下时,馆长会开分馆。为了不让读者扑空,每个分馆都会存一份完整的藏书——这就是副本。MergeTree 的兄弟引擎 ReplicatedMergeTree 专门干这事:多台机器各存一份相同数据,任何一台挂了,读者还能从其他馆借到书。

副本同步的「总协调员」是 ZooKeeper(ZK)。所有分馆的元数据、part 清单、合并任务、突变版本都登记在 ZK 上。实际上,每个分馆的机器只负责在自己本地执行合并和写入,一旦本地产生了新 part,就向 ZK 注册,其他副本看到后异步拉取。数据文件本身由各副本直接传输,ZK 不扛数据流量——它只是那个「互通有无的公告栏」。

如果数据量大到单机放不下,则需要 分布式表。它本身不存数据,只是一个「查询路由器」:像图书馆的中央检索系统,把请求转发到后端多个本地 MergeTree 表,再汇总结果。注意区分:副本解决的是「可用性」(一馆挂掉还有别馆),分布式解决的是「扩展性」(数据多到需要分馆各存一部分)。两者可以叠加:先按分片拆到不同机器组,每组内部再用 ReplicatedMergeTree 做副本。

假设单副本故障率为 f,有 r 个副本时整体不可用概率约为 f 的 r 次方,副本数越多可用性越高,但 ZK 协调压力也越大。

availability≈1−f r,r=replica_count\displaystyle \text{availability} \approx 1 - f^{\,r}, \qquad r = \text{replica\_count}
  1. f(单副本故障率)一台机器在某段时间内出故障的概率,数值越小表示机器越可靠
    0<f<1 0 < f < 1
  2. r(副本数)同一份数据保存的份数,每多一份就多一层保险
    r∈{1,2,3,… } r \in \{1, 2, 3, \dots\}
  3. 幂运算所有副本同时故障才导致整体不可用,概率指数级下降 回到图书馆:分馆越多读者越安心,但分馆之间同步的「公告栏」负担也越重。所以生产环境通常配 2~3 个副本就足够,盲目加副本反而会让 ZK 变成新瓶颈。理解「副本保可用、分片保扩展、ZK 是协调者」三角关系,是设计高可用 ClickHouse 集群的入门必修课。
    fr→极小 f^{r} \to \text{极小}

AI 应用场景

用户行为特征存储

阶段:特征工程与样本管理

解决的问题:推荐系统需要海量用户行为日志(点击、曝光、转化)作为特征来源。传统方案用 Hive 冷存储延迟高,用 Redis 热存储成本高且难做范围查询。ClickHouse MergeTree 以列式压缩 + 稀疏索引实现亚秒级筛选,按用户 ID 排序后,取最近 N 天行为只需扫少量 granule。

具体案例:某短视频平台的召回特征库。每天新增 50 亿条曝光日志,写入按天分区,排序键设为 (user_id, toYYYYMMDD(event_time))。拉取单个用户近 7 天特征时,分区裁剪 + 稀疏索引让 P99 延迟稳定在 80ms 内,特征拼接脚本可直接查询结果集。

相关角色:特征平台工程师、推荐系统架构师

模型训练样本回放

阶段:训练数据准备 / 离线评估

解决的问题:时序模型训练需要按时间窗口回放历史样本,并排除「未来数据泄漏」。用 MergeTree 的 ORDER BY (sample_id, event_time) + TTL 管理样本生命周期,可按样本 ID 精确定位,配合窗口函数完成正负样本拼接。

具体案例:某广告 CTR 预估团队用 Mutations 批量修正标注错误样本,后台重写 part 后自动生成新版本样本集;物化视图实时维护「用户-广告」统计特征,训练与上线共用一套数据管道。

相关角色:ML 数据工程师、算法工程师

Python 演示

python81 行
""“模拟 ClickHouse MergeTree 的 part 合并与稀疏索引查找”""import random  class SparseIndex:    ""“稀疏索引:每 granularity 行记录一个索引条目”""     def init(self, granularity=2):        self.granularity = granularity        self.index = []     def build(self, sorted_data):        self.index = []        for i in range(0, len(sorted_data), self.granularity):            self.index.append(sorted_data[i])        return self.index     def search(self, sorted_data, key):        # 二分定位候选区间,再顺序扫描        lo, hi = 0, len(self.index) - 1        while lo < hi:            mid = (lo + hi) // 2            if self.index[mid] <= key:                lo = mid + 1            else:                hi = mid        start = max(0, (lo - 1) * self.granularity)        end = min(len(sorted_data), (lo + 1) * self.granularity + 1)        return [v for v in sorted_data[start:end] if v == key]  class MergeTreeSim:    ""“最小 MergeTree 模拟:写入小 part,后台合并为有序大 part”""     def init(self):        self.parts = []          # 每个元素是一个有序 list        self._count = 0          # 写入总行数     def insert(self, rows):        for row in rows:            self.parts.append([row])   # 每次写入一个 1 行小 part            self._count += 1        self._background_merge()     def _background_merge(self):        # 模拟后台合并:优先合并两个最小的 part        while len(self.parts) >= 2:            self.parts.sort(key=len)            a = self.parts.pop(0)            b = self.parts.pop(0)            merged = sorted(a + b)            self.parts.append(merged)            # 若最大 part 已超过阈值,本轮合并停止(模拟分段合并)            if len(self.parts[-1]) >= 8:                break     def query(self, key):        result = []        for part in self.parts:            idx = SparseIndex(granularity=2)            idx.build(part)            result.extend(idx.search(part, key))        return result  if name == "main":    random.seed(42)    mt = MergeTreeSim()    for _ in range(10):        mt.insert([random.randint(1, 50) for _ in range(3)])     print(“合并后 parts 数量:”, len(mt.parts))    print(“各 part 行数:”, [len(p) for p in mt.parts])    print(“查询 7 的结果:”, mt.query(7))     data = sorted(random.sample(range(1, 100), 10))    idx = SparseIndex(granularity=2)    index_entries = idx.build(data)    print(“\n排序数据:”, data)    print(“稀疏索引条目:”, index_entries)    print(“等值查找 42:”, idx.search(data, 42))

常见误区

误区我用过 MySQL,MergeTree 的 PRIMARY KEY 应该也是唯一的,插入重复主键数据应该会报错吧?为什么我插了两遍同样的 user_id 都在表里?

正解MergeTree 的主键只是稀疏索引的排序依据,绝不保证唯一性。它相当于目录卡上的一排标签,同一本书可以重复出现多次。如果需要去重,必须用 ReplacingMergeTree 或专门的聚合去重机制,而不是依赖主键约束。

为何常见:几乎所有从关系型数据库转来的工程师都会踩这个坑,因为「主键 = 唯一」是 SQL 世界的铁律,而 MergeTree 把主键重新定义为「排序键前缀」,语义完全不同。

误区我执行了 `ALTER TABLE ... UPDATE`,应该跟 MySQL 一样立即生效吧?但等了半天查询结果还是旧的,是不是卡住了?

正解MergeTree 的更新删除是异步 Mutation,后台要把所有包含目标行的 part 整个重写一遍再切换。行多、part 多时非常慢,而且会放大 IO 和磁盘占用。正确姿势是用 TTL 或分区迁移来管理淘汰,尽量避免高频突变。

为何常见:「增删改查要实时」是数据库用户的默认预期,但 MergeTree 是为分析设计的,把「改」放在了后台重写路径上,概念错位导致误判。

误区查询经常按天筛选,那我就按天建分区,一天一个目录,这样查询只扫一个目录,比按月分区快 30 倍吧?

正解分区粒度过细会制造大量小 part,后台合并线程被迫频繁工作,ZK 元数据膨胀,写入和合并双双变慢。分区的本质是管理边界,查询加速要靠排序键的稀疏索引。通常按「月」或「周」分区已足够,配合分区裁剪收益最佳。

为何常见:直觉上「切得越细找得越快」,但忽略了每个分区目录里还有多个 part 的调度开销,这是典型的局部最优陷阱。

误区建表时没想好查询模式,先随便定一个 ORDER BY,等上线后慢了再 ALTER 改排序键不就行了?

正解修改排序键等于全表重写(Mutation),数据量上亿时耗时和资源消耗难以承受。排序键一旦定下,part 内物理顺序和稀疏索引就固定了。必须在建模阶段认真分析高频查询的 WHERE 等值条件,将最常查的字段放到排序键最前面。

为何常见:很多工程师带着 OLTP 的「改表成本低」惯性思维来到 OLAP 世界,没有意识到列式存储的物理排序是全局重排。

误区ReplicatedMergeTree 副本越多,读性能肯定越强吧?多搞几个副本,查询并发就能翻几倍。

正解副本解决的是高可用,不是读扩展。普通 MergeTree 查询只在单个副本上全量执行,不会把一部分数据拆到多个副本并行算。读扩展要靠分布式表 + 分片,副本加太多反而给 ZooKeeper 同步带来巨大压力。

为何常见:「分布式系统里副本 = 并行」的朴素认知。实际副本是「冗余备份」,只有配合分片路由才能真正水平扩展读能力。

面试题

Q1. MergeTree 中 PRIMARY KEY 与 ORDER BY 的关系是?

单选题 · easy

  • A.主键是独立唯一约束
  • B.主键是排序键的前缀← 正确答案
  • C.主键用于强制去重
  • D.主键与排序键完全无关

答案:B。MergeTree 的 PRIMARY KEY 只是排序键(ORDER BY)的一个前缀,用来构建稀疏索引,不保证唯一性。就像目录卡上选取的排序项前缀,它描述的是一种组织方式,而不是数据库层面的唯一性约束。

Q2. MergeTree 最适合哪种负载场景?

单选题 · easy

  • A.高频逐行事务更新
  • B.海量数据分析查询← 正确答案
  • C.实时流式事件处理
  • D.文档全文检索服务

答案:B。MergeTree 的不可变 part、后台合并、列式压缩和稀疏索引都是为「海量数据 + 复杂分析查询」设计的。它不支持高频行级更新,也不适合全文检索,因此分析型数据仓库是最佳场景。

Q3. 每次 INSERT 写入 MergeTree 表,直接产生的结果是?

单选题 · mid

  • A.追加一个新的 data part← 正确答案
  • B.原地修改已有数据文件
  • C.立即触发全表重新排序
  • D.删除并重建整个分区

答案:A。MergeTree 是追加写模型,每次 INSERT 至少生成一个新的不可变 part,与旧 part 并行存在。旧数据不会被修改,真正的整理动作由后台合并完成。这是它吞吐高的根本原因。

Q4. 关于稀疏索引,下列说法正确的是?

单选题 · mid

  • A.每一行都对应一条索引项
  • B.索引精确记录每个主键值
  • C.每 8192 行记录一个索引项← 正确答案
  • D.索引需要全表扫描才生效

答案:C。稀疏索引的默认粒度是 8192 行,索引文件只记录每个 granule 的首行主键值,查询时先二分索引定位候选块,再在块内扫描。它比稠密索引体积小得多,非常适合 OLAP 的范围扫描场景。

Q5. MergeTree 中 ALTER UPDATE 的实现机制是?

单选题 · mid

  • A.同步逐行更新记录
  • B.后台异步 Mutation 重写 part← 正确答案
  • C.直接在索引文件上修改
  • D.删除旧数据并重放日志

答案:B。Mutation 不是原地修改,而是后台把包含目标行的 part 整个重写为新 part,再原子切换引用。因此它比普通写入贵得多,频繁使用会产生严重的写放大和磁盘压力。

Q6. 以下哪些是 MergeTree 分区的合理用途?

多选题 · mid

  • A.按日期边界裁剪查询范围← 正确答案
  • B.配合 TTL 管理数据生命周期← 正确答案
  • C.强制实现主键唯一性
  • D.替代排序键加速单行查找

答案:AB。分区让查询元数据层面跳过不相关目录,同时 TTL 可以按分区或按列过期数据。分区与唯一性无关,也不能替代排序键的稀疏索引,单行查找还是要靠索引粒度之内的扫描。

Q7. 后台合并(Merge)主要解决什么问题?

单选题 · mid

  • A.提升写入请求并发数
  • B.加密压缩磁盘数据
  • C.将小 part 合并为有序大 part← 正确答案
  • D.把数据复制到其他节点

答案:C。合并把多个无序小 part 归并排序成一个有序大 part,减少文件数量、优化索引覆盖、提升查询效率。它不负责写入并发,也不承担跨节点复制,那是副本层的职责。

Q8. ReplicatedMergeTree 使用 ZooKeeper 的目的包括?

多选题 · hard

  • A.协调各副本的元数据← 正确答案
  • B.存储实际的列数据行
  • C.监控副本存活与状态← 正确答案
  • D.承担聚合计算任务

答案:AC。ZK 是副本间的协调者,维护 part 清单、mutation 版本和副本健康状态。真实数据通过副本间直连传输,ZK 不存数据也不做计算。它更像图书馆的公告栏,而不是书架本身。

Q9. 下列哪些属于 MergeTree 性能优化手段?

多选题 · hard

  • A.设置 TTL 自动清理过期数据← 正确答案
  • B.为高并发读写增加更多分区
  • C.按查询模式配置排序键
  • D.用主键强制代替稀疏索引
  • E.单行更新需重写整个 part,写放大极高
  • F.更新操作会触发分布式锁竞争
  • G.单行更新无法使用稀疏索引
  • H.更新前必须清空分区缓存

答案:A。MergeTree 以不可变 part 为存储单位,任何单行修改都要把目标 part 整体读出、修改、重写。若目标行所在 part 很大,一次更新的实际 IO 远超一行数据。因此高频单行更新场景应选用支持行级更新的数据库,而不是 ClickHouse。