2

ClickHouse的数据类型

作为一款分析型数据库,ClickHouse提供了许多数据类型,它们可以划分为基础类型、复合类型和特殊类型。其中基础类型使ClickHouse具备了描述数据的基本能力,而另外两种类型则使ClickHouse的数据表达能力更加丰富立体。

基础类型
基础类型只有数值、字符串和时间三种类型,没有Boolean类型,但可以使用整型的0或1替代。

数值类型
数值类型分为整数、浮点数和定点数三类,接下来分别进行说明。

Int
在普遍观念中,常用Tinyint、Smallint、Int和Bigint指代整数的不同取值范围。而ClickHouse则直接使用Int8、Int16、Int32和Int64指代4种大小的Int类型,其末尾的数字正好表明了占用字节的大小(8位=1字节)

image.png

ClickHouse支持无符号的整数,使用前缀U表示
image.png

Float
与整数类似,ClickHouse直接使用Float32和Float64代表单精度浮点数以及双精度浮点数
image.png

在使用浮点数的时候,应当要意识到它是有限精度的。假如,分别对Float32和Float64写入超过有效精度的数值,下面我们看看会发生什么。例如,将拥有20位小数的数值分别写入Float32和Float64,此时结果就会出现数据误差:

select toFloat32('012345678921345') AS a,toTypeName(a);

image.png

select toFloat64('0123456789213451234567898765') AS a,toTypeName(a);

image.png

可以发现,Float32从小数点后第8位起及Float64从小数点后第17位起,都产生了数据溢出。ClickHouse的浮点数支持正无穷、负无穷以及非数字的表达方式。

正无穷:

select 8 / 0;

image.png

负无穷:

select -8 / 0;

image.png

非数字:

select 0 / 0;

image.png

Decimal

如果要求更高精度的数值运算,则需要使用定点数。ClickHouse提供了Decimal32、Decimal64和Decimal128三种精度的定点数。可以通过两种形式声明定点:简写方式有Decimal32(S)、Decimal64(S)、Decimal128(S)三种,原生方式为Decimal(P, S),其中:
P代表精度,决定总位数(整数部分+小数部分),取值范围是1~38;
S代表规模,决定小数位数,取值范围是0~P。

image.png

在使用两个不同精度的定点数进行四则运算的时候,它们的小数点位数S会发生变化。在进行加法运算时,S取最大值。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相加后S=4:

SELECT toDecimal32(2,4) + toDecimal32(2,2);

image.png

在进行减法运算时,其规则与加法运算相同,S同样会取最大值。例如toDecimal32(4,4)与toDecimal64(2,2)相减后S=4:

SELECT toDecimal32(4,4) - toDecimal32(2,2);

image.png

在进行乘法运算时,S取两者S之和。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相乘后S=4+2=6:

SELECT toDecimal32(2,4) * toDecimal32(2,2);

image.png

在进行除法运算时,S取被除数的值,此时要求被除数S必须大于除数S,否则会报错。例如toDecimal64(2,4)与toDecimal32(2,2)相除后S=4:

SELECT toDecimal64(2,4) / toDecimal32(2,2);

image.png

总结如下:
image.png

在使用定点数时还有一点值得注意:由于现代计算器系统只支持32位和64位CPU,所以Decimal128是在软件层面模拟实现的,它的速度会明显慢于Decimal32与Decimal64。

字符串类型

字符串类型可以细分为String、FixedString和UUID三类。从命名来看仿佛不像是由一款数据库提供的类型,反而更像是一门编程语言的设计。

String
字符串由String定义,长度不限。因此在使用String的时候无须声明大小。它完全代替了传统意义上数据库的Varchar、Text、Clob和Blob等字符类型。String类型不限定字符集,因为它根本就没有这个概念,所以可以将任意编码的字符串存入其中。但是为了程序的规范性和可维护性,在同一套程序中应该遵循使用统一的编码,例如“统一保持UTF-8编码”就是一种很好的约定。

FixedString
FixedString类型和传统意义上的Char类型有些类似,对于一些字符有明确长度的场合,可以使用固定长度的字符串。定长字符串通过FixedString(N)声明,其中N表示字符串长度。但与Char不同的是,FixedString使用null字节填充末尾字符,而Char通常使用空格填充。比如在下面的例子中,字符串‘abc’虽然只有3位,但长度却是5,因为末尾有2位空字符填充:

SELECT  toFixedString('abc',5),length(toFixedString('abc',5)) AS LENGTH ;

image.png

UUID

UUID是一种数据库常见的主键类型,在ClickHouse中直接把它作为一种数据类型。UUID共有32位,它的格式为8-4-4-4-12。如果一个UUID类型的字段在写入数据时没有被赋值,则会依照格式使用0填充,例如:

CREATE TABLE UUID_TEST (
    c1 UUID,
    C2 String
    ) ENGINE = Memory;

第一行UUID有值,第二行UUID没有值

INSERT INTO UUID_TEST SELECT generateUUIDv4(),'t1';
INSERT INTO UUID_TEST(C2) VALUES('t2');
SELECT * FROM UUID_TEST;

image.png

可以看到,第二行没有被赋值的UUID被0填充了。

时间类型

时间类型分为DateTime、DateTime64和Date三类。ClickHouse目前没有时间戳类型。时间类型最高的精度是秒,也就是说,如果需要处理毫秒、微秒等大于秒分辨率的时间,则只能借助UInt类型实现。

DateTime
DateTime类型包含时、分、秒信息,精确到秒,支持使用字符串形式写入:

CREATE TABLE Datetime_TEST(
    c1 DateTime
) ENGINE = Memory ;
INSERT INTO Datetime_TEST VALUES('2019-06-22 00:00:00');
SELECT c1,toTypeName(c1) FROM Datetime_TEST;

image.png

Date
Date类型不包含具体的时间信息,只精确到天,它同样也支持字符串形式写入:

CREATE TABLE Date_TEST (
    c1 Date
) ENGINE = Memory;
INSERT INTO Date_TEST VALUES('2019/06/24');
SELECT c1,toTypeName(c1) FROM Date_TEST;

image.png

复合类型

除了基础数据类型之外,ClickHouse还提供了数组、元组、枚举和嵌套四类复合类型。这些类型通常是其他数据库原生不具备的特性。拥有了复合类型之后,ClickHouse的数据模型表达能力更强了。

Array
数组有两种定义形式,常规方式array(T):

SELECT array(1,2) as a,toTypeName(a);

image.png
或者简写方式[T]:

SELECT [1,2];

image.png

通过上述的例子可以发现,在查询时并不需要主动声明数组的元素类型。因为ClickHouse的数组拥有类型推断的能力,推断依据:以最小存储代价为原则,即使用最小可表达的数据类型。例如在上面的例子中,array(1, 2)会通过自动推断将UInt8作为数组类型。但是数组元素中如果存在Null值,则元素类型将变为Nullable,例如:

SELECT [1,2,null] as a,toTypeName(a);

image.png

在同一个数组内可以包含多种数据类型,例如数组[1,2.0]是可行的。但各类型之间必须兼容,例如数组[1, '2']则会报错。
在定义表字段时,数组需要指定明确的元素类型,例如:

CREATE TABLE Array_TEST (
    c1 Array(String)
) engine = Memory ;

Tuple
组类型由1~n个元素组成,每个元素之间允许设置不同的数据类型,且彼此之间不要求兼容。元组同样支持类型推断,其推断依据仍然以最小存储代价为原则。与数组类似,元组也可以使用两种方式定义,常规方式tuple(T):

SELECT tuple(1,'a',now()) as x,toTypeName(x) ;

image.png

或者简写方式(T):

SELECT (1,2.0,null) as x,toTypeName(x);

image.png

在定义表字段时,元组也需要指定明确的元素类型:

CREATE TABLE Tuple_TEST(
    c1 Tuple(String,Int8)
) ENGINE = Memory;

元素类型和泛型的作用类似,可以进一步保障数据质量。在数据写入的过程中会进行类型检查。例如,写入INSERT INTO Tuple_TEST VALUES( ('abc' , 123) )是可行的,而写入INSERT INTO Tuple_TEST VALUES( ('abc' , 'efg') )则会报错。

Enum
ClickHouse支持枚举类型,这是一种在定义常量时经常会使用的数据类型。ClickHouse提供了Enum8和Enum16两种枚举类型,它们除了取值范围不同之外,别无二致。枚举固定使用(String:Int) Key/Value键值对的形式定义数据,所以Enum8和Enum16分别会对应(String:Int8)和(String:Int16),例如:

CREATE TABLE Enum_TEST (
    c1 Enum8('ready' = 1,'start' = 2,'success' = 3,'error' = 4)
) ENGINE = Memory;

在定义枚举集合的时候,有几点需要注意。首先,Key和Value是不允许重复的,要保证唯一性。其次,Key和Value的值都不能为Null,但Key允许是空字符串。在写入枚举数据的时候,只会用到Key字符串部分,例如:

INSERT INTO Enum_TEST VALUES('ready');
INSERT INTO Enum_TEST VALUES('start');

数据在写入的过程中,会对照枚举集合项的内容逐一检查。如果Key字符串不在集合范围内则会抛出异常,比如执行下面的语句就会出错:

INSERT INTO Enum_TEST VALUES('stop');

可能有人会觉得,完全可以使用String代替枚举,为什么还需要专门的枚举类型呢?这是出于性能的考虑。因为虽然枚举定义中的Key属于String类型,但是在后续对枚举的所有操作中(包括排序、分组、去重、过滤等),会使用Int类型的Value值。

Nested

嵌套类型,顾名思义是一种嵌套表结构。一张数据表,可以定义任意多个嵌套类型字段,但每个字段的嵌套层级只支持一级,即嵌套表内不能继续使用嵌套类型。对于简单场景的层级关系或关联关系,使用嵌套类型也是一种不错的选择。例如,下面的nested_test是一张模拟的员工表,它的所属部门字段就使用了嵌套类型:

CREATE TABLE nested_test (
    name String,
    age UInt8,
    dept Nested(
        id UInt8,
        name String
        )
) ENGINE = Memory;

ClickHouse的嵌套类型和传统的嵌套类型不相同,导致在初次接触它的时候会让人十分困惑。以上面这张表为例,如果按照它的字面意思来理解,会很容易理解成nested_test与dept是一对一的包含关系,其实这是错误的。不信可以执行下面的语句,看看会是什么结果:

INSERT INTO nested_test VALUES ('nauu',18,10000,'研发部');
Code: 53, e.displayText() = DB::Exception: Type mismatch in IN or VALUES section. Expected: Array(UInt8). Got: UInt64 (version 19.17.4.11)

注意上面的异常信息,它提示期望写入的是一个Array数组类型。现在大家应该明白了,嵌套类型本质是一种多维数组的结构。嵌套表中的每个字段都是一个数组,并且行与行之间数组的长度无须对齐。所以需要把刚才的INSERT语句调整成下面的形式:

INSERT INTO nested_test VALUES ('bruce',30,[10000,10001,10002],['研发部','技术支持部','测试部']);

需要注意的是,在同一行数据内每个数组字段的长度必须相等。例如,在下面的示例中,由于行内数组字段的长度没有对齐,所以会抛出异常:

INSERT INTO nested_test VALUES ('bruce',30,[10000,10001],['研发部','技术支持部','测试部']);
Code: 190, e.displayText() = DB::Exception: Elements 'dept.id' and 'dept.name' of Nested data structure 'dept' (Array columns) have different array sizes. (version 19.17.4.11)

在访问嵌套类型的数据时需要使用点符号,例如:

select * from nested_test;

image.png

特殊类型

ClickHouse还有一类不同寻常的数据类型,我将它们定义为特殊类型

Nullable
准确来说,Nullable并不能算是一种独立的数据类型,它更像是一种辅助的修饰符,需要与基础数据类型一起搭配使用。Nullable类型与Java8的Optional对象有些相似,它表示某个基础数据类型可以是Null值。其具体用法如下所示:

CREATE TABLE Null_TEST (
    c1 String,
    c2 Nullable(UInt8)
) ENGINE = TinyLog;
INSERT INTO Null_TEST VALUES ('nauu',null);
INSERT INTO Null_TEST VALUES ('bruce',20);
SELECT c1,c2,toTypeName(c2) from Null_TEST;

在使用Nullable类型的时候还有两点值得注意:首先,它只能和基础类型搭配使用,不能用于数组和元组这些复合类型,也不能作为索引字段;其次,应该慎用Nullable类型,包括Nullable的数据表,不然会使查询和写入性能变慢。因为在正常情况下,每个列字段的数据会被存储在对应的[Column].bin文件中。如果一个列字段被Nullable类型修饰后,会额外生成一个[Column].null.bin文件专门保存它的Null值。这意味着在读取和写入数据时,需要一倍的额外文件操作。

Domain

域名类型分为IPv4和IPv6两类,本质上它们是对整型和字符串的进一步封装。IPv4类型是基于UInt32封装的,它的具体用法如下所示:

CREATE TABLE IP4_TEST (
    url String,
    ip IPv4
) ENGINE = Memory;
INSERT INTO IP4_TEST VALUES('www.nauu.com','192.0.0.0');
SELECT url,ip,toTypeName(ip) FROM IP4_TEST;

image.png

IPv4类型支持格式检查,格式错误的IP数据是无法被写入的,例如:

INSERT INTO IP4_TEST VALUES ('www.nauu.com','192.0.0');

image.png

出于性能的考量,同样以IPv4为例,IPv4使用UInt32存储,相比String更加紧凑,占用的空间更小,查询性能更快。IPv6类型是基于FixedString(16)封装的,它的使用方法与IPv4别无二致,此处不再赘述。

在使用Domain类型的时候还有一点需要注意,虽然它从表象上看起来与String一样,但Domain类型并不是字符串,所以它不支持隐式的自动类型转换。如果需要返回IP的字符串形式,则需要显式调用IPv4NumToString或IPv6NumToString函数进行转换。

如何定义数据表

在知晓了ClickHouse的主要数据类型之后,接下来我们开始介绍DDL操作及定义数据的方法。DDL查询提供了数据表的创建、修改和删除操作,是最常用的功能之一。

数据库
数据库起到了命名空间的作用,可以有效规避命名冲突的问题,也为后续的数据隔离提供了支撑。任何一张数据表,都必须归属在某个数据库之下。创建数据库的完整语法如下所示:
image.png

其中,IF NOT EXISTS表示如果已经存在一个同名的数据库,则会忽略后续的创建过程;[ENGINE = engine]表示数据库所使用的引擎类型(是的,你没看错,数据库也支持设置引擎)。

数据库目前一共支持5种引擎,如下所示。

数据库目前一共支持5种引擎,如下所示。
Ordinary:默认引擎,在绝大多数情况下我们都会使用默认引擎,使用时无须刻意声明。在此数据库下可以使用任意类型的表引擎。
Dictionary:字典引擎,此类数据库会自动为所有数据字典创建它们的数据表。
Memory:内存引擎,用于存放临时数据。此类数据库下的数据表只会停留在内存中,不会涉及任何磁盘操作,当服务重启后数据会被清除。
Lazy:日志引擎,此类数据库下只能使用Log系列的表引擎。
MySQL:MySQL引擎,此类数据库下会自动拉取远端MySQL中的数据,并为它们创建MySQL表引擎的数据表。

在绝大多数情况下都只需使用默认的数据库引擎。例如执行下面的语句,即能够创建属于我们的第一个数据库:

CREATE DATABASE DB_TEST;

默认数据库的实质是物理磁盘上的一个文件目录,所以在语句执行之后,ClickHouse便会在安装路径下创建DB_TEST数据库的文件目录:

image.png

与此同时,在metadata路径下也会一同创建用于恢复数据库的DB_TEST.sql文件:

image.png

使用SHOW DATABASES查询,即能够返回ClickHouse当前的数据库列表:

SHOW DATABASES;

image.png

使用USE查询可以实现在多个数据库之间进行切换,而通过SHOW TABLES查询可以查看当前数据库的数据表列表。删除一个数据库,则需要用到下面的DROP查询。

image.png

数据表
ClickHouse数据表的定义语法,是在标准SQL的基础之上建立的,所以熟悉数据库的读者们在看到接下来的语法时,应该会感到很熟悉。ClickHouse目前提供了三种最基本的建表方法,其中,第一种是常规定义方法,它的完整语法如下所示:

image.png

使用[db_name.]参数可以为数据表指定数据库,如果不指定此参数,则默认会使用default数据库。
第二种定义方法是复制其他表的结构,具体语法如下所示:
image.png
image.png

上述语句将会把default.hits_v1的表结构原样复制到new_db.hits_v1,并且ENGINE表引擎可以与原表不同。

第三种定义方法是通过SELECT子句的形式创建,它的完整语法如下:

image.png

在这种方式下,不仅会根据SELECT子句建立相应的表结构,同时还会将SELECT子句查询的数据顺带写入,例如执行下面的语句:

image.png

上述语句会将SELECT * FROM hits_v1的查询结果一并写入数据表。ClickHouse和大多数数据库一样,使用DESC查询可以返回数据表的定义结构。如果想删除一张数据表,则可以使用下面的DROP语句:

image.png

默认值表达式

表字段支持三种默认值表达式的定义方法,分别是DEFAULT、MATERIALIZED和ALIAS。无论使用哪种形式,表字段一旦被定义了默认值,它便不再强制要求定义数据类型,因为ClickHouse会根据默认值进行类型推断。如果同时对表字段定义了数据类型和默认值表达式,则以明确定义的数据类型为主,例如下面的例子:

CREATE TABLE dfv_v1 (
    id String,
    c1 default 1000,
    c2 String DEFAULT c1
) ENGINE = TinyLog;

c1字段没有定义数据类型,默认值为整型1000; c2字段定义了数据类型和默认值,且默认值等于c1,现在写入测试数据:

INSERT INTO dfv_v1(id) VALUES('A000');

在写入之后执行以下查询:

select t1.*,toTypeName(c1),toTypeName(c2) from dfv_v1 t1;

image.png

由查询结果可以验证,默认值的优先级符合我们的预期,其中c1字段根据默认值被推断为UInt16;而c2字段由于同时定义了数据类型和默认值,所以它最终的数据类型来自明确定义的String。默认值表达式的三种定义方法之间也存在着不同之处,可以从如下三个方面进行比较。

(1)数据写入:在数据写入时,只有DEFAULT类型的字段可以出现在INSERT语句中。而MATERIALIZED和ALIAS都不能被显式赋值,它们只能依靠计算取值。例如试图为MATERIALIZED类型的字段写入数据,将会得到如下的错误。

image.png

(2)数据查询:在数据查询时,只有DEFAULT类型的字段可以通过SELECT 返回。而MATERIALIZED和ALIAS类型的字段不会出现在SELECT 查询的返回结果集中。

(3)数据存储:在数据存储时,只有DEFAULT和MATERIALIZED类型的字段才支持持久化。如果使用的表引擎支持物理存储(例如TinyLog表引擎),那么这些列字段将会拥有物理存储。而ALIAS类型的字段不支持持久化,它的取值总是需要依靠计算产生,数据不会落到磁盘。

可以使用ALTER语句修改默认值,例如:

image.png

修改动作并不会影响数据表内先前已经存在的数据。但是默认值的修改有诸多限制,例如在合并树表引擎中,它的主键字段是无法被修改的;而某些表引擎则完全不支持修改(例如TinyLog)。

临时表

ClickHouse也有临时表的概念,创建临时表的方法是在普通表的基础之上添加TEMPORARY关键字,它的完整语法如下所示:

image.png

它的生命周期是会话绑定的,所以它只支持Memory表引擎,如果会话结束,数据表就会被销毁;
临时表不属于任何数据库,所以在它的建表语句中,既没有数据库参数也没有表引擎参数。
针对第二个特殊项,读者心中难免会产生一个疑问:既然临时表不属于任何数据库,如果临时表和普通表名称相同,会出现什么状况呢?接下来不妨做个测试。首先在DEFAULT数据库创建测试表并写入数据:

CREATE TABLE tmp_v1 (
    title String
) ENGINE = Memory;
INSERT INTO tmp_v1 VALUES('click');
CREATE TEMPORARY TABLE tmp_v1(createtime DateTime);
INSERT INTO tmp_v1 VALUES(now());
select * from tmp_v1;

现在查询tmp_v1看看会发生什么:

image.png

通过返回结果可以得出结论:临时表的优先级是大于普通表的。当两张数据表名称相同的时候,会优先读取临时表的数据。

在ClickHouse的日常使用中,通常不会刻意使用临时表。它更多被运用在ClickHouse的内部,是数据在集群间传播的载体。

分区表

数据分区(partition)和数据分片(shard)是完全不同的两个概念。数据分区是针对本地数据而言的,是数据的一种纵向切分。而数据分片是数据的一种横向切分。数据分区对于一款OLAP数据库而言意义非凡:借助数据分区,在后续的查询过程中能够跳过不必要的数据目录,从而提升查询的性能。合理地利用分区特性,还可以变相实现数据的更新操作,因为数据分区支持删除、替换和重置操作。假设数据表按照月份分区,那么数据就可以按月份的粒度被替换更新。分区虽好,但不是所有的表引擎都可以使用这项特性,目前只有合并树(MergeTree)家族系列的表引擎才支持数据分区。接下来通过一个简单的例子演示分区表的使用方法。首先由PARTITION BY指定分区键,例如下面的数据表partition_v1使用了日期字段作为分区键,并将其格式化为年月的形式:

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');

image.png
最后通过system.parts系统表,查询数据表的分区状态:

select table,partition,path from system.parts where table = 'partition_v1';

image.png

可以看到,partition_v1按年月划分后,目前拥有两个数据分区,且每个分区都对应一个独立的文件目录,用于保存各自部分的数据。合理设计分区键非常重要,通常会按照数据表的查询场景进行针对性设计。例如在刚才的示例中数据表按年月分区,如果后续的查询按照分区键过滤,例如:

select * from partition_v1 where EventTime = toDate('2019-05-01');

image.png

那么在后续的查询过程中,可以利用分区索引跳过6月份的分区目录,只加载5月份的数据,从而带来查询的性能提升。当然,使用不合理的分区键也会适得其反,分区键不应该使用粒度过细的数据字段。例如,按照小时分区,将会带来分区数量的急剧增长,从而导致性能下降。

视图

ClickHouse拥有普通和物化两种视图,其中物化视图拥有独立的存储,而普通视图只是一层简单的查询代理。创建普通视图的完整语法如下所示:

image.png

普通视图不会存储任何数据,它只是一层单纯的SELECT查询映射,起着简化查询、明晰语义的作用,对查询性能不会有任何增强。
物化视图支持表引擎,数据保存形式由它的表引擎决定,创建物化视图的完整语法如下所示:
image.png
一个建立物化视图语句如下:

CREATE MATERIALIZED VIEW agg_view
ENGINE = AggregatingMergeTree()
PARTITION BY city
ORDER BY (id,city)
AS SELECT
id,city,
uniqState(code) AS code,
sumState(value) as value
FROM agg_table_basic
group by id,city;

物化视图创建好之后,如果源表被写入新数据,那么物化视图也会同步更新。POPULATE修饰符决定了物化视图的初始化策略:如果使用了POPULATE修饰符,那么在创建视图的过程中,会连带将源表中已存在的数据一并导入,如同执行了SELECT INTO一般;反之,如果不使用POPULATE修饰符,那么物化视图在创建之后是没有数据的,它只会同步在此之后被写入源表的数据。物化视图目前并不支持同步删除,如果在源表中删除了数据,物化视图的数据仍会保留。

物化视图本质是一张特殊的数据表,例如使用SHOW TABLE查看数据表的列表:

show tables;

image.png

数据表的基本操作
目前只有MergeTree、Merge和Distributed这三类表引擎支持ALTER查询。

追加新字段
假如需要对一张数据表追加新的字段,可以使用如下语法:
image.png

例如,在数据表的末尾增加新字段:
image.png

或是通过AFTER修饰符,在指定字段的后面增加新字段:
image.png

对于数据表中已经存在的旧数据而言,新追加的字段会使用默认值补全。

修改数据类型
如果需要改变表字段的数据类型或者默认值,需要使用下面的语法:

image.png
修改某个字段的数据类型,实质上会调用相应的toType转型方法。如果当前的类型与期望的类型不能兼容,则修改操作将会失败。例如,将String类型的IP字段修改为IPv4类型是可行的:

image.png

而尝试将String类型转为UInt类型就会出现错误:

image.png

修改备注
做好信息备注是保持良好编程习惯的美德之一,所以如果你还没有为列字段添加备注信息,那么就赶紧行动吧。追加备注的语法如下所示:
image.png

例如,为ID字段增加备注:

image.png

删除已有字段

image.png

例如,执行下面的语句删除URL字段:

image.png

上述列字段在被删除之后,它的数据也会被连带删除。进一步来到testcol_v1的数据目录查验,会发现URL的数据文件已经被删除了:

image.png

移动数据表

在Linux系统中,mv命令的本意是将一个文件从原始位置A移动到目标位置B,但是如果位置A与位置B相同,则可以变相实现重命名的作用。ClickHouse的RENAME查询就与之有着异曲同工之妙,RENAME语句的完整语法如下所示:

image.png

RENAME可以修改数据表的名称,如果将原始数据库与目标数据库设为不同的名称,那么就可以实现数据表在两个数据库之间移动的效果。例如在下面的例子中,testcol_v1从default默认数据库被移动到了db_test数据库,同时数据表被重命名为testcol_v2:

image.png

需要注意的是,数据表的移动只能在单个节点的范围内。换言之,数据表移动的目标数据库和原始数据库必须处在同一个服务节点内,而不能是集群中的远程节点。

清空数据表

image.png

例如执行下面的语句,就能将db_test.testcol_v2的数据一次性清空:

image.png

数据分区的基本操作

了解并善用数据分区益处颇多,熟练掌握它的使用方法,可以为后续的程序设计带来极大的灵活性和便利性,目前只有MergeTree系列的表引擎支持数据分区。

查询分区信息

ClickHouse内置了许多system系统表,用于查询自身的状态信息。其中parts系统表专门用于查询数据表的分区信息。例如执行下面的语句,就能够得到数据表partition_v2的分区状况:

select table,partition,path from system.parts where table = 'partition_v2';

image.png

删除指定分区

合理地设计分区键并利用分区的删除功能,就能够达到数据更新的目的。删除一个指定分区的语法如下所示:

image.png

假如现在需要更新partition_v2数据表整个7月份的数据,则可以先将7月份的分区删除:

image.png

然后将整个7月份的新数据重新写入,就可以达到更新的目的:

image.png

查验数据表,可以看到7月份的数据已然更新:
image.png

复制分区数据

ClickHouse支持将A表的分区数据复制到B表,这项特性可以用于快速数据写入、多表间数据同步和备份等场景,它的完整语法如下:

image.png

不过需要注意的是,并不是任意数据表之间都能够相互复制,它们还需要满足两个前提条件:
两张表需要拥有相同的分区键;
它们的表结构完全相同。

假设数据表partition_v2与先前的partition_v1分区键和表结构完全相同,那么应先在partition_v1中写入一批8月份的新数据:

INSERT INTO partition_v1 VALUES
('A006-v1','www.nauu.com','2019-08-01'),
('A007-v1','www.brunce.com','2019-08-02');
ALTER TABLE partition_v2 REPLACE PARTITION 201908 FROM partition_v1;
select * from  partition_v2;

image.png

重置分区数据
如果数据表某一列的数据有误,需要将其重置为初始值,此时可以使用下面的语句实现:
image.png

对于默认值的含义,遵循如下原则:如果声明了默认值表达式,则以表达式为准;否则以相应数据类型的默认值为准。例如,执行下面的语句会重置partition_v2表内201908分区的URL数据重置。

alter table partition_v2 clear column URL in PARTITION 201908;

image.png

卸载与装载分区

表分区可以通过DETACH语句卸载,分区被卸载后,它的物理数据并没有删除,而是被转移到了当前数据表目录的detached子目录下。而装载分区则是反向操作,它能够将detached子目录下的某个分区重新装载回去。卸载与装载这一对伴生的操作,常用于分区数据的迁移和备份场景。卸载某个分区的语法如下所示:
image.png

例如,执行下面的语句能够将partition_v2表内整个8月份的分区卸载:

此时再次查询这张表,会发现其中2019年8月份的数据已经没有了。而进入partition_v2的磁盘目录,则可以看到被卸载的分区目录已经被移动到了detached目录中:

alter table partition_v2 detach partition 201908;
select * from  partition_v2;

image.png
此时再次查询这张表,会发现其中2019年8月份的数据已经没有了。而进入partition_v2的磁盘目录,则可以看到被卸载的分区目录已经被移动到了detached目录中:
image.png

记住,一旦分区被移动到了detached子目录,就代表它已经脱离了ClickHouse的管理,ClickHouse并不会主动清理这些文件。这些分区文件会一直存在,除非我们主动删除或者使用ATTACH语句重新装载它们。装载某个分区的完整语法如下所示:
image.png

再次执行下面的语句,就可以将刚才已被卸载的201908分区重新装载回去:

alter table partition_v2 attach partition 201908;
select * from  partition_v2;

image.png

分布式DDL执行

ClickHouse支持集群模式,一个集群拥有1到多个节点。CREATE、ALTER、DROP、RENMAE及TRUNCATE这些DDL语句,都支持分布式执行。这意味着,如果在集群中任意一个节点上执行DDL语句,那么集群中的每个节点都会以相同的顺序执行相同的语句。这项特性意义非凡,它就如同批处理命令一样,省去了需要依次去单个节点执行DDL的烦恼。将一条普通的DDL语句转换成分布式执行十分简单,只需加上ON CLUSTERcluster_name声明即可。例如,执行下面的语句后将会对ch_cluster集群内的所有节点广播这条DDL语句:

CREATE TABLE partition_v1 ON ch_cluster(
    ID String,
    URL String,
    EventTime Date
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventTime)
ORDER BY ID;

当然,如果现在执行这条语句是不会成功的。因为到目前为止还没有配置过ClickHouse的集群模式,目前还不存在一个名为ch_cluster的集群。

数据的写入

INSERT语句支持三种语法范式,三种范式各有不同,可以根据写入的需求灵活运用。其中,第一种是使用VALUES格式的常规语法:
image.png

其中,c1、c2、c3是列字段声明,可省略。VALUES后紧跟的是由元组组成的待写入数据,通过下标位与列字段声明一一对应。数据支持批量声明写入,多行数据之间使用逗号分隔。例如执行下面的语句,将批量写入多条数据:

在使用VALUES格式的语法写入数据时,支持加入表达式或函数,例如:

insert into partition_v2 values('A0014',toString(1+2),now());

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

image.png

ClickHouse支持多种数据格式(更多格式可参见官方手册),以常用的CSV格式写入为例:

INSERT INTO partition_v2 FORMAT CSV
'A0017','www.nauu.com','2019-10-01'
'A0018','www.nauu.com','2019-10-01';

第三种是使用SELECT子句形式的语法:
image.png

通过SELECT子句可将查询结果写入数据表,假设需要将partition_v1的数据写入partition_v2,则可以使用下面的语句:

insert into partition_v2 select * from partition_v1;

在通过SELECT子句写入数据的时候,同样也支持加入表达式或函数,例如:

insert into partition_v2 select 'AOO20','www.jack.com',now();

ClickHouse内部所有的数据操作都是面向Block数据块的,所以INSERT查询最终会将数据转换为Block数据块。也正因如此,INSERT语句在单个数据块的写入过程中是具有原子性的。在默认的情况下,每个数据块最多可以写入1048576行数据(由max_insert_block_size参数控制)。也就是说,如果一条INSERT语句写入的数据少于max_insert_block_size行,那么这批数据的写入是具有原子性的,即要么全部成功,要么全部失败。需要注意的是,只有在ClickHouse服务端处理数据的时候才具有这种原子写入的特性,例如使用JDBC或者HTTP接口时。因为max_insert_block_size参数在使用CLI命令行或者INSERT SELECT子句写入时是不生效的。

数据的删除与修改

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

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

image.png

数据删除的范围由WHERE查询子句决定。例如,执行下面语句可以删除partition_v2表内所有ID等于A003的数据:

ALTER TABLE partition_v2 DELETE WHERE ID='A000';

由于演示的数据很少,DELETE操作给人的感觉和常用的OLTP数据库无异。但是我们心中应该要明白这是一个异步的后台执行动作。再次进入数据目录,让我们看看删除操作是如何实现的:

ALTER TABLE partition_v1 DELETE WHERE ID='A000';

删除前的数据文件
image.png
删除后的数据文件
image.png

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

image.png

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

image.png

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

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

image.png

UPDATE支持在一条语句中同时定义多个修改字段,分区键和主键不能作为修改字段。例如,执行下面的语句即能够根据WHERE条件同时修改partition_v2内的URL字段:

ALTER TABLE partition_v2 UPDATE URL = 'www.nauu.com' where ID='A0014';

至此数据类型介绍技术,基于《ClickHouse原理与应用实践》一书。


小明的数据脚印
145 声望41 粉丝