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. EPAI手绘建模APP介绍

    ​        本软件是一个基于OpenCASCADE.android JNI开发的APP.底层用c++实现,UI层用android实现.底层和UI层之间通过JNI接口和json数据格式通信.    ...

  2. 基于三菱Q系列cc-Link的立体仓库控制系统

    系统说明: 方案选择: 工艺流程: 触摸屏设计: 程序设计:采用SFC进行编程,结构清晰,逻辑明了 本文章为学习记录,水平有限,望各路大佬们轻喷!!!    转载请注明出处!!!

  3. web页面打开直接调用vlc播放视频

    简介 大家都知道现在我们在网页所播放的视频都是h264编码格式,可以供所有设备正常播放.然而,相比h265它的体积更大.质量更差.目前h265大多应用于安防,体积小可以更好的存储,不过它也有着缺点,成 ...

  4. windows远程桌面和远程协助有什么区别

    一言以蔽之: windows远程桌面是被动式的,只要你开启了这个功能选项,对方有地址和密码就能使用这台计算机. windows远程协助是主动式的,需要向对方提出协助请求,对方答应后即可登录电脑,协助操 ...

  5. 扩展Unity编辑器顶部Toolbar,增加自定义按钮

    游戏需要增加几种启动模式,要在编辑器顶部Toolbar处增加几个按钮:进行下扩展. 这部分Unity没有直接提供接口,需通过反射实现.看了下有一个开源库: https://github.com/mar ...

  6. 化繁为简|AIRIOT智慧水务信息化建设解决方案

    ​ "生产自动化,管理信息化"是现代化水厂建设的目标之一,需要在水质要求.工艺.生产.管理.环境等监测方面达到精细化管理标准,这是一个高度智能化,实现化繁为简智慧进阶的工程.传统水 ...

  7. 开源低代码框架 ReZero API 正式版本发布 ,界面操作直接生成API

    一.ReZero简介 ReZero是一款.NET中间件 : 全网唯一界面操作就能生成API ,  可以集成到任何.NET6+ API项目,无破坏性,也可让非.NET用户使用exe文件 免费开源:MIT ...

  8. vue-element-admin 运行踩坑笔记

      npm WARN deprecated svgo@1.3.2: This SVGO version is no longer supported. Upgrade to v2.x.x. npm E ...

  9. 像阿里OSS一样的文件对像存储服务,容器实现 docker初探及minio测试

    像阿里OSS一样的文件对像存储服务,容器实现 docker run -p 8000:9000 --name oss-minio -d -e "MINIO_ACCESS_KEY=AKIAIOS ...

  10. 宝塔面板6.X在Docker中安装宝塔面板5.9.1 – 我是不是太闲了?

    我是不是太闲了,问你们三遍,场景是这样的,我在一台VPS里面安装宝塔面板6.X,宝塔面板的软件商店有Docker管理器2.0这一个免费好用的小玩意.大鸟安装好Docker管理器2.0,然后在Docke ...