ClickHouse列存储 | 青训营笔记

239 阅读6分钟

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

ClickHouse

关系数据库:

  • 关系数据库是数据以表的形式进行存储,然后各个表之间建立关系

非关系型数据库:

  • 支持存储和操作非结构化或板结构化数据,没有固定的表结构,

且数据间不存在表与表之间的关系,数据之间是独立的

单机数据库:在一台计算机上完成数据的存储和查询的数据库系统

分布式数据库:分布式数据库由位于不同站点的两个或多个文件组成,数据库可以存储在多台计算机上,位于同一个物理位置,或分散在不同的网路上

【使用场景】

OLTP数据库:Online transactional processing,是一种高速分析数据库专为多个用户执行大量事务而设计

OLAP数据库:Online analytical processing 旨在同时分析多个数据维度,帮助团队更好的理解其数据中的复杂关系

OLAP数据库

  • 大量数据的读写,PB级别的存储
  • 多维分析,负载的聚合函数
  • 窗口函数,自定以UDF(用户自定义函数)
  • 离线/实时分析

列式存储

行式存储:数据按行写入 image-20230216164520472.png

列式存储:先写完每列(如写完id,再写name字段)

image-20230216164601213.png

列存储的优点

数据压缩

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

数据处理

  1. 可以选择特定的列做计算而不是读所有列
  2. 对聚合计算友好

延迟物化

  • 物化:将列数据转换为可以被计算或者输出的行数据或者内存数据结果的过程,物化后的数据通常可以用来做数据过滤,聚合计算,Join

  • 延迟物化:尽可能推迟物化操作的发生

    • 缓存友好
    • CPU/内存带宽友好
    • 可以利用到执行计划和算子的优化,如filter
    • 保留直接在压缩列做计算的机会

向量化

  • SIMD:单指令多数据
行存列存
优点数据被保存在一起,Insert/update容易1. 查询时只有涉及到的列会被读取 2.投影很高效 3.任何列都能作为索引 4.便于做延迟物化和向量化计算 5.压缩效率高
缺点选择几列数据时,所有数据也会被读取1.选择完成时,被选择的列要重新组装 2.insert/update比较麻烦 3.点查询不适合
使用场景点查询(返回记录少,基于索引的简单查询) 增、删、改操作多的场景1.统计分析类查询(数据仓库业务)2.即时查询(查询条件不确定,行存表扫描难以使用索引)

存储设计

引擎架构

image-20230216171832272.png

表结构和定义

image-20230216171528587.png

集群结构:主备同步

sql语句

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 //part物理文件夹的名字
│   ├── 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  //part部分
│   ├── 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:逻辑结构
  • 每个column都是一个文件,所有的column文件都在自己的part文件夹下
  • 一个part有一个主键索引
  • 每个column都有列索引

索引设计

  • 对于大数据量,B(B+)数的深度太高
  • 索引数据量太大,多个列如何平衡查询和存储 (LSM-Tree)
  • OLAP场景写入量非常大,如何优化写入

【LSM-Tree】:着重优化顺序写入,数据结构如下:

  • SSTables

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

    • 在内存中的数据保存在memtable中,大多数实现都是B树
    • 当存储的数据到达一定量后顺序写入磁盘

1.主键索引

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;
  1. index_granularity索引粒度这个参数规定了数据按照索引规定排序以后,间隔多少行会建立一个索引的Marks,即索引值,每个主键的一行被称为一个mark
  1. 稀疏索引的意义即是Clickhouse不对所以的列都建立索引(相比较Mysql的B树索引会为每行都建立),而是间隔index_granularity列才建立一个。

2.排序

数据按照主键顺序依次排序,UserID最先,URL其次,然后是EventTime

image-20230216174748029.png

3.granule

每8192行作为一个granule,作为引擎做数据处理的最小数据单位,引擎读取数据不是按照一行一行读取,而是最少读取一个granule

image-20230216174958211.png

4.对应关系

每个granule都对应primary.idx主键索引里面的一行,每个主键的一行被称为一个mark

image-20230216175101706.png

5.mark文件的每一行存储两个地址

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

image-20230216180607486.png

索引的缺陷和优化

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

优化:

  • 二级索引

    • 在URL上构建二级索引(分大小)
  • 构建多个索引

    • 再建一个表(数据需要同步两份,查询需要用户判断查哪张表)
    • 建立一个物化视图(数据自动同步到隐式表,查询需要用户判断查哪张表)
    • 使用Projection投影(数据自动同步到隐式表,查询自动路由到最优的表)

数据查询

SELECT
    URL,
    count(URL) AS Count
FROM hits_UserID_URL
WHERE UserID = 749927693
GROUP BY URL
ORDER BY Count DESC
LIMIT 10

1.通过主键找到需要的mark

2.切分marks,然后并发的调度reader

3.reader通过mark block_offset得到需要读的数据文件的偏移量

4.Reader通过mark granule_offset得到解压后数据的偏移量