当前位置: 首页 > news >正文

Clickhouse原理剖析

文章目录

  • 前言
  • 设计哲学
  • 列式存储
  • 向量化执行
  • MergeTree引擎
  • 执行流水线
  • 拓扑架构
  • 最佳实践
  • 拓展

前言

在现代化的BI领域,大部分企业内部具备大规模的数据,而传统的关系型数据库难以满足OLAP系统中对于大规模的数据高效的分析和处理的需要,同时传统基于Hadoop的生态,因为其组件化的设计导致系统的变得复杂和臃肿,但是现在化系统中对于实效性的要求越来越高,Hadoop在海量数据和高实效性的双重压力面前显得有些力不从心,而轻量高效的ClickHouse出现使这种痛点得以解决

设计哲学

// Clickhouse设计哲学
type ClickHouseDesign struct {Storage   ColumnarStorage   // 列式存储Execution VectorizedEngine  // 向量化执行Indexing  SparseIndexing    // 稀疏索引
}

列式存储

  1. 数据按列存储:每个列的数据单独存储在磁盘的连续区域中
  2. 查询时只读取所需列:避免读取整行中无关的数据,大幅减少 I/O
  3. 极致的数据压缩:
  • 同列数据类型一致,压缩效率极高(如LZ4)
  • 数据压缩比通常可达 5-10 倍,显著降低存储成本和 I/O 压力
  1. CPU 缓存友好:连续读取同类型数据,能更高效地利用 CPU 缓存

适合在宽表中获取小部分行数据

  • 行存结构
 [RowID, CreatedDate, UserID, Action, Country ][1,     2025-6-15,   1001,   Click,  US      ][2,     2025-6-16,   1002,   View,   CN      ]
  • 列存结构
  CreatedDate: [2025-6-15, 2025-6-16]UserIDs:     [1001, 1002]Actions:     [Click, View]Countries:   [US, CN]

向量化执行

  1. 批量处理(Array-at-a-time):不再逐行处理,而是一次性处理一整列数据块(Block)。
  2. 利用 SIMD 指令:
  • Single Instruction Multiple Data,单指令流多数据流。
  • 现代CPU支持一条指令同时处理多个数据(如 16 个整数相加)。
  • 向量化引擎自动将操作编译为SIMD指令,极大提升聚合、过滤等计算密集型操作速度。
  1. 减少函数调用/分支预测开销:处理一个数据块只需少量函数调用,避免逐行处理的高昂开销。
  • 传统行处理:
[Row 1001] [Row 1002] [Row 1003]↓         ↓          ↓[处理]      [处理]      [处理]↓         ↓          ↓[结果1]     [结果2]     [结果3]
  • ClickHouse向量化:
[1001,1002,1003] │ ← 列内数据批量载入CPU缓存↓
[SIMD指令批量处理]↓
[结果1,结果2,结果3]

MergeTree引擎

Partition 202506  # 分区目录(按分区键生成)
├─ Part1  # 数据片段目录(有序) 命名格式:分区ID_最小块号_最大块号_合并层级
│  ├─ primary.idx    # 稀疏索引(index_granularity行为一块,抽取一块中的第一行记录到文件中)
│  ├─ UserID.bin     # 列数据
│  ├─ EventTime.bin
│  ├─ UserID.mrk2   # 列标记文件(连接索引和数据)
│  └─ EventTime.mrk2
│
└─ Part2├─ primary.idx└─ ...# 主键索引结构示例
Granule 0: (2025-06-01, 1000)
Granule 1: (2025-06-01, 2000) ← 每8192行一个标记
Granule 2: (2025-06-02, 1500)
  • 核心思想
    写入时追加,后台合并优化

  • 数据写入流程

1. 数据片段写入流程:
- 每个 Part 包含数据文件(bin)、索引文件(idx)和列标记文件(mrk)写入的数据首先在内存中积累,达到阈值后生成新的数据片段(Part)2. 后台数据片段合并过程:
- 按排序键(Sorting Key)对数据重新排序
- 应用去重逻辑(如有)
- 优化存储空间定期将小 Part 合并为大 Part,减少文件数量
  • 数据查询流程
根据条件过滤出相关的Partition
在Partition 中的 primary.idx 通过二分查找定位到目标数据所在的 Granule 索引号 (Granule Number)
读取该列对应的 .mrk2 文件,找到该 Granule Number 对应的记录
根据 offset_in_compressed_file 定位到 .bin 文件中的压缩块位置
读取该压缩块并解压
在解压后的数据块中,根据 offset_in_decompressed_block 找到目标 Granule 的起始位置
扫描该 Granule (约 8192 行) 以找到满足查询条件的行Partition → primary.idx → .mrk2 → .bin

执行流水线

func executePipeline(query Query) Result {// 阶段1:数据读取reader := make(chan DataBlock)go func() {defer close(reader)for part := range locateParts(query) {reader <- readColumnData(part, query.Columns)}}()// 阶段2:向量化处理processor := make(chan ResultBlock)go func() {defer close(processor)for block := range reader {result := vectorProcess(block)processor <- result}}()// 阶段3:结果归并var finalResult Resultfor res := range processor {finalResult = mergeResults(finalResult, res)}return finalResult
}

拓扑架构

CREATE TABLE my_db.my_table ON CLUSTER cluster1
(...
)
ENGINE = MergeTree()
ORDER BY (event_date, user_id)
PARTITION BY toYYYYMM(event_date);CREATE TABLE my_db.my_table ON CLUSTER cluster1
(...
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/my_table/{shard}', '{replica}')
ORDER BY (event_date, user_id)
PARTITION BY toYYYYMM(event_date);
# 拥有相同ClickhouseKeeper路径的不同副本之间会自动相互复制,不同副本都可以读写-- 创建分布式表时指定分片键
CREATE TABLE distributed_table (id Int32,name String,age In32,date Date
) ENGINE = Distributed('my_cluster',     -- 集群名称'my_db',          -- 数据库名'my_table',       -- 本地表名id                -- 分片键,可以是某个列(如id)、多列组合(如name, age)、任意表达式(如hash(id))、复合键分片(id、toYYYYMM(date))、按写入顺序依次分配到各个分片rand()
)

当一个副本写入新数据时,会在ClickhouseKeeper中放入一个复制日志条目,所有副本会定期向Clickhouse相应的路径中拉取复制日志,发现新条目后,会通过http协议从源副本下载新数据

最佳实践

  • 分区剪枝,查询时,先根据分区键过滤无关分区,大幅减少扫描数据量。
-- 仅扫描2025年06月的分区
SELECT * FROM my_table WHERE date >= '2025-06-01' AND date < '2025-06-12';
  • 索引加速,通过稀疏索引快速定位数据块,结合排序键避免全表扫描。
-- 利用排序键(id)快速定位
SELECT * FROM my_table WHERE id = 1000;

拓展

  • 跳数索引

创建时添加

CREATE TABLE user_activity (event_date Date,user_id UInt32,event_type String,device String,INDEX idx_device device TYPE minmax GRANULARITY 3,
) ENGINE = MergeTree()
ORDER BY (event_date, user_id)
PARTITION BY toYYYYMM(event_date);

创建后添加

ALTER TABLE user_activity ADD INDEX idx_device device TYPE minmax GRANULARITY 3;

跳数索引是用来补充主键索引的
通过合理配置跳数索引,可以将特定查询性能提升10-100倍

相关文章:

  • Elasticsearch 搜索的流程
  • 最新发布 | “龙跃”(MindLoongGPT)大模型正式发布!龙跃而起,推动中国方案走向全球智能体前沿
  • 电脑的虚拟内存对性能影响大吗
  • [go] 垃圾回收源码解析
  • MCU双分区方案,如何优雅地获知当前运行分区?
  • 新高考需求之一
  • pyhton基础【15】函数进阶一
  • 从厨房到代码台:用做菜思维理解iOS开发 - Swift入门篇①
  • ADIOS2 介绍与使用指南
  • Vue3 + Vite + TypeScript SVG图标解决方案
  • 一款基于 React 的开源酷炫动画库
  • C指针总结复习(结合deepseek)
  • 71-Oracle Undo与Flashback管理(Guarantee设置)深度解析
  • 艾立泰数字化方案重塑汽车包装载具管理
  • oracle 表空间与实例妙用,解决业务存储与权限处理难题
  • C++11 static_assert(基于Boost库)从入门到精通
  • Halcon ——— OCR字符提取与多类型识别技术详解
  • STM32学习笔记
  • 全链接神经网络,CNN,RNN各自擅长解决什么问题
  • DataWhale-零基础络网爬虫技术(三、爬虫进阶技术)
  • 大渡口网站建设/宁波企业网站seo
  • 昆山做网站费用/郑州网络推广方法
  • 买网站需要注意什么/福州今日头条新闻
  • adobe mu做可视化网站/5188关键词挖掘工具
  • pc网站如何做移动网站/网站关键词怎么设置
  • 工程建设安全管理/百度关键词优化服务