clickhouse学习笔记(二)
EXPLAIN查看执行计划
1 | EXPLAIN [AST | SYNTAX | PLAN | PIPELINE] [setting = value, ...] SELECT ... [FORMAT ...] |
PLAN:用于查看执行计划,默认值。
- header 打印计划中各个步骤的 head 说明,默认关闭,默认值0;
- description 打印计划中各个步骤的描述,默认开启,默认值1;
- actions 打印计划中各个步骤的详细信息,默认关闭,默认值0。
AST :用于查看语法树;
SYNTAX:用于优化语法;
PIPELINE:用于查看 PIPELINE 计划。
- header 打印计划中各个步骤的 head 说明,默认关闭;
- graph 用DOT图形语言描述管道图,默认关闭,需要查看相关的图形需要配合graphviz查看;
- actions 如果开启了graph,紧凑打印打,默认开启。
查看执行计划
1 | explain select database,table,count(1) cnt from system.parts where database in ('datasets','system') group by database,table order by database,cnt desc limit 2 by database; |
可以来查看执行的顺序
1 | EXPLAIN header = 1, actions = 1, description = 1 |
更加详细的查看执行计划
AST语法树
1 | EXPLAIN AST SELECT number from system.numbers limit 10; |
只做了解
语法优化
1 | //先做一次查询 |
建表优化
数据类型
时间字段类型
在clickhouse中能不用String就不用String,DateTime函数不许与转换,执行效率高
1 | create table t_type2( |
空值存储类型
最好不使用null,使用-1或者其他默认值来代替null
分区和索引
分区一般是按照天分区
单表1亿数据,分区大概10-30个最好
必须指定order by的索引列,索引列一般是查询中经常被当作筛选条件的属性会放进来,可以是单一维度,也可以是组合维度,满足查询频率大的在前的原则
表参数
索引粒度建议不要更改
如果表不是必须要保留全量的历史数据建议指定TTL
写入和删除优化
当写入过快报错:
1 | 1. Code: 252, e.displayText() = DB::Exception: Too many parts(304). Merges are processing significantly slower than inserts |
in_memory_parts_enable_wal默认为true,使用wal预写日志,提高写入性能
常见配置
主要在config.xml和users.xml中,基本上都在users.xml中配置
- config.xml的配置项
https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/
- users.xml的配置项
https://clickhouse.tech/docs/en/operations/settings/settings/
CPU资源
配置 | 描述 |
---|---|
background_pool_size |
后台线程池的大小,merge线程就是在该线程池中执行,该线程池不仅仅是给merge线程用的,默认值16,允许的前提下建议改成cpu个数的2倍(线程数)。 |
background_schedule_pool_size |
执行后台任务(复制表、Kafka流、DNS缓存更新)的线程数。默认128,建议改成cpu个数的2倍(线程数)。 |
background_distributed_schedule_pool_size |
设置为分布式发送执行后台任务的线程数,默认16,建议改成cpu个数的2倍(线程数)。 |
max_concurrent_queries |
最大并发处理的请求数(包含select, insert等),默认值100,推荐150(不够再加)~300。 |
max_threads |
设置单个查询所能使用的最大cpu个数,默认是cpu核数。 |
内存资源
配置 | 描述 |
---|---|
max_memory_usage |
此参数在 users.xml 中,表示单次 Query 占用内存最大值,该值可以设置的比较大,这样可以提升集群查询的上限。保留一点给 OS,比如128G内存的机器,设置为100GB。 |
max_bytes_before_external_group_by |
一般按照 max_memory_usage 的一半设置内存,当 group 使用内存超过阈值后会刷新到磁盘进行。因为 ClickHouse 聚合分两个阶段:查询并及建立中间数据、合并中间数据,结合上一项,建议50GB。 |
max_bytes_before_external_sort |
当 order by 已使用 max_bytes_before_external_sort 内存就进行溢写磁盘(基于磁盘排序),如果不设置该值,那么当内存不够时直接抛错,设置了该值 order by 可以正常完成,但是速度相对存内存来说肯定要慢点(实测慢的非常多,无法接受)。 |
max_table_size_to_drop |
此参数在 config.xml 中,应用于需要删除表或分区的情况,默认是50GB,意思是如果删除50GB以上的分区表会失败。建议修改为0,这样不管多大的分区表都可以删除。 |
Clickhuose语法优化规则
官网有准备的测试数据,可以使用官网的测试数据进行测试
Count优化
当使用count()/count(*)的时候,而不是count(字段),且没有 where 条件,就会直接使用 system.tables 的 total_rows
消除子查询的重复字段
1 | EXPLAIN SYNTAX SELECT |
谓词下推
当group by中有having子句,但是没有with cube,with rollup或者with totals的时候,having过滤会先下推到where去提前过滤,然后再group by,提高性能
示例
1 | EXPLAIN SYNTAX SELECT UserID FROM hits_v1 GROUP BY UserID HAVING UserID = '8585742290196126178'; |
1 | EXPLAIN SYNTAX |
聚合计算外推
1 | EXPLAIN SYNTAX |
聚合函数消除
如果对聚合键,也就是 group by key 使用 min、max、any 聚合函数,则将函数消除
1 | EXPLAIN SYNTAX |
删除重复的order by
1 | XPLAIN SYNTAX |
s删除重复的limit by key
1 | EXPLAIN SYNTAX |
删除重复的Using key
1 | EXPLAIN SYNTAX |
三元运算优化
1 | EXPLAIN SYNTAX |
查询优化
单表查询
Prewhere替代where
Prewhere只支持MergeTree引擎的表,当查询列多余筛选列的时候使用prewhere可十倍提高性能
数据采样
1 | SELECT Title,count(*) AS PageViews |
随机采样百分之10的数据
列裁剪和分区裁剪
数据量太大的时候避免使用select * 的操作,查询的性能会跟随字段越少,消耗的io越少,资源越高
1 | 反例: |
分区裁剪是指定where分区,只读取需要的分区
orderby结合where和limit
千万以上数据集进行order by查询时需要搭配where条件和limit语句一起使用。
1 | #正例: |
不要构建虚拟列
1 | 反例: |
uniqCombined替代distinct
1 | 反例: |
使用物化视图
详见后文
其他注意事项
配置join_use_nulls
这个是配置了如果当join没有join上的话返回的不是null而是默认值
1
2
3
4SET join_use_nulls = 1;
SELECT ...
FROM hits_v1 AS a
LEFT JOIN visits_v1 AS b ON a.UserID = b.UserID;关闭虚拟内存
当物理内存和虚拟内存的数据进行交换的时候会导致查询变慢,资源允许的话关闭虚拟内存
批量写入时先排序
当批量写入数据之前最好对导入的数据先排序,会更快的使clickhouse进行合并
多表关联
准备数据
1 | #创建小表 |
这里的第二个语句中的where 1 = 0
是一个永久的假条件,目的是直接按照hits_v1的表结构创建hits_v2,避免控制台打印数据,这样就可以只创建表结构,但是不插入数据了
用IN代替JOIN
当多表联查的时候,查询的数据仅仅从一张表出,那么就可以使用in而不使用join
1 | insert into hits_v2 |
大小表join
clickhouse中不管是left join还是right join还是inner join都是小表必须在右边
1 | insert into table hits_v2 |
分布式表使用global
两张分布式表上的IN和JOIN之前必须加上GLOBAL关键字,右表只会在接收查询请求的那个节点查询一次,并将其分发到其他节点上。如果不加GLOBAL关键字的话,每个节点都会单独发起一次对右表的查询,而右表又是分布式表,就导致右表一共会被查询N²次(N是该分布式表的分片数量),这就是查询放大,会带来很大开销。
使用字典表
可以在clickhouse中创建一个外部表的形式来创建一个表
比如我们现在有一张表在mysql中,我们需要在clickhouse中创建一个这个mysql的外部表,关联这个外部表做处理
示例
在mysql中执行下面的操作
1 | CREATE DATABASE dic_test_db; |
在clickhouse中执行下面的操作:
1 | CREATE DICTIONARY dic_loc_info |
这里的字典数据的刷新时间根据业务需求来定
查询的时候需要在clickhouse中使用dictGet函数来进行查询数据
1 | SELECT dictGet('dic_loc_info', 'local_name', toUInt64(1)) AS location_name; |
这个查询将返回 local_id
为1的记录的 local_name。
这样我们就在clickhouse中创建了一个外部的字典表
字典数据通常被加载到内存中,用于小型且频繁访问的数据。这可以节省存储空间和I/O操作,提高查询效率。
数据一致性(重点)
我们在使用 ReplacingMergeTree、SummingMergeTree 这类表引擎的时候,会出现短暂数据不一致的情况。
在某些对一致性非常敏感的场景,通常有以下几种解决方案。
准备数据:
1 | CREATE TABLE test_a( |
手动进行OPTIMIZE
不推荐这种方式,每次插入新数据的时候都手动合并一次
1
OPTIMIZE TABLE test_a FINAL;
通过group by去重
- 执行去重的查询 argMax(field1,field2):按照 field2 的最大值取 field1 的值。
1
2
3
4
5
6
7
8SELECT
user_id ,
argMax(score, create_time) AS score,
argMax(deleted, create_time) AS deleted,
max(create_time) AS ctime
FROM test_a
GROUP BY user_id
HAVING deleted = 0;
当我们更新数据时,会写入一行新的数据,例如上面语句中,通过查询最大的 create_time 得到修改后的score字段值。
创建视图,方便测试
视图不存储数据,只存储操作,当调用视图的时候也就是把一个复杂操作重新执行一遍
1
2
3
4
5
6
7
8
9CREATE VIEW view_test_a AS
SELECT
user_id ,
argMax(score, create_time) AS score,
argMax(deleted, create_time) AS deleted,
max(create_time) AS ctime
FROM test_a
GROUP BY user_id
HAVING deleted = 0;插入重复数据,再次查询
1
2
3
4
5
6
7#再次插入一条数据
INSERT INTO TABLE test_a(user_id,score,create_time) VALUES(0,'AAAA',now())
#再次查询
SELECT *
FROM view_test_a
WHERE user_id = 0;删除数据测试
1
2
3
4
5
6
7#再次插入一条标记为删除的数据
INSERT INTO TABLE test_a(user_id,score,deleted,create_time) VALUES(0,'AAAA',1,now());
#再次查询,刚才那条数据看不到了
SELECT *
FROM view_test_a
WHERE user_id = 0;
- 执行去重的查询
这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合 表级别的 TTL 最终将物理数据删除。
其实这种方式就是拿insert去模仿更新数据和删除数据的操作,这种方式是比较推荐的
- 通过FINAL查询
不使用final查询的情况:
这个执行计划显示查询将由2个线程并行读取数据,读取操作是并行进行的,因此能够加快读取速度。
使用final查询的情况:
MergeTree 0 → 1:
从MergeTree表读取数据,但这一部分是串行执行的。
这个执行计划显示在执行 FINAL 查询时,数据读取阶段仍然是串行的,但是在后续的数据处理阶段(如表达式转换和数据合并排序)是并行的。
这就是为什么 FINAL 查询比普通查询慢,因为它涉及更多的数据处理步骤,而且读取数据时没有并行化。
物化视图
它就是一张表,它也像是一张时刻在预计算的表。物化视图不会随着基础表的变化而变化,所以它也称为快照
物化视图和普通视图的区别
普通视图不保存数据,保存的仅仅是查询语句
普通视图不保存数据,保存的仅仅是查询语句,查询的时候还是从原表读取数据,可以将普通视图理解为是个子查询。物化视图则是把查询的结果根据相应的引擎存入到了磁盘或内存中,对数据重新进行了组织,你可以理解物化视图是完全的一张新表
优缺点
优点:查询快
缺点:占资源
基本语法
1 | CREATE [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]table_name [TO[db.]name] [ENGINE = engine] [POPULATE] AS SELECT ... |
限制:
- 必须指定物化视图的engine
- 生产环境不使用POPULATE,这个会把源表中的所有数据都insert到视图中。如果想把数据都导入到视图中还不如使用insert把数据都导入进去
示例
1 | #建表语句 |
1 | INSERT INTO hits_test |
创建视图
1 | #建表语句 |
这里我们刚刚创建了物化视图是没有数据的,物化视图是需要增量后面的数据进入视图,也可以使用insert into 的方式导入历史数据
导入增量数据
1 | #导入增量数据 |
导入历史数据
1 | #导入历史数据 |
MaterializeMysql引擎
现在我们很多场景,为了增加数据的实时性,使用binlog的形式把mysql写入到clickhouse做后面的数据计算处理。clickhouse增加列MaterializeMysql的database粒度的引擎,这个database可以映射到mysql上的一个database,在clickhouse创建对应的ReplacingMergeTree。ck作为mysql的副本,读取binlog执行DDL和DML,数据库实时同步