ClickHouse merge引擎详解以及应用

一、理解 Merge引擎 (通常用于系统表,非用户数据)

  • 用途:Merge引擎本身不存储数据 ,它的主要作用是提供对多个底层表 (通常是结构相同的 MergeTree表)的统一查询视图 ,可以将它看作一个逻辑上的联合查询器

  • 工作机制:

    • 指定一个数据库和一个用于匹配表名的正则表达式(例如 ^your_table_prefix

    • 当向 Merge表执行查询时,ClickHouse 引擎会:

      • 在指定数据库中查找所有匹配该正则表达式的表

      • 将这些表(逻辑上)"合并"在一起

      • 在合并后的数据集上执行查询

  • 典型场景:

    • 处理 MergeTree表的分区: ​ 在旧版本中(或在特定系统表如 system.query_log的设计中),ClickHouse 可能会将不同时间段的数据存储在结构相同但表名后缀不同的表中(例如 your_log_table_202311, your_log_table_202312),创建一个 Merge表(如 your_log_table_all)指向 ^your_log_table_就可以方便地查询所有历史日志。

    • 查询系统表: ​ 许多 ClickHouse 的系统表(如 system.parts, system.query_log)本身就是 Merge表,它们动态聚合了来自多个内部表(通常是不同MergeTree分区)的信息。

  • MergeTree的关键区别:Merge不拥有数据 ,不管理分区合并,不提供排序或索引,它只是提供查询视图。数据存储、优化和分区管理完全依赖于其指向的底层表(尤其是 MergeTree表)

二、核心:MergeTree引擎族(存储和优化的基石)

MergeTree及其衍生变体(如 ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree, CollapsingMergeTree, VersionedCollapsingMergeTree等)才是 ClickHouse 真正的核心列式存储引擎,负责数据的物理存储、组织、压缩和优化。理解其机制是查询优化的根本。

原理:基于LSM树优化,数据按主键排序分区存储,支持数据分片、副本、索引合并(Merge)和后台压缩

适用场景:时序数据存储(如日志、传感器数据)

MergeTree的关键机制 (优化基础)

  • 1.分区 ( PARTITION BY):

    • 将表数据划分为逻辑片段(分区),通常按时间(如 toYYYYMM(date))或业务关键字段

    • 优化作用:

      • 分区裁剪 (Partition Pruning): ​ WHERE 子句匹配分区键时,查询只需扫描相关分区的数据文件,极大减少 I/O

      • 数据管理方便(删除分区 ALTER TABLE .DROP PARTITION是删除文件,速度快)

  • 2.排序键 / 主键 (PRIMARY KEY/ ORDER BY):

    • ORDER BY是必须的,PRIMARY KEY通常是 ORDER BY的前缀或相同。 ​ 这个顺序定义了数据在磁盘上的物理排序顺序(每个分区内)

    • 优化作用:

      • 索引基石: ​ 稀疏主索引 (index[.idx]文件)基于排序键构建。主键列作为索引列

      • 区间查询加速:WHEREORDER BY子句匹配排序键前缀时,引擎能快速定位数据范围,避免全表扫描

      • **局部性:**​ 相同排序键值的数据物理上相邻,利于压缩和聚合计算

      • 是数据标记 (data.mrk/.mrk2) 能准确定位颗粒位置的依据

  • 3.索引粒度 (index_granularity):

    • 定义主索引中每个条目指向的数据行数(默认 8192)

    • 优化作用:

      • 在索引大小(查找速度)和数据扫描精度之间取得平衡。较小的粒度索引更大,扫描更精确;较大的粒度索引更小,扫描可能包含更多无关数据
  • 4.数据标记 (data.mrk/ .mrk2):

    • 映射主索引条目到磁盘上压缩数据块内的具体字节偏移位置

    • 优化作用:

      • 实现高效的精确数据定位。引擎通过索引找到标记,再通过标记找到目标数据块并解压扫描所需列

      • .mrk2适用于自适应索引粒度

  • 5.后台合并 (OPTIMIZE/ 自动触发):

    • 定期将小的数据片段(新插入、新分区生成的数据)合并成更大的片段

    • 优化作用:

      • 减少需要打开和扫描的小文件数量,提升查询效率

      • 根据特定引擎的逻辑进行数据聚合/去重(如 ReplacingMergeTree的最终去重发生在合并时)

MergeTree衍生引擎 (针对特定场景优化)

  • ReplacingMergeTree: ​ 适合需要根据排序键更新/覆盖行的场景。合并时保留相同排序键的最新版本或指定版本

    • 原理:相同排序键的数据保留最后插入的版本(去重发生在合并时)

    • 适用场景:需要最终一致性的数据去重(如用户画像更新)

  • SummingMergeTree/ AggregatingMergeTree: ​ 适合预聚合场景。插入数据后,在后台合并时自动对指定的数值列进行聚合求和 (SummingMergeTree) 或根据 AggregateFunction状态(如 sumState, uniqState)进行聚合 ​ (AggregatingMergeTree)。查询时通常结合 sumMerge, uniqMerge等函数

    • 原理 :预聚合数据,配合AggregateFunction类型(如sumState, uniqState)

    • 适用场景:实时OLAP聚合(如PV/UV统计)

  • CollapsingMergeTree/ VersionedCollapsingMergeTree: ​ 适合处理需要根据状态(sign) 或状态+版本(sign+ version)折叠删除/失效行的场景(如用户会话、状态变更流水)

三、ClickHouse 数据查询优化策略

1. 最有效的优化:利用 MergeTree的特性进行查询过滤

  • 强制分区裁剪: ​ 写 WHERE子句时 明确包含分区键 ​ 的过滤条件,例如 WHERE event_date >= '2023-11-01' AND event_date < '2023-12-01'

  • 善用主键索引:

    • 前缀匹配:WHEREORDER BY子句尽可能使用排序键(主键)的前缀列 ,查询 WHERE A = x AND B = yWHERE B = y AND A = x更高效(如果主键是 (A, B)

    • **避免跳过索引前缀:**​ 无法命中索引前缀的查询效率会急剧下降

    • **高性能主键列选择:**​ 将经常用于过滤且高基数的列(如 UserID)放在主键靠前位置(在分区键之后)

2. 查询语法优化

  • 精简查询列:只 SELECT 需要的列 。ClickHouse 是列存,只读取涉及的列文件。避免 SELECT *

  • 明智使用 PREWHERE:

    • PREWHERE首先应用过滤条件读取主键和可能涉及到的少量轻量列(即使不在 SELECT 列表中),符合条件后再读取 SELECT 需要的其他列

    • 适合在过滤性好的非主键条件上使用,能极大减少需要读取和解压的数据量。但计算量大的条件不宜放这里

  • 避免全量 DISTINCT:SELECT DISTINCT在大量数据上性能极差(占用大量内存),优先考虑 GROUP BY替代,或利用 uniq等近似聚合函数。思考是否真的需要全量去重

  • 使用近似计算: ​ 当允许一定误差时,使用 uniq, quantile, any等近似函数代替 count(DISTINCT), quantileExact, min/max。性能提升巨大

  • 利用索引跳数 (Data Skipping Index):

    • 在主键之外的其他常用过滤列上创建辅助索引(如 minmax, set, bloom_filter, ngrambf等)

    • 在数据合并时计算并存储每个索引颗粒(index granule)上该列的统计摘要

    • 查询时,利用这些摘要快速判断该颗粒是否可能包含目标数据,决定是否跳过扫描

3. 聚合计算优化 (Summing/AggregatingMergeTree, GROUP BY)

  • 预聚合引擎应用: ​ 对于固定的报表或复杂聚合查询,使用 SummingMergeTreeAggregatingMergeTree

    • 插入时保存状态(原始值或 AggregateFunction状态),写入负担略有增加。

    • 后台合并时执行实际的聚合计算,聚合结果存储在大片段中

    • 查询时:

      • SummingMergeTree指定列使用 sum(引擎会聚合好底层片段数据)

      • AggregatingMergeTree,使用 groupBitmapState, uniqState等插入,查询时用 groupBitmapMerge, uniqMerge等函数汇总结果

  • 高效使用 GROUP BY:

    • 确保 GROUP BY子句包含在高选择性的过滤条件后

    • GROUP BY 优化: ​ 在 settings中可以开启 group_by_two_level_threshold, distributed_aggregation_memory_efficient等优化内存使用和分布式聚合行为

4. 内存与资源管理

  • **控制内存使用 (max_memory_usage):**​ 防止单个查询耗尽内存导致 OOM

  • **外部聚合/排序 (max_bytes_before_external_group_by, max_bytes_before_external_sort):**​ 当聚合或排序的中间结果超过此阈值,会将其溢出到磁盘。牺牲一定速度避免 OOM,非常适合大数据量聚合

  • 使用物化视图 (MATERIALIZED VIEW): ​ 针对频繁执行的复杂查询预先计算并存储结果。自动从源表(通常是 MergeTree)增量更新,注意: ​ 物化视图本质也是后台 MergeTree表,设计时需注意排序键、分区键以匹配查询模式

  • **使用投影 (PROJECTIONS):**​ ClickHouse 22+ 特性。在单个表内定义基于特定列的预计算视图(包括排序、聚合等),自动维护,查询优化器可能自动选择最优投影。相比物化视图更轻量,管理更集中

5. 数据结构与表设计优化

  • 选择合适的压缩编解码器 (如 LZ4, ZSTD): ​ 更高的压缩比减少 I/O 但增加 CPU 开销(解压),需要权衡,ZSTD通常是个不错的平衡点

  • 数据规范化与扁平化: ​ ClickHouse 处理平坦表结构(避免 JOIN)效率最高。优先考虑去规范化(Denormalization),如果必须 JOIN

    • 优先考虑事实表JOIN维度表(确保维度表是小表)

    • 尽可能在过滤后再 JOIN

    • 利用 JOIN引擎 (Join, Dictionary) 预加载小维度表

  • 列选择: ​ 谨慎添加太多列(尤其是很少查询的列),考虑使用 Map或嵌套数据结构存储属性

6. 数据一致性考虑(特定 MergeTree引擎)

  • **ReplacingMergeTree/ CollapsingMergeTree:**​ 理解后台合并的异步性。查询时可能看到未合并状态的重复或待折叠行

    • 查询时去重/折叠: ​ 使用 FINAL关键字(SELECT ... FROM your_repl_table FINAL ...),但性能开销大(强制合并所需数据)

    • 使用版本号/标志: ​ 在 WHERE 子句中主动过滤(如 WHERE version = (SELECT max(version) ... )),结合 GROUP BY取最新

    • 手动触发合并:OPTIMIZE TABLE your_table FINAL(生产环境慎用,开销大)

    • **使用 AggregatingMergeTree 存储状态:**这是处理聚合场景的更优解(最终一致性)

    • **接受最终一致性:**​ 如果应用允许短暂的数据中间状态,这是最简单的方式

7. 监控与分析

  • 使用 EXPLAIN:

    • EXPLAIN PLAN:查看优化器生成的执行计划

    • EXPLAIN PIPELINE:查看物理执行管道,了解扫描、过滤、聚合、排序等步骤在哪个线程执行以及是否并行

    • EXPLAIN ESTIMATE:估算查询涉及的颗粒(granules)数量

  • **system.query_log:**​ 记录执行过的查询及其详细信息(耗时、读取行数、内存用量等),用于慢查询分析

  • **system.parts:**​ 监控表分区的状态和数量

  • **system.metrics/ system.asynchronous_metrics:**​ 监控服务器级资源使用

四.总结与建议

  1. 核心在 MergeTreeMergeTree分区键排序键索引粒度设计是性能的基石,投入 80% 的优化精力在这里

  2. 过滤先行: ​ 最大化利用分区裁剪主键索引减少 I/O

  3. 列存精要:SELECT只取所需列

  4. PREWHERE 利器: ​ 善用 PREWHERE预过滤

  5. 聚合优化: ​ 对重复聚合查询,强推 SummingMergeTree/AggregatingMergeTree、物化视图、投影

  6. **资源管控:**​ 设置内存限制,配置外部聚合预防 OOM

  7. 查询分析: ​ 利用 EXPLAINquery_log诊断瓶颈

  8. **近似结果:**​ 如可接受误差,大胆用近似计算函数

  9. **善用跳数索引:**​ 对高频查询的非主键过滤条件,添加合适的跳过索引

  10. 理解异步:ReplacingMergeTree等引擎需注意最终一致性

相关推荐
@insist1231 小时前
信息系统管理工程师-任务、标准、EDM 过程与关键域
java·大数据·数据库
qq_366086221 小时前
关于接口路径中中文值解码问题
java·开发语言·数据库
ClouGence2 小时前
数据库为什么需要 SSL?一文了解 SSL 连接原理与配置
数据库·开源
一十九的酒2 小时前
Oracle 密码恢复实战:使用 SPARE4 哈希值还原用户密码
数据库·oracle·密码丢失·密码恢复·账户锁定
冰暮流星2 小时前
mysql之新建表及对表的查询
android·数据库·mysql
C语言Plus2 小时前
C++ SqlBuilder一个简单、灵活且类型安全的 C++ SQL 构建器库
数据库·sql·安全·c++20
喝茶与编码2 小时前
真实业务场景:高并发 Upsert 死锁频发?InnoDB 锁机制与重试机制深度解析
数据库·python
BreezeJiang2 小时前
做完 AI 日记本后,我终于理解了:向量数据库不是用来替代 MySQL 的
数据库
VALENIAN瓦伦尼安教学设备2 小时前
转子/行星/平行轴齿轮箱综合故障模拟实验台可复现常见机械问题
大数据·数据库·人工智能·嵌入式硬件·算法