前面我们介绍MergeTree引擎的时候,唯独建表语句中的 PROJECTION 定义项没有介绍,在了解完二级索引和物化视图后,本篇文章就来细说一下这个非常重要的功能。
在创建MergeTree表的时候可以定义ORDER BY字段,如果基于ORDER BY字段查询,速度会很快,但是ORDER BY字段是固定的,一旦查询非ORDER BY字段或者不是按照ORDER BY字段顺序查询,效率都会降低。此时,我们可以创建二级索引(跳数索引)或者物化视图等,但是二级索引只是一些粗粒度的索引,例如min/max等,而物化视图数据是独立存储的,无法感知删除操作,还会拖累原表写入性能。针对这种情况,ClickHouse还提供了另外一个新功能——Projection。(Projection是ClickHouse 21.6才有的新特性,使用的时候需要注意版本支持。)
Projection类似物化视图但是是part-level存储,在一张表的分区子目录里面有一个独立的projection数据存储,也就是说它和原表分区在一起,数据是同源的,而且提供了一致性保证。这也就意味着如果原表的数据变了,那么projection也会发生变化。其次,projection的使用是无感的,它更像是原表的一个智能索引,对一张表可以创建很多个projection,在查询时会根据算法去自动匹配。如果能匹配上,就用一个最优的projection提供查询加速,如果没有命中还是查原表。有了这些特性以后,projection比使用物化视图方便非常多。
※注意:带有FINAL修饰符的SELECT语句不支持projection。
projection优化默认是不开启的,有两个配置项需要设置:
创建projection有两种方法,第一种是在建表的时候定义,第二种是在后期添加:
-- 建表指定
CREATE TABLE table_name
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
...
INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2,
...
PROJECTION p1 (SELECT WatchID, Title ORDER BY WatchID),
PROJECTION p2 (SELECT UserID, SearchPhrase, count() GROUP BY UserID, SearchPhrase)
) ENGINE = MergeTree()
ORDER BY ...
PARTITION BY ...;
-- ALTER添加
ALTER TABLE table_name ADD PROJECTION p1
(
SELECT
WatchID, Title
ORDER BY WatchID
);
ALTER TABLE table_name ADD PROJECTION p2
(
SELECT
UserID, SearchPhrase, count()
GROUP BY UserID, SearchPhrase
);
再来看一下table_name的底层文件存储结构。

可以发现在对应的分区目录下,多了一个projection子目录,而且目录中的文件内容和外部原表的数据存储结构非常像,只是没有元数据信息。此时,projection的原理就基本明了了。projection基于定义维护了一份独立的数据存储,但是因为是跟踪原表分区目录存在的,所以不需要保存元数据信息。如果projection的定义语句是ORDER BY,则projection的子目录存储引擎就是MergeTree,如果projection的定义语句是GROUP BY,底层存储引擎变成AggregatingMergeTree,所有聚合函数都转换为AggregateFunction。当在查询中,能匹配到的projection时,就会使用部分预处理好的数据直接返回,这也正是projection加速的本质。例如可以定义一个包含GROUP BY的聚合projection,对于明细数据可以直接查原表,聚合数据通过projection获得。
目前projection不支持跨表操作,所以对于有跨表的预聚合的场景,例如join,还是要利用物化视图。