Clickhouse Projection 特性探索

Posted GrowingIO

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Clickhouse Projection 特性探索相关的知识,希望对你有一定的参考价值。

​年初的clickhouse meetup上快手团队分享了clickhouse projection在其公司内部的实践。分享包括了projection原理、使用、性能测试等内容。从性能测试的数据上看,projeciton对查询性能有着百倍级别的提升,意味着之前分钟级的查询响应延迟,将会提升到秒级响应。秒级的查询响应延迟,将会提升到毫秒级的响应,对于使用者将会有更加完美的体验。

看完了快手同学的clickhouse projeciton的分享,在我脑中也产生了几个问题?

  1. 没有projection功能之前,clickhouse还存在什么问题?
  2. clickhouse projection如何解决的问题?
  3. clickhouse projection适用于哪些场景?
  4. clickhouse proejction有什么要注意的吗?

没有projection功能之前,clickhouse还存在什么问题?

clickhosue作为一款olap引擎,处于数据平台中的最顶层,直接对接平台用户。查询性能的好坏,直接决定着用户的使用体验。

  1. clickhouse的查询性能虽然已经非常完美,但是面对超大数据量的场景还是会存在一定的问题,原因是clickhouse是基于内存计算的MPP架构分析型数据库,与Spark, Hive, MR等计算框架不同,计算 过程中的临时数据没有磁盘选项。查询过程中,数据会加载到内存中。如果内存配置不够,将会导致查询失败,对clickhouse集群的稳定也会有一定的影响。
  2. 用户在数据查询的场景中,会有着一定的使用习惯。比如,每天定时都会查看一些特定的图表。这些图表中包含全量的数据统计,复杂的数据查询逻辑等。这些查询相较于其他查询,可以归属于异常查询。这些查询可能因为内存问题导致查询失败,也可能因为复杂的计算逻辑导致查询时间过长,影响平台上其他用户的查询。

clickhouse projection如何解决的问题?

在OLAP领域中,根据数据模型主要分为ROLAP(Relational OLAP) 关系OLAP,MOLAP(Multidimension OLAP) 多维OLAP 两种。ROLAP将数据表达为二维关系模型,类似关系型数据库模型,数据表达能力较好,对外提供SQL接口。MOLAP将OLAP分析所用到的多维数据物理上存储为多维数组的形式,形成“立方体”的结构。维的属性值被映射成多维数组的下标值或下标的范围,而汇总数据作为多维数组的值存储在数组的单元中,采用预聚合的思想,加速数据查询,但是数据模型不够灵活。

clickhouse作为ROLAP典型代表之一,纯列式存储单表查询性能几乎没有对手。projection 名字起源于vertica,相当于传统意义上的物化视图。它借鉴 MOLAP 预聚合的思想,在数据写入的时候,根据 projection 定义的表达式,计算写入数据的聚合数据同原始数据一并写入。数据查询的过程中,如果查询SQL通过分析可以通过聚合数据得出,直接查询聚合数据减少计算的开销,解决了由于数据量导致的内存问题。

projeciton 底层存储上属于part目录下数据的扩充,可以理解为查询索引的一种形式。

从数据写入逻辑的核心代码上看(clickhouse version 21.7),多个projection在part目录下以多个子目录存储,projection目录下存储基于原始数据聚合的数据。所以,projection写入与原始数据写入同步,只有创建projection之后写入的数据才会被物化,保证数据的一致性。

MergeTreeDataWrite.cpp.390

如果存在projection配置,将projection part添加new_data_part中。
if (metadata_snapshot->hasProjections())
{
    for (const auto & projection : metadata_snapshot->getProjections())
    {
        /// 1. 获取projection query的执行计划。
        /// 2. 当前Block作为输入,计算聚合结果
        /// 3. 获取数据流
        auto in = InterpreterSelectQuery(
                      projection.query_ast,
                      context,
                      Pipe(std::make_shared<SourceFromSingleChunk>(block, Chunk(block.getColumns(), block.rows()))),
                      SelectQueryOptions{
                          projection.type == ProjectionDescription::Type::Normal ? QueryProcessingStage::FetchColumns : QueryProcessingStage::WithMergeableState})
                      .execute()
                      .getInputStream();
        in = std::make_shared<SquashingBlockInputStream>(in, block.rows(), std::numeric_limits<UInt64>::max());
        in->readPrefix();
        // 4. 读取prjeciton计算的数据块
        auto projection_block = in->read();
        if (in->read())
            throw Exception("Projection cannot grow block rows", ErrorCodes::LOGICAL_ERROR);
        in->readSuffix();
        if (projection_block.rows())
        {
            // 5. 将聚合的数据(.proj)添加到new_data_part中
            new_data_part->addProjectionPart(projection.name, writeProjectionPart(projection_block, projection, new_data_part.get()));
        }
    }
}

从文件系统目录上看,p2.proj 为data part下p2 projection的数据目录,目录下聚合列,聚合函数作为单独的列存文件存储。


├── dim1.bin
├── dim1.mrk2
├── dim2.bin
├── dim2.mrk2
├── dim3.bin
├── dim3.mrk2
├── event_key.bin
├── event_key.mrk2
├── event_time.bin
├── event_time.mrk2
├── p2.proj
│   ├── checksums.txt
│   ├── columns.txt
│   ├── count%28%29.bin
│   ├── count%28%29.mrk2
│   ├── count.txt
│   ├── default_compression_codec.txt
│   ├── dim3.bin
│   ├── dim3.mrk2
│   ├── groupBitmap%28user%29.bin
│   ├── groupBitmap%28user%29.mrk2
│   └── primary.idx

clickhouse projection适用于哪些场景?

为了探索projection适用于哪些场景,准备了典型的用户行为数据集,数据量为1亿条, 数据模型选择事件模型,模型中包含了用户做过什么事件,以及事件对应的维度。

维度选择上,dim1,dim2为普通维度值,维度值种类有10种。dim3为高基维维度,维度值种类有100000种。

event_key事件标识
event_time事件时间
dim1普通维度
dim2普通维度
dim3高基维度

如何为数据表构建projection?

  1. 建表的时候指定多个projection 定义,projection中为基本的select语句,可以省略from table子句,默认与源表保持一致。
CREATE TABLE event_projection1
(
    `event_key` String,
    `user` UInt32,
    `event_time` DateTime64(3, \'Asia/Shanghai\'),
    `dim1` String,
    `dim2` String,
    `dim3` String,
    PROJECTION p1
    (
        SELECT
            groupBitmap(user),
            count(1)
        GROUP BY dim1
    )
)
ENGINE = MergeTree()
ORDER BY (event_key, user, event_time)
  1. alter table 语句补充projection定义

ALTER TABLE event_projection1

ADD PROJECTION p2
(
    SELECT
        count(1),
        groupBitmap(user)
    GROUP BY dim1, dim3
)

怎么查询才能命中projection?

  1. select表达式必须为projection定义中select 表达式的子集。
  2. group by clause必须为projection定义中group by clause的子集。
  3. where clause key必须为projeciton定义中的group by column的子集。

如何知道是否命中了projection?

  1. explain查看执行计划,ReadFromStorage (MergeTree(with projection)) 表示命中projection
EXPLAIN SQL
expain actions=1 select dim, count(1) from event_projection group by dim1


执行计划:
Expression ((Projection + Before ORDER BY))                              
Actions: INPUT :: 0 -> dim1 String : 0                                    
         INPUT :: 1 -> count() UInt64 : 1                                 
Positions: 0 1                                                            
  SettingQuotaAndLimits (Set limits and quota after reading from storage) 
    ReadFromStorage (MergeTree(with projection))
  1. clickouse 查询关键日志
查询命中了projection p
(SelectExecutor): Choose aggregate projection p
(SelectExecutor): projection required columns: dim1, count()
(SelectExecutor): Reading approx. 63 rows with 4 streams

查询效果如何?

|场景: select dim1, count(1) from event_projection group by dim1|

projection定义查询耗时存储插入时间
无projection5.347s650M7min
dim1聚合0.018s654M12min
(dim1 + dim3) 聚合0.319s923M20min
  1. 命中projection相比没有命中projection对于查询性能的提升非常明显。
  2. 构建projection对于存储,数据插入有一定的额外开销。
  3. 如果构建projection的时候混入了高基维度,查询耗时相比没有混入高基维度,查询性能同比降低了近200倍,存储与插入时间也付出了更多的额外开销。

场景: 不同聚合函数性能提升效果对比

聚合函数没有projection普通维度聚合高基维度聚合
count(1)5.347s0.018s0.319s
groupBitmap(user)7.936s0.040s5.840s

 

  1. 相同的条件下groupBitmap没有count聚合函数的性能提升效果好,
  2. 高基维的场景下,即使命中了projection与没有命中projection,查询效果几乎相同, 而且付出了额外的存储计算开销。

综上以上测试可以得出,高基维度对于projection特性并不友好,查询性能提升有限,并且还有付出不小的额外开销,不建议projection构建的时候应用高基维度。

clickhouse projection有什么要注意的吗?

  1. 额外的存储开销

上面有提到,每个projection在part目录下存到单独的目录独立存储,projection目录下存储基于原始数据计算的聚合数据。projection数据可以抽象理解为一张聚合表,按照不同的维度聚合,聚合度不同,projection的存储开销也会同。

  1. 影响数据写入速度

通过源代码分析可以发现, projection写入与原始数据写入过程保持一致。每一份数据part写入都会基于原始数据Block结合projection定义计算聚合数据,增加了数据写入的额外开销,也增加了数据写入的时间,降低了数据的时效性。

  1. 历史数据不会自动物化

projection基于part粒度存储,并且与数据写入保证一致,创建了projection之后插入的数据才会被物化。同时,part之间的merge包含projection之间的merge,如果part之间的projeciton定义不一致,将会导致part merge失败,可以通过projection materialization操作将part中projection数据拉齐。

projection materialization: projection计算基于原始数据block,对于比较大part计算的过程中很容易出现内存问题。可以构建insert select pipeline模拟新数据产生的过程,中间会生成多个临时小part,小part中的proejction进行多段merge。

  1. part过多导致projection不能命中

数据查询命中projeciton其中的一个条件为50%以上的part 覆盖projection。存在部分场景,由于数据频繁写入,导致生成很多小part,part数量增加增大了计算覆盖率的分母,导致没有达成命中projection的条件。但是,伴随着part的合并part数量的减少,之后的查询有可能命中projection。

本篇文章只是针对clickhouse proejction特性进行了简单的介绍,并进行了基础的性能测试。

在性能测试中,也发现了高基维度对于clickhouse projection的影响。后续将会其他文章对 clickhouse 的查询流程,底层存储进行细致的详解,分析其影响的内部原因。

以上是关于Clickhouse Projection 特性探索的主要内容,如果未能解决你的问题,请参考以下文章

ClickHouse 使用物化字段投影 PROJECTION 提升性能

Clickhouse —— PROJECTION 创建物化删除

Clickhouse —— PROJECTION 创建物化删除

Clickhouse —— PROJECTION 创建物化删除

ClickHouse SQL 极简教程使用物化字段投影 PROJECTION 提升性能

2021年ClickHouse最王炸功能来袭,性能轻松提升40倍