开始学习《ClickHouse原理解析与应用实践》,写博客作读书笔记。
本文全部内容都来自于书中内容,个人提炼。
第一章 -> 第二章:
【略过,网上资料很多,不用看这个】
ClickHouse提供了DDL 与DML的功能,并支持大部分标准的SQL。
ClickHouse的 UPDATE和DELETE是借助ALTER变种实现的。
数据类型可以划分为基础类型、复合类型和特殊类型。
基础类型只有数值、字符串和时间三种类型,没有Boolean类型, 但可以使用整型的0或1替代。
1.数值类型
对于int。
ck使用Int8、Int16、Int32和 Int64指代四种大小的int。数字为占用位数。
同时还支持加前缀U的无符号类型:UInt8、UInt16、UInt32和 UInt64。
名称 | 大小(字节) | 范围 | 普遍观念 |
---|---|---|---|
Int8 | 1 | [-128,127] | Tinyint |
Int16 | 2 | [-32768,32767] | Smallint |
Int32 | 4 | [-2147483648,2147483647] | Int |
Int64 | 8 | [-9223372036854775808,9223372036854775807] | Bigint |
UInt8 | 1 | [0,255] | Tinyint Unsigned |
UInt16 | 2 | [0,65535] | Smallint Unsigned |
UInt32 | 4 | [0,4294967295] | Int Unsigned |
UInt64 | 8 | [0,18446744073709551615] | Bigint Unsigned |
对于Float。
ck使用Float32和Float64代表单精度 浮点数以及双精度浮点数。
超过有效精度会产生数据溢出。
ck支持正无穷、负无穷以及非数字的表达方式。
对于Decimal。
ck提供Decimal32、Decimal64和Decimal128三种精度的定点数。
可以通过两种形式声明定点:
其中:
名称 | 灯效 | 范围 |
---|---|---|
Decimal32(S) | Decimal(1-9,S) | [-1*10^(9-S) , 1*10^(9-S)] |
Decimal64(S) | Decimal(10-18,S) | [-1*10^(18-S) , 1*10^(18-S)] |
Decimal128(S) | Decimal(19-38,S) | [-1*10^(38-S) , 1*10^(38-S)] |
两个Decimal加法运算时,S取最大值。
两个Decimal减法运算时,S取最大值。
两个Decimal乘法运算时,S取两者S之和。
两个Decimal除法运算时,S取被除数的值,此时要求被除数S必须大于 除数S,否则会报错。
现代计算器系统只支持 32位和64位CPU,所以Decimal128速度会明显慢于Decimal32与Decimal64。
2.字符串类型
字符串分为String、FixedString和UUID三类。
对于String。
长度不限,不限定字符集,但在同一套程序中应该遵循使用统一的编码。
对于FixedString。
Char类型有些类似。通过 FixedString(N)声明。
使用固定长度,但与Char不同的是, FixedString使用null字节填充末尾字符,而Char通常使用空格填充。
对于UUID。
数据库常见的主键类型。
UUID共有32位,它的格式为8-4-4-4-12。
如果一个UUID类型的字段在写入数据时没有被赋值,则会依照格式使用0填充。
3.时间类型
分为DateTime、DateTime64和Date三类。
ClickHouse目前没有时间戳类型。
对于DateTime。
DateTime类型包含时、分、秒信息,精确到秒,支持使用字符串形式写入。
对于DateTime64。
DateTime64可以记录亚秒,它在DateTime之上增加了精度的设置。
对于Date。
只精确到天。
ClickHouse还提供了数组、元组、枚举和嵌套四类复合类型。
1.Array
数组有两种定义形式:
ck数组拥有类型推断的能力。推断依据:以最小存储代价为原则,即使用最小可表达的数据类型。
在定义表字段时,数组需要指定明确的元素类型。
- CREATE TABLE Array_TEST (
- c1 Array(String)
- ) engine = Memory
2.Tuple
由1~n个元素组成,每个元素彼此之间不要求兼容。
同样支持类型推断,其推断依据仍然以最小存储代价为原则。
元组有两种定义形式:
在定义表字段时,元组需要指定明确的元素类型。
- CREATE TABLE Tuple_TEST (
- c1 Tuple(String,Int8)
- ) ENGINE = Memory;
写入INSERT INTO Tuple_TEST VALUES(('abc',123))是可行的,而写入INSERT INTO Tuple_TEST VALUES(('abc','efg'))则会报错。
3.Enum
定义常量时经常会使用。
ck提供Enum8 和Enum16两种枚举类型,
枚举固定使用(String:Int)Key/Value 键值对的形式定义数据,所以Enum8和Enum16分别会对应(String:Int8)和(String:Int16)。
Key和Value是不允许重复的,要保证唯一性。
Key和Value的值都不能为Null,但Key允许是空字符串。
在写入枚举数据的时候,只会用到Key字符串部分。
- CREATE TABLE Enum_TEST (
- c1 Enum8('ready' = 1, 'start' = 2, 'success' = 3, 'error' = 4)
- ) ENGINE = Memory;
-
- INSERT INTO Enum_TEST VALUES('ready');
- INSERT INTO Enum_TEST VALUES('start');
insert时会对照枚举集合项的内容逐一检查。如果Key字符串不在集合范围内则会抛出异常。
enum可在查询中提升性能。
虽然枚举定义中的Key属于String类型,但是在后续对枚举的所有操作中(包括排序、分组、 去重、过滤等),会使用Int类型的Value值。
4.Nested
嵌套类型本质是一种多维数组的结构。嵌套表中的每个字段都是一个数组。
行与行之间数组的长度无须对齐。但是在同一行数据内每个数组字段的长度必须相等。
- CREATE TABLE nested_test (
- name String,
- age UInt8 ,
- dept Nested(
- id UInt8,
- name String
- )
- ) ENGINE = Memory;
-
- INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001,10002], ['研发部','技术支持中心','测试部']);
-
- --行与行之间,数组长度无须对齐
- INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001], ['研发部','技术支持中心']);
-
- --INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001], ['研发部','技术支持中心','测试部']);
- --DB::Exception: Elements 'dept.id' and 'dept.name' of Nested data structure 'dept' (Array columns) have different
1.Nullable
辅助修饰符。需要与基础数据类型一起搭配使用(不能用于数组和元组这些复合类型,也不能作为 索引字段),表示某个基础数据类型可以是Null值。
慎用Nullable。会使查询和写入性能变慢。
因为列字段会有专门保存null值的文件。
2.Domain
域名类型分为IPv4和IPv6两类,本质上它们是对整型和字符串的进一步封装。
IPv4类型比string的好处:
另外IPv6类型是 基于FixedString(16)封装。
但Domain类型并不是字符串,所以不支持隐式转换,需要返回IP的字符串形式,需要显式调用 IPv4NumToString或IPv6NumToString函数进行转换。
DDL查询提供了数据表的创建、修改和删 除操作。
数据库起到命名空间的作用,可以有效规避命名冲突。支持了数据隔离。
CREATE DATABASE IF NOT EXISTS db_name [ENGINE = engine]
【这块内容已经过时,书上只列了5个引擎。现在已经8个了,建议去官网看数据库的各个引擎介绍。Database Engines | ClickHouse Docs】
在创建数据库后,物理磁盘上会生成一个文件目录。
这里创建了一个aikytest1的数据库。
去ck的数据目录下查看,发现确实有个文件夹。因为没建表,文件夹是空的。
去metadata下,会一同创建用于恢复数据库sql文件。
第一种是常规定义方法。
默认会使用 default数据库。
- CREATE TABLE [IF NOT EXISTS] [db_name.]table_name (
- name1 [type] [DEFAULT|MATERIALIZED|ALIAS expr],
- name2 [type] [DEFAULT|MATERIALIZED|ALIAS expr], 省略…
- ) ENGINE = engine
末尾的ENGINE参数, 它被用于指定数据表的引擎。
表引擎决定了数据表的特性,也决定了数据将会被如何存储及加载。
第二种是复制其他表的结构。
CREATE TABLE [IF NOT EXISTS] [db_name1.]table_name AS [db_name2.] table_name2 [ENGINE = engine]
支持在不同的数据库之间复制表结构。AS后的复制给AS前的。
ENGINE 表引擎可以与原表不同。
第三种是通过SELECT子句的形式创建。
CREATE TABLE [IF NOT EXISTS] [db_name.]table_name ENGINE = engine AS SELECT …
不仅会根据SELECT子句建立相应的表结构,同时还会将SELECT子句 查询的数据顺带写入。
ENGINE 表引擎可以与原表不同。
【现在官网还有使用表函数的方式From a Table Function】
CREATE TABLE [IF NOT EXISTS] [db.]table_name AS table_function()
创建一个与指定的表函数具有相同结果的表。 创建的表也将以与指定的相应表函数相同的方式工作。
表字段支持三种默认值表达式的定义方法,分别是DEFAULT、 MATERIALIZED和ALIAS。
表字段被定义了默认值,不再强制要求定义数据类型,因为可以根据默认值进行类型推断。如果定义了则以明确定义的数据类型为主。
三种定义方法的不同:
可以使用ALTER语句修改默认值:
ALTER TABLE [db_name.]table MODIFY COLUMN col_name DEFAULT value
修改动作并不会影响数据表内先前已经存在的数据。
创建临时表的方法是在普通表的基础之上添加TEMPORARY关键字。
- CREATE TEMPORARY TABLE [IF NOT EXISTS] table_name (
- name1 [type] [DEFAULT|MATERIALIZED|ALIAS expr],
- name2 [type] [DEFAULT|MATERIALIZED|ALIAS expr],
- )
临时表的特殊处:
临时表的优先级是大于普通表的,同名情况下优先选择临时表。
更多被运用在ClickHouse的内部,是数据在集群间传播的载体。
数据分区(partition)和数据分片(shard)是完全不同的两个概念。
数据分区是针对本地数据而言的,是数据的一种纵向切分。
数据分片是数据的一种横向切分。
目前只有合并树(MergeTree)家族系列的表引擎才支持数据分区。由PARTITION BY指定分区键。
举例如下:
- CREATE TABLE partition_v1 (
- ID String,
- URL String,
- EventTime Date
- ) ENGINE = MergeTree()
- PARTITION BY toYYYYMM(EventTime)
- ORDER BY ID;
-
- INSERT INTO partition_v1 VALUES
- ('A000','www.nauu.com', '2019-05-01'),
- ('A001','www.brunce.com', '2019-06-02');
通过system.parts系统表,查询数据表的分区状态:
SELECT table,partition,path from system.parts WHERE table = 'partition_v1'
按年月划分后,目前拥有两个数据分区,每个分区都对应一个独立的文件目录,用于保存各自部分的数据。
如果后续的查询按照分区键过滤,可以利用分区索引跳过6月份的分区目录。带来查询的性能提升。
分区键不应该使用粒度过细的数据字段。
ClickHouse拥有普通和物化两种视图,其中物化视图拥有独立的存储,而普通视图只是一层简单的查询代理。
创建普通视图:
CREATE VIEW [IF NOT EXISTS] [db_name.]view_name AS SELECT ...
普通视图不会存储任何数据,它只是一层单纯的SELECT查询映射,起着简化查询、明晰语义的作用, 对查询性能不会有任何增强。
物化视图支持表引擎,数据保存形式由它的表引擎决定,创建物化视图的完整语法如下所示:
CREATE [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]table_name [TO[db.]name] [ENGINE = engine] [POPULATE] AS SELECT .
物化视图创建好之后,如果源表被写入新数据,那么物化视图也会同步更新。
POPULATE修饰符决定了 物化视图的初始化策略:如果使用了POPULATE修饰符,那么在创建视图的过程中,会连带将源表中已存在的数据一并导入;如果不使用POPULATE修饰符,那么物化视图在创 建之后是没有数据的,它只会同步在此之后被写入源表的数据。
物化视图目前并不支持同步删除,如果在源表中删除了数据,物化视图的数据仍会保留。
物化视图本质是一张特殊的数据表。所以删除视图直接使用DROP TABLE查询。
- --追加新字段
- ALTER TABLE tb_name ADD COLUMN [IF NOT EXISTS] name [type] [default_expr] [AFTER name_after]
-
- --举例:
- --末尾增加字段:
- ALTER TABLE testcol_v1 ADD COLUMN OS String DEFAULT 'mac'
- --指定字段的后面增加新字段:
- ALTER TABLE testcol_v1 ADD COLUMN IP String AFTER ID
-
对于数据表中已经存在的旧数据而言,新追加的字段会使用默认值补全。
ALTER TABLE tb_name MODIFY COLUMN [IF EXISTS] name [type] [default_expr]
修改某个字段的数据类型,会调用toType转型方法。
如果不能兼容,则修改操作将会失败。
追加备注的语法如下 所示:
- -- 追加备注的语法如下所示:
- ALTER TABLE tb_name COMMENT COLUMN [IF EXISTS] name 'some comment'
-
- -- 举例:
- ALTER TABLE testcol_v1 COMMENT COLUMN ID '主键ID'
- --删除某个字段:
- ALTER TABLE tb_name DROP COLUMN [IF EXISTS] name
-
- -- 举例:
- ALTER TABLE testcol_v1 DROP COLUMN URL
数据也会被连带删除。
ck的RENAME查询和linux命令mv类似。
- RENAME TABLE [db_name11.]tb_name11 TO [db_name12.]tb_name12, [db_name21.]tb_name21 TO [db_name22.]tb_name22, ...
-
- -- 举例,default数据库中的testcol_v1表转移到db_test库,重命名testcol_v2:
- RENAME TABLE default.testcol_v1 TO db_test.testcol_v2
数据表的移动只能在单个节点的范围内。
- --不删表,清数据:
- TRUNCATE TABLE [IF EXISTS] [db_name.]tb_name
system.parts系统表专门用于查询数据表的分区信息。
- -- 删除某个分区
- ALTER TABLE tb_name DROP PARTITION partition_expr
如果:
那么支持将A表的分区数据复制到B表。
ALTER TABLE B REPLACE PARTITION partition_expr FROM A
只是复制数据,不会删掉原表分区数据。
- -- 如果数据表某一列的数据有误,需要将其重置为初始值
- ALTER TABLE tb_name CLEAR COLUMN column_name IN PARTITION partition_expr
如果声明了默认值表达 式,则以表达式为准;否则以相应数据类型的默认值为准。
表分区可以通过DETACH语句卸载,分区被卸载后,它的物理数据并没有删除,而是被转移到了当前数据表目录的detached子目录下。
装载分区则是反向操作,它能够将detached子目录下的某个分区重新装载回去。
卸载与装载这一对伴生的操作,常用于分区数据的迁移 和备份场景。
- --卸载某个分区
- ALTER TABLE tb_name DETACH PARTITION partition_expr
一旦分区被移动到了detached子目录,就代表它已经脱离了ClickHouse的管理,ClickHouse并不会主动清理这些文件。这些分区文件会一直存在,除非我们主动删除或者使用ATTACH语句重新装载它们。
- -- 装载某个分区的完整语法如下所示
- ALTER TABLE tb_name ATTACH PARTITION partition_expr
【尝试了一下发现detached文件夹没删,但是分片已经移到外面了】
可以通过FREEZE与FETCH实现。
留到第11章专门介绍。
ClickHouse支持集群模式,只需加上ON CLUSTER cluster_name声明即可。
在集群中任意一个节点上执行DDL语句,那么集群中的 每个节点都会以相同的顺序执行相同的语句。
- -- 举例,在集群上执行,但是需要配置ch_cluster的集群:
- CREATE TABLE partition_v3 ON CLUSTER ch_cluster(
- ID String,
- URL String,
- EventTime Date
- ) ENGINE = MergeTree()
- PARTITION BY toYYYYMM(EventTime)
- ORDER BY ID
INSERT语句支持三种语法范式。
第一种是使用VALUES格式的常规语法
使用VALUES格式的语法写入数据时,支持加入表达式或函数。
INSERT INTO [db.]table [(c1, c2, c3…)] VALUES (v11, v12, v13…), (v21, v22, v23…), ...
第二种是使用指定格式的语法
INSERT INTO [db.]table [(c1, c2, c3…)] FORMAT format_name data_set
ClickHouse支持多种数据格式(更多格式可参见官方手册),以常用的CSV格式写入为例:
- INSERT INTO partition_v2 FORMAT CSV \
- 'A0017','www.nauu.com', '2019-10-01' \
- 'A0018','www.nauu.com', '2019-10-01'
第三种是使用SELECT子句形式的语法
INSERT INTO [db.]table [(c1, c2, c3…)] SELECT ...
在通过SELECT子句写入数据的时候,同样也支持加入表达式或函数。但是表达式和函数会带来额外的性能开销,从而导致写入性能的下降。
以INSERT查询最终会 将数据转换为Block数据块。INSERT语句在单个数据块的写入过程中是具有原子性的。
默认,每个block最多可以写入1048576行数据(由max_insert_block_size参数控制)
原子性只有在ClickHouse服务端处理数据的时候才具有这种原子写入的特性,例如使用JDBC或者HTTP接口时。因为max_insert_block_size参数在使用CLI命令行或者INSERT SELECT子句写入时是不生效的。
ck的了DELETE和UPDATE被称为Mutation查询。可以看作ALTER语句的 变种。
- -- DELETE语句的完整语法如下所示
- ALTER TABLE [db_name.]table_name DELETE WHERE filter_expr
尝试如下,删除表中一条数据:
在数据目录下,看到每一个原有的数据目录都额外增加了 一个同名目录,并且在末尾处增加了_3的后缀。目录下还多了一个名为mutation_3.txt的文件
mutation_3.txt的文件完整地记录了这次DELETE操作的执行语句和时间,而文件名的后缀_3与新增目录的后缀对应。
查询system.mutations系统表查看_3的来源:
每执行一条ALTER DELETE语句,都会在mutations系统表中生成一条对应的执行计划。
当is_done等于1时表示执行完毕。
在数据表的根目录下,会以mutation_id为名生成与之对应的日志文件用于记录相关信息。
数据删除的过程是以数据表的每个分区目录为单位,将所有目录重写为新的目录,新目录的命名规则是在原有名称上加上 system.mutations.block_numbers.number。
数据在重写的过程中会将需要删除的数据去掉。
旧的数据目录并不会立即删除,而是会被标记成非激活状态(active为0)。等到MergeTree引擎的下一次合并动作触发时,这些非激活目录才会被真正从物理意义上删除。
数据修改和数据删除的执行流程大致相同。
ALTER TABLE [db_name.]table_name UPDATE column1 = expr1 [, ...] WHERE filter_expr
但是数据修改中,分区键和主键不能作为修改字段。
ClickHouse的数据类型。
数据库、数据表、临时表、 分区表和视图的基本操作以及对元数据和分区的基本操作。
如何写入、修改和删除数据。