
ClickHouse有2类解析器,一类是完整SQL解析器(递归式解析器),一类是数据格式解析器(快速流式解析器)。除了 INSERT 查询,其它情况下仅使用完整SQL解析器。因此ClickHouse基本兼容sql语法,同时ClickHouse也具有其独特的查询函数。
1. 数据查询
废话不多说,直接切入主题。
1.1 with子句
支持cte,即公用表表达式。如:
select pow(pow(2,2),3)
相当于 :
with pow(2,2) as a select pow(a,3)
1.定义变量
with 10 as start
select * from table where num > start;
2.调用函数
with sum(num) as bytes
select database,formatReadableSize(bytes) as format from table group by database
3.定义子查询
需注意with的子查询语句只能返回一行。
with (
select sum(num) from table
) as total_bytes
select database,(sum(num) / total_bytes) as usage from table group by database
4.子查询中重复使用with
对结果集中的usage再次使用取整函数。
with (
round(usage)
) as usage_v1
select database,usage,usage_v1
from (
with (
select sum(num) from table
) as total_bytes
select database,(sum(num) / total_bytes) as usage from table group by database
)
1.2 from子句
表示从何处读数据。对象包括:
数据表
子查询
表函数
比如从函数numbers()读取:
select number from numbers(5)
1.3 sample子句
数据采样,取部分数据进行近似计算。采用幂等设计,数据不变化,多次采样结果相同。适用MergeTree系列引擎的数据表,在建表时声明。
需注意以下几点:
按照intHash32(UserID)分布后的结果采样查询
sample by表达式必须同时包含在主键声明中
sample key 必须是int类型,否则查询报错
create table new_table(
CounterID UInt64,
UserID UInt64,
EventDate DATE
) ENGINE = MergeTree()
partition by toYYYYMM(EventDate)
order by (CounterID, intHash32(UserID))
sample by intHash32(UserID)
1.SAMPLE factor
按因子系数采样,取值支持0~1之间的小数。如果factor设置为0或者1,则效果等同于不进行数据采样。
select CounterID from new_table sample 0.1
select CounterID from new_table sample 1/10 (十进制)
查询结果需要乘以采样系数。如果因子系数为0.1,那么采样系数为10
select CounterID * 10 from new_table sample 0.1
select CounterID,_sample_factor from new_table sample 0.1
select CounterID * any(_sample_factor) from new_table sample 0.1
2.sample rows
按样本数量采样,rows大于1或大于总行数等同于不使用采样。采样数据的最小粒度是由index_granularity索引粒度决定的。即设置一个小于索引粒度或者较小的rows值没有什么意义,应该设置一个较大的值。
select CounterID from new_table sample 10000
3.sample factor offset n
按因子系数和偏移量采样。factor表示采样因子,n表示偏移多少数据后才开始采样,它们两个的取值都是0~1之间的小数。
在计算OFFSET偏移量后,按照SAMPLE比例采样出现了溢出,则数据会被自动截断。
比如,从数据的二分之一处开始,按0.1的系数采样数据:
select CounterID from new_table sample 0.1 offset 0.5
1.4 array join子句
数组元素的行转列。注意一条select中只能有一条array join。
比如,下面演示的是将数组的元素按列输出:
food [1,2,3] --->
food 1
food 2
food 3
1.INNER ARRAY JOIN
数组被展开成了多行,并且排除掉了空数组。如果为原有的数组字段添加一个别名,则能够访问展开前的数组字段。
select title,value,v from new_table array join value as v;
2.LEFT ARRAY JOIN
支持LEFT连接策略。数组被展开成了多行,并且不会排除空数组。同时对多个数组字段进行ARRAY JOIN操作时,查询的计算逻辑是按行合并而不是产生笛卡儿积。
select title,value,v from new_table left array join value as v;
select title,value,v,arrayMap(x -> x*2,value) as mapv,v_1 from new_table left array join value as v , mapv as v_1;
* 嵌套数据类型的本质是数组,array join支持嵌套数据类型
创建嵌套类型:
create table new_table(
title String,
nest Nested(
v1 UInt32,
v2 UInt32
)
)ENGINE = Log
往上表中写入数据:
insert into new_table values ('food',[1,2,3],[10,20,30]),('fruit',[4,5],[40,50]),('meat',[],[])
写入的数据需要注意:(1)同一行数据各数组长度对齐,(2)多行之间长度无限制。对该表进行查询:
select title,nest.v1,nest.v2 from new_table;
title nest.v1 nest.v2
food [1,2,3] [10,20,30]
fruit [4,5] [40,50]
meat [] []
select title,nest.v1,nest.v2 from new_table array join nest
select title,nest.v1,nest.v2 from new_table array join nest.v1,nest.v2
1.5 join子句
join子句连接精度分为ALL(默认)、ANY和ASOF三种,连接类型分为外连接、内连接和交叉连接三种。
1.连接精度
all:如果左表内的一行数据,在右表中有多行数据与之连接匹配,则返回右表中全部连接的数据。
A all inner join B on
any:如果左表内的一行数据,在右表中有多行数据与之连接匹配,则仅返回右表中第一行连接的数据。
A any inner join B on
asof:模糊连接,它允许在连接键之后追加定义一个模糊连接的匹配条件asof_column。且仅返回右表中第一行连接匹配的数据。
A asof inner join B on A.id=B.id and a.time = b.time
等价于:A.id=B.id and a.time >= b.time
* 支持使用USING的简写形式
A asof inner join B using(id,time)
还需注意:(1)sof_colum必须是整型、浮点型和日期型这类有序序列的数据类型;(2)asof_colum不能是数据表内的唯一字段,即连接键(JOIN KEY)和asof_colum不能是同一个字段。
2.连接类型
交集、并集、笛卡尔积或其他。
inner(交集)
INNER JOIN表示内连接,在查询时会以左表为基础逐行遍历数据,然后从右表中找出。与左边连接的行,它只会返回左表与右表两个数据集合中交集的部分,其余部分都会被排除。
OUTER(可省略该修饰符)
OUTER JOIN表示外连接,它可以进一步细分为左外连接(LEFT)、右外连接(RIGHT)和全外连接(FULL)三种形式。
left:左表全部返回,右表补充左表属性,没有匹配则填充默认值。
right:右表全部返回,左表补充右表属性,没有匹配则填充默认值。底层实现:内部进行inner连接计算交集,并记录右表未被连接的行追加至交集尾部。
full:返回左右表的并集,没有匹配泽填充默认值。底层实现:内部进行left连接计算左表,并记录右表未被连接的行追加至交集尾部。
CROSS(交叉连接,笛卡尔积)
返回左右表的笛卡尔积,不需要声明join key,结果包含所有组合。底层实现:以左表为基础,逐行与右表全集相乘。
3.多表连接
多表查询转换为两两连接的形式。关联查询会被转换为指定的连接查询,规则为:(在复杂业务查询场景下无法确定最终的意图,不建议使用)
(1)转换为cross join:查询语句不包含where
select a.id,b.name,c.date from tb1 as a,tb2 as b,tb3 as c;
(2)转换为inner join:查询语句包含where
select a.id,b.name,c.date from tb1 as a,tb2 as b,tb3 as c where a.id=b.id and a.id=c.id;
4.join 注意事项
(1)性能
优化性能,遵循左大右小。因为无论哪种连接方式,右表会全部加载。
join查询没有缓存支持,每次查询都需要重新连接。建议使用上层应用缓存或者join表引擎。
大量维度属性补全的查询场景,建议使用字典代替join查询。多表查询,两两连接会滚雪球,中间表越来越大。
(2)空值策略和简写形式
空值策略join_use_nulls参数指定,默认为0,使用数据类型的默认值;为1,使用Null填充。
join key简写,连接字段相同时使用using
select a.id,b.name from tb1 as a join tb2 as b on a.id=b.id;
select a.id,b.name from tb1 as a join tb2 as b using id;
1.6 where和prewhere
1.WHERE子句基于条件表达式来实现数据过滤。
如果过滤条件恰好是主键字段,则能够进一步借助索引加速查询(是否启用索引的依据)。
2.PREWHERE只能用于MergeTree系列的表引擎。
首先只会读取PREWHERE指定的列字段数据,用于数据过滤的条件判断。待数据过滤之后再读取SELECT声明的列字段以补全其余属性。处理的数据量更少,性能更高。
3.where自动优化为prewhere
默认情况是自动进行优化的,也可以使用以下操作命令强制开闭该功能:
set optimize_move_to_prewhere=0 强制关闭自动优化,
set optimize_move_to_prewhere=1 强制开启自动优化(默认)。
也存在不会自动优化的情况:
使用常量表达式
使用默认值为alias类型的字段
包含了arrayJoin、globalIn、globalNotIn或者indexHint的查询
SELECT查询的列字段与WHERE谓词相同
使用了主键字段
虽然不会自动优化,但是以上场景依然可以手动使用prewhere。
1.7 group by 聚合查询
Group By聚合是ClickHouse最凸显性能的地方。
Group By后面声明聚合字段,一般声明后只能使用这些字段,一些场合也可以使用any、max、min等聚合函数访问其他字段。可以配合WITH ROLLUP、WITH CUBE和WITHTOTALS三种修饰符获取额外的汇总信息。在Group by中null值将作为特定值处理。
1.修饰词
WITH ROLLUP
ROLLUP能够按照聚合键从右向左上卷数据,基于聚合函数依次生成分组小计和总计。
比如:
select project,appid,count() from lpm.lpmclientpoint where day='2020-06-01' group by project,appid with rollup order by project
其将会生成一行所有数据总量的信息,一行对project分组的总数信息。
WITH CUBE
基于聚合键之间所有的组合生成小计信息。如果设聚合键的个数为n,则最终小计组合的个数为2的n次方。
比如:
select project,appid,count() from lpm.lpmclientpoint where day='2020-06-01' group by project,appid with cube order by project
其将会生成一行所有数据总量的信息,一行对project分组的总数信息,一行对appid分组的总数信息。
WITH TOTALS
基于聚合函数对所有数据进行总计,结果附加了一行Totals汇总合计。
2.HAVING子句
与Group By同时出现,在聚合计算之后实现二次过滤数据。where是先筛选数据再聚合过滤,having是先聚合再过滤,where效率比having要高。
3.ORDER BY子句
表属性中的排序决定了分区,如果在查询时数据跨越了多个分区,则它们的返回顺序是无法预知的,每一次查询返回的顺序都可能不同。在这种情形下,如果需要数据总是能够按照期望的顺序返回,就需要借助ORDER BY子句来指定全局顺序。
如果不写asc、desc,默认使用升序asc。
比如:
select * from new_table order by a asc,b desc;
等价于:select * from new_table order by a,b desc; (未声明则使用asc)
* 针对null值排序策略:
nulls last
默认,null排在后面。顺序为 value、NaN、NULL
null first
null排在最前。顺序为NULL、NaN、其他值(value)
4.LIMIT BY子句
最多返回前n行数据,TOP N的查询场景。by后面的列名用于选取数量。limit 3 by a (选取出a的排名前三的数据,多个分组后,可能有好几个数据会返回,但是a只有三种类型)
LIMIT BY也支持跳过OFFSET偏移量获取数据。
5.LIMIT子句
用法如下:
limit n 返回前n行
limit n offset m 从m行开始返回n行
limit m,n 从m行开始返回n行
limit确保每次返回数据唯一,需要和order by一起使用。
6.SELECT子句
放在开头,最后执行。
比如,基于正则查询:
select columns('^n'),column('^p') from new_table;
返回以n及包含p开头的列字段。
7.DISTINCT子句
去重。但相比于group by,dinstinct的执行计划更简单。
select distinct a from new_table; (返回a的所有去重值)
select name from new_table group by a;
当DISTINCT与ORDER BY同时使用时,其执行的优先级是先DISTINCT后ORDER BY。并且所有NULL值归为一组。
8.UNION ALL子句
联合左右两边的两组子查询,将结果一并返回。一次查询可以声明多次的UNION ALL。UNION ALL不能直接使用其他子句(例如ORDER BY、LIMIT等),这些子句只能在它联合的子查询中使用。
首先,列字段的数量必须相同;其次,列字段的数据类型必须相同或相兼容;最后,列字段的名称可以不同,查询结果中的列名会以左边的子查询为准。
目前ClickHouse只支持UNION ALL子句,如果想得到UNION DISTINCT子句的效果,可以使用嵌套查询来变相实现。
select distinct name from
(
select name,v1 from table_1
union all
select title,v1 from table_2
)
2. SQL执行计划
2.1 查看SQL执行计划
clickhouse可以借助后台服务日志,来达到查看sql执行计划的目的,进一步我们可以根据执行计划对sql语句进行优化。
客户端打印查询日志:
clickhouse-client -h 127.0.0.1 --send_logs_level=trace <<< 'select * from new_table' > /dev/null
具体的操作为:
通过将ClickHouse服务日志设置到DEBUG或者TRACE级别,可以变相实现EXPLAIN查询,以分析SQL的执行日志。
需要真正执行了SQL查询,ClickHouse才能打印计划日志,所以如果表的数据量很大,最好借助LIMIT子句以减小查询返回的数据量。
在日志中,分区过滤信息部分:selected xxx parts by date。其中by date是固定的,无论我们的分区键是什么字段,这里都不会变。这是由于在早期版本中,MergeTree分区键只支持日期字段。
不要使用SELECT * 全字段查询。
尽可能利用各种索引(分区索引、一级索引、二级索引),这样可避免全表扫描。
2.2 查看全字段全表扫描
打印查询日志中的相关关键信息:
Expression:查询线程
key condition:unknow 未使用主键索引
MinMax index condition:unknow 未使用分区索引
selected 12 parts 扫描分区数及MarkRange
read 80000 rows 读取的数据行数及大小
MemoryTracker:peak memory usage (for query) 消耗内存峰值
2.3 查看单字段全表扫描
读取部分字段相比于读取全部字段,内存峰值会降低很多。通过以下语句可以验证:
clienthouse-client -h 127.0.0.1 --send_logs_level=trace <<< 'select a from new_table' > /dev/null
clienthouse-client -h 127.0.0.1 --send_logs_level=trace <<< 'select * from new_table' > /dev/null
虽然都是会扫描所有分区,读取所有数据,但是结果集只有a列数据,比全集少很多。
2.4 查看使用分区索引
通过增加where子句,可以使用分区索引。以下语句可以验证:
select a from new_table where date='2020-12-01'
查看打印的查询日志,可以看到where自动优化为prewhere,查询中启动分区索引,只扫描部分分区。
2.5 使用主键索引
启用主键索引,分区扫描减少,markrange减少。可以通过以下语句在打印日志中查看到:
select a from new_table where date='2020-12-01' and ID=110;(ID为索引关键词)
* 下期是关于ClickHouse集群相关的高可用配置。
参考资料:
[1] Yandex.clickhouse官方文档[EB/OL]:https://clickhouse.tech/docs/en/
[2] 朱凯.ClickHouse原理解析与应用实践[M]




