列存储ClickHouse | 青训营笔记

133 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 15 天。

1 列式存储

1.1 概念

行式存储:

img

列式存储:

img

1.2 优点

1.2.1 数据压缩

  • 数据压缩可以使读的数据量更少,在IO密集型计算中获得大的性能优势
  • 相同类型压缩效率更高
  • 排序之后压缩效率更高
  • 可以针对不同类型使用不同的压缩算法
  • 几种常见的压缩算法
    • LZ4
    • Run-length encoding
    • Delta encoding

1.2.2 数据处理

  • 查询优化

    • 可以选择特定的列做计算而不是读所有列
    • 对聚合计算友好
  • 延迟物化

    • 物化:将列数据转换为可以被计算或者输出的行数据或者内存数据结果的过程,物化后的数据通常可以用来做数据过滤,聚合计算,Join
    • 延迟物化:尽可能推迟物化操作的发生
    • 缓存友好
    • CPU / 内存带宽友好
    • 可以利用到执行计划和算子的优化,例如filter
    • 保留直接在压缩列做计算的机会
  • 向量化

    • SIMD:single instruction multiple data,对于现代多核CPU,其都有能力用一条指令执行多条数据
    • 执行模型:数据需要按批读取,函数的调用需要明确数据类型
    • 列存数据库适合设计出这样的执行模型,从而使用向量化技术

1.2.3 列存 VS 行存

image.png

2 ClickHouse的存储设计

2.1 架构

img

2.2 表定义和结构

img

2.3 集群架构

img

2.4 存储架构

文件组织

img

文件内容

  • 给定表

    CREATE TABLE test.test_insert_local
    (
        `p_date` Date,
        `id` Int32
    )
    ENGINE = MergeTree
    PARTITION BY p_date
    ORDER BY id
    SETTINGS index_granularity = 8192
    
  • 文件组织

    ├── 20220101_1_1_0
    │   ├── checksums.txt
    │   ├── columns.txt
    │   ├── count.txt
    │   ├── data.bin
    │   ├── data.mrk3
    │   ├── default_compression_codec.txt
    │   ├── minmax_p_date.idx
    │   ├── partition.dat
    │   ├── primary.idx
    │   └── versions.txt
    ├── 20220102_2_2_0
    │   ├── checksums.txt
    │   ├── columns.txt
    │   ├── count.txt
    │   ├── data.bin
    │   ├── data.mrk3
    │   ├── default_compression_codec.txt
    │   ├── minmax_p_date.idx
    │   ├── partition.dat
    │   ├── primary.idx
    │   └── versions.txt
    ├── detached
    └── format_version.txt
    

part和partition

  • part是物理文件夹的名字
  • partition是逻辑结构

part和column

  • 每个column都是一个文件
  • 所有的column文件都在自己的part文件夹下

column和index

  • 一个part有一个主键索引
  • 每个column都有列索引

2.5 索引设计

  • 主键索引
CREATE TABLE hits_UserID_URL
(
    `UserID` UInt32,
    `URL` String,
    `EventTime` DateTime
)
ENGINE = MergeTree
PRIMARY KEY (UserID, URL)
ORDER BY (UserID, URL, EventTime)
SETTINGS index_granularity = 8192, index_granularity_bytes = 0;
  • 数据按照主键顺序依次排序:UserID首先做排序,然后是URL,最后是EventTime

  • 数据被组织成granule

    • granule是引擎做数据处理的最小数据单位,引擎读数据的时候不是按照一行一行读取的,而是最少读取一个granule

    • 方便构建稀疏索引

    • 方便并行计算

  • 每个granule都对应primary.idx里面的一行

  • 默认每8192行记录主键的一行值,primary.idx需要被全部加载到内存里面

  • 每个主键的一行数据被称为一个mark

  • 每个列都有这样一个mark文件,mark文件存储所有granule在物理文件里面的地址,每一列都有一个mark文件

  • mark文件里面的每一行存储两个地址

    • 第一个地址称为block_offset,用于定位一个granule的压缩数据在物理文件中的位置,压缩数据会以一个block为单位解压到内存中。
    • 第二个地址称为granule_offset,用于定位一个granule在解压之后的block中的位置。

2.6 索引的缺陷和优化

  • 缺陷:数据按照key的顺序做排序,因此只有第一个key的过滤效果好,后面的key过滤效果依赖第一个key的基数大小
  • 二级索引:在URL列上构建二级索引
  • 构建多个主键索引
    • 再建一个表(数据需要同步两份,查询需要用户判断查哪张表)
    • 建一个物化视图(数据自动同步到隐式表,查询需要用户判断查哪张表)
    • 使用Projection(数据自动同步到隐式表,查询自动路由到最优的表)

2.7 数据合并

  • 一个part内的数据是有序的
  • 不同part之间的数据是无序的
  • 数据合并是将多个part合并成一起的过程
  • part的合并发生在一个分区内
  • 数据的可见性
    • 数据合并过程中,未被合并的数据对查询可见
    • 数据合并完成后,新part可见,被合并的part被标记删除

2.8 数据查询

给定查询

SELECT
    URL,
    count(URL) AS Count
FROM hits_UserID_URL
WHERE UserID = 749927693
GROUP BY URL
ORDER BY Count DESC
LIMIT 10
  • 通过主键找到需要读的mark
  • 切分marks,然后并发的调度reader
  • Reader 通过mark block_offset得到需要读的数据文件的偏移量
  • Reader 通过mark block_offset得到需要读的数据文件的偏移量
  • 构建列式filter做数据过滤

3 ClickHouse的典型使用场景

3.1 大宽表存储和查询

  • 动态表结构
    • map中的每个key都是一列
    • map中的每一列都可以单独的查询
    • 使用方式同普通列,可以做任何计算
CREATE TABLE test_multi_columns
(
    `p_date` Date,
    `id` Int32,
    `map_a` Map(String, Int32)
)
ENGINE = MergeTree
PARTITION BY p_date
ORDER BY map_a
  • 大宽表查询:可以建非常多的列查询的时候引擎可以快速选择需要的列,查询的时候引擎可以快速选择需要的列

3.2 离线数据分析

3.2.1 数据导入

数据可以通过spark生成clickhouse格式的文件

导入到hdfs上由hive2ch导入工具完成数据导入

数据直接导入到各个物理节点

img

3.2.2 数据按列导入

保证查询可以及时访问已有数据

可以按需加载需要的列

img

3.3 实时数据分析

  • 数据可以被立刻查询

  • 使用memory table减少parts数量

    • 数据先缓存在内存中

    • 到达一定阈值再写到磁盘