ClickHouse 数据类型

1. 基础数据类型

1.1 数值类型

1.1.1 Int

ClickHouse 关系型数据库 字节数
Int8 TinyInt 1
Int16 SmallInt 2
Int32 Int 4
Int64 BigInt 8

1.1.2 Float

ClickHouse 精度 字节数
Float32 7位 4
Float64 16位 8

1.1.3 Decimal

ClickHouse 原生写法 简写
Decimal32 Decimal32(P,S) Decimal32(S),等效于Decimal(1~9,S)
Decimal64 Decimal64(P,S) Decimal64(S),等效于Decimal(10~18,S)
Decimal128 Decimal128(P,S) Decimal128(S),等效于Decimal(19~38,S)

说明:

  1. P代表精度,决定总位数(整数部分+小数部分),取值范围是[1,38];S代表规模,决定小数位数,取值范围是[0,P]

  2. 两个不同精度的定点数进行加减法的时候,它们的小数点位数S会发生变化,取S的最大值

    toDecimal64(2,4)与toDecimal32(2,2)相加后S=4

    :) SELECT toDecimal64(2,4) + toDecimal32(2,2);
    ┌─plus(toDecimal64(2, 4), toDecimal32(2, 2))─┐
    │                                     4.0000 │
    └────────────────────────────────────────────┘
    
  3. 两个不同精度的定点数进行乘法的时候,S是两者之和

    :) SELECT toDecimal64(2,4) * toDecimal32(2,2)
    ┌─multiply(toDecimal64(2, 4), toDecimal32(2, 2))─┐
    │                                       4.000000 │
    └────────────────────────────────────────────────┘
    
  4. 两个不同精度的定点数进行除法的时候,S是被除数(分子)的S,但要求分子的精度大于等于分母的精度

  5. 由于现代计算器系统只支持32位和64位CPU,所以Decimal128是在软件层面模拟实现的,它的速度会明显慢于Decimal32与Decimal64

1.1.4 数值类型的特殊值

inf:正无穷

-inf:负无穷

nan:非数字

1.2 字符串类型

1.2.1 String

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

1.2.2 FixedString

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

:) SELECT toFixedString('abc',5), LENGTH(toFixedString('abc',5)) AS LENGTH;
┌─toFixedString('abc', 5)─┬─LENGTH─┐
│ abc                     │      5 │
└─────────────────────────┴────────┘

1.2.3 UUID

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

CREATE TABLE UUID_TEST (c1 UUID,c2 String) ENGINE = Memory;
INSERT INTO UUID_TEST SELECT generateUUIDv4(),'t1';
INSERT INTO UUID_TEST(c2) VALUES('t2');
SELECT * FROM UUID_TEST;
┌───────────────────────────────────c1─┬─c2─┐
│ 308297a4-c66e-4ecd-ac34-f948b0f9752c │ t1 │
└──────────────────────────────────────┴────┘
┌───────────────────────────────────c1─┬─c2─┐
│ 00000000-0000-0000-0000-000000000000 │ t2 │
└──────────────────────────────────────┴────┘

1.3 时间类型

1.3.1 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;
┌──────────────────c1─┬─toTypeName(c1)─┐
│ 2019-06-22 00:00:00 │ DateTime       │
└─────────────────────┴────────────────┘

1.3.2 DateTime64

DateTime64可以记录亚秒,它在DateTime之上增加了精度的设置,例如:

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

有问题:无论数据类型是Datetime64还是DateTime64,建表都会报错:

DB::Exception: Unknown data type family: DateTime64. Maybe you meant: ['DateTime']

1.3.3 Date

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

CREATE TABLE Date_TEST (c1 Date) ENGINE = Memory;
INSERT INTO Date_TEST VALUES('2019-06-22');
SELECT c1, toTypeName(c1) FROM Date_TEST;
┌─────────c1─┬─toTypeName(c1)─┐
│ 2019-06-22 │ Date           │
└────────────┴────────────────┘

2. 复合类型

2.1 Array

# 常规方式
SELECT array(1, 2) as a , toTypeName(a);
┌─a─────┬─toTypeName([1, 2])─┐
│ [1,2] │ Array(UInt8)       │
└───────┴────────────────────┘

# 简写方式
SELECT [1, 2];
┌─[1, 2]─┐
│ [1,2]  │
└────────┘

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

SELECT [1, 2, null] as a , toTypeName(a);
┌─a──────────┬─toTypeName([1, 2, NULL])─┐
│ [1,2,NULL] │ Array(Nullable(UInt8))   │
└────────────┴──────────────────────────┘

在同一个数组内可以包含多种数据类型,例如数组[1,2.0]是可行的。但各类型之间必须兼容,例如数组[1,'2']则会报错。

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

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

2.2 Tuple

组类型由1~n个元素组成,每个元素之间允许设置不同的数据类型,且彼此之间不要求兼容。元组同样支持类型推断,其推断依据仍然以最小存储代价为原则。

# 常规方式
SELECT tuple(1,'a',now()) AS x, toTypeName(x);
┌─x─────────────────────────────┬─toTypeName(tuple(1, 'a', now()))─┐
│ (1,'a','2021-08-16 16:08:30') │ Tuple(UInt8, String, DateTime)   │
└───────────────────────────────┴──────────────────────────────────┘

# 简写方式
SELECT (1,2.0,null) AS x, toTypeName(x);
┌─x──────────┬─toTypeName(tuple(1, 2., NULL))───────────┐
│ (1,2,NULL) │ Tuple(UInt8, Float64, Nullable(Nothing)) │
└────────────┴──────────────────────────────────────────┘

在定义表字段时,元组也需要指定明确的元素类型,元素类型和泛型的作用类似,可以进一步保障数据质量。在数据写入的过程中会进行类型检查。

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

2.3 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');
select * from Enum_TEST;
┌─c1────┐
│ ready │
└───────┘
┌─c1────┐
│ start │
└───────┘

数据在写入的过程中,会对照枚举集合项的内容逐一检查。如果Key字符串不在集合范围内则会抛出异常。

使用枚举类型可以提高计算性能,虽然枚举定义中的Key属于String类型,但是在后续对枚举的所有操作中(包括排序、分组、去重、过滤等),会使用Int类型的Value值。

2.4 Nested

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

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

以上面这张表为例,如果按照它的字面意思来理解,会很容易理解成nested_test与dept是一对一的包含关系,其实这是错误的:

INSERT INTO nested_test VALUES ('nauu',18,10000,'研发部');
# 报错:DB::Exception: Type mismatch in IN or VALUES section. Expected: Array(UInt8). Got: UInt64

注意上面的异常信息,它提示期望写入的是一个Array数组类型。

嵌套类型本质是一种多维数组的结构。嵌套表中的每个字段都是一个数组,并且行与行之间数组的长度无须对齐。所以需要把刚才的INSERT语句调整成下面的形式:

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 array sizes..

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

SELECT name, dept.id, dept.name FROM nested_test;
┌─name──┬─dept.id────┬─dept.name──────────────────────────┐
│ bruce │ [16,17,18] │ ['研发部','技术支持中心','测试部'] │
└───────┴────────────┴────────────────────────────────────┘
┌─name──┬─dept.id─┬─dept.name─────────────────┐
│ bruce │ [16,17] │ ['研发部','技术支持中心'] │
└───────┴─────────┴───────────────────────────┘

3. 特殊类型

3.1 Nullable

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

CREATE TABLE Null_TEST (
    c1 String,
    c2 Nullable(UInt8)
) ENGINE = TinyLog;
# 通过Nullable修饰后c2字段可以被写入Null值
INSERT INTO Null_TEST VALUES ('nauu',null);
INSERT INTO Null_TEST VALUES ('bruce',20);
SELECT c1,c2,toTypeName(c2) FROM Null_TEST;
┌─c1────┬───c2─┬─toTypeName(c2)──┐
│ nauu  │ ᴺᵁᴸᴸ │ Nullable(UInt8) │
│ bruce │   20 │ Nullable(UInt8) │
└───────┴──────┴─────────────────┘

在使用Nullable类型的时候还有两点值得注意:

首先,它只能和基础类型搭配使用,不能用于数组和元组这些复合类型,也不能作为索引字段;

其次,应该慎用Nullable类型,包括Nullable的数据表,不然会使查询和写入性能变慢。因为在正常情况下,每个列字段的数据会被存储在对应的[Column].bin文件中。如果一个列字段被Nullable类型修饰后,会额外生成一个[Column].null.bin文件专门保存它的Null值。这意味着在读取和写入数据时,需要一倍的额外文件操作。

3.2 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;

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

INSERT INTO IP4_TEST VALUES ('www.nauu.com','192.0.0');
# DB::Exception: Invalid IPv4 value.

IPv4使用UInt32存储,相比String更加紧凑,占用的空间更小,查询性能更快

IPv6类型是基于FixedString(16)封装的,它的使用方法与IPv4别无二致。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容