ClickHouse-你没见过的列存储 | 青训营笔记

121 阅读5分钟
  • 这是我参与「第五届青训营 」伴学笔记创作活动的第 19 天

1. ClickHouse存储设计

1.1 索引设计

Log-structured merge-tree(LSM tree)是一种为大吞吐写入场景而设计的数据结构

  • 着重优化顺序写入

  • 主要数据结构

    1. SSTables
    2. Memtable

SSTables

  1. Key按顺序存储到文件中,称为segment
  2. 包含多个segment
  3. 每个segment写入磁盘后都是不可更改的,新加的数据只能生成新的segment

Memtable

  1. 在内存中的数据保存在memtable中,大多数实现都是一颗Binary search tree
  2. 当mentable存储的数据到达一定的阈值的时候,就会按顺序写入到磁盘

数据查询

  • 需要从最新的segment开始遍历每个key
  • 也可以为每个segment建一个索引

Compaction(合并):

  • Compaction指将多个segment合并成一个segment的过程
  • 一般是有一个后台线程完成
  • 不同的segment写入新的segment的时候也需要排序,形成新的segment之后,旧的segment文件就会被删除

主键索引

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

数据被划分为granules

  1. granules是最小的数据读取单元

  2. 不同的granules可以并行读取

  3. 每个granules都对应primary.idx里面的一行

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

  5. 里面保存的每一行数据称为一个index mark,每个列都有这样一个mark文件

    1. mark文件保存的是每个granules的物理地址

    2. 每一列都有一个自己的mark文件

    3. mark文件里面的每一行保存两个地址

      1. block_offset:用于定位一个granule的压缩数据在物理文件中的位置,压缩数据会以一个block为单位解压到内存中
      2. granule_offset:用于定位一个granule在解压后的block中的位置

缺陷:

  • 数据按照key的顺序做排序,因此只有第一个key的过滤效果好,后面的key过滤效果依赖第一个key的基数大小

1.2 查询优化

secondary_index:在URL列上构建二级索引

构建多个主键索引

  • 再建一个表,使用需要优化的字段做主键第一位

    create table hits_URL_UserID
    (
        '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;
    
    1. 数据需要同步两份
    2. 查询需要用户判断查哪张表
  • 建一个物化视图

    • 物化视图:可以通过select查询将一个表的数据写入一张隐式表

    1. 数据自动同步到隐式表
    2. 查询需要用户判断查哪张表
  • 使用Projection

    • Projection:类似于物化视图,但是不是将数据写入新的表,而是存储在原始表中,以一个列文件的形式存在

    1. 数据自动同步到隐式表
    2. 查询自动路由到最优的表

小结

  • 主键包含的数据顺序写入
  • 主键构造一个主键索引
  • 每个列构建一个稀疏索引
  • 通过mark的选择让主键索引可以定位到每一列的索引
  • 可以通过多种手段优化非主键列的索引

1.3 数据合并

一个part内的数据是有序的

不同part之间的数据是无序的

数据合并是将多个part合并成一起的过程

数据的可见性:

  1. 数据合并过程中,未被合并的数据对查询可见
  2. 数据合并完成后,新part可见,被合并的part被标记删除

1.4 数据查询

  1. 通过主键找到需要读的mark
  2. 切分mark,然后并发的调度reader
  3. Reader通过mark block_offset得到需要读的数据文件的偏移量
  4. Reader通过mark granule_offset得到解压后数据的偏移量
  5. 构建列式filter做数据过滤

2. ClickHouse应用场景

2.1 大宽表存储和查询

  1. 大宽表查询

    • 可以建非常多的列
    • 可以增加,删除,清空每一列的数据
    • 查询的时候引擎可以快速选择需要的列
    • 可以将列涉及到的过滤条件下推到存储层,从而加速查询
  2. 动态表结构

    • map中的每个key都是一列
    • map中的每一列都可以单独查询
    • 使用方式同普通列,可以做任何运算

2.2 离线数据分析

  1. 数据导入

    • 数据可以通过spark生成clickhouse格式的文件
    • 导入到hdfs上由hive2ch导入工具完成数据导入
    • 数据直接导入到各个物理节点
  2. 数据按列导入

    • 保证查询可以及时访问已有数据
    • 可以按需加载需要的列

2.3 实时数据分析

使用memory table减少parts数量

  1. 数据先缓存在内存中
  2. 到达一定阈值再写到磁盘

复杂类型查询

  1. bitmap索引(构建、查询)

  2. bitmap64类型

  3. lowcardinality

    • 对于低基数列使用字典编码
    • 减少数据存储和读写的IO使用
    • 可以做运行时的压缩数据过滤

总结

  1. ClickHouse是标准的列存结构
  2. 存储设计是LSM-Tree架构
  3. 使用稀疏索引加速查询
  4. 每个列都有丰富的压缩算法和索引结构
  5. 基于列存设计的高效设计处理逻辑