列存储 ClickHouse | ⻘训营笔记

110 阅读3分钟

这是我参与「第五届⻘训营 」笔记创作活动的第18天。

今天的老师介绍了列存储的概念,以及介绍了字节内部使用的开源列存储产品ClickHouse。这篇文章是对课后习题的讲解。

  • 列存和行存的差别是什么,使用场景有什么不同
    • 列存是将数据表按照列排列存入内存中。
      • 由于数据表中相同列数据类型相同,列存可以针对每一列类型做不同的压缩算法,压缩效率高
      • 对于数据汇聚操作效率更高
    • 行存将数据表中按照行排列存入内存中。
      • 行存适合进行UPDATE/INSERT更新,因为数据表中一行的数据在内存中相邻,一次索引就可以进行更新。
  • 列存的优点有哪些
    • 查询时只有涉及到的列会被读取。
    • 投影(Projection)很高效。
    • 任何列都能作为索引。
    • 便于做延迟物化和向量化计算
    • 压缩效率高,每一列可以使用不同的压缩算法
  • 列存的缺点有哪些
    • 选择完成时,被选择的列要重新组装。
    • INSERT/UPDATE比较麻烦。
    • 点查询不适合。
  • 列存适合什么样的索引
    • 统计分析类查询(OLAP,比如数据仓库业务,此类型的表上会做大量的汇聚计算,且涉及的列操作较少,关联、分组操作较多)。
    • 即时查询(查询条件不确定,行存表扫描难以使用索引)
  • ClickHouse的列存是什么样的存储架构 image.png
    • 对于单个表,结构如下
    CREATE TABLE test.test_insert_local
    (
        `p_date` Date,
        `id` Int32
    )
    ENGINE = MergeTree
    PARTITION BY p_date
    ORDER BY id
    SETTINGS index_granularity = 8192
    
    • ENGINE表示使用索引引擎
    • partition是此表的逻辑结构,每个partition对应物理文件夹名字为part
    ├── 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文件夹,都有一个主键索引
      • 每个column都是一个文件,在自己的part文件夹下
      • 每个column都有列索引
  • ClickHouse的索引是怎么设计的
    • 以此表为例子
    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 image.png
    • 数据被组织成granule,引擎处理的最小单位,每个granule都对应primary.idx中的一行
      image.png
      • primary.idx会记录每个granule中的一行,来作为稀疏索引,通常为每个granule第一行,最后一个是最后一行。
    • 每个列都有这样一个mark文件,mark文件存储所有granule在物理文件里面的地址,每一列都有一个mark文件,mark文件里面的每一行存储两个地址
      • 第一个地址称为block_offset,用于定位一个granule的压缩数据在物理文件中的位置,压缩数据会以一个block为单位解压到内存中。
      • 第二个地址称为granule_offset,用于定位一个granule在解压之后的block中的位置。
  • ClickHouse的查询是怎么使用索引的
    • 通过主键找到需要读的mark
    • 切分marks,然后并发的调度reader(因为一次读取多列,组成一行数据)
    • Reader 通过mark block_offset得到需要读的数据文件的偏移量
    • Reader 通过mark granule_offset得到解压之后数据的偏移量
    • 构建列式filter做数据过滤(多个字段合并一行数据)