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. C#/C++ 通过ODBC连接OceanBase Oracle租户

    概述 近期我们项目正处于将Oracle数据库迁移到OceanBase Oracle租户模式的阶段.考虑到我们项目采用了C++和C#混合开发,并且使用了多种技术,因此存在多种数据库连接方式.然而,针对C ...

  2. CMake快速入门教程

    前言 Make工具因遵循不同的规范和标准,执行的Makefile的格式也是不同.主流的Make工具包括: GNU Make QT的qmake 微软的 MS nmake BSD的 pmake 每个平台都 ...

  3. 关于Java Chassis 3的契约优先(API First)开发

    本文分享自华为云社区<Java Chassis 3技术解密:契约优先(API First)开发>,作者: liubao68. 契约优先(API First)开发是指应用程序开发过程中,将A ...

  4. 基于Python实现MapReduce

    一.什么是MapReduce 首先,将这个单词分解为Map.Reduce. Map阶段:在这个阶段,输入数据集被分割成小块,并由多个Map任务处理.每个Map任务将输入数据映射为一系列(key, va ...

  5. C#的GroupBy方法是如何工作的

    前言:先贴结果 GroupBy方法是如何工作的? 一.准备6个待分组的学生对象 class student { public string name;//姓名 public int grade;//年 ...

  6. 通过XML标记生成word

    思路 word生成可以通过标签,也可以通过XML元素. word文档实际上是由文档对象模型描述的,因此我们能够通过对文档对象进行操作去生成word. 由于word允许我们附加XML结构(元素),如下: ...

  7. vue我自己的动态菜单思路

    1.在router里把所有的路由都加上. 2.后端存储路由path和其他设计需要的信息. 3.登录后,后端返回菜单树,根据权限不同,返回的菜单不同,并且还要返回每个path代表的页面具有的权限数组.可 ...

  8. ClipBord 复制 Html winform

    剪贴板针对不同的数据格式,在组织剪贴板内容时,有不同的格式要求.最简单的是,复制一般的.txt文本内容.使用方法 ClipBord.Clear(); ClipBord.SetText(str); 复制 ...

  9. 用友u8 使用 api资源管理器新增单据的一些方法

    一般都使用传xml对象的方式.这种方式方便在只需传入正确的视图就行了.但是如果字段不全,或者字段的数据类型与api要求的不服,会报些莫名其妙的错误,比如这些: 项目大类0不存在项目大类0不存在货位不合 ...

  10. 使用SQL语句完成数据表的去重工作

    引入问题 可能大家都遇到过这样一种情况,一张表存在若干行数据,只是主键值相同,但是其它字段都是相同的,这就是我们通常所说的数据库重复数据,那我们是如何将这些数据删除呢?现在我们就介绍一种方法来实现数据 ...