年初的clickhouse meetup上快手团队分享了clickhouse projection在其公司内部的实践。分享包括了projection原理、使用、性能测试等内容。从性能测试的数据上看,projeciton对查询性能有着百倍级别的提升,意味着之前分钟级的查询响应延迟,将会提升到秒级响应。秒级的查询响应延迟,将会提升到毫秒级的响应,对于使用者将会有更加完美的体验。
看完了快手同学的clickhouse projeciton的分享,在我脑中也产生了几个问题?
- 没有projection功能之前,clickhouse还存在什么问题?
- clickhouse projection如何解决的问题?
- clickhouse projection适用于哪些场景?
- clickhouse proejction有什么要注意的吗?
没有projection功能之前,clickhouse还存在什么问题?
clickhosue作为一款olap引擎,处于数据平台中的最顶层,直接对接平台用户。查询性能的好坏,直接决定着用户的使用体验。
- clickhouse的查询性能虽然已经非常完美,但是面对超大数据量的场景还是会存在一定的问题,原因是clickhouse是基于内存计算的MPP架构分析型数据库,与Spark, Hive, MR等计算框架不同,计算 过程中的临时数据没有磁盘选项。查询过程中,数据会加载到内存中。如果内存配置不够,将会导致查询失败,对clickhouse集群的稳定也会有一定的影响。
- 用户在数据查询的场景中,会有着一定的使用习惯。比如,每天定时都会查看一些特定的图表。这些图表中包含全量的数据统计,复杂的数据查询逻辑等。这些查询相较于其他查询,可以归属于异常查询。这些查询可能因为内存问题导致查询失败,也可能因为复杂的计算逻辑导致查询时间过长,影响平台上其他用户的查询。
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?
- 建表的时候指定多个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)
- alter table 语句补充projection定义
ALTER TABLE event_projection1
ADD PROJECTION p2
(
SELECT
count(1),
groupBitmap(user)
GROUP BY dim1, dim3
)
怎么查询才能命中projection?
- select表达式必须为projection定义中select 表达式的子集。
- group by clause必须为projection定义中group by clause的子集。
- where clause key必须为projeciton定义中的group by column的子集。
如何知道是否命中了projection?
- 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))
- 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定义 | 查询耗时 | 存储 | 插入时间 |
无projection | 5.347s | 650M | 7min |
dim1聚合 | 0.018s | 654M | 12min |
(dim1 + dim3) 聚合 | 0.319s | 923M | 20min |
- 命中projection相比没有命中projection对于查询性能的提升非常明显。
- 构建projection对于存储,数据插入有一定的额外开销。
- 如果构建projection的时候混入了高基维度,查询耗时相比没有混入高基维度,查询性能同比降低了近200倍,存储与插入时间也付出了更多的额外开销。
场景: 不同聚合函数性能提升效果对比
聚合函数 | 没有projection | 普通维度聚合 | 高基维度聚合 |
count(1) | 5.347s | 0.018s | 0.319s |
groupBitmap(user) | 7.936s | 0.040s | 5.840s |
- 相同的条件下groupBitmap没有count聚合函数的性能提升效果好,
- 高基维的场景下,即使命中了projection与没有命中projection,查询效果几乎相同, 而且付出了额外的存储计算开销。
综上以上测试可以得出,高基维度对于projection特性并不友好,查询性能提升有限,并且还有付出不小的额外开销,不建议projection构建的时候应用高基维度。
clickhouse projection有什么要注意的吗?
- 额外的存储开销
上面有提到,每个projection在part目录下存到单独的目录独立存储,projection目录下存储基于原始数据计算的聚合数据。projection数据可以抽象理解为一张聚合表,按照不同的维度聚合,聚合度不同,projection的存储开销也会同。
- 影响数据写入速度
通过源代码分析可以发现, projection写入与原始数据写入过程保持一致。每一份数据part写入都会基于原始数据Block结合projection定义计算聚合数据,增加了数据写入的额外开销,也增加了数据写入的时间,降低了数据的时效性。
- 历史数据不会自动物化
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。
- part过多导致projection不能命中
数据查询命中projeciton其中的一个条件为50%以上的part 覆盖projection。存在部分场景,由于数据频繁写入,导致生成很多小part,part数量增加增大了计算覆盖率的分母,导致没有达成命中projection的条件。但是,伴随着part的合并part数量的减少,之后的查询有可能命中projection。
本篇文章只是针对clickhouse proejction特性进行了简单的介绍,并进行了基础的性能测试。
在性能测试中,也发现了高基维度对于clickhouse projection的影响。后续将会其他文章对 clickhouse 的查询流程,底层存储进行细致的详解,分析其影响的内部原因。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。