简介
大家都说 MySQL 和 ClickHouse 很像,如果你不了解 ClickHouse,那我觉得你是对的,但若是你了解,那你更需要往下看看。
数据定义
数据类型
作为一款分析性数据库,ClickHouse 提供了很多数据类型,这里面主要可以分为基础类型、复合类型和特殊类型。
基础类型
基础类型主要包含有数值、字符串和时间三种类型,没有 Boolean 类型,但是可以使用整形的 0 和 1 来代替。
数值类型
数值类型分为整数、浮点数和定点数三类。Int:ClickHouse使用Int8、Int16、Int32、Int64指代四种大小的Int类型,其末尾的数字正好表明占用字节的大小。Float:与整数类似,ClickHouse直接使用Float32、Float64代表单精度浮点数及双精度浮点数。Decimal:如果需要更高精度的数值运算,则需要使用定点数,ClickHouse提供了Decimal32、Decimal64、Decimal128三种精度的定点数。可以通过两种形式声明定点:简写方式有Decimal32(S)、Decimal64(S)、Decimal128(S)三种;原生方式为Decimal(P, S),其中P代笔精度,决定总位数(整数部份+小数部分),S代表规模,决定小数位数。
字符串类型
字符串类型可以细分为String、FixedString、UUID三类。String:字符串由String定义,长度不限。FixedString:定长字符串,在使用时需表明字符串的长度。UUID:UUID是一种数据库常见的主键类型,在ClickHouse中被当作了一种数据类型,UUID共有32位,格式为8-4-4-4-12,如果一个UUID类型的字段在写入时没有被赋值则会被依照格式使用0填充。
时间类型
时间类型分为DataTime、DateTime64、Date三类,ClickHouse目前没有时间戳类型,时间类型最高精度是秒,也就是说如果需要处理毫秒、微秒等大于秒分辨率的时间,则只能借助UInt类型实现。DateTime:包含时、分、秒信息,精确到秒,支持使用字符串形式写入。DateTime64:可以记录亚秒,在DateTime之上增加了精度的设置。Date:不包含具体的时间信息,只精确到天,同样也支持字符串形式写入。
复合类型
除了基础类型之外,ClickHouse 还提供了数组、元组、枚举和嵌套四类复合类型。
Array:数组有两种定义方式,常规方式为array(T),或者简写方式[T]。通过上述发现在查询时并不需要主动声明数组的元素类型,因为ClickHouse的数组拥有类型推断的能力,而推断依据则是以最小存储代价为原则,即使用最小可表达的数据类型。Tuple:元组类型由1~n个元素组成,每个元素之间允许设置不同的数据类型,且彼此之间不要求兼容。元组同样支持类型推断,其推断仍然以最小的存储代价为原则。与数组类似元组也有两种方式定义,常规方式为tuple(T),或者简写方式(T)。Enum:ClickHouse支持枚举类型,提供了Enum8和Enum16两种枚举类型,他们除了取值范围不同之外,别无二致。枚举固定使用(String:Int) Key/Value键值对的形式定义数据,所以Enum8和Enum16分别对应(String:Int8)和(String:Int16)。
Key/Value是不允许重复的,要保证唯一性。另外也不允许为Null,但是Key允许是空字符串。Nested:嵌套类型,顾名思义就是一种嵌套表结构。一张表结构,可以定义任意多个嵌套类型字段,但每个字段的嵌套层级只支持一级,即嵌套表内不能继续使用嵌套类型。对于简单场景的层级关系或关联关系,使用嵌套类型也是不错的选择。
特殊类型
ClickHouse 还有一类不同的数据类型,那就是特殊类型。
Nullable:准确说这并不是一种独立的数据类型,更像是一种辅助的修饰符,需要与基础数据一起搭配使用,表示某个基础数据可以是Null值。Domain:域名类型分为IPv4和IPv6两类,本质上就是对整形和字符串的进一步封装。但是在使用时不可当作字符串使用,因为其并不支持隐式的自动类型转换,如果需要返回IP的字符串形式,需要显式调用IPv4NumToString或IPv6NumToString函数进行转换。
数据表
之前说的都是数据类型,接下来就是 DDL 操作及定义数据的方法。
数据库
1 | CREATE DATABASE IF NOT EXISTS db_name [ENGINE = engine] |
数据库目前一共支持五种默认引擎:
Ordinary:默认引擎,大多数情况下使用的引擎,使用时无需声明。Dictionary:字典引擎,此类数据库会为所有数据字典创建他们的数据表。Memory:内存引擎,用于存放临时数据。Lazy: 日志引擎,只能使用Log系列的表引擎。MySQL:会自动拉取远端MySQL中的数据,并为他们创建MySQL表引擎的数据表。
数据表
1 | CREATE TABLE [IF NOT EXISTS] [db_name.]table_name ( |
上面是常见的创建表方式,下面则是通过复制其他表结构来创建表
1 | CREATE TABLE [IF NOT EXISTS] [db_name1.]table_name1 AS [db_name2.]table_name2 [ENGINE = engine] |
默认值表达式
表字段支持三种默认值表达式的定义方法,分别是 DEFAULT、 MATERIALIZED、 ALIAS,无论使用哪种形式,表字段一旦被定义了默认值,便不再强制要求定义数据类型,因为 ClickHouse 会根据默认值进行类型推断。若是对表字段定义了数据类型和默认值表达式,则以明确定义的数据类型为主。
默认值表达式的三种定义方法之间也存在着不同之处:
- 在数据写入时,只有
DEFAULT类型的字段可以出现在INSERT语句中,而MATERIALIZED和ALIAS不能被显式赋值,只能通过计算取值。 - 在数据查询时,只有
DEFAULT类型的字段可以出现在SELECT语句中,而MATERIALIZED和ALIAS不能出现在返回结果集中。 - 在数据存储时,只有
DEFAULT和MATERIALIZED类型的字段才支持持久化,而ALIAS类型的字段不能被持久化,它的取值依赖于计算。
临时表
1 | CREATE TEMPORARY TABLE [IF NOT EXISTS] [db_name.]table_name ( |
相比普通表而言,临时表由以下两点特殊之处:
- 临时表的生命周期与会话绑定,所以他仅支持
Memory表引擎,如果会话结束,数据表就会被销毁。 - 临时表不属于任何数据库,所以在建表语句中,没有数据库参数和表引擎参数。
临时表的优先级大于普通表,当两张表名称相同时,会优先读取临时表的数据。
分区表
数据分区(partition)和数据分片(shard)是完全不同的两个概念,数据分区是针对本地数据而言的,是数据的一种纵向切分,而数据分片是数据的横向切分。但是目前只有合并树(MergeTree)家族系列的引擎支持数据分区。
1 | CREATE TABLE [IF NOT EXISTS] [db_name.]table_name ( |
视图
ClickHouse 拥有普通和物化两种视图,其中物化视图拥有独立的存储,而普通视图只是一层简单的查询代理。
普通视图不会存储任何数据,只是一层简单的 SELECT 查询映射,起着简化查询、明晰语义的作用,对查询性能不会有影响。而物化视图支持表引擎,数据保存形式由他的表引擎决定,物化视图创建好之后,如果源表被写入数据,那么物化视图也会同步更新。POPULATE 修饰符决定了物化视图的初始策略,若使用 POPULATE 则在创建视图时会将源表的数据一并导入,反之不使用 POPULATE 则物化视图初始是没有数据的,只会在同步之后写入源表的数据。另外物化视图不支持删除数据,源表中的数据被删除,物化视图中的数据不会被删除。
1 | # 创建普通视图的语法 |
数据表操作
基本操作
追加新字段
1
ALTER TABLE tb_name ADD COLUMN [IF NOT EXISTS] name [type] [default_expr] [AFTER name_after]
修改数据类型
1
ALTER TABLE tb_name MODIFY COLUMN [IF EXISTS] name [type] [default_expr]
修改备注
1
ALTER TABLE tb_name COMMENT COLUMN [IF EXISTS] name 'some comment'
删除已有字段
1
ALTER TABLE tb_name DROP COLUMN [IF EXISTS] name
移动数据表
1
RENAME TABLE [db_name1.]tb_name1 TO [db_name2.]tb_name2
其原理与
Linux系统中的mv命令异曲同工。清空数据表
1
TRUNCATE TABLE [IF EXISTS] [db_name.]tb_name
查询分区信息
ClickHouse内置了许多system系统表,用于查询自身的状态信息,其中的parts系统表专门用于查询数据表的分区信息。1
SELECT partition_id, name, table, database FROM system.parts WHERE table = 'partition_v2'
删除指定分区
1
ALTER TABLE tb_name DROP PARTITION partition_expr
复制分区数据
ClickHouse支持将A表的分区数据复制到B区,可以用于快速数据写入、多表间数据同步和备份等场景。当然此项特性支持是有前提的:
- 两张表需要拥有相同的分区键。
- 他们的表结构完全相同。
1
ALTER TABLE B REPLACE PARTITION partition_expr FROM A
重置分区数据
如果数据表的某一列数据有误,需要将其重置为初始值,如果声明了默认值表达式,则以表达式为准,否则以数据类型的默认值为准。1
ALTER TABLE tb_name CLEAR COLUMN column_name IN PARTITION partition_expr
卸载装载分区
表分区可以通过DETACH语句卸载,分区被卸载后他的物理数据会被转移到当前数据表目录的detached子目录下,而装载分区则是反向操作,可以将detached子目录下的某个分区重新装载回去。1
2
3
4# 卸载分区语法
ALTER TABLE tb_name DETACH PARTITION partition_expr
# 装载分区语法
ALTER TABLE tb_name ATTACH PARTITION partition_expr备份还原分区
分区在被备份之后,会被统一保存到ClickHouse根路径/shadow/N子目录下,其中N是一个自增长的整数,它的含义是备份的次数,具体次数由shadow子目录下的increment.txt文件记录。而分区备份实质上是对原始目录文件进行硬链接,所以不会有额外的存储空间。整个备份目录会一直向上追溯到data根路径的整个链路。1
/data/[database]/[table]/[partition_folder]
FETCH的工作原理与ReplicatedMergeTree同步数据的原理类似,FETCH通过指定的zk_path找到ReplicatedMergeTree的所有副本实例,然后从中选择一个最合适的副本,并下载相应的分区数据。1
2
3
4# 备份分区语法
ALTER TABLE tb_name FREEZE PARTITION partition_expr
# 还原分区语法
ALTER TABLE tb_name FETCH PARTITION partition_expr FROM zk_pathFETCH只支持ReplicatedMergeTree系列的表引擎。
分布式的 DDL 操作
ClickHouse 支持集群模式,一个集群拥有一个到多个节点。CREATE、 ALTER、 DROP、 RENAME、 TRUNCATE 这些 DDL 语句,都支持分布式执行。这意味着在集群中任意一个节点上执行 DDL 语句,那么集群中的每个节点都会以相同的顺序执行相同的语句。
将一条普通的 DDL 语句转化为分布式执行十分简单,只需要加上 ON CLUSTER cluster_name 声明即可。
1 | CREATE TABLE partition_v3 ON CLUSTER ch_cluster( |
数据操作
写入
INSERT语句支持三种语法范式,三种范式各有不同,可以根据写入的需求灵活变化。- 使用
VALUES格式的常规用法:1
INSERT INTO [db.]tb_name [(c1, c2, c3)] VALUES (v11, v12, v13), (v21, v22, v23)
- 使用指定格式语法:
1
INSERT INTO [db.]tb_name [(c1, c2, c3)] FORMAT format_name data_set
- 使用
SELECT子句形式语法:1
INSERT INTO [db.]tb_name [(c1, c2, c3)] SELECT ...
- 使用
修改和删除
ClickHouse提供DELETE和UPDATE的能力,这类操作被称为Mutation查询,可以看作是ALTER语句的变种,虽然能实现同样的功能,但是与通常意义上的DELETE和UPDATE不一样:Mutation语句其实是很重的操作,更适用于批量数据的修改和删除。- 不支持事务,一旦语句被提交,就会立刻对数据产生影响且无法回滚。
Mutation语句的执行是一个异步的后台过程,语句提交之后会立即返回。但是立即返回并不代表具体逻辑已经执行完毕,具体执行进度需要通过system.mutations系统表查询。
DELETE 和 UPDATE 语句语法如下:
1 | ALTER TABLE [db_name.]tb_name DELETE WHERE filter_expr |
数据查询
WITH 子句
ClickHouse 支持 CTE (Common Table Expression,公共表表达式)以增强查询语句的表达。
定义变量
可以定义变量,这些变量能够在后续的查询子句中被直接访问。调用函数
可以访问SELECT子句中的列字段,并调用函数做进一步的加工处理。定义子查询
可以定义子查询。
在WITH中使用子查询时有一些需要特别注意,该查询语句只能返回同一行数据,如果结果集的数据大于一行则会抛出异常。在子查询中重复使用
WITH
在子查询中可以嵌套使用WITH子句。
FROM 子句
FROM 子句表示从何处读取数据。
从数据表中读取数据。
从子查询中取数。
从表函数中取数。
FROM 关键字可以省略,此时会从虚拟表中取数。在 ClickHouse 中,并没有数据库中常见的 DUAL 虚拟表,取而代之的就是 system.one。
SAMPLE 子句
SAMPLE 子句能够实现数据采样的功能,使查询仅返回采样数据而不是全部数据,从而减少查询负载。SAMPLE 子句的采样设计是一种幂等设计,也就是说在数据不发生变化的情况下,使用相同的采样规则总是返回相同的数据。SAMPLE 子句只能用于 MergeTree 系列引擎的数据表,并且要求在 CREATE TABLE 时声明 SAMPLE BY 抽样表达式。
SAMPLE factorSAMPLE factor表示按因子系数采样,其中factor表示采样因子,它的取值支持0~1之间的小数。如果factor设置为0或者1,则效果等同于不进行数据采样。SAMPLE rowsSAMPLE rows表示按样本数量采样,其中rows表示至少采样多少行数据,它的取值必须是大于1的整数。如果rows的取值大于表内数据的总行数,则效果等于rows=1(即不使用采样)。SAMPLE factor OFFSET nSAMPLE factor OFFSET n表示按因子系数和偏移量采样,其中factor表示采样因子,n表示偏移多少数据后才开始采样,他们两个的取值都是0~1之间的小数。
ARRAY JOIN 子句
ARRAY JOIN 子句允许在数据表内部,与数组或嵌套类型的字段进行 JOIN 操作,从而将一行数据展开为多行。
在一条 SELECT 语句中,只能存在一个 ARRAY JOIN(使用子查询除外)。目前仅支持 INNER 和 LEFT 两种策略。
INNER ARRAY JOINARRAY JOIN在默认情况下使用的是INNER JOIN策略。1
2
3SELECT title, value FROM query_v1 ARRAY JOIN value
# food 1
# food 2LEFT ARRAY JOINARRAY JOIN子句支持LEFT连接策略。1
2
3SELECT title, value, v FROM query_v1 LEFT ARRAY JOIN value AS v
# food [1, 2, 3] 1
# food [1, 2, 3] 2
JOIN 子句
JOIN 子句可以对左右两张表的数据进行连接,这是最常用的查询子句之一。它的语法包含连接精度和连接类型两部分。连接精度可以分为 ALL、 ANY 和 ASOF 三种,而连接类型也可以分为外连接(外集合)、内连接(交集)和交叉连接(并集)三种。
在进行多表连接时,会转化为两两连接的形式。
注意事项
- 优化
JOIN性能,首先应遵循左大右小的原则。 JOIN查询目前没有缓存的支持。- 如果是在大量维度属性补全的查询场景中,建议使用字典代替
JOIN查询。 - 连接查询的空值是由默认值填充的,连接查询的空值策略是通过
join_use_nulls参数指定的,默认是0。
WHERE 和 PREWHERE 子句
WHERE 子句基于条件表达式来实现数据过滤。另外还提供了全新的 PREWHERE 子句,PREWHERE 目前只能作用于 MergeTree 系列的表引擎,可以看作是 WHERE 的一种优化,其作用与 WHERE 相同,均用来过滤数据,不过 PREWHERE 只会读取指定的列字段数据,用于数据过滤的条件判断,待数据过滤之后再补齐 SELECT 声明的列字段以补全其余属性。
GROUP BY 子句
GROUP BY 又称聚合查询,在 GROUP BY 后声明的表达式,通常被称为聚合键或者 Key,数据会按照聚合键进行聚合。
WITH ROLLUPROLLUP能够按照聚合键从右向左上卷数据,基于聚合函数依次生成分组小计和总计。WITH CUBECUBE会像立方体模型一样,基于聚合键之间所有的组合生成小计信息。如果聚合键的个数为n,则最终小计组合的个数为2^n。WITH TOTALS
使用TOTALS修饰符后,会基于聚合函数对所有数据进行总计。
HAVING 子句
HAVING 子句需要与 GROUP BY 子句同时出现,不能单独使用。它能够在聚合计算之后实现二次过滤数据。
执行优先级: WHERE > GROUP BY > HAVING
ORDER BY 子句
ORDER BY 子句通过声明排序键来指定查询数据返回时的顺序。可以使用多个排序键,每个排序键后需紧跟 ASC 或者 DESC来确定排列顺序,默认 ASC。
对于 NULL 值的排序,目前拥有 NULLS LAST 和 NULLS FIRST 两种排序方式,含义跟其修饰符有关。另外就是 NaN,总是紧跟在 NULL 身边。
LIMIT BY 子句
LIMIT BY 子句和常见的 LIMIT 所有不同,它运行于 ORDER BY 之后和 LIMIT 之前,能够按照指定分组,最多返回前 n 行数据(如果数据少于 n 行,则按照实际数量返回),常用于 TOP N 的查询场景。另外也支持跳过 OFFSET 偏移量获取数据。
1 | LIMIT n BY express |
LIMIT 子句
LIMIT 子句用于返回指定的前 n 行数据,常用于分页场景。
1 | LIMIT n |
SELECT 子句
SELECT 子句决定了一次查询语句最终返回哪些字段或者表达式。
DISTINCT 子句
DISTINCT 子句能够去除重复数据,使用场景广泛。
UNION ALL 子句
UNIUN ALL 子句能够联合左右两边的两组子查询,将结果一并返回。在一次查询中可以声明多个 UNION ALL 以便联合多组查询,但 UNION ALL 不能直接使用其他子句,这些子句只能在其联合的子查询中使用。
1 | select a1, b1 from tabl1 |
上述查询中,UNION ALL 两侧的子查询中能够得到节点信息,首先两侧列字段数量必须相同,其次列字段的数据类型必须相同或兼容,最后列字段的名字可以不同但会以左侧的子查询为准。
SQL 执行计划
ClickHouse 目前并没有直接提供 EXPLAIN 查询,但是借助后台的服务日志,能变相实现该功能。
- 通过将
ClickHouse服务日志设置到DEBUG或者TRACE日志级别,可以变相实现EXPLAIN查询,以分析SQL的执行日志。 - 需要真正执行
SQL查询,CH才能打印计划日志,所以如果表的数据量很大,最好借助LIMIT子句以减少查询返回的数据量。 - 在日志中,分区过滤信息部分
Selected xxx parts by date,,其中by date部分是固定的,无论我们的分区键是什么字段,这里都不会变。这是由于在早期版本中,MergeTree分区键只支持日期字段。 - 不要使用
select *全字段查询。 - 尽可能利用各种索引(分区索引、一级索引、二级索引),这样可以避免全表扫描。
实用工具
clickhouse-local
clickhouse-local 可以独立运行大部分 SQL 查询,不需要依靠任何 ClickHouse 的服务端程序,可以理解成是 ClickHouse 服务的单机版微内核,是一个轻量级的应用程序。clickhouse-local 只能够使用 File 表引擎,他的数据与同机运行的 ClickHouse 服务也是完全隔离的,相互之间并不能访问。同时是非交互式运行的,每次执行都需要指定数据来源。
核心参数如下:
-S/--structure:表结构的简写方式。-N/--table:表名称,默认值是table。-if/--input-format:输入数据的格式,默认值是TSV。-f/--file:输入数据的地址,默认值是stdin标准输入。-q/--query:待执行的SQL语句,多条语句之间以分号间隔。
clickhouse-benchmark
clickhouse-benchmark 是基准测试的小工具,可以自动运行 SQL 查询,并生成相应的运行指标报告。
核心参数如下:
-i/--iterations:SQL查询执行的次数,默认值为0。-c/--concurrency:同时执行查询的并发数,默认值是1。-r/--randomize:在执行多条SQL语句的时候,按照随机顺序执行。-h/--host:服务端地址,默认值是localhost。--confidence:设置对比测试中置信区间的范围,默认值是5(99.5%)。取值范围有0(80%), 1(90%), 2(95%), 3(98%), 4(99%)和5(99.5%)。
clickhouse-mysql
clickhouse-mysql 用于将 MySQL 的数据或者 CSV 文件同步到 ClickHouse的工具。
其他信息参考另一篇博客,有更详细的说明
引用
个人备注
此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!