Clickhouse技术分享: Projection调研

背景介绍

Clickhouse的查询性能是有目共睹的优秀, 但与之对应单个查询对于机器资源的消耗也是非常巨大的, 导致Clickhouse整体的QPS会比较低.

当用户需要提高QPS时, 往往会通过建立物化视图, 进预计算, 查询时直接走物化视图来进行加速.

但这种方案有两个缺点:

  1. 实际上有多张表, 明细查询可能需要走底表, 聚合查询需要查物化视图, 用户管理起来会有一定麻烦程度
  2. 如果出现慢查询, 需要用户新建一张物化视图, 然后导入数据, 再通过上线变更的方式, 来规避, 整体流程过长

针对以上问题, 业界的预聚合引擎, 类似麒麟都实现了SQL rewrite的功能, 来自动替换用户的查询SQL, 这样上面的问题就直接解决了.

Clickhouse并没有打算基于物化视图的SQL rewrite, 而是实现一个Projection的功能, 号称是DataPart-Level的物化视图

Originated from Vertica

  • Projections are collections of table columns,
  • Projections store data in a format that optimizes query execution

看一下这两个的对比, 这里的Query Routing就是我们需要的能力

Feature Materialized View Projection 备注
Data Consistency NO YES 物化视图需要Merge后保持一致
Schema Consistency NO YES
Query Routing NO YES 就是SQL Rewrite
Query Index Optimization NO YES
Partial Materialization NO Yes (but not recommended) 这个没必要
Complex Queries YES No (May support ARRAY JOIN) Joins或者子查询
Special Engines YES NO Projection不依赖引擎

物化视图在复杂查询上还是有比较大的优势的, 而Clickhouse对于复杂查询的支持很差, 因此Projection的能力已经满足Clickhouse的要求.

使用方法

建表时指定projection

1
2
3
4
5
6
7
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [compression_codec] [TTL expr1],
...
PROJECTION projection_name_1 (SELECT <COLUMN LIST EXPR> [GROUP BY] [ORDER BY]),
...
) ENGINE = MergeTree()

其中projection_name_1就是Projection的名字, 目前语法只支持GROUP BYORDER BY, 且两者不能同时出现.

不支持Join等其他更加复杂的表达式

如果是GROUP BY的话, 底层存储会使用AggregatedMergeTree

数据存储

image-20210727112308287

当创建一个物化视图的时候, 会在DataPart的目录下, 生成以ProjectionName命名的文件夹, 文件夹里面存储着Projection的数据

如上图的tp1.proj目录

DDL语法

添加Projection

1
ALTER TABLE [db.]table ADD PROJECTION name AS SELECT <COLUMN LIST EXPR>  [GROUP BY] [ORDER BY];

删除Projection

1
ALTER TABLE [db.]table DROP PROJECTION name;

物化Projection

1
ALTER TABLE [db.]table MATERIALIZE PROJECTION name [IN PARTITION partition_name];

删除Projection数据

1
ALTER TABLE [db.]table CLEAR PROJECTION name [IN PARTITION partition_name];

语句查询

目前Projection依然是实验特性, 默认是关闭的, 需要通过配置项开启

1
set allow_experimental_projection_optimization=1;

如果需要判断是否命中Projection, 那么可以设置以下配置, 如果没有使用Projection, 那么程序会直接抛出异常

1
set force_optimize_projection=1;

后续就能正常使用Select语句进行查询

使用场景

预聚合

创建一张基表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE default.part_local
(
`P_PARTKEY` UInt32,
`P_NAME` String,
`P_MFGR` LowCardinality(String),
`P_CATEGORY` LowCardinality(String),
`P_BRAND` LowCardinality(String),
`P_COLOR` LowCardinality(String),
`P_TYPE` LowCardinality(String),
`P_SIZE` UInt8,
`P_CONTAINER` LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY P_PARTKEY;

创建Projection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 删除历史Projection
alter table default.part_local drop projection tp;

-- 创建Projection
ALTER TABLE default.part_local
ADD PROJECTION tp
(
SELECT
P_BRAND,
P_CATEGORY,
COUNT(*) AS c,
xxHash32(sum(P_SIZE)) AS s
GROUP BY
P_BRAND,
P_CATEGORY
);

-- 物化历史数据
alter TABLE default.part_local MATERIALIZE PROJECTION tp;

查询使用Projection

1
2
3
4
5
6
SELECT
P_CATEGORY,
count(*) AS c,
sqrt(sum(P_SIZE)) AS s
FROM default.part_local
GROUP BY P_CATEGORY

当查询的依赖的列都在Projection时, 就会触发SQL Rewrite.

另外一个值得注意的是, 对于这个列xxHash32(sum(P_SIZE)) AS s, Projection实际储的是sum(P_SIZE), 而非xxHash32(sum(P_SIZE))

因此在这个查询的Case中, 能够命中sqrt(sum(P_SIZE))查询语法. 如果查询max(P_SIZE), 则无法命中Projection

排序键替换

另外一个常见的场景, 就是用户需要查询两类的索引, 而CK的主键索引类似组合索引, 遵循最左匹配原则.

举个例子, 有个表如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE default.part_local
(
`P_PARTKEY` UInt32,
`P_NAME` String,
`P_MFGR` LowCardinality(String),
`P_CATEGORY` LowCardinality(String),
`P_BRAND` LowCardinality(String),
`P_COLOR` LowCardinality(String),
`P_TYPE` LowCardinality(String),
`P_SIZE` UInt8,
`P_CONTAINER` LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY P_PARTKEY;

有两类常用的查询SQL

1
2
3
select * from default.part_local where P_PARTKEY = 1;

select * from default.part_local where P_BRAND = 'XX';

这时排序键只能满足P_PARTKEY的查询, 即使将排序键设置为(P_PARTKEY,P_BRAND ) , 查询where P_BRAND = 'XX'也无法命中索引.

先前的处理方式, 就只能建一个额外的物化视图

1
2
3
4
5
CREATE MATERIALIZED VIEW  part_mv1 
ENGINE = MergeTree
ORDER BY P_BRAND
as select *
from default.part_local;

查询P_BRAND需要指定物化视图

1
select * from default.part_mv1 where P_BRAND = 'XX';

Clickhouse实际上有二级索引的能力, 但是由于本身主键索引已经稀疏索引了, 二级索引实现为跳数索引, 匹配率就更加差了, 因此在高QPS情况下, 效果非常差

线上只有在低QPS场景, 才允许业务使用

有了Projection后, 使用建一个Projection来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alter table default.part_local drop projection tp1;

ALTER TABLE default.part_local
ADD PROJECTION tp1
(
SELECT
P_PARTKEY,
P_NAME,
P_MFGR,
P_CATEGORY,
P_BRAND,
P_COLOR,
P_TYPE,
P_SIZE,
P_CONTAINER
ORDER BY P_BRAND
);

alter TABLE default.part_local MATERIALIZE PROJECTION tp1;

然后查询P_BRAND就能命中主键索引

1
select * from default.part_mv1 where P_BRAND = 'XX';

这个版本的一个比较大的问题, 一旦开启Projection后, 按照P_PARTKEY查询依然会走Projection, 然后实际上查询原始表的效果最好

源码分析

附录: 资料地址

名称 链接地址
设计文档 ISSUE地址
PullRequest PR地址
Meetup分享PPT 链接地址
Meetup分享视频 观看地址
物化视图Rewrite Calcite官网文档