ClickHouse使用操作

这章主要介绍在ClickHouse使用的各个操作的注意点。常规的统一语法不做详细介绍。

1. Join操作

在ClickHouse中,对连接操作定义了不同的精度,包含ALL、ANY和ASOF三种类型,默认为ALL。可以通过join_default_strictness配置修改默认精度(位于system.setting表中)。下面分别说明这3种精度。

首先建表并插入测试数据:

--表join_tb1
CREATE TABLE join_tb1
(
`id` String,
`name` String,
`time` DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(time)
ORDER BY id --表 join_tb2
CREATE TABLE join_tb2
(
`id` String,
`rate` UInt8,
`time` DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(time)
ORDER BY id --表 join_tb3
CREATE TABLE join_tb3
(
`id` String,
`star` UInt8
)
ENGINE = MergeTree
ORDER BY id --插入数据
INSERT INTO join_tb1 VALUES
('1', 'ClickHouse', '2019-05-01 12:00:00')
('2', 'Spark', '2019-05-01 12:30:00')
('3', 'ElasticSearch', '2019-05-01 13:00:00')
('4', 'HBase', '2019-05-01 13:30:00')
(NULL, 'ClickHouse', '2019-05-01 14:00:00')
(NULL, 'Spark', '2019-05-01 14:30:00') INSERT INTO join_tb2 VALUES
('1', 100, '2019-05-01 11:55:00')
('1', 105, '2019-05-01 11:50:00')
('2', 90, '2019-05-01 12:01:00')
('3', 80, '2019-05-01 13:10:00')
('5', 70, '2019-05-01 14:00:00')
('6', 60, '2019-05-01 13:55:00') INSERT INTO join_tb3 VALUES
('1', 1000)
('2', 900)

1.1. ALL

如果左表内的一行数据,在右表中有多行数据与之连接匹配,则返回右表种全部连接的数据。连接依据为:left.key=right.key。

SELECT a.id, a.name, b.rate FROM join_tb1 AS a ALL INNER JOIN join_tb2 AS b ON a.id=b.id

SELECT
a.id,
a.name,
b.rate
FROM join_tb1 AS a
ALL INNER JOIN join_tb2 AS b ON a.id = b.id ┌─id─┬─name──────────┬─rate─┐
│ 1 │ ClickHouse │ 100 │
│ 1 │ ClickHouse │ 105 │
│ 2 │ Spark │ 90 │
│ 3 │ ElasticSearch │ 80 │
└────┴───────────────┴──────┘

1.2. ANY

如果左表内的一行数据,在右表中有多行数据与之连接匹配,则仅返回右表中第一行连接的数据。连接依据同样为:left.key=right.key

SELECT
a.id,
a.name,
b.rate
FROM join_tb1 AS a
ANY INNER JOIN join_tb2 AS b ON a.id = b.id ┌─id─┬─name──────────┬─rate─┐
│ 1 │ ClickHouse │ 100 │
│ 2 │ Spark │ 90 │
│ 3 │ ElasticSearch │ 80 │
└────┴───────────────┴──────┘

1.3. ASOF

ASOF 是一种模糊连接,允许在连接键之后追加定义一个模糊连接的匹配条件asof_column,例如:

SELECT
a.id,
a.name,
b.rate,
a.time,
b.time
FROM join_tb1 AS a
ASOF INNER JOIN join_tb2 AS b ON (a.id = b.id) AND (a.time >= b.time) ┌─id─┬─name───────┬─rate─┬────────────────time─┬──────────────b.time─┐
│ 1 │ ClickHouse │ 100 │ 2019-05-01 12:00:00 │ 2019-05-01 11:55:00 │
│ 2 │ Spark │ 90 │ 2019-05-01 12:30:00 │ 2019-05-01 12:01:00 │
└────┴────────────┴──────┴─────────────────────┴─────────────────────┘

根据官网介绍的语法:

SELECT expressions_list
FROM table_1
ASOF LEFT JOIN table_2
ON equi_cond AND closest_match_cond

https://clickhouse.tech/docs/en/sql-reference/statements/select/join/

ASOF会先以 left.key = right.key 进行连接匹配,然后根据AND 后面的 closest_match_cond(也就是这里的a.time >= b.time)过滤出最符合此条件的第一行连接匹配的数据。

另一种写法是使用USING,语法为:

SELECT expressions_list
FROM table_1
ASOF JOIN table_2
USING (equi_column1, ... equi_columnN, asof_column)

举例:

SELECT
a.id,
a.name,
b.rate,
a.time,
b.time
FROM join_tb1 AS a
ASOF INNER JOIN join_tb2 AS b USING (id, time) Query id: 075f7e4a-7355-4e11-ae3b-0e3275912a3e ┌─id─┬─name───────┬─rate─┬────────────────time─┬──────────────b.time─┐
│ 1 │ ClickHouse │ 100 │ 2019-05-01 12:00:00 │ 2019-05-01 11:55:00 │
│ 2 │ Spark │ 90 │ 2019-05-01 12:30:00 │ 2019-05-01 12:01:00 │
└────┴────────────┴──────┴─────────────────────┴─────────────────────┘

对 asof_colum 字段的使用有2点需要注意:

  1. asof_column 必须是整型、浮点型和日期型这类有序序列的数据类型
  2. asof_column不能是数据表内的唯一字段,也就是说连接键(JOIN KEY)和asof_column不能是同一字段

1.4. Join性能

在执行JOIN时,ClickHouse对执行的顺序没有特别优化,JOIN操作会在WHERE以及聚合查询前运行。

JOIN操作结果不会缓存,所以每次JOIN操作都会生成一个全新的执行计划。如果应用程序会大量使用JOIN,则需进一步考虑借助上层应用侧的缓存服务或使用JOIN表引擎来改善性能(JOIN表引擎不支持ASOF精度)。JOIN表引擎会在内存中保存JOIN结果。

在某些情况下,IN的效率比JOIN要高。

在使用JOIN连接维度表时,JOIN操作可能并不会特别高效,因为右则表对每个query来说,都需要加载一次。在这种情况下,外部字典(external dictionaries)的功能会比JOIN性能更好。

1.5. JOIN的内存限制

默认情况下,ClickHouse使用Hash Join 算法。它会将右侧表(right_table)加载到内存,并为它创建一个hash table。在达到了内存使用的一个阈值后,ClickHouse会转而使用Merge Join 算法。

可以通过以下参数限制JOIN操作消耗的内存:

  1. max_rows_in_join:限制hash table中的行数
  2. max_bytes_in_join:限制hash table的大小

在达到任何上述limit后,ClickHouse会以join_overflow_mode 的参数进行动作。此参数包含2个可选值:

  1. THROW:抛出异常并终止操作
  2. BREAK:终止操作但并不抛出异常

2. WHERE与PREWHERE子句

WHERE可以通过表达式来过滤数据,如果过滤条件恰好为主键字段,则可以进一步借助索引加速查询,所以WHERE子句是决定查询语句是否能使用索引的判断依据(前提是表引擎支持索引)。

除此之外,ClickHouse还提供了PREWHERE子句用于条件过滤,它可以更有效地进行过滤优化,仅用于MergeTree表系列引擎。

PREWHERE与WHERE不同之处在于:使用PREWHERE时,首先只会去PREWHERE指定的列字段数据,用于数据过滤的条件判断。在数据过滤之后再读取SELECT声明的列字段以补全其余属性。所以在一些场合下,PREWHERE相比WHERE而言,处理的数据更少,性能更高。

默认情况下,即使在PREWHERE子句没有显示指定的情况下,它也会自动移动到WHERE条件到PREWHERE阶段。

下面做个对比:

# 默认自动开启了PREWHERE,查询速度为:
select WatchID, Title, GoodEvent from hits_v1 where JavaEnable=1; …
6535088 rows in set. Elapsed: 1.428 sec. Processed 8.87 million rows, 863.90 MB (6.21 million rows/s., 604.82 MB/s.) # 关闭PREWHERE
set optimize_move_to_prewhere=0 # 关闭自动PREWHERE,查询速度为
6535088 rows in set. Elapsed: 1.742 sec. Processed 8.87 million rows, 864.55 MB (5.09 million rows/s., 496.20 MB/s.)

可以看到2条语句处理的数据总量没有变化,但是其数据处理量稍有降低(PREWHERE为863.90MB),且每秒吞吐量上升(PREWHER为604.82MB/s,WHERE为496.20MB/s)。

对比2条语句的执行计划:

# PREWHERE
explain select WatchID, Title, GoodEvent from hits_v1 prewhere JavaEnable=1; EXPLAIN
SELECT
WatchID,
Title,
GoodEvent
FROM hits_v1
PREWHERE JavaEnable = 1 Query id: 103fd24a-e718-4304-9f75-4900528c1d1a ┌─explain───────────────────────────────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY)) │
│ SettingQuotaAndLimits (Set limits and quota after reading from storage) │
│ ReadFromStorage (MergeTree) │
└───────────────────────────────────────────────────────────────────────────┘ # WHERE
explain select WatchID, Title, GoodEvent from hits_v1 where JavaEnable=1; EXPLAIN
SELECT
WatchID,
Title,
GoodEvent
FROM hits_v1
WHERE JavaEnable = 1 Query id: 9b470524-1320-4e9f-bade-cf8c2c9944c8 ┌─explain─────────────────────────────────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY)) │
│ Filter (WHERE) │
│ SettingQuotaAndLimits (Set limits and quota after reading from storage) │
│ ReadFromStorage (MergeTree) │
└─────────────────────────────────────────────────────────────────────────────┘

可以看到相比WHERE语句,PREWHERE语句的执行计划省去了一次Filter操作。

3. Group By

Group By的用法非常常见,ClickHouse中执行聚合查询时,若是SELECT后面只声明了聚合函数,则GROUP BY 关键字可以省略:

SELECT
SUM(data_compressed_bytes) AS compressed,
SUM(data_uncompressed_bytes) AS uncompressed
FROM system.parts Query id: e38e3ec1-968d-4442-ba7d-b8555f27e0d0 ┌─compressed─┬─uncompressed─┐
│ 1851073942 │ 9445387666 │
└────────────┴──────────────┘

聚合查询还能配合WITH ROLLUP、WITH CUBE和WITH TOTALS三种修饰符获取额外的汇总信息。

3.1. WITH ROLLUP

ROLLUP便是上卷数据,按聚合键从右到左,基于聚合函数依次生成分组小计和总计。如果设聚合键的个数为n,则最终会生成小计的个数为n+1。例如:

SELECT
table,
name,
SUM(bytes_on_disk)
FROM system.parts
GROUP BY
table,
name
WITH ROLLUP
ORDER BY table ASC ┌─table──────────────────────────────────────────┬─name───────────────────────────────────┬─SUM(bytes_on_disk)─┐
│ │ │ 1857739143 │
│ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ │ 638 │
│ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ 953e60a1e8747360786c2b70a223788d_2_4_1 │ 318 │
│ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ acb795a12c7ba41b0ed4c3d94a008ecd_1_3_1 │ 320 │
│ agg_table │ │ 358 │
│ agg_table │ 201909_2_2_0 │ 358 │

可以看到第1行是一个汇总,统计的SUM(bytes_on_disk)的总行数。而每个table字段都有一个汇总(例如.inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 表第一行以及agg_table 第一行)。

3.2. WITH CUBE

CUBE也是数仓里重要的概念,基于聚合键之间所有的组合生成统计信息。如果聚合键的个数为n,则最终聚合数据的个数为2的n次方。例如:

--建表
CREATE TABLE person
(
`id` int,
`name` String,
`course` String,
`year` DateTime,
`points` int
)
ENGINE = MergeTree
ORDER BY id --插入数据
INSERT INTO person VALUES
(1, 'jane', 'CS', '2021-01-02 11:00:00', 50),
(2, 'tom', 'CS', '2021-01-03 11:00:00', 60),
(3, 'bob', 'BS', '2021-01-03 11:00:00', 50),
(4, 'alice', 'BS', '2021-01-01 11:00:00', 40),
(5, 'jane', 'ACC', '2021-01-02 11:00:00', 70),
(6, 'bob', 'ACC', '2021-01-03 11:00:00', 90),
(7, 'jane', 'MATH', '2021-01-04 11:00:00', 100) --Cube计算
SELECT
name,
course,
year,
AVG(points)
FROM person
GROUP BY
name,
course,
year
WITH CUBE ┌─name──┬─course─┬────────────────year─┬─AVG(points)─┐
│ jane │ ACC │ 2021-01-02 11:00:00 │ 70 │
│ bob │ ACC │ 2021-01-03 11:00:00 │ 90 │
│ alice │ BS │ 2021-01-01 11:00:00 │ 40 │
… ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
│ │ │ 2021-01-01 11:00:00 │ 40 │
│ │ │ 2021-01-03 11:00:00 │ 66.66666666666667 │
│ │ │ 2021-01-02 11:00:00 │ 60 │
│ │ │ 2021-01-04 11:00:00 │ 100 │
└──────┴────────┴─────────────────────┴───────────────────┘
┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
│ │ │ 1970-01-01 00:00:00 │ 65.71428571428571 │
└──────┴────────┴─────────────────────┴───────────────────┘

可以看到结果中会生成 8 个统计结果(部分结果已省略)。

3.3. WITH TOTALS

WITH TOTALS会基于聚合函数对所有数据进行统计(比原结果多一行总的统计结果),例如:

SELECT
name,
course,
year,
AVG(points)
FROM person
GROUP BY
name,
course,
year
WITH TOTALS ┌─name──┬─course─┬────────────────year─┬─AVG(points)─┐
│ jane │ ACC │ 2021-01-02 11:00:00 │ 70 │
│ bob │ ACC │ 2021-01-03 11:00:00 │ 90 │
│ alice │ BS │ 2021-01-01 11:00:00 │ 40 │
│ jane │ CS │ 2021-01-02 11:00:00 │ 50 │
│ jane │ MATH │ 2021-01-04 11:00:00 │ 100 │
│ tom │ CS │ 2021-01-03 11:00:00 │ 60 │
│ bob │ BS │ 2021-01-03 11:00:00 │ 50 │
└───────┴────────┴─────────────────────┴─────────────┘ Totals:
┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
│ │ │ 1970-01-01 00:00:00 │ 65.71428571428571 │
└──────┴────────┴─────────────────────┴───────────────────┘

4. 查看SQL执行计划

ClickHouse目前并没有直接提供EXPLAIN的详细查询计划,当前EXPLAIN仅是输出一个简单的计划。不过我们仍可以借助后台服务日志来实现此功能,例如执行以下语句即可看到详细的执行计划:

clickhouse-client --password xxx --send_logs_level=trace <<<'select * from tutorial.hits_v1' > /dev/null

打印信息如下(仅截取关键信息):

tutorial.hits_v1  (SelectExecutor): Key condition: unknown
=> 查询未使用主键索引 tutorial.hits_v1 (SelectExecutor): MinMax index condition: unknown
=> 未使用分区索引 tutorial.hits_v1 (SelectExecutor): Not using primary index on part 201403_1_29_2
=> 未在分区 201403_1_29_2 下使用primary index tutorial.hits_v1 (SelectExecutor): Selected 1 parts by partition key, 1 parts by primary key, 1094 marks by primary key, 1094 marks to read from 1 ranges
=> 选择了1个分区,共计1094个marks executeQuery: Read 8873898 rows, 7.88 GiB in 21.9554721 sec., 404177 rows/sec., 367.50 MiB/sec.
=> 读取 8873898条数据,7.88G 数据,耗时21.955秒… MemoryTracker: Peak memory usage (for query): 361.67 MiB.
=> 消耗内存量

下面优化一下查询:

clickhouse-client --password xxx --send_logs_level=trace <<<"select WatchID from tutorial.hits_v1 where EventDate='2014-03-17'" > /dev/null

打印结果为:

InterpreterSelectQuery: MergeTreeWhereOptimizer: condition "EventDate = '2014-03-17'" moved to PREWHERE
=> 自动调用了PREWHERE tutorial.hits_v1 (SelectExecutor): Key condition: (column 1 in [16146, 16146])
=> 使用了主键索引 tutorial.hits_v1 (SelectExecutor): MinMax index condition: (column 0 in [16146, 16146])
=> 使用了分区索引 tutorial.hits_v1 (SelectExecutor): Selected 1 parts by partition key, 1 parts by primary key, 755 marks by primary key, 755 marks to read from 64 ranges
=> 根据分区键选择了一个分区 executeQuery: Read 6102294 rows, 58.19 MiB in 0.032661599 sec., 186833902 rows/sec., 1.74 GiB/sec.
=> 读到的数据,以及速度 MemoryTracker: Peak memory usage (for query): 11.94 MiB.
=> 消耗内存量

总的来说,ClickHouse未直接通过EXPLAIN语句提供查看语句执行的详细过程,但是可以变相的将日志设置到DEBUG或是TRACE级别,实现此功能,并分析SQL的执行日志。

ClickHouse介绍(四)ClickHouse使用操作的更多相关文章

  1. HTML 事件(四) 模拟事件操作

    本篇主要介绍HTML DOM中事件的模拟操作. 其他事件文章 1. HTML 事件(一) 事件的介绍 2. HTML 事件(二) 事件的注册与注销 3. HTML 事件(三) 事件流与事件委托 4.  ...

  2. 从零开始学习jQuery (四) 使用jQuery操作元素的属性与样式

    本系列文章导航 从零开始学习jQuery (四) 使用jQuery操作元素的属性与样式 一.摘要 本篇文章讲解如何使用jQuery获取和操作元素的属性和CSS样式. 其中DOM属性和元素属性的区分值得 ...

  3. Lucene.Net 2.3.1开发介绍 —— 四、搜索(一)

    原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(一) 既然是内容筛选,或者说是搜索引擎,有索引,必然要有搜索.搜索虽然与索引有关,那也只是与索引后的文件有关,和索引的程序是无关的,因此 ...

  4. Java Spring Boot VS .NetCore (四)数据库操作 Spring Data JPA vs EFCore

    Java Spring Boot VS .NetCore (一)来一个简单的 Hello World Java Spring Boot VS .NetCore (二)实现一个过滤器Filter Jav ...

  5. {MySQL数据库初识}一 数据库概述 二 MySQL介绍 三 MySQL的下载安装、简单应用及目录介绍 四 root用户密码设置及忘记密码的解决方案 五 修改字符集编码 六 初识sql语句

    MySQL数据库初识 MySQL数据库 本节目录 一 数据库概述 二 MySQL介绍 三 MySQL的下载安装.简单应用及目录介绍 四 root用户密码设置及忘记密码的解决方案 五 修改字符集编码 六 ...

  6. 孤荷凌寒自学python第四十四天Python操作 数据库之准备工作

     孤荷凌寒自学python第四十四天Python操作数据库之准备工作 (完整学习过程屏幕记录视频地址在文末,手写笔记在文末) 今天非常激动地开始接触Python的数据库操作的学习了,数据库是系统化设计 ...

  7. Html5 学习系列(四)文件操作API

    原文:Html5 学习系列(四)文件操作API 引言 在之前我们操作本地文件都是使用flash.silverlight或者第三方的activeX插件等技术,由于使用了这些技术后就很难进行跨平台.或者跨 ...

  8. X-Cart 学习笔记(四)常见操作

    目录 X-Cart 学习笔记(一)了解和安装X-Cart X-Cart 学习笔记(二)X-Cart框架1 X-Cart 学习笔记(三)X-Cart框架2 X-Cart 学习笔记(四)常见操作 五.常见 ...

  9. Lucene.Net 2.3.1开发介绍 —— 四、搜索(三)

    原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(三) Lucene有表达式就有运算符,而运算符使用起来确实很方便,但另外一个问题来了. 代码 4.3.4.1 Analyzer anal ...

  10. Lucene.Net 2.3.1开发介绍 —— 四、搜索(二)

    原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(二) 4.3 表达式用户搜索,只会输入一个或几个词,也可能是一句话.输入的语句是如何变成搜索条件的上一篇已经略有提及. 4.3.1 观察 ...

随机推荐

  1. Win10下小米路由器4A百兆版刷Openwrt固件【图片详细版】

    将原来的小米路由器换成了华为,早就听闻刷软路由可以实现去广告,解锁灰色歌单等诸多骚操作.就来榨取这个小米4A的剩余价值来着的. 注意 1. 必须使用路由模式,中继模式是打不开telnet的 更新固件 ...

  2. vue的pc端项目+element实现分页效果

    效果图: 直接使用element操作很简单,记录一下要点: 根据ele提供的api修改data v-for="(i,s) in dataView.slice((currentPage-1)* ...

  3. Python使用HTMLTestRunner运行所有用例并产生报告

    #coding:utf-8import unittestimport osimport sysimport HTMLTestRunnercase_path = os.path.join(os.path ...

  4. goland配置在远程linux里运行代码开发,并debug调适

    环境: windows 10 phpstudy8.1.1.3 Vmware安装centos7.6 场景 window10里goland开发,在远程linux里运行,并debug断点调适 步骤: win ...

  5. ruby执行周期性任务 whenever

    ruby执行周期性任务 下面看看怎么将任务写入cron服务. $ whenever #不带参数的whenever会显示转换程cron任务的代码,不写入cron任务表 $ whenever -w #写入 ...

  6. 谈谈 Spring 的过滤器和拦截器

    前言 我们在进行 Web 应用开发时,时常需要对请求进行拦截或处理,故 Spring 为我们提供了过滤器和拦截器来应对这种情况.那么两者之间有什么不同呢?本文将详细讲解两者的区别和对应的使用场景. ( ...

  7. JavaScript 中 toString 的奇妙使用

    JavaScript 中的toString()方法,我们通常会一些其他类型的变量,转为字符串类型.但这里还有一些其他奇妙的用法. 不同的类型调用 toString() 会得到不同的结果.我们来一一分析 ...

  8. Vue cli之组件的嵌套

    前面显示Home.vue页面组件的内容时,我们是在App.vue通过import导入使用的.这个过程就是组件的嵌套使用.那么我们除了App.vue可以导入其他页面以外,也可以通过在Home.vue中导 ...

  9. mysql 5.6 inner join and 慢优化

    慢示例: SELECT o.id FROM o_schoolnotice o INNER JOIN o_schoolnotice_parent p ON o.id = p.noticeid AND p ...

  10. Android 13 - Media框架(13)- OpenMax(一)

    关注公众号免费阅读全文,进入音视频开发技术分享群! 这一节我们将了解Android OpenMax框架,该框架了解完成之后,我们会再回过头去了解 ACodec,将 MediaCodec - ACode ...