Clickhouse技术分享: Projection调研
背景介绍
Clickhouse的查询性能是有目共睹的优秀, 但与之对应单个查询对于机器资源的消耗也是非常巨大的, 导致Clickhouse整体的QPS会比较低.
当用户需要提高QPS时, 往往会通过建立物化视图, 进预计算, 查询时直接走物化视图来进行加速.
但这种方案有两个缺点:
- 实际上有多张表, 明细查询可能需要走底表, 聚合查询需要查物化视图, 用户管理起来会有一定麻烦程度
- 如果出现慢查询, 需要用户新建一张物化视图, 然后导入数据, 再通过上线变更的方式, 来规避, 整体流程过长
针对以上问题, 业界的预聚合引擎, 类似麒麟都实现了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 | CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster] |
其中projection_name_1
就是Projection的名字, 目前语法只支持GROUP BY
和ORDER BY
, 且两者不能同时出现.
不支持Join等其他更加复杂的表达式
如果是GROUP BY
的话, 底层存储会使用AggregatedMergeTree
数据存储
当创建一个物化视图的时候, 会在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 | CREATE TABLE default.part_local |
创建Projection
1 | -- 删除历史Projection |
查询使用Projection
1 | SELECT |
当查询的依赖的列都在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 | CREATE TABLE default.part_local |
有两类常用的查询SQL
1 | select * from default.part_local where P_PARTKEY = 1; |
这时排序键只能满足P_PARTKEY
的查询, 即使将排序键设置为(P_PARTKEY,P_BRAND )
, 查询where P_BRAND = 'XX'
也无法命中索引.
先前的处理方式, 就只能建一个额外的物化视图
1 | CREATE MATERIALIZED VIEW part_mv1 |
查询P_BRAND
需要指定物化视图
1 | select * from default.part_mv1 where P_BRAND = 'XX'; |
Clickhouse实际上有二级索引的能力, 但是由于本身主键索引已经稀疏索引了, 二级索引实现为跳数索引, 匹配率就更加差了, 因此在高QPS情况下, 效果非常差
线上只有在低QPS场景, 才允许业务使用
有了Projection后, 使用建一个Projection来解决
1 | alter table default.part_local drop 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官网文档 |