DML(data manipulation language)是数据操纵语言:它们是SELECT、UPDATE、INSERT、DELETE,就象它的名字一样,这4条命令是用来对数据库里的数据进行操作的语言。

python Clickhouse 插入date类型_clickhouse


日常工作中,我们更多地还是对数据表中的数据进行操作,而对于OLAP类型的数据库而言,这些操作还都是查询操作。

1 增加INSERT

跟绝大部分关系型数据库一样,ClickHouse使用INSERT语句进行数据的插入。并且INSERT语支持三种语法范式,三种范式各有不同,可以根据写入的需求灵活运用。
其中,第一种是使用 VALUES 格式的常规语法

-- 中括号表示里面的内容可以省略
INSERT INTO [db.]table_name [(col1, col2, col3...)] VALUES (val1, val2, val3, ...), (val1, val2, val3, ...), ...

在使用VALUES格式的语法写入数据时,还支持加入表达式或函数,例如:
INSERT INTO partition_v1 VALUES('matsuri', toString(1+2), now())

这个和其它关系型数据库没什么两样。

第二种是使用指定格式的语法

INSERT INTO [db.]table_name [(col1, col2, col3...)] FORMAT format_name data_set

ClickHouse 支持多种数据格式,以常用的 CSV 格式写入为例:
clickhouse-client --user default --password bigdata --query 'insert into partition_v1 format CSV' < /data.csv


echo "'meta1','www.meta1.com','2019-02-01'" > data.csv
echo "'meta','www.meta.com','2019-01-01'" >> data.csv

第三种是使用 SELECT 子句形式的语法

INSERT INTO [db.]table_name [(col1, col2, col3...)] SELECT ...
通过 SELECT 子句可将查询结果写入数据表,
假设需要将partition_v1的数据写入partition_v2,则可以使用下面的语句:
INSERT INTO partition_v2 SELECT * FROM partition_v1
当然也可以这么做:

-- 加入表达式也是可以的,比如这里的 now()
INSERT INTO partition_v2 SELECT 'aqua', 'www.aqua.com', now()

虽然 VALUES 和 SELECT 子句的形式都支持声明表达式或函数,但是表达式或函数会带来额外的性能开销,从而导致写入性能下降。所以如果追求极致的写入性能,应该尽量避免使用它们。

在前面曾介绍过,ClickHouse 内部所有的数据操作都是面向 Block 数据块的,所以 INSERT 查询最终会将数据转换为 Block 数据块。也正因为如此,INSERT 语句在单个数据块的写入过程中是具有原子性的。在默认情况下,每个数据块最多可以写入 1048576 条数据(由 max_insert_block_size 参数控制)。也就是说,如果一条 INSERT 语句写入的数据行数少于 max_insert_block_size,那么这批数据的写入是具有原子性的,要么全部成功,要么全部失败。

但是需要注意的是,只有在 ClickHouse 服务端处理数据的时候才具有这种原子写入的特性,例如使用 HTTP 接口,因为 max_insert_block_size 参数在使用 CLI 命令行或者 INSERT SELECT 子句写入时是不生效的。

2 删除ALTER DELETE

ClickHouse 提供了 DELETE 和 UPDATE 的能力,这类操作被称为 Mutation 查询,它可以看作 ALTER 语句的变种。虽然 Mutation 能最终实现修改和删除,但不能完全以通常意义上的 UPDATE 和 DELETE 来理解,我们必须清醒地认识到它的不同。
首先,Mutation 语句是一种 “很重” 的操作,更适用于批量数据的修改和删除;
其次,它不支持事务,一旦语句被提交执行,就会立刻对现有数据造成影响,无法回滚;
最后,Mutation 语句的执行是一个异步的后台过程,语句被提交之后就会立即返回。所以这并不代表具体逻辑已经执行完毕,它的具体执行进度需要通过 system.mutations 系统表查询

DELETE 语句的完整语法如下所示:

ALTER TABLE [db_name.]table_name DELETE WHERE filter_expr
数据删除的范围由 WHERE 查询子句决定。

如果数据很少的话,那么 DELETE 操作给人的感觉和常用的 OLTP 数据库无异,但我们心中应该要明白这是一个异步的后台执行动作。

下面我们来实际删除数据,就以 partition_v1 为例吧,先来看看对应目录(/var/lib/clickhouse/data/default/partition_v1)里面的内容:

python Clickhouse 插入date类型_子查询_02


执行该语句:ALTER TABLE partition_v1 DELETE WHERE ID =‘meta1’ 进行数据删除,执行完之后再看一下目录结构:

python Clickhouse 插入date类型_clickhouse_03


可以发现,在执行了 DELETE 操作后数据目录发生了一些变化,每一个原有的数据目录都额外增加了一个同名目录,并且在末尾处增加了 _6后缀。此外,目录下还多了一个名为 mutation_6.txt 文件,里面的内容如下:

format version: 1
create time: 2022-11-13 00:36:22
commands: DELETE WHERE ID = \'meta1\'

原来mutation_6.txt 是一个日志文件,它完整地记录了这次 DELETE 操作的执行语句和时间,而文件名的后缀 _6与新增目录的后缀对应。那么后缀的数字从何而来呢?继续查询 system.mutations 系统表,一探究竟:

SELECT database, table, mutation_id, block_numbers.number as num, is_done FROM system .mutations
┌─database─┬─table────────┬─mutation_id────┬─num─┬─is_done─┐
│ default  │ partition_v1 │ mutation_6.txt │ [6] │       1 │
└──────────┴──────────────┴────────────────┴─────┴─────────┘

至此,整个 Mutation 操作的逻辑就比较清晰了。每执行一条 ALTER DELETE 语句,都会在 mutations 系统表中生成一条对应的执行计划,当 is_done 等于 1 时表示执行完毕。与此同时,在数据表的根目录下,会以 mutation_id 作为名字生成与之对应的日志文件用于记录相关信息。而数据删除的过程是以数据表的每个分区目录为单位,将所有目录重写为新的目录,新目录的命名规则是在原有名称上加上 system.mutations.block_numbers.number。数据在重写的过程中会将需要删除的数据去掉,旧的数据目录并不会立即删除,而是会被标记成非激活状态(active 为 0)。等到 MergeTree 引擎的下一次合并动作触发时,这些非激活目录才会被真正从物理意义上删除。

3 修改ALTER UPDATE

数据修改除了需要指定具体的要更新的列字段之外,整个逻辑与数据删除别无二致,它的完整语法如下所示:

ALTER TABLE [db_name.]table_name UPDATE column1 = expr1 [, ...] WHERE filter_expr

4 查询SELECT

作为一款OLAP型的数据库,它的查询功能可谓是重中之重,而且我相信大家在绝大部分时间都在使用它的查询功能,事实上,在日常运转的过程中,数据查询也是ClickHouse的主要工作之一。ClickHouse完全使用SQL作为查询语言,能够以 SELECT查询语句的形式从数据库中选取数据,这也是它具备流行潜质的重要原因。

虽然ClickHouse拥有优秀的查询性能,但是我们也不能滥用查询,掌握 ClickHouse 所支持的各种查询子句,并选择合理的查询形式是很有必要的。使用不恰当的 SQL 语句进行查询不仅会带来低性能,还可能导致不可预知的系统错误。

虽然在上面的示例中,我们已经见识过一些查询语句的用法,但那些都是为了演示效果简化后的代码,与真正的生产环境中的代码相差较大。例如在绝大部分场景中,都应该避免使用 SELECT * 来查询数据,因为通配符 * 对于采用列式存储的 ClickHouse 而言没有任何好处。假如面对一张拥有数百个列字段的数据表,下面这两条 SELECT 语句的性能可能会相差 100 倍之多:

SELECT * FROM table;
SELECT col FROM table;

使用通配符 * 和按列查询相比,性能可能相差 100 倍,另外 ClickHouse 对于 SQL 语句的解析是大小写敏感的,这意味着 SELECT a 和 SELECT A 表示的语义是不相同的,但关键字大小写不敏感,不过还是建议遵循规范使用大写。此外 ClickHouse 的类型也大小写敏感,比如:UInt8 不可以写成 uint8,String 不可以写成 string;还有大部分函数也是大小写敏感,这些函数都是 ClickHouse 独有的,或者说你在其它关系型数据库中见不到的,但是像 min、max、length、sum、count 等等这些在其它关系型库中也能看到的函数,在 ClickHouse 中则是大小写不敏感的。

下面介绍 ClickHouse 的查询语法,ClickHouse 支持的查询子句和我们平常使用的关系型数据库非常类似,但是在此基础上又提供了很多新的功能,我们来看一下。

4.1 WITH子句

ClickHouse支持CTE(Common Table Expression,公共表表达式),以增强查询语句的表达。
例如下面的函数嵌套:

SELECT pow(pow(2, 2), 3); 
┌─pow(pow(2, 2), 3)─┐
│                64 │
└───────────────────┘

在改用 CTE 的形式后,可以极大地提高语句的可读性和可维护性,简化后的语句如下所示:

with pow(2,2) as a select pow(a,3);
┌─pow(a, 3)─┐
│        64 │
└───────────┘

WITH 表达式 AS var
通过WITH,我们即可在查询中使用var来代替表达式。

而根据表达式的不同,可以有以下几种用法:

4.1.1 表达式为常量

此时相当于为常量起了一个有意义的名字,这些名字能够在后续的查询子句中被直接访问。例如下面示例中的start,被直接用在紧接着的 WHERE 子句中:

WITH 10 AS start SELECT number FROM system.numbers WHERE number > start LIMIT 5
┌─number─┐
│     11 │
│     12 │
│     13 │
│     14 │
│     15 │
└────────┘

如果没有 WITH 子句,那么直接把 start 换成 10 即可,只不过通过 WITH 我们给 10 这个常量起了一个有意义的名字。当然常量不仅是整数,字符串、浮点数、甚至数组都是可以的。

4.1.2 表达式为函数调用

感觉此时就类似于替换,例如在下面的示例中,对 data_uncompressed_bytes 使用聚合函数求和后,又紧接着在 SELECT 子句中对其进行了格式化处理。

WITH SUM(data_uncompressed_bytes) AS bytes SELECT database, formatReadableSize(bytes) AS format FROM system.columns GROUP BY database ORDER BY bytes DESC
┌─database───────────┬─format─────┐
│ system             │ 295.93 MiB │
│ dbtest             │ 0.00 B     │
│ default            │ 0.00 B     │
│ INFORMATION_SCHEMA │ 0.00 B     │
│ information_schema │ 0.00 B     │
└────────────────────┴────────────┘

如果不使用 WITH 子句,那么 SELECT 里面出现的就是 formatReadableSize(SUM(data_uncompressed_bytes)),这样读起来不是很方便,所以使用 WITH 子句将里面的聚合函数调用起一个名字叫 bytes,那么后面的查询直接使用 bytes 即可。

4.1.3 表达式为子查询

表达式也可以是一个子查询,例如在下面的示例中,借助子查询可以得出各 database 未压缩数据大小与数据总和大小的比例的排名:

-- SELECT sum(data_uncompressed_bytes) FROM system.columns会得到一个数值
-- 因此本质上和表达式为常量是类似的,只不过多了一个计算的过程
WITH (SELECT sum(data_uncompressed_bytes) FROM system.columns) AS total_bytes 
SELECT database, (sum(data_uncompressed_bytes) / total_bytes) * 100 AS database_disk_usage 
FROM system.columns 
GROUP BY database 
ORDER BY database_disk_usage DESC

┌─database───────────┬─database_disk_usage─┐
│ system             │                 100 │
│ dbtest             │                   0 │
│ default            │                   0 │
│ INFORMATION_SCHEMA │                   0 │
│ information_schema │                   0 │
└────────────────────┴─────────────────────┘

使用 WITH 子句时有一点需要特别注意,表达式只能返回的数据不能超过 1 行,否则会抛出异常。我们举个栗子:

with (select number from numbers(1)) as a select a;
┌─a─┐
│ 0 │正常返回,结果集只有一行
└───┘

with (select number from numbers(2)) as a select a;
表达式返回了两行数据,所以报错

这里的 WITH AS 就类似于编程语言中的变量赋值,但你不可能让一个变量指代多个值,如果想这么做,那么就将这些值放在一个容器(列表、集合、字典等等)里面。同理,如果 WITH 的表达式返回了多行数据,那么可以将其变成一个数组:

-- 这里见到了一个函数 groupArray,我们可以把它当成是普通的聚合函数来理解
-- 类似于sum,sum是对同一组的元素进行求和,groupArray是将同一组的元素组合成数组
with (select groupArray(number) from numbers(10)) as arr select arr;
┌─arr───────────────────┐
│ [0,1,2,3,4,5,6,7,8,9] │
└───────────────────────┘

4.1.4 在子查询中重复使用WITH

在子查询中可以嵌套使用 WITH 子句,例如在下面的示例中,在计算出各 database 未压缩数据大小与数据总和的比例之后,又进行了取整函数的调用:

WITH round(database_disk_usage) AS database_disk_usage_v1
SELECT database, database_disk_usage, database_disk_usage_v1
FROM (
    -- 嵌套
    WITH (SELECT sum(data_uncompressed_bytes) FROM system.columns) AS total_bytes
    SELECT database, (sum(data_uncompressed_bytes) / total_bytes) * 100 AS database_disk_usage
    FROM system.columns GROUP BY database ORDER BY database_disk_usage DESC
)

虽然看起来有点复杂,但是不难理解,不考虑WITH的话,
那么就是一个嵌套子查询:SELECT ... FROM (SELECT ... FROM),
只不过两个SELECT里面的database_disk_usage_v1、total_bytes是用WITH声明的变量。

database          |database_disk_usage|database_disk_usage_v1|
------------------+-------------------+----------------------+
system            |              100.0|                 100.0|
dbtest            |                0.0|                   0.0|
default           |                0.0|                   0.0|
INFORMATION_SCHEMA|                0.0|                   0.0|
information_schema|                0.0|                   0.0|

而且我们看到如果子查询是作为一张表使用的,那么在关系型数据库中应该起一个别名,但在 ClickHouse 可以不用

总的来说,WITH 子句就相当于起一个别名,如果你看某个表达式长得不顺眼,那么就可以使用 WITH 将它替换掉,就这么简单。而且一个 WITH 子句是可以为多个表达式起别名的,举个栗子:

WITH 1 AS a, 2 AS b SELECT a + b;
┌─plus(a, b)─┐
│          3 │
└────────────┘

所以 ClickHouse 的 WITH 中的表达式必须只能有一行,它就等价于为某个复杂的表达式起一个别名,不可以放在 FROM 后面作为临时表来使用

核心就一句话:给表达式起别名,后续使用别名来对表达式进行替换。

4.2 FROM子句

FROM 子句表示从何处读取数据,目前支持如下 3 种形式。
一、 从数据表中取数

SELECT name FROM people

二、从子查询中取数

SELECT max_id FROM
(SELECT max(id) AS max_id FROM people)
-- 在其它关系型数据库中, 如果子查询作为一张表来使用, 那么必须要起一个别名。
-- 但是在ClickHouse中不需要, 个人觉得这是个不错的决定,因为起别名我们又不用。

三、从表函数中取数

SELECT number FROM numbers(N) -- 会返回 0 到 N - 1

另外 FROM 关键字可以省略,我们在介绍 WITH 子句的时候多次省略 FROM,因为 SELECT 后面是标量,此时会从虚拟表中取值。在 ClickHouse 中,并没有数据库中常见的 DUAL 虚拟表,取而代之的是 system.one。举个栗子:

SELECT 1, 2, 3 FROM system.one:
-- 等价于
SELECT 1, 2, 3;  
┌─1─┬─2─┬─3─┐
│ 1 │ 2 │ 3 │
└───┴───┴───┘

-- 此时可以当成计算器来使用
select 1+2,2*3;
┌─plus(1, 2)─┬─multiply(2, 3)─┐
│          3 │              6 │
└────────────┴────────────────┘

select [1,2,3],'test';
┌─[1, 2, 3]─┬─'test'─┐
│ [1,2,3]   │ test   │
└───────────┴────────┘

4.3 ARRAY JOIN子句

ARRAY JOIN 子句允许在数据表的内部,与数组或嵌套类型的字段进行 JOIN 操作,从而将一行数组展开为多行。

首先新建一张包含Array数组字段的测试表:

CREATE TABLE t1 (
    title String,
    value Array(UInt8)
) ENGINE = Memory();

-- 然后写入数据
INSERT INTO t1 VALUES ('food', [1, 2, 3]), ('fruit', [3, 4]), ('meat', []);

-- 查询
SELECT * FROM t1;
┌─title─┬─value───┐
│ food  │ [1,2,3] │
│ fruit │ [3,4]   │
│ meat  │ []      │
└───────┴─────────┘

在一条SELECT语句中,只能存在一个ARRAY JOIN(使用子查询除外),目前支持INNER 和 LEFT两种JOIN策略:

4.3.1 INNER ARRAY JOIN

ARRAY JOIN 在默认情况下使用的是 INNER JOIN 策略,例如下面的语句:

select title,value from t1 array join value;
title|value|
-----+-----+
food |    1|
food |    2|
food |    3|
fruit|    3|
fruit|    4|

从查询结果可以发现,最终的数据基于value数组被展开成了多行,并且排除掉了空数组,同时会自动和其它字段进行组合(相当于按行合并)。在使用 ARRAY JOIN 时,如果还想访问展开前的数组字段,那么只需为原有的数组字段添加一个别名即可,例如:

-- 如果不给ARRAY JOIN后面的value起一个别名,那么value就是展开后的结果
-- 如果给ARRAY JOIN后面的value起一个别名val,那么value就还是展开前的数组字段
-- 而val才是展开后的结果,所以再反过来,让val出现在SELECT中即可
SELECT title, value, val FROM t1 ARRAY JOIN value AS val;
title|value  |val|
-----+-------+---+
food |[1,2,3]|  1|
food |[1,2,3]|  2|
food |[1,2,3]|  3|
fruit|[3,4]  |  3|
fruit|[3,4]  |  4|

4.3.2 LEFT ARRAY JOIN

ARRAY JOIN子句支持LEFT连接策略,例如执行下面的语句:

SELECT title, value, val FROM t1 LEFT ARRAY JOIN value AS val;
title|value  |val|
-----+-------+---+
food |[1,2,3]|  1|
food |[1,2,3]|  2|
food |[1,2,3]|  3|
fruit|[3,4]  |  3|
fruit|[3,4]  |  4|
meat |[]     |  0|

在改为 LEFT 连接查询后,可以发现,在 INNER JOIN 中被排除掉的空数组出现在了返回的结果集中。但此时的 val 是零值,所以 LEFT ARRAY JOIN 个人觉得不是很常用,一般都是用 ARRAY JOIN。

4.3.3 嵌套类型

在前面介绍数据定义时曾介绍过,嵌套数据类型的本质是数组,所以 ARRAY JOIN 也支持嵌套数据类型。
首先新建一张包含嵌套类型的测试表:

CREATE TABLE t3(
    title String,
    nested Nested
    (
        v1 UInt32,
        v2 UInt64
    )
) ENGINE = Log();

-- 接着写入测试数据
-- 在写入嵌套数据类型时,记得同一行数据中各个数组的长度需要对齐,而对多行数据之间的数组长度没有限制
INSERT INTO t3
VALUES ('food', [1, 2, 3], [10, 20, 30]),
       ('fruit', [4, 5], [40, 50]),
       ('meat', [], [])

select * from t3
title|nested.v1|nested.v2 |
-----+---------+----------+
food |[1,2,3]  |[10,20,30]|
fruit|[4,5]    |[40,50]   |
meat |[]       |[]        |

对嵌套类型数据的访问,ARRAY JOIN 既可以直接使用字段列名

select title,nested.v1,nested.v2 from t3;
title|nested.v1|nested.v2 |
-----+---------+----------+
food |[1,2,3]  |[10,20,30]|
fruit|[4,5]    |[40,50]   |
meat |[]       |[]        |

select title,nested.v1,nested.v2 from t3 array join nested;
title|nested.v1|nested.v2|
-----+---------+---------+
food |        1|       10|
food |        2|       20|
food |        3|       30|
fruit|        4|       40|
fruit|        5|       50|

对嵌套类型数据的访问,ARRAY JOIN也可以使用点访问符的形式

-- nested 只有 v1 和 v2
-- 所以 ARRAY JOIN nested.v1, nested.v2 等价于 ARRAY JOIN nested
SELECT title, nested.v1, nested.v2 FROM t3 ARRAY JOIN nested.v1, nested.v2

title|nested.v1|nested.v2|
-----+---------+---------+
food |        1|       10|
food |        2|       20|
food |        3|       30|
fruit|        4|       40|
fruit|        5|       50|

嵌套类型也支持 ARRAY JOIN 部分嵌套字段,在这种情形下,只有被 ARRAY JOIN 的数组才会展

SELECT title, nested.v1, nested.v2 FROM t3 ARRAY JOIN nested.v1
title|nested.v1|nested.v2 |
-----+---------+----------+
food |        1|[10,20,30]|
food |        2|[10,20,30]|
food |        3|[10,20,30]|
fruit|        4|[40,50]   |
fruit|        5|[40,50]   |

在查询嵌套类型时也能够通过别名的形式访问原始数组

SELECT title, 
       nested.v1, nested.v2, 
       n.v1, n.v2  
from t3 ARRAY JOIN nested AS n;

title|nested.v1|nested.v2 |n.v1|n.v2|
-----+---------+----------+----+----+
food |[1,2,3]  |[10,20,30]|   1|  10|
food |[1,2,3]  |[10,20,30]|   2|  20|
food |[1,2,3]  |[10,20,30]|   3|  30|
fruit|[4,5]    |[40,50]   |   4|  40|
fruit|[4,5]    |[40,50]   |   5|  50|

-- 所以ARRAY JOIN nested后面如果没有AS,那么这个nested.v1和nest.v2就是展开后的值
-- 如果ARRAY JOIN nested AS n,起了一个别名,那么nested.v1和nest.v2就是展开前值,也就是数组本身
-- 而n.v1 和 n.v2 是展开后的值
-- 另外 ARRAY JOIN nested AS n,这个 n 可以不使用

SELECT title, 
       nested.v1, nested.v2
from t3 ARRAY JOIN nested AS n;

title|nested.v1|nested.v2 |
-----+---------+----------+
food |[1,2,3]  |[10,20,30]|
food |[1,2,3]  |[10,20,30]|
food |[1,2,3]  |[10,20,30]|
fruit|[4,5]    |[40,50]   |
fruit|[4,5]    |[40,50]   |

4.4 JOIN子句

JOIN 子句可以对左右两张表的数据进行连接,这是最常用的查询子句之一,它的语法包含连接精度和连接类型两部分。
连接精度分为 ALL、ANY 和 ASOF 三种(准确的说是五种,还有 SEMI 和 ANTI)。
而连接类型也可分为外连接、内连接和交叉连接三种。
除此之外,JOIN 查询还可以根据其执行策略被划分为本地查询和远程查询。

--创建表
create table tbl_1(
	id UInt8,
	code1 String,
	count UInt8
)engine = Memory;

create table tbl_2(
	id UInt8,
	code2 String,
	count UInt8
)engine = Memory;

--写入测试数据
INSERT INTO TABLE tbl_1 VALUES
	(1,'A001',30),
	(2,'A002',28),
	(3,'A003',32);
	
INSERT INTO TABLE tbl_2 VALUES
	(1,'B001',35),
	(1,'B001',29),
	(3,'B003',31),
	(4,'B004',38);

--查询数据
SELECT * FROM tbl_1;
/*
┌─id─┬─code1─┬─count─┐
│  1 │ A001  │    30 │
│  2 │ A002  │    28 │
│  3 │ A003  │    32 │
└────┴───────┴───────┘
*/

SELECT * FROM tbl_2;
/*
┌─id─┬─code2─┬─count─┐
│  1 │ B001  │    35 │
│  1 │ B001  │    29 │
│  3 │ B003  │    31 │
│  4 │ B004  │    38 │
└────┴───────┴───────┘
*/

4.4.1 连接精度

连接精度决定了 JOIN 查询在连接数据时所使用的策略,目前支持 ALL、ANY 和 ASOF 三种类型。如果不主动声明,则默认是 ALL。可以通过 join_default_strictness 配置参数修改默认的连接精度类型。
一、ALL INNER JOIN

SELECT t1.id, t1.code1, t2.code2 
FROM tbl_1 AS t1 
ALL INNER JOIN tbl_2 AS t2
ON t1.id = t2.id;

id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 1|A001 |B001 |
 3|A003 |B003 |

二、ANY INNER JOIN

SELECT t1.id, t1.code1, t2.code2 
FROM tbl_1 AS t1 
ANY INNER JOIN tbl_2 AS t2
ON t1.id = t2.id;

id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 3|A003 |B003 |

所以结论很清晰了,如果左表内的一行数据,在右表中有多行数据与之连接匹配,那么当连接精度为 ALL,会返回右表中全部连接的数据;当连接精度为 ANY,会仅返回右表中第一行连接的数据

这就是连接精度,没什么好稀奇的,不过在关系型数据库中则没有连接精度的概念,因为当出现这种情况,只有一个策略,那就是直接返回右表中匹配的全部数据。而在 ClickHouse 中,给了我们自由选择的权利。

三、ASOF INNER JOIN
除了 ALL 和 ANY 之外还有一个 ASOF,它是做什么的呢?首先无论 ALL 还是 ANY,在连接的时候必须是等值连接。比如上面的 t1.id = t2.id,如果改成 t1.id >= t2.id 就是错误的,如果是多个连接条件,那么这些连接条件都必须是等值连接。但 ASOF 表示模糊连接,也就是它允许你在等值连接的后面加上一个非等值连接,举个栗子:

SELECT t1.id, t1.code1, t2.code2, t1.count AS count1, t2.count AS count2
FROM tbl_1 AS t1 
ALL INNER JOIN tbl_2 AS t2
ON t1.id = t2.id;
/*
┌─id─┬─code1─┬─code2─┬─count1─┬─count2─┐
│  1 │ A001  │ B001  │     30 │     35 │
│  1 │ A001  │ B001  │     30 │     29 │
│  3 │ A003  │ B003  │     32 │     31 │
└────┴───────┴───────┴────────┴────────┘
*/

SELECT t1.id, t1.code1, t2.code2, t1.count AS count1, t2.count AS count2
FROM tbl_1 AS t1 
ASOF INNER JOIN tbl_2 AS t2
ON t1.id = t2.id AND t1.count > t2.count;
/*
┌─id─┬─code1─┬─code2─┬─count1─┬─count2─┐
│  1 │ A001  │ B001  │     30 │     29 │
│  3 │ A003  │ B003  │     32 │     31 │
└────┴───────┴───────┴────────┴────────┘
*/

SELECT t1.id, t1.code1, t2.code2, t1.count AS count1, t2.count AS count2
FROM tbl_1 AS t1 
ASOF INNER JOIN tbl_2 AS t2
ON t1.id = t2.id AND t1.count < t2.count;
/*
┌─id─┬─code1─┬─code2─┬─count1─┬─count2─┐
│  1 │ A001  │ B001  │     30 │     35 │
└────┴───────┴───────┴────────┴────────┘
*/

所以结论很清晰,如果连接精度为 ALL 或者 ANY,那么所有的连接条件必须为等值连接,如果出现了非等值连接则报错。而这两者的唯一区别就在于:

ALL:如果右表有多条数据匹配,返回所有的匹配的数据
ANY:如果右表有多条数据匹配,返回第一条匹配的数据

如果连接精度为 ASOF,那么允许在等值连接条件后面追加一个非等值连接,所以上面的 t1.id = t2.id 是等值连接,t1.count > t2.count 是非等值连接。

但需要注意的是:使用非等值连接时,这个非等值可以是 >、>=、<、<=,但不能是 !=;并且对于 ASOF 而言,连接条件必须是等值连接和非等值连接的组合,两者缺一不可。

对于ASOF而言,如果右表中有多行数据匹配,只会返回第一行

4.4.2 连接类型

连接类型就比较简单了,这个和关系型数据库是完全类似的。
一、左连接

-- 省略连接精度,默认为 ALL
-- 左连接
SELECT t1.id, t1.code1, t2.code2 
FROM tbl_1 t1 LEFT JOIN tbl_2 t2 
USING(id); -- 等价于 t1.id = t2.id
/*
id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 1|A001 |B001 |
 2|A002 |     |
 3|A003 |B003 |
*/

二、右连接

-- 省略连接精度,默认为 ALL
-- 右连接
SELECT t1.id, t1.code1, t2.code2 
FROM tbl_1 t1 RIGHT JOIN tbl_2 t2 
USING(id); -- 等价于 t1.id = t2.id
/*
id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 1|A001 |B001 |
 3|A003 |B003 |
 4|     |B004 |
*/

三、全连接

-- 省略连接精度,默认为 ALL
-- 全连接
SELECT t1.id, t1.code1, t2.code2 
FROM tbl_1 t1 FULL JOIN tbl_2 t2 
USING(id); -- 等价于 t1.id = t2.id
/*
id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 1|A001 |B001 |
 2|A002 |     |
 3|A003 |B003 |
 4|     |B004 |
*/

和关系型数据库类似,但有一点区别,就是当没有与之匹配的记录时,会使用对应类型的空值进行补全,而不是 Null。这里没有指定连接精度,默认为 ALL,此外 LEFT / RIGHT / FULL JOIN 后面都可以加上一个 OUTER,不过也可以不加。
四、交叉连接
最后是交叉连接,交叉连接直接会去笛卡尔积,不需要任何的连接条件。

SELECT t1.id, t1.code1, t2.code2 
FROM tbl_1 t1 CROSS JOIN tbl_2 t2;
/*
id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 1|A001 |B001 |
 1|A001 |B003 |
 1|A001 |B004 |
 2|A002 |B001 |
 2|A002 |B001 |
 2|A002 |B003 |
 2|A002 |B004 |
 3|A003 |B001 |
 3|A003 |B001 |
 3|A003 |B003 |
 3|A003 |B004 |
 */

4.4.3 SEMI和ANTI

我们之前说连接精度不止ALL、ANY、ASOF 三种,还有 SEMI 和 ANTI,只不过这两个比较特殊,因为它们只能用在 LEFT JOIN 和 RIGHT JOIN 上面,所以我们单独介绍。

t1 SEMI LEFT JOIN t2 USING(id):遍历t1中的id,如果存在于t2中,则输出
t1 SEMI RIGHT JOIN t2 USING(id):遍历t2中的id,如果存在于t1中,则输出
t1 ANTI LEFT JOIN t2 USING(id):遍历t1中的id,如果不存在于t2中,则输出
t1 ANTI RIGHT JOIN t2 USING(id):遍历t2中的id,如果不存在于t1中,则输出

举个例子:

SELECT  t1.id,t1.code1,t2.code2
from tbl_1 as t1
SEMI LEFT JOIN tbl_2 as t2
on t1.id = t2.id
遍历左表,id为1、3的记录出现在了右表中,所以输出
/*
id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 3|A003 |B003 |
*/

SELECT  t1.id,t1.code1,t2.code2
from tbl_1 as t1
SEMI RIGHT JOIN tbl_2 as t2
on t1.id = t2.id
同理,但右表中出现了两条id为1的记录,所以这里也会输出两条id为1的记录,因为遍历的是右表。
/*
id|code1|code2|
--+-----+-----+
 1|A001 |B001 |
 1|A001 |B001 |
 3|A003 |B003 |
*/

ANTI 则与之类似,只不过它的策略是不出现才输出,可以自己尝试一下。另外可能有人发现,这个 SEMI 的功能貌似有些重复了,因为我们使用 ALL 和 ANY 完全可以取代。其实如果你用过 hive 的话,会发现 SEMI LEFT JOIN 和 ANTI LEFT JOIN 是 IN/EXISTS 的一种更加高效的实现:

-- 这种子查询应该非常常见了,查询一张表,
-- 而过滤的条件该表的某个字段的取值要出现在另一张表的某个字段中
SELECT id, code1 FROM tbl_1 WHERE id in (SELECT id FROM tbl_2);
/*
┌─id─┬─code1─┐
│  1 │ A001  │
│  3 │ A003  │
└────┴───────┘
*/

-- 而通过 SEMI LEFT JOIN 的话,效率会更高一些
SELECT t1.id, t1.code1 FROM tbl_1 t1 
SEMI LEFT JOIN tbl_2 t2 USING(id)
/*
┌─id─┬─code1─┐
│  1 │ A001  │
│  3 │ A003  │
└────┴───────┘
*/
-- ANTI 则是为了 NOT IN/EXISTS

两者的输出是一致的,所以 SEMI / ANTI LEFT JOIN 是为了 IN/EXISTS 这类场景而设计的,至于 SEMI RIGHT JOIN、ANTI RIGHT JOIN 就用的不是很多了。

Hive 里有一个 LEFT SEMI JOIN,单词顺序调换了一下,用途是类似的,不过它的局限性要比 ClickHouse 中的 SEMI LEFT JOIN 大很多。

4.4.4 多表连接

在进行多张数据表的连接查询时,ClickHouse 会将它们转为两两连接的形式。我们首先再创建一张表:

create table tbl_3(
	id UInt8,
	code3 String,
	count UInt8
)engine = Memory;

--写入测试数据
INSERT INTO TABLE tbl_3 VALUES
	(3,'C003',40);

然后对三张测试表进行连接查询:

SELECT t1.id, t1.code1, t2.code2, t3.code3
FROM tbl_1 AS t1 INNER JOIN tbl_2 AS t2 ON t1.id = t2.id
LEFT JOIN tbl_3 AS t3 ON t1.id = t3.id;
在执行上述查询时,tbl_1 和 tbl_2 会先进行内连接,之后再将它们的结果集合 tbl_3 进行左连接。

t1.id|t1.code1|t2.code2|t3.code3|
-----+--------+--------+--------+
    1|A001    |B001    |        |
    1|A001    |B001    |        |
    3|A003    |B003    |C003    |

另外 ClickHouse 也支持关联查询的语法,只不过会自动转成指定的连接查询,举个栗子:

-- 关联查询,如果没有 WHERE,那么三张表会做笛卡尔积
SELECT t1.id, t1.code1, t2.code2, t3.code3
FROM tbl_1 t1, tbl_2 t2, tbl_3 t3 
WHERE t1.id = t2.id AND t1.id = t3.id
/*
┌─t1.id─┬─t1.code1─┬─t2.code2─┬─t3.code3─┐
│     3 │ A003     │ B003     │ C003     │
└───────┴──────────┴──────────┴──────────┘
*/

以上就是关联查询,虽然也能实现,不过还是不推荐这种做法,因为此时连接条件和过滤条件都写在了 WHERE 子句里面,看起来会比较混乱。所以更推荐连接查询(ClickHouse 会自动转化),也就是 JOIN ON 的形式,此时 ON 后面写连接条件,而数据过滤条件写 WHERE 里面(当然我们这里不需要过滤)。

SELECT t1.id, t1.code1, t2.code2, t3.code3
FROM tbl_1 t1 INNER JOIN tbl_2 t2 ON t1.id = t2.id
INNER JOIN tbl_3 t3 ON t1.id = t3.id
/*
┌─t1.id─┬─t1.code1─┬─t2.code2─┬─t3.code3─┐
│     3 │ A003     │ B003     │ C003     │
└───────┴──────────┴──────────┴──────────┘
*/

4.4.5 注意事项

最后,还有两个关于 JOIN 查询的注意事项。
一、关于性能
最后,还有两个关于 JOIN 查询的注意事项。为了能够优化 JOIN 查询性能,首先应该遵循左大右小的原则,即数据量小的表要放在右侧。这是因为在执行 JOIN 查询时,无论使用的是哪种连接方式,右表都会被全部加载到内存中与左表进行比较。

其次,JOIN 查询目前没有缓存的支持,这意味着每一次 JOIN 查询,即便是连续执行相同的 SQL,也都会生成一次全新的执行计划。如果应用程序会大量使用 JOIN 查询,则需要进一步考虑借助上层应用侧的缓存服务或使用 JOIN 表引擎来改善性能。

最后,如果是在大量维度属性补全的查询场景中,则建议使用字典代替 JOIN 查询。因为在进行多表的连接查询时,查询会转换成两两连接的形式,而这种滚雪球式的查询很可能带来性能问题。

二、空值策略
在之前的介绍中,连接查询的空值(那些未被连接的数据)是由默认值填充的,这与其他数据库所采取的策略不同(由Null 填充)。

连接查询的空值策略通过 join_use_nulls 参数指定的,默认为 0。当参数值为 0 时,空值由数据类型的默认值填充;而当参数值为 1 时,空值由 Null 填充。

4.5 WHERE与PREWHERE子句

WHERE子句基于条件表达式来实现数据过滤,如果过滤条件恰好是主键字段,则能够进一步借助索引过滤数据区间,从而加速查询,所以WHERE子句是一条查询语句能否启用索引的判断依据,前提是表引擎支持索引特性。

除了 WHERE,ClickHouse 还支持全新的 PREWHERE 子句,PREWHERE 目前只能用于 MegeTee 系列的表引擎,它可以看作对是 WHERE 的一种优化,其作用与 WHERE 相同,均是用来过滤数据。但它们的不同之处在于。使用 PREWHERE 时,首先只会读取 PREWHERE 指定的列字段数据,用于数据过滤的条件判断。待数据过滤之后再读取 SELECT 声明的列字段以补全其余属性。所以在一些场合下,PREWHERE 相比 WHERE 而言,处理的数据量更少,性能更高

既然 WHERE 子句性能更优,那么是否需要将所有的 WHERE 子句都替换成 PREWHERE 子句呢?其实大可不必,因为 ClickHouse 实现了自我优化的功能,会在条件合适的情况下将 WHERE 替换为 PREWHERE。如果想开启这项特性,只需要将 optimize_move_to_prewhere 设置为 1 即可,当然默认就为 1,即开启状态。

但凡事也有例外,以下情形不会自动优化:

1)使用了常量表达式:
SELECT id, code1 FROM tbl WHERE 1 = 1

2)使用了默认值为 ALIAS 类型的字段:
-- 假设 code 的默认值类型是 ALIAS
SELECT id, code1 FROM tbl WHERE code1 = 'A000'

3)包含了 arrayJoin、globalIn、globalNotIn 或者 indexHint 查询的:
SELECT title, nested.v1, nested.v2 FROM tbl ARRAY JOIN nested WHERE nested.v1 = 1

4)SELECT 查询的列字段和 WHERE 谓词相同:
SELECT v3 FROM tbl WHERE v3 = 1

5)使用了主键字段:
SELECT id FROM tbl WHERE id = 'A000'

虽然在上述情形中 ClickHouse 不会自动将谓词移动到 PREWHERE,但仍然可以主动使用 PREWHERE。以主键字段为例,当使用 PREWHERE 进行主键查询时,首先会通过稀疏索引过滤数据区间(index_granularity 粒度),接着会读取 PREWHERE 指定的条件列进一步过滤,这样一来就有可能截掉数据区间的尾巴,从而返回低于 index_granularity 粒度的数据范围。但即便如此,相比其他场合移动谓词所带来的性能提升,这类效果还是比较有限的,所以目前 ClickHouse 在这类场合下仍然保持不移动的处理方式。

4.6 GROUP BY子句

GROUP BY 又称聚合查询,是最常用的子句之一,它是让 ClickHouse 最凸显卓越性能的地方。在 GROUP BY 后声明的表达式,通常称为聚合键或者 Key,数据会按照聚合键进行聚合。ClickHouse 的聚合查询中,和关系型数据库也是类似的。

-- 只有聚合函数,可以省略 GROUP BY
SELECT sum(data_compressed_bytes) AS compressed,
       sum(data_uncompressed_bytes) AS undata_compressed_bytes
FROM system.parts;
/*
compressed|undata_compressed_bytes|
----------+-----------------------+
  31889453|              585525456|
*/

-- SELECT 子句中的字段要么出现在 GROUP BY 子句中,要么出现在聚合函数中
SELECT table, count() FROM system.parts GROUP BY table;
/*
table                  |count()|
-----------------------+-------+
part_log               |      2|
metric_log             |     50|
partition_v1           |      4|
trace_log              |     58|
query_thread_log       |      4|
query_log              |      4|
session_log            |      7|
asynchronous_metric_log|     59|
*/


-- 错误的语法,rows 既没有出现在 GROUP BY 中,也没有出现在聚合函数中
SELECT table, count(), rows() FROM system.parts GROUP BY table;

如果聚合键对应的列包含 Null 值,那么所有的 Null 会被归为同一组。

select arrayJoin([1,2,3,Null,Null]) as v,count() group by v;
/*
v|count()|
-+-------+
1|      1|
2|      1|
3|      1|
 |      2|
*/

所有的 Null 被分为了一组,但是注意:count(字段) 不会把 Null 计算在内,所以直接 count() 就行。

比较简单,但除了上述特性之外,聚合查询还能配合 WITH ROLLUP、WITH CUBE、WITH TOTALS 三种修饰符获取额外的汇总信息。

4.6.1 WITH ROLLUP

测试数据如下:

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel

python Clickhouse 插入date类型_子查询_04


以上是普通的 GROUP BY,没什么难的,然后看看它和 WITH ROLLUP 搭配会有什么效果:

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel WITH ROLLUP

python Clickhouse 插入date类型_sed_05


我们注意到,多了四条数据,上面三条,就是按照 product、channel 汇总之后,再单独按 product 汇总,而此时会给对应的 channel 设为零值(这里是空字符串,关系型数据库中为 Null)。同理最后一条数据是全量汇总,不需要指定 product 和 channel,所以显示为 product 和 channel 都显示为零值。我们看到这就相当于按照 product 单独聚合然后再自动拼接在上面了,排好序,并且自动将 channel 赋值为零值,同理最后一条数据也是如此。当然我们也可以写多个语句,然后通过 UNION 也能实现上面的效果,有兴趣可以自己试一下。但是 ClickHouse 提供了 WITH ROLLUP 这个非常方便的功能,我们就要利用好它。

GROUP BY 子句加上 WITH ROLLUP 选项时,首先按照全部的分组字段进行分组汇总;然后从右往左依次去掉一个分组字段再进行分组汇总,被去掉的字段显示为零值;最后,将所有的数据进行一次汇总,所有的分组字段都显示为零值。

4.6.2 WITH CUBE

CUBE 代表立方体,它用于对分组字段进行各种可能的组合,能够产生多维度的交叉统计结果,CUBE 通常用于数据仓库中的交叉报表分析。

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel WITH CUBE

python Clickhouse 插入date类型_子查询_06


从以上结果可以看出,CUBE 返回了更多的分组数据,其中不仅包含了 ROLLUP 汇总的结果,还包含了相当于按照 channel 进行聚合的记录。因此随着分组字段的增加,CUBE 产生的组合将会呈指数级增长。

4.6.3 WITH TOTALS

WITH TOTALS 反而是最简单的,只包含一个全局汇总的结果。

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel WITH TOTALS;

python Clickhouse 插入date类型_子查询_07

4.7 HAVING 子句

HAVING 子句要和 GROUP BY 子句同时出现,不能单独使用,它能够在聚合计算之后实现数据的二次过滤。

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel 
HAVING channel = '京东';
聚合之后,选择channel为京东的记录

SELECT  product,channel,sum(amount) as amount
from sales_data
where channel = '京东'
group by product,channel;
先选择channel为京东的记录,然后再聚合

python Clickhouse 插入date类型_clickhouse_08


对于上面的栗子,使用 WHERE 比使用 HAVING 的效率更高,因为 WHERE 等同于使用了谓词下推,在聚合之前就减少了数据过滤,从而减少了后续聚合时需要处理的数据量。

所以使用 HAVING 进行过滤,那么应该是对聚合之后的结果进行过滤。如果不是聚合之后的,那么使用 WHERE 就好,举个栗子:

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel;

python Clickhouse 插入date类型_sed_09

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel
HAVING amount > 250000;

python Clickhouse 插入date类型_子查询_10


因为 WHERE 的优先级大于 GROUP BY,所以如果按照聚合值进行统计,那么就必须要借助于 HAVING。

4.8 ORDER BY子句

ORDER BY子句通过声明排序键来指定查询数据返回的顺序,通过先前的介绍我们知道,在 MergeTree 表引擎中也有 ORDER BY 参数用于指定排序键,那么这两者有何不同呢?在 MergeTree 中指定 ORDER BY 后,数据在各个分区内会按照其定义的规则排序,这是一种分区内的局部排序。如果在查询时数据跨越了多个分区,则它们的返回顺序是无法预知的,每一次查询返回的顺序都有可能不同。在这种情况下,如果需要数据总是能够按照期望的顺序范围,就需要借助 ORDER BY子句来指定全局顺序。

ORDER BY 在使用时可以定义多个排序键,每个排序键后需紧跟 ASC(升序)或 DESC(降序)来确定排列顺序。如若不写,则默认为 ASC。例如下面的两条语句即是等价的:

SELECT * FROM tbl ORDER BY v1 ASC, v2 DESC;
SELECT * FROM tbl ORDER BY v1, v2 DESC;

数据首先会按照 v1 升序,如果 v1 字段中出现了相同的值,那么再按照 v2 降序。

然后是 Null 值的排序,目前 ClickHouse 有 Null 值最后和 Null 值最前两种策略,可以通过如下进行设置:

4.8.1 NULLS LAST

Null 值排在最后,无论升序还是降序,这也是默认的行为。
value -> NaN -> Null

select arrayJoin([1,22,Null,3,NaN,-1/0,1/0]) v order by v;
等价于
select arrayJoin([1,22,Null,3,NaN,-1/0,1/0]) v order by v NULLS LAST;
/*
v        |
---------+
-Infinity|
      1.0|
      3.0|
     22.0|
 Infinity|
      NaN|
         |
*/

4.8.2 NULLS FIRST

Null 值排在最后,无论升序还是降序。
NULL -> NaN -> value

select arrayJoin([1,22,Null,3,NaN,-1/0,1/0]) v order by v NULLS FIRST ;
/*
v        |
---------+
         |
      NaN|
-Infinity|
      1.0|
      3.0|
     22.0|
 Infinity|
*/

经过测试不难发现,对于 NaN 而言,它总是跟在 Null 的身边。

4.9 LIMIT BY子句

LIMIT BY 子句和大家常见的 LIMIT 有所不同,它运行于 ORDER BY 之后和 LIMIT 之前,它能够按照指定分组,最多返回前 n 行数据(少于 n 行则按照实际数量返回),常用于 TOP N 的查询场景。
LIMIT BY 语法规则如下:

LIMIT n BY express

个人觉得这个 LIMIT BY 非常强大,我们举个栗子:

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel;
直接按照product,channel聚合之后的结果

python Clickhouse 插入date类型_clickhouse_11

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel
limit 1 BY channel;
直接按照product,channel聚合之后,然后按照channel进行分组,然后选择出现的第一条数据。

python Clickhouse 插入date类型_数据_12


当然聚合之后没有排序,我们还可以排一下序:

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel
ORDER BY amount ASC
limit 1 BY channel;

此时会选择每个渠道对应的金额最高的数据,当然我们也可以 LIMIT 多条数据、也可以 BY 多个字段。这个功能可以说是非常常用了,我们平时使用的 LIMIT,一般是全局排序之后选择前 N 条数据,而这里的 LIMIT BY 是按照指定的字段分组,然后每一组选择前 N 条数据。

LIMIT BY 会从上往下在每个组中选择指定条数的数据,因此使用 LIMIT BY 应该同时指定 ORDER BY,否则拿出的数据没有太大意义,除非数据本身就是有序的。

当然 LIMIT BY 也可以指定偏移量,因为不一定从一条开始选择,而指定偏移量有两种方式:

LIMIT N OFFSET M BY ...
LIMIT M, N BY ...

例如

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel
ORDER BY amount ASC
limit 2 BY channel;
按照channel分组,选择出现的前两条数据

python Clickhouse 插入date类型_clickhouse_13

SELECT  product,channel,sum(amount) as amount
from sales_data
group by product,channel
ORDER BY amount ASC
limit 1,1 BY channel;
按照channel分组,选择出现的第二条数据,因为过滤掉了1条。

python Clickhouse 插入date类型_子查询_14

4.10 LIMIT 子句

LIMIT 子句用于返回指定的前 N 行数据,常用于分页场景,它的三种语法形式如下:

LIMIT N
LIMIT N OFFSET M
LIMIT M, N

用法和 LIMIT BY 中的 LIMIT 一致,如果把 LIMIT BY 中的 BY 去掉,那么就变成了 LIMIT。比较简单,这里用一张图来介绍一下 LIMIT 和 LIMIT BY 之前的关系:

python Clickhouse 插入date类型_数据_15


比较简单,但是在使用 LIMIT 的时候需要注意一点,如果数据跨越了多个分区,那么在没有使用 ORDER BY 指定全局顺序的情况下,每次 LIMIT 查询所返回的数据可能有所不同。如果对返回的数据的顺序比较敏感,则应搭配 ORDER BY 一起使用。

4.11 SELECT和DISTINCT子句

SELECT子句决定了一次查询语句最终能返回哪些字段或表达式,与直观感受不同,虽然 SELECT 位于 SQL 语句的起始位置,但它的执行的顺序却排在了上面介绍的所有子句的后面

在其它子句执行之后,SELECT 会将选取的字段或表达式作用于每行数据之上,如果使用 * 通配符,则会返回所有字段。但正如开篇所言,大多数情况下都不建议这么做,因为对于一款列式存储数据库而言,这绝对是劣势而不是优势。

在选择列字段时,ClickHouse 还为特定场景提供了一种基于正则查询的形式,例如下面会选择以 n 开头和包含字母 p 的字段:

SELECT COLUMNS('^n'), COLUMNS('p') FROM system.databases;

python Clickhouse 插入date类型_数据_16

DISTINCT 子句能够去除重复数据,使用场景也很广泛,很多人经常会拿它和 GROUP BY 进行对比:

SELECT  product,channel
from sales_data
group by product,channel;

python Clickhouse 插入date类型_数据_17

SELECT DISTINCT product,channel
from sales_data;

python Clickhouse 插入date类型_clickhouse_18


虽然顺序不同,但显然结果集的内容是一致的,那么这两者之间有什么区别呢?如果观察它们的执行计划(后面会说)不难发现,DISTINCT 子句的执行计划会更加简单,与此同时,DISTINCT 也能够和 GROUP BY 搭配使用,所以它们是互补而不是互斥的关系。

另外,如果使用了 LIMIT 且没有 ORDER BY 子句,那么 DISTINCT 在满足条件时能够立即结束查询。假设我只需要去重之后的前三条数据,那么 GROUP BY 会对全体数据进行分组,然后再选择前三条;而 DISTINCT 在去重时发现已经有三条了,于是直接返回,后面的数据就不需要看了,因为看了也没意义,LIMIT 决定了只返回三条。

SELECT product,channel
from sales_data
group by product,channel
LIMIT 3;

python Clickhouse 插入date类型_sed_19

SELECT DISTINCT product,channel
from sales_data
LIMIT 3;

python Clickhouse 插入date类型_sed_20


两个查询返回的结果集不一样,这是因为 GROUP BY 和 DISTINCT 处理数据的顺序不同。一开始我们就看到了,如果没有 LIMIT,那么两个结果集顺序不同,但内容是一样的,只是这里加了 LIMIT,所以相当于选择了相同内容的不同部分。

如果有 ORDER BY,那么会先执行 DISTINCT,再执行 ORDER BY。并且对于 Null 而言,如果有多个 Null,那么 DISTINCT 之后只会保留一个 Null。

4.12 UNION ALL 子句

UNION ALL 子句能够联合左右两边的两组子查询,将结果一并返回。在一次查询中可以声明多次 UNION ALL 以便联合多组查询,但 UNION ALL 不能直接使用其他子句(例如 ORDER BY、LIMIT 等),这些子句只能在它联合的子查询中使用。

SELECT name, v1 FROM union_v1
UNION ALL
SELECT title, v1 FROM union_v1

对于 UNION ALL 两侧的子查询有以下几点信息:首先,列字段的数量必须相同;其次,列字段的数据类型必须相同或相兼容;最后,列字段的名称可以不同,查询结果中的列名会以左边的子查询为准。

对于联合查询还有一点要说明,目前 ClickHouse 只支持 UNION ALL 子句,如果想得到 UNION DISTINCT 子句的效果,可以使用嵌套查询来变相实现,例如:

SELECT DISTINCT name FROM
(
    SELECT name, v1 FROM union_v1
    UNION ALL
    SELECT title, v1 FROM union_v1
)

4.13 SAMPLE 子句

SAMPLE 子句能够实现数据采样的功能,使查询仅返回采样数据而不是全部数据,从面有效减少查询负载。SAMPLE 子句的采样机制是一种幂等设计,也就是说在数据不发生变化的情况下,使用相同的采样规则总是能等返回相同的数据,所以这项特性非常适合在那些可以接受近似查询结果的场合使用。例如在数据量十分巨大的情况下,对查询时效性的要求大于准确性时就可以尝试使用 SAMPLE 子句。
SAMPLE 子句只能用于 MergeTree 系列引擎的数据表,并且要求在 CREATE TABLE 时声明 SAMPLE BY 表达式,例如:

CREATE TABLE hits_v1 (
    CounterID UInt64,
    EventDate Date,
    UserID UInt64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, intHash32(UserID))
-- SAMPLE BY 声明的表达式必须要包含在主键的声明中
SAMPLE BY intHash32(UserID)

SAMPLE BY 表示 hits_v1内的数据,可以按照 intHash32(UserID) 分布后的结果采样查询。但需要注意:SAMPLE BY 所声明的表达式必须同时包含在主键的声明内,并且选择的字段必须是 Int 类型,如果不是 ClickHouse 在建表的时候也不会报错,但查询的时候会出异常。