ClickHouse数据库数据定义手记之数据类型

前提

前边一篇文章详细分析了如何在Windows10系统下搭建ClickHouse的开发环境,接着需要详细学习一下此数据库的数据定义,包括数据类型、DDLDMLClickHouse作为一款完备的DBMS,提供了类似于MySQL(其实有部分语法差别还是比较大的)的DDLDML功能,并且实现了大部分标准SQL规范中的内容。系统学习ClickHouse的数据定义能够帮助开发者更深刻地理解和使用ClickHouse。本文大纲(右侧分支)????????

本文会详细分析ClickHouse目前最新版本(20.10.3.30)支持的所有数据类型。

数据类型

ClickHouse的数据类型从大体的来看主要包括:

  • 数值类型

  • 字符串类型

  • 日期时间类型

  • 复合类型

  • 特殊类型

这里做一份汇总的表格????

 

大类类型类型名称一般概念JavaType备注
数值类型Int88bit整型TINYINTByte|Integer-
数值类型Int1616bit整型SMALLINTShort|Integer-
数值类型Int3232bit整型INTInteger-
数值类型Int6464bit整型BIGINTLong-
数值类型Int128128bit整型`---
数值类型Int256256bit整型---
数值类型UInt8无符号8bit整型TINYINT UNSIGNED-Java中不存在无符号整数类型,选择类型时只要不溢出就行
数值类型UInt16无符号16bit整型SMALLINT UNSIGNED-Java中不存在无符号整数类型,选择类型时只要不溢出就行
数值类型UInt32无符号32bit整型INT UNSIGNED-Java中不存在无符号整数类型,选择类型时只要不溢出就行
数值类型UInt64无符号64bit整型BIGINT UNSIGNED-Java中不存在无符号整数类型,选择类型时只要不溢出就行
数值类型Float3232bit单精度浮点数FLOATFloat-
数值类型Float6464bit双精度浮点数DOUBLEDouble-
数值类型Decimal(P,S)高精度数值,P为总位长,S为小数位长DECIMALBigDecimal-
数值类型Decimal32(S)高精度数值,P总位长属于[1,9]S为小数位长DECIMALBigDecimalDecimal(P,S)特化类型
数值类型Decimal64(S)高精度数值,P总位长属于[10,18]S为小数位长DECIMALBigDecimalDecimal(P,S)特化类型
数值类型Decimal128(S)高精度数值,P总位长属于[19,38]S为小数位长DECIMALBigDecimalDecimal(P,S)特化类型
字符串类型String不定长字符串,长度随意不限广义上类似LONGTEXTString替代了传统DBMS中的VARCHARBLOBCLOBTEXT等类型
字符串类型FixedString(N)定长字符串,使用null字节填充末尾字符有点类似VARCHARString-
字符串类型UUID特殊字符串,32位长度,格式为:8-4-4-4-4-12-String一般使用内置函数生成
日期时间类型Date日期DATELocalDate-
日期时间类型DateTime日期时间类似DATE_TIMELocalDateTime | OffsetDateTime只精确到秒,不包含毫秒
日期时间类型DateTime64日期时间类似DATE_TIMELocalDateTime | OffsetDateTime只精确到秒,不包含毫秒,但是包含亚秒,即10 ^ (-n)
复合类型Array(T)数组-类似T[]-
复合类型Tuple(S,T...R)元组---
复合类型Enum枚举---
复合类型Nested嵌套---
特殊类型NullableNULL修饰类型,不是独立的数据类型---
特殊类型Domain域名--存储IPV4IPV6格式的域名

ClickHouse中类型严格区分大小写,一般为驼峰表示,例如DateTime不能写成DATETIME或者DATE_TIME,同理,UUID不能写成uuid

下面就每种类型再详细分析其用法。

数值类型

数值类型主要包括整型数值、浮点数值、高精度数值和特殊的布尔值。

整型

整型数值指固定长度(bit数)的整数,可以使用带符号和无符号的表示方式。先看整型数值的表示范围????????

「带符号整型数值」

 

类型字节(byte)数范围
Int81[-128, 127]
Int162[-32768, 32767]
Int324[-2147483648, 2147483647]
Int648[-9223372036854775808, 9223372036854775807]
Int12816[-170141183460469231731687303715884105728, 170141183460469231731687303715884105727]
Int25632[-57896044618658097711785492504343953926634992332820282019728792003956564819968,57896044618658097711785492504343953926634992332820282019728792003956564819967]

Int128和Int256能表示的整数范围十分巨大,占用的字节大小也随之增大,一般很少使用。

「无符号整型数值」

 

类型字节(byte)数范围
UInt81[0, 255]
UInt162[0, 65535]
UInt324[0, 4294967295]
UInt648[0, 18446744073709551615]
UInt25632[0, 115792089237316195423570985008687907853269984665640564039457584007913129639935]

值得注意的是,UInt128类型并不支持,因此不存在UInt128。UInt256能表示的整数范围十分巨大,占用的字节大小也随之增大,一般很少使用。

一般在使用MySQL的时候会定义一个BIGINT UNSIGNED类型的自增趋势的主键,在ClickHouse中对标UInt64类型。做一个小测试,在ClickHouse命令行客户端中执行:

SELECT \
toInt8(127) AS a,toTypeName(a) AS aType, \
toInt16(32767) AS b,toTypeName(b) AS bType, \
toInt32(2147483647) AS c,toTypeName(c) AS cType, \
toInt64(9223372036854775807) AS d,toTypeName(d) AS dType, \
toInt128(170141183460469231731687303715884105727) AS e,toTypeName(e) AS eType, \
toInt256(57896044618658097711785492504343953926634992332820282019728792003956564819967) AS f,toTypeName(f) AS fType, \
toUInt8(255) AS g,toTypeName(g) AS gType, \
toUInt16(65535) AS h,toTypeName(h) AS hType, \
toUInt32(4294967295) AS i,toTypeName(i) AS iType, \
toUInt64(18446744073709551615) AS j,toTypeName(j) AS jType, \
toUInt256(115792089237316195423570985008687907853269984665640564039457584007913129639935) AS k,toTypeName(k) AS kType; 

输出结果:

SELECTtoInt8(127) AS a,toTypeName(a) AS aType,toInt16(32767) AS b,toTypeName(b) AS bType,toInt32(2147483647) AS c,toTypeName(c) AS cType,toInt64(9223372036854775807) AS d,toTypeName(d) AS dType,toInt128(1.7014118346046923e38) AS e,toTypeName(e) AS eType,toInt256(5.78960446186581e76) AS f,toTypeName(f) AS fType,toUInt8(255) AS g,toTypeName(g) AS gType,toUInt16(65535) AS h,toTypeName(h) AS hType,toUInt32(4294967295) AS i,toTypeName(i) AS iType,toUInt64(18446744073709551615) AS j,toTypeName(j) AS jType,toUInt256(1.157920892373162e77) AS k,toTypeName(k) AS kType┌───a─┬─aType─┬─────b─┬─bType─┬──────────c─┬─cType─┬───────────────────d─┬─dType─┬────────────────────────────────────────e─┬─eType──┬────────────────────f─┬─fType──┬───g─┬─gType─┬─────h─┬─hType──┬──────────i─┬─iType──┬────────────────────j─┬─jType──┬──────────────────────────────────────────────────────────────────────────────k─┬─kType───┐
│ 127 │ Int8  │ 32767 │ Int16 │ 2147483647 │ Int32 │ 9223372036854775807 │ Int64 │ -170141183460469231731687303715884105728 │ Int128 │ -9223372036854775808 │ Int256 │ 255 │ UInt8 │ 65535 │ UInt16 │ 4294967295 │ UInt32 │ 18446744073709551615 │ UInt64 │ 115792089237316195423570985008687907853269984665640564039448360635876274864128 │ UInt256 │
└─────┴───────┴───────┴───────┴────────────┴───────┴─────────────────────┴───────┴──────────────────────────────────────────┴────────┴──────────────────────┴────────┴─────┴───────┴───────┴────────┴────────────┴────────┴──────────────────────┴────────┴────────────────────────────────────────────────────────────────────────────────┴─────────┘1 rows in set. Elapsed: 0.009 sec.

尴尬,上面的shell执行结果有点长,变形了。

浮点数

浮点数包括单精度浮点数Float32和双精度浮点数Float64????????

 

类型字节(byte)大小有效精度(排除最左边的零小数位数)备注
Float3247小数点后除去左边的零后第8位起会产生数据溢出
Float64816小数点后除去左边的零后第17位起会产生数据溢出

可以做一个小测试:

f5abc88ff7e4 :) SELECT toFloat32('0.1234567890') AS a,toTypeName(a);SELECTtoFloat32('0.1234567890') AS a,toTypeName(a)┌──────────a─┬─toTypeName(toFloat32('0.1234567890'))─┐
│ 0.12345679 │ Float32                               │
└────────────┴───────────────────────────────────────┘1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) SELECT toFloat32('0.0123456789') AS a,toTypeName(a);SELECTtoFloat32('0.0123456789') AS a,toTypeName(a)┌───────────a─┬─toTypeName(toFloat32('0.0123456789'))─┐
│ 0.012345679 │ Float32                               │
└─────────────┴───────────────────────────────────────┘1 rows in set. Elapsed: 0.036 sec.f5abc88ff7e4 :) SELECT toFloat64('0.12345678901234567890') AS a,toTypeName(a);SELECTtoFloat64('0.12345678901234567890') AS a,toTypeName(a)┌───────────────────a─┬─toTypeName(toFloat64('0.12345678901234567890'))─┐
│ 0.12345678901234568 │ Float64                                         │
└─────────────────────┴─────────────────────────────────────────────────┘1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) SELECT toFloat64('0.01234567890123456789') AS a,toTypeName(a);SELECTtoFloat64('0.01234567890123456789') AS a,toTypeName(a)┌────────────────────a─┬─toTypeName(toFloat64('0.01234567890123456789'))─┐
│ 0.012345678901234568 │ Float64                                         │
└──────────────────────┴─────────────────────────────────────────────────┘1 rows in set. Elapsed: 0.005 sec.

特别地,与标准的SQL相比,ClickHouse支持如下特殊的浮点数类别:

  • Inf - 表示正无穷

  • -Inf - 表示负无穷

  • NaN - 表示不是数字

验证一下:

f5abc88ff7e4 :) SELECT divide(0.5,0);SELECT 0.5 / 0┌─divide(0.5, 0)─┐
│            inf │
└────────────────┘1 rows in set. Elapsed: 0.007 sec.f5abc88ff7e4 :) SELECT divide(-0.5,0);SELECT -0.5 / 0┌─divide(-0.5, 0)─┐
│            -inf │
└─────────────────┘1 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :) SELECT divide(0.0,0.0);SELECT 0. / 0.┌─divide(0., 0.)─┐
│            nan │
└────────────────┘1 rows in set. Elapsed: 0.005 sec.

高精度数值

高精度数值类型Decimal一般又称为为「定点数」,可以指定总位数和固定位数小数点,表示一定范围内的精确数值。Decimal的原生表示形式为Decimal(P,S),两个参数的意义是:

  • P:代表精度,决定总位数(也就是决定整数部分加上小数部分一共有多少位数字),取值范围是[1,76]

  • S:代表规模(scale),决定小数位数,取值范围是[0,P]

Decimal(P,S)衍生出的简单表示形式有:Decimal32(S)Decimal64(S)Decimal128(S)Decimal256(S)。见下表:

 

类型P的取值范围S的取值范围数值范围
Decimal(P,S)[1,76][0,P](-1*10^(P - S), 1*10^(P - S))
Decimal32(S)[1,9][0,P](-1*10^(9 - S), 1*10^(9 - S))
Decimal64(S)[10,18][0,P](-1*10^(18 - S), 1*10^(18 - S))
Decimal128(S)[19,38][0,P](-1*10^(38 - S), 1*10^(38 - S))
Decimal256(S)[39,76][0,P](-1*10^(76 - S), 1*10^(76 - S))

如果觉得衍生类型不好理解,还是直接使用Decimal(P,S)就行。它的定义格式如下:

column_name Decimal(P,S)# 如
amount Decimal(10,2)

对于四则运算,使用两个不同精度的Decimal数值进行(内置函数)运算,运算结果小数位的规则如下(假设S1为左值的小数位,S2为右值的小数位,S为结果小数位):

  • 对于加法和减法,S = max(S1,S2)

  • 对于乘法,S = S1 + S2

  • 对于除法,S = S1(结果小数位和被除数小数位一致)

f5abc88ff7e4 :) SELECT toDecimal32(2,4) AS x, toDecimal32(2,2) AS y,x+y;SELECTtoDecimal32(2, 4) AS x,toDecimal32(2, 2) AS y,x + y┌──────x─┬────y─┬─plus(toDecimal32(2, 4), toDecimal32(2, 2))─┐
│ 2.0000 │ 2.00 │                                     4.0000 │
└────────┴──────┴────────────────────────────────────────────┘1 rows in set. Elapsed: 0.019 sec.f5abc88ff7e4 :) SELECT toDecimal32(2,4) AS x, toDecimal32(2,5) AS y,y/xSELECTtoDecimal32(2, 4) AS x,toDecimal32(2, 5) AS y,y / x┌──────x─┬───────y─┬─divide(toDecimal32(2, 5), toDecimal32(2, 4))─┐
│ 2.0000 │ 2.00000 │                                      1.00000 │
└────────┴─────────┴──────────────────────────────────────────────┘1 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :) SELECT toDecimal32(2,4) AS x, toDecimal32(2,4) AS y,y*xSELECTtoDecimal32(2, 4) AS x,toDecimal32(2, 4) AS y,y * x┌──────x─┬──────y─┬─multiply(toDecimal32(2, 4), toDecimal32(2, 4))─┐
│ 2.0000 │ 2.0000 │                                     4.00000000 │
└────────┴────────┴────────────────────────────────────────────────┘1 rows in set. Elapsed: 0.004 sec.

重点注意:如果从事的是金融领域等追求准确精度的数值存储,不能使用浮点数,而应该考虑「使用整型或者定点数」,舍入尽可能交由程序规则处理,毕竟数据库是存储数据的工具,不应该承担太多处理数据计算的职能。

布尔值

ClickHouse中不存在布尔值类型,官方建议使用UInt8类型,通过值01表示falsetrue

字符串类型

字符串类型主要包括:

  • 不定长(动态长度)字符串String

  • 固定长度字符串FixedString(N),这里的N是最大「字节数」,而不是长度,例如UTF-8字符占用3个字节,GBK字符占用2个字节

  • 特殊字符串UUID(存储的是数值,只是形式是字符串)

ClickHouse中没有编码的概念,字符串可以包含一组任意字节,这些字节按原样存储和输出。这个编码和解码操作推测完全移交给客户端完成。一般情况下,推荐使用UTF-8编码存储文本类型内容,这样就能在不进行转换的前提下读取和写入数据。

String

String类型不限制字符串的长度,可以直接替代其他DBMSVARCHARBLOBCLOB等字符串类型,相比VARCHAR这类要考虑预测数据最大长度,显然String无比便捷。使用Java语言开发,直接使用String类型承接即可。String类型的数据列的定义如下:

column_name String

FixedString

FixedString类型的数据列的定义如下:

column_name FixedString(N)

FixedString表示固定长度N的字符串,这里的N代表N个字节(N bytes),而不是N个字符或者N个码点(code point)。一些使用FixedString类型的典型场景:

  • 二进制表示存储IP地址,如使用FixedString(16)存储IPV6地址

  • 哈希值的二进制表示形式,如FixedString(16)存储MD5的二进制值,FixedString(32)存储SHA256的二进制值

当写入FixedString类型数据的时候:

  • 如果数据字节数大于N,则会返回一个Too large value for FixedString(N)的异常

  • 如果数据字节数小于N,则会使用null字节填补剩下的部分

官方文档提示查询条件WHERE中如果需要匹配FixedString类型的列,传入的查询参数要自行补尾部的\0,否则有可能导致查询条件失效。也就是更加建议写入数据和查询条件都是固定字节数的参数。

内置函数length()会直接返回N,而内置函数empty()在全为null字节的前提下会返回1,其他情况返回0

UUID

UUID这个概念很常见,Java中也有静态方法java.util.UUID#randomUUID()直接生成UUID,因为其独特的唯一性有时候可以选择生成UUID作为数据库的主键类型。ClickHouse直接定义了一种UUID类型,严格来说这种类型不是字符串,但是因为在文档上它的位置顺序排在字符串类型之下,日期时间类型之上,形式上看起来也像字符串,并且它仅仅支持字符串类型的内置函数,所以笔者也把它归类为字符串类型。ClickHouse中的UUID实际上是一个16字节的数字,它的通用格式如下:

8-4-4-4-4-12## 例子
61f0c404-5cb3-11e7-907b-a6006ad3dba0## 零值
00000000-0000-0000-0000-000000000000

UUID类型列定义格式如下:

column_name UUID

可以通过内置函数generateUUIDv4()直接生成UUID数据,测试一下:

f5abc88ff7e4 :) CREATE TABLE test_u(id UInt64,u UUID) ENGINE = Memory;CREATE TABLE test_u
(`id` UInt64,`u` UUID
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.018 sec.f5abc88ff7e4 :) INSERT INTO test_u VALUES (1,generateUUIDv4());INSERT INTO test_u VALUESOk.1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) SELECT * FROM test_u;SELECT *
FROM test_u┌─id─┬────────────────────────────────────u─┐
│  1 │ fc379d2c-0753-45a3-8589-1ef95ee0d8c9 │
└────┴──────────────────────────────────────┘1 rows in set. Elapsed: 0.004 sec.

日期时间类型

日期时间类型包括Date(表示年月日)、DateTime(表示年月日时分秒)和DateTime64(表示年月日时分秒「亚秒」)。

Date

Date表示年月日,但是这种类型在ClickHouse中使用2字节(2 byte -> 16 bit)无符号整数去存储距离Unix纪元(1970-01-01)的天数,不支持时区,能够表示的最大年份为2105年。基于这个特性,在插入Date类型数据的时候可以采用yyyy-MM-dd格式或者无符号整数。见下面的测试:

f5abc88ff7e4 :) CREATE TABLE test_dt(date Date) ENGINE = Memory;CREATE TABLE test_dt
(`date` Date
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.025 sec.f5abc88ff7e4 :) INSERT INTO dt VALUES(1),(2),('0000-00-00'),('2020-11-11');INSERT INTO dt VALUESReceived exception from server (version 20.10.3):
Code: 60. DB::Exception: Received from clickhouse-server:9000. DB::Exception: Table default.dt doesn't exist..0 rows in set. Elapsed: 0.007 sec.f5abc88ff7e4 :) INSERT INTO test_dt VALUES(1),(2),('0000-00-00'),('2020-11-11');INSERT INTO test_dt VALUESOk.4 rows in set. Elapsed: 0.025 sec.f5abc88ff7e4 :) SELECT * FROM test_dt;SELECT *
FROM test_dt┌───────date─┐
│ 1970-01-02 │
│ 1970-01-03 │
│ 1970-01-01 │
│ 2020-11-11 │
└────────────┘4 rows in set. Elapsed: 0.005 sec.

Date类型中的0或者'0000-00-00'代表1970-01-01

DateTime

DateTime是通常概念中的年月日时分秒,支持时区,但是不支持毫秒表示,也就是此类型精确到秒。它的定义格式为:

column_name DateTime[(time_zone)]

可以表示的范围:[1970-01-01 00:00:00, 2105-12-31 23:59:59]。使用DateTime的时候需要注意几点:

  • DateTime时间点实际上保存为Unix时间戳(笔者探究过这里的单位应该是秒),与时区或者夏时制无关

  • DateTime的时区并不存储在列数据或者结果集中,而是存储在列元数据中

  • 创建表定义DateTime类型的列的时候如果不指定时区,则使用服务器或者操作系统中设置的默认时区

  • 创建表定义DateTime类型的列的时候如果不指定时区,ClickHouse客户端会使用ClickHouse服务端的时区,也可以通过参数--use_client_time_zone指定

  • 可以通过配置值date_time_input_formatdate_time_output_format分别指定DateTime类型数据的输入和输出格式

  • DateTime类型数据插入的时候,整数会被视为Unix时间戳,并且会使用UTC作为时区(零时区),字符串会被视为使用了时区的日期时间(取决于服务或者系统),再基于时区转化为对应的Unix时间戳进行存储」

可以测试一下:

f5abc88ff7e4 :) CREATE TABLE test_dt(t DateTime,tz DateTime('Asia/Shanghai')) ENGINE = Memory;CREATE TABLE test_dt
(`t` DateTime,`tz` DateTime('Asia/Shanghai')
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.029 sec.f5abc88ff7e4 :) INSERT INTO test_dt VALUES(1605194721,'2020-11-01 00:00:00');   # <-------------- 这里的1605194721是北京时间2020-11-12 23:25:21的Unix时间戳INSERT INTO test_dt VALUESOk.1 rows in set. Elapsed: 0.006 sec.f5abc88ff7e4 :) SELECT * FROM test_dt;SELECT *
FROM test_dt┌───────────────────t─┬──────────────────tz─┐
│ 2020-11-12 15:25:21 │ 2020-11-01 00:00:00 │
└─────────────────────┴─────────────────────┘1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) SELECT toDateTime(t, 'Asia/Shanghai') AS sh_time,toDateTime(tz, 'Europe/London') AS lon_time FROM test_dt;SELECTtoDateTime(t, 'Asia/Shanghai') AS sh_time,toDateTime(tz, 'Europe/London') AS lon_time
FROM test_dt┌─────────────sh_time─┬────────────lon_time─┐
│ 2020-11-12 23:25:21 │ 2020-10-31 16:00:00 │
└─────────────────────┴─────────────────────┘1 rows in set. Elapsed: 0.004 sec.

DateTime64

DateTime64其实和DateTime类型差不多,不过可以额外表示亚秒,所谓亚秒,精度就是10 ^ (-n)10的负n次方)秒,例如0.1秒、0.01秒等等。它的定义格式为:

column_name DateTime64(precision [, time_zone])

测试一下:

f5abc88ff7e4 :) SELECT toDateTime64(now(), 5, 'Asia/Shanghai') AS column, toTypeName(column) AS x;SELECTtoDateTime64(now(), 5, 'Asia/Shanghai') AS column,toTypeName(column) AS x┌────────────────────column─┬─x──────────────────────────────┐
│ 2020-11-12 23:45:56.00000 │ DateTime64(5, 'Asia/Shanghai') │
└───────────────────────────┴────────────────────────────────┘1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) CREATE TABLE test_dt64(t DateTime64(2),tz DateTime64(3,'Asia/Shanghai')) ENGINE = Memory;CREATE TABLE test_dt64
(`t` DateTime64(2),`tz` DateTime64(3, 'Asia/Shanghai')
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.017 sec.f5abc88ff7e4 :) INSERT INTO test_dt64 VALUES(1605194721,'2020-11-01 00:00:00');INSERT INTO test_dt64 VALUESOk.1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) SELECT * FROM test_dt64;SELECT *
FROM test_dt64┌──────────────────────t─┬──────────────────────tz─┐
│ 1970-07-05 18:52:27.21 │ 2020-11-01 00:00:00.000 │
└────────────────────────┴─────────────────────────┘1 rows in set. Elapsed: 0.004 sec.

复合类型

复合类型主要包括数组Array(T)、元组Tuple(T,S....R)、枚举Enum和嵌套Nested,这里的复合指的是同类型多元素复合或者多类型多元素复合。

Array

数组类型Array(T)中的T可以是任意的数据类型(但是同一个数组的元素类型必须唯一),类似于泛型数组T[]。它的定义如下:

column_name Array(T)## 定义
major Array(String)## 写入
VALUES (['a','b','c']), (['A','B','C'])

编写测试例子:

f5abc88ff7e4 :) CREATE TABLE test_arr(a Array(UInt8),b Array(String)) ENGINE = Memory;CREATE TABLE test_arr
(`a` Array(UInt8),`b` Array(String)
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.017 sec.f5abc88ff7e4 :) INSERT INTO test_arr VALUES([1,2,3],['throwable','doge']);INSERT INTO test_arr VALUESOk.1 rows in set. Elapsed: 0.005 sec.f5abc88ff7e4 :) SELECT * FROM test_arr;SELECT *
FROM test_arr┌─a───────┬─b────────────────────┐
│ [1,2,3] │ ['throwable','doge'] │
└─────────┴──────────────────────┘1 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :)

需要注意的是:

  • 可以使用array()函数或者[]快速创建数组

  • 快速创建数组时,ClickHouse会自动将参数类型定义为可以存储所有列出的参数的"最窄"的数据类型,可以理解为「最小代价原则」

  • ClickHouse无法确定数组的数据类型(常见的是快速创建数组使用了多类型元素),将会返回一个异常(例如SELECT array(1, 'a')是非法的)

  • 如果数组中的元素存在NULL,元素类型将会变为Nullable(T)

f5abc88ff7e4 :) SELECT array(1, 2) AS x, toTypeName(x);SELECT[1, 2] AS x,toTypeName(x)┌─x─────┬─toTypeName(array(1, 2))─┐
│ [1,2] │ Array(UInt8)            │
└───────┴─────────────────────────┘1 rows in set. Elapsed: 0.006 sec.f5abc88ff7e4 :) SELECT [1, 2, NULL] AS x, toTypeName(x);SELECT[1, 2, NULL] AS x,toTypeName(x)┌─x──────────┬─toTypeName([1, 2, NULL])─┐
│ [1,2,NULL] │ Array(Nullable(UInt8))   │
└────────────┴──────────────────────────┘1 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :) SELECT array(1, 'a')SELECT [1, 'a']Received exception from server (version 20.10.3):
Code: 386. DB::Exception: Received from clickhouse-server:9000. DB::Exception: There is no supertype for types UInt8, String because some of them are String/FixedString and some of them are not.0 rows in set. Elapsed: 0.015 sec.

Tuple

元组(Tuple(S,T...R))类型的数据由1-n个元素组成,每个元素都可以使用单独(可以不相同)的数据类型。它的定义如下:

column_name Tuple(S,T...R)## 定义
x_col Tuple(UInt64, String, DateTime)## 写入
VALUES((1,'throwables','2020-11-14 00:00:00')),((2,'throwables','2020-11-13 00:00:00'))

需要注意的是:

  • 类似于数组类型Array,元组Tuple对于每个元素的类型推断也是基于「最小代价原则」

  • 创建表的时候明确元组Tuple中元素的类型定义后,数据写入的时候元素的类型会进行检查,必须一一对应,否则会抛出异常(如x_col Tuple(UInt64, String)只能写入(1,'a')而不能写入('a','b')

f5abc88ff7e4 :) SELECT tuple(1,'1',NULL) AS x, toTypeName(x);SELECT(1, '1', NULL) AS x,toTypeName(x)┌─x────────────┬─toTypeName(tuple(1, '1', NULL))─────────┐
│ (1,'1',NULL) │ Tuple(UInt8, String, Nullable(Nothing)) │
└──────────────┴─────────────────────────────────────────┘1 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :) CREATE TABLE test_tp(id UInt64, a Tuple(UInt64,String)) ENGINE = Memory;CREATE TABLE test_tp
(`id` UInt64,`a` Tuple(UInt64, String)
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.018 sec.f5abc88ff7e4 :) INSERT INTO test_tp VALUES(1,(999,'throwable')),(2,(996,'doge'));INSERT INTO test_tp VALUESOk.2 rows in set. Elapsed: 0.003 sec.f5abc88ff7e4 :) INSERT INTO test_tp VALUES(1,('doge','throwable'));INSERT INTO test_tp VALUESException on client:
Code: 6. DB::Exception: Cannot parse string 'doge' as UInt64: syntax error at begin of string. Note: there are toUInt64OrZero and toUInt64OrNull functions, which returns zero/NULL instead of throwing exception.: while executing 'FUNCTION CAST(_dummy_0, 'Tuple(UInt64, String)') Tuple(UInt64, String) = CAST(_dummy_0, 'Tuple(UInt64, String)')': data for INSERT was parsed from query

这里可以看出ClickHouse在处理Tuple类型数据写入发现类型不匹配的时候,会尝试进行类型转换,也就是按照写入的数据对应位置的元素类型和列定义Tuple中对应位置的类型做转换(如果类型一致则不需要转换),类型转换异常就会抛出异常。类型为Tuple(UInt64,String)实际上可以写入('111','222')或者(111,'222'),但是不能写入('a','b')「转换过程会调用内置函数」,如无意外会消耗额外的性能和时间,因此更推荐在写入数据的时候确保每个位置元素和列定义时候的元素类型一致。

Enum

枚举类型Enum算是ClickHouse中独创的复合类型,它使用有限键值对K-V(String:Int)的形式定义数据,有点像Java中的HashMap结构,而KEYVALUE都不允许NULL值,但是KEY允许设置为空字符串。Enum的数据查询一般返回是KEY的集合,写入可以是KEY也可以是VALUE。它的定义如下:

column_name Enum('str1' = num1, 'str2' = num2 ...)# 例如
sex Enum('male' = 1,'female' = 2,'other' = 3)

Enum可以表示的值范围是16位,也就是VALUE只能从[-32768,32767]中取值。它衍生出两种简便的类型Enum8(本质是(String:Int18),代表值范围是8位,也就是[-128,127])和Enum16(本质是(String:Int16),代表值范围是16位,也就是[-32768,32767]),如果直接使用原生类型Enum则会根据实际定义的K-V对数量最终决定具体选用Enum8或是Enum16存储数据。测试一下:

f5abc88ff7e4 :) CREATE TABLE test_e(sex Enum('male' = 1,'female' = 2,'other' = 3)) ENGINE = Memory;CREATE TABLE test_e
(`sex` Enum('male' = 1, 'female' = 2, 'other' = 3)
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.021 sec.f5abc88ff7e4 :) INSERT INTO test_e VALUES(1),(2),('other');INSERT INTO test_e VALUESOk.3 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :) SELECT sex,CAST(sex,'Int8') FROM test_eSELECTsex,CAST(sex, 'Int8')
FROM test_e┌─sex────┬─CAST(sex, 'Int8')─┐
│ male   │                 1 │
│ female │                 2 │
│ other  │                 3 │
└────────┴───────────────────┘3 rows in set. Elapsed: 0.005 sec.

ClickHouse中的Enum本质就是String:Int,特化一个这样的类型,方便定义有限集合的键值对,枚举的VALUE是整型数值,会直接参与ORDER BYGROUP BYINDISTINCT等操作。按照常规思维来说,排序、聚合、去重等操作使用整型对比使用字符串在性能上应该有不错的提升,所以在使用有限状态集合的场景使用Enum类型比使用String定义枚举集合理论上有天然优势。

Nested

嵌套类型Nested算是一种比较奇特的类型。如果使用过GO语言,Nested类型数据列定义的时候有点像GO语言的结构体:

column_name Nested(field_name_1 Type1,field_name_2 Type2
)## 定义
major Nested(id UInt64,name String
)## 写入
VALUES ([1,2],['Math','English'])## 查询
SELECT major.id,major.name FROM

ClickHouse的嵌套类型和固有思维中传统的嵌套类型大有不同,「它的本质是一种多维数组结构」,可以这样理解:

major Nested(id UInt64,name String
)↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓major.id Array(UInt64)
major.name Array(String)↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ Java中的实体类
class Entity {Long id;List majors;
}class Major {Long id;String name;
}

嵌套类型行与行之间的数组长度无须固定,但是同一行中嵌套表内每个数组的长度必须对齐,例如:

 

行号major.idmajor.name
1[1,2]['M','N']
2[1,2,3]['M','N','O']
3(异常)[1,2,3,4]['M','N']

测试一下:

f5abc88ff7e4 :) CREATE TABLE test_nt(id UInt64,n Nested(id UInt64,name String)) ENGINE Memory;CREATE TABLE test_nt
(`id` UInt64,`n` Nested(    id UInt64,     name String)
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.020 sec.f5abc88ff7e4 :) INSERT INTO test_nt VALUES (1,[1,2,3],['a','b','c']),(2,[999],['throwable']);INSERT INTO test_nt VALUESOk.2 rows in set. Elapsed: 0.003 sec.f5abc88ff7e4 :) SELECT * FROM test_nt;SELECT *
FROM test_nt┌─id─┬─n.id────┬─n.name────────┐
│  1 │ [1,2,3] │ ['a','b','c'] │
│  2 │ [999]   │ ['throwable'] │
└────┴─────────┴───────────────┘2 rows in set. Elapsed: 0.005 sec.

可以通过ARRAY JOIN子句实现嵌套类型的子表数据平铺,类似于MySQL中的行转列:

f5abc88ff7e4 :) SELECT n.id,n.name FROM test_nt ARRAY JOIN n;SELECTn.id,n.name
FROM test_nt
ARRAY JOIN n┌─n.id─┬─n.name────┐
│    1 │ a         │
│    2 │ b         │
│    3 │ c         │
│  999 │ throwable │
└──────┴───────────┘

特殊类型

特殊类型主要包括Nullable、域名DomainNothing

Nullable

Nullable不算一种独立的类型,它是一种其他类型的类似辅助修饰符的修饰类型,与其他基本类型搭配使用。如果熟悉Java中的java.lang.OptionalNullable的功能就是与Optional相似,表示某个基本数据类型可以为Null值(写入时候不传值)。它的定义如下:

column_name Nullable(TypeName)# 如
amount Nullable(Decimal(10,2))
age Nullable(UInt16)
createTime Nullable(DateTime)

需要注意几点:

  • NULLNullable的默认值,也就是INSERT时候可以使用NULL指定空值或者不传值

  • 不能使用Nullable修饰复合数据类型,但是复合数据类型中的元素可以使用Nullable修饰

  • Nullable修饰的列不能添加索引

  • 官网文档有一段提醒:Nullable几乎总是造成负面的性能影响,在设计数据库的时候必须牢记这一点,这是因为Nullable中的列的NULL值和列的非NULL值会存放在两个不同的文件,所以不能添加索引,查询和写入还会涉及到「非单个文件的操作」

测试一下:

f5abc88ff7e4 :) CREATE TABLE test_null(id UInt64,name Nullable(String)) ENGINE = Memory;CREATE TABLE test_null
(`id` UInt64,`name` Nullable(String)
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.022 sec.f5abc88ff7e4 :) INSERT INTO test_null VALUES(1,'throwable'),(2,NULL);INSERT INTO test_null VALUESOk.2 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :) SELECT * FROM test_null;SELECT *
FROM test_null┌─id─┬─name──────┐
│  1 │ throwable │
│  2 │ NULL      │
└────┴───────────┘ 2 rows in set. Elapsed: 0.004 sec.f5abc88ff7e4 :)

Domain

Domain类型也是ClickHouse独有的类型,是基于其他类型进行封装的一种特殊类型,包括IPv4(本质上是基于UInt32封装,以紧凑的二进制形式存储)和IPv6(本质上是基于FixedString(16)封装)两种类型。它们的定义如下:

column_name IPv4
column_name IPv6

Domain类型的局限性:

  • 不能通过ALTER TABLE改变当前Domain类型列的类型

  • 不能通过字符串隐式转换从其他列或者其他表插入Domain类型的列数据,例如A表有String类型存储的IP地址格式的列,无法导入B表中Domain类型的列

  • Domain类型对存储的值不做限制,但是写入数据的时候会校验是否满足IPv4或者IPv6的格式

此外,Domain类型数据的INSERT或者SELECT都做了人性化格式化操作,所以在使用INSERT语句的时候可以直接使用字符串形式写入,查询的结果虽然在客户端命令行展示的是可读的"字符串",但是如果想查询到字符串格式的结果需要使用内置函数IPv4NumToString()IPv6NumToString()(这里也就说明了不支持隐式类型转换,文档中也提到CAST()内置函数可以把IPv4转化为UInt32,把IPv6转化为FixedString(16))。测试一下:

f5abc88ff7e4 :) CREATE TABLE test_d(id UInt64,ip IPv4) ENGINE = Memory;CREATE TABLE test_d
(`id` UInt64,`ip` IPv4
)
ENGINE = MemoryOk.0 rows in set. Elapsed: 0.029 sec.f5abc88ff7e4 :) INSERT INTO test_d VALUES(1,'192.168.1.0');INSERT INTO test_d VALUESOk.1 rows in set. Elapsed: 0.003 sec.f5abc88ff7e4 :) SELECT ip,IPv4NumToString(ip) FROM test_d;SELECTip,IPv4NumToString(ip)
FROM test_d┌──────────ip─┬─IPv4NumToString(ip)─┐
│ 192.168.1.0 │ 192.168.1.0         │
└─────────────┴─────────────────────┘1 rows in set. Elapsed: 0.004 sec.

Nothing

Nothing不是一种显式的数据类型,它存在的唯一目的就是表示不希望存在值的情况,使用者也无法创建Nothing类型。例如字面量NULL其实是Nullable(Nothing)类型,空的数组array()(内置函数)是Nothing类型。

f5abc88ff7e4 :) SELECT toTypeName(array());SELECT toTypeName([])┌─toTypeName(array())─┐
│ Array(Nothing)      │
└─────────────────────┘1 rows in set. Elapsed: 0.006 sec.

所有类型的零值

ClickHouse中所有列定义完毕之后如果没有定义默认值(这个比较复杂,在以后介绍DDL相关的文章的时候再说),如果不使用Nullable,那么写入数据的时候空的列会被填充对应类型的零值。各类型零值归类如下:

  • 数值类型的零值为数字0

  • 字符串类型的零值为空字符串''UUID的零值为00000000-0000-0000-0000-000000000000

  • 日期时间类型的零值为其存储的时间偏移量的零值

  • Enum类型是定义的VALUE值最小的为零值

  • Array类型的零值为[]

  • Tuple类型的零值为[类型1的零值,类型2的零值......]

  • Nested类型的零值为多维数组并且每个数组都是[]

  • 特殊地,可以认为Nullable修饰的类型的零值为NULL

使用JDBC驱动

这里模拟一个场景,基本上使用所有的ClickHouse中常用的类型。定义一张订单表:

CREATE TABLE ShoppingOrder (id UInt64 COMMENT '主键',orderId UUID COMMENT '订单ID',amount Decimal(10,2) COMMENT '金额',createTime DateTime COMMENT '创建日期时间',customerPhone FixedString(11) COMMENT '顾客手机号',customerName String COMMENT '顾客姓名',orderStatus Enum('init' = 0,'cancel' = -1,'paid' = 1) COMMENT '订单状态',goodsIdList Array(UInt64) COMMENT '货物ID数组',address Nested(province String, city String, street String, houseNumber UInt64) COMMENT '收货地址'
) ENGINE = Memory;// 合成一行
CREATE TABLE ShoppingOrder (id UInt64 COMMENT '主键',orderId UUID COMMENT '订单ID',amount Decimal(10,2) COMMENT '金额',createTime DateTime COMMENT '创建日期时间',customerPhone FixedString(11) COMMENT '顾客手机号',customerName String COMMENT '顾客姓名', orderStatus Enum('init' = 0,'cancel' = -1,'paid' = 1) COMMENT '订单状态',goodsIdList Array(UInt64) COMMENT '货物ID数组',address Nested(province String, city String, street String, houseNumber UInt64) COMMENT '收货地址') ENGINE = Memory;

创建完成后,调用DESC ShoppingOrder

f5abc88ff7e4 :) DESC ShoppingOrder;DESCRIBE TABLE ShoppingOrder┌─name────────────────┬─type─────────────────────────────────────────┬─default_type─┬─default_expression─┬─comment──────┬─codec_expression─┬─ttl_expression─┐
│ id                  │ UInt64                                       │              │                    │ 主键         │                  │                │
│ orderId             │ UUID                                         │              │                    │ 订单ID       │                  │                │
│ amount              │ Decimal(10, 2)                               │              │                    │ 金额         │                  │                │
│ createTime          │ DateTime                                     │              │                    │ 创建日期时间 │                  │                │
│ customerPhone       │ FixedString(11)                              │              │                    │ 顾客手机号   │                  │                │
│ customerName        │ String                                       │              │                    │ 顾客姓名     │                  │                │
│ orderStatus         │ Enum8('cancel' = -1, 'init' = 0, 'paid' = 1) │              │                    │ 订单状态     │                  │                │
│ goodsIdList         │ Array(UInt64)                                │              │                    │ 货物ID数组   │                  │                │
│ address.province    │ Array(String)                                │              │                    │ 收货地址     │                  │                │
│ address.city        │ Array(String)                                │              │                    │ 收货地址     │                  │                │
│ address.street      │ Array(String)                                │              │                    │ 收货地址     │                  │                │
│ address.houseNumber │ Array(UInt64)                                │              │                    │ 收货地址     │                  │                │
└─────────────────────┴──────────────────────────────────────────────┴──────────────┴────────────────────┴──────────────┴──────────────────┴────────────────┘12 rows in set. Elapsed: 0.004 sec.

引入clickhouse-jdbc依赖:

ru.yandex.clickhouseclickhouse-jdbc0.2.4

编写测试案例:

@RequiredArgsConstructor
@Getter
public enum OrderStatus {INIT("init", 0),CANCEL("cancel", -1),PAID("paid", 1),;private final String type;private final Integer status;public static OrderStatus fromType(String type) {for (OrderStatus status : OrderStatus.values()) {if (Objects.equals(type, status.getType())) {return status;}}return OrderStatus.INIT;}
}@Data
public class Address {private String province;private String city;private String street;private Long houseNumber;
}@Data
public class ShoppingOrder {private Long id;private String orderId;private BigDecimal amount;private OffsetDateTime createTime;private String customerPhone;private String customerName;private Integer orderStatus;private Set goodsIdList;/*** 这里实际上只有一个元素*/private List
 addressList; }@Test public void testInsertAndSelectShoppingOrder() throws Exception {ClickHouseProperties props = new ClickHouseProperties();props.setUser("root");props.setPassword("root");// 不创建数据库的时候会有有个全局default数据库ClickHouseDataSource dataSource = new ClickHouseDataSource("jdbc:clickhouse://localhost:8123/default", props);ClickHouseConnection connection = dataSource.getConnection();PreparedStatement ps = connection.prepareStatement("INSERT INTO ShoppingOrder VALUES(?,?,?,?,?,?,?,?,?,?,?,?)");// 这里可以考虑使用Snowflake算法生成自增趋势主键long id = System.currentTimeMillis();int idx = 1;ps.setLong(idx ++, id);ps.setString(idx ++, "00000000-0000-0000-0000-000000000000");ps.setBigDecimal(idx ++, BigDecimal.valueOf(100L));ps.setTimestamp(idx ++, new Timestamp(System.currentTimeMillis()));ps.setString(idx ++, "12345678901");ps.setString(idx ++, "throwable");ps.setString(idx ++, "init");ps.setString(idx ++, "[1,999,1234]");ps.setString(idx ++, "['广东省']");ps.setString(idx ++, "['广州市']");ps.setString(idx ++, "['X街道']");ps.setString(idx , "[10087]");ps.execute();ClickHouseStatement statement = connection.createStatement();ResultSet rs = statement.executeQuery("SELECT * FROM ShoppingOrder");List orders = Lists.newArrayList();while (rs.next()) {ShoppingOrder order = new ShoppingOrder();order.setId(rs.getLong("id"));order.setOrderId(rs.getString("orderId"));order.setAmount(rs.getBigDecimal("amount"));order.setCreateTime(OffsetDateTime.ofInstant(rs.getTimestamp("createTime").toInstant(), ZoneId.systemDefault()));order.setCustomerPhone(rs.getString("customerPhone"));order.setCustomerName(rs.getString("customerName"));String orderStatus = rs.getString("orderStatus");order.setOrderStatus(OrderStatus.fromType(orderStatus).getStatus());// Array(UInt64) -> ArrayArray goodsIdList = rs.getArray("goodsIdList");BigInteger[] goodsIdListValue = (BigInteger[]) goodsIdList.getArray();Set goodsIds = Sets.newHashSet();for (BigInteger item : goodsIdListValue) {goodsIds.add(item.longValue());}order.setGoodsIdList(goodsIds);List
 addressList = Lists.newArrayList();// Array(String) -> ArrayArray province = rs.getArray("address.province");List provinceList = arrayToList(province);// Array(String) -> ArrayArray city = rs.getArray("address.city");List cityList = arrayToList(city);// Array(String) -> ArrayArray street = rs.getArray("address.street");List streetList = arrayToList(street);// UInt64 -> ArrayArray houseNumber = rs.getArray("address.houseNumber");BigInteger[] houseNumberValue = (BigInteger[]) houseNumber.getArray();List houseNumberList = Lists.newArrayList();for (BigInteger item : houseNumberValue) {houseNumberList.add(item.longValue());}int size = provinceList.size();for (int i = 0; i < size; i++) {Address address = new Address();address.setProvince(provinceList.get(i));address.setCity(cityList.get(i));address.setStreet(streetList.get(i));address.setHouseNumber(houseNumberList.get(i));addressList.add(address);}order.setAddressList(addressList);orders.add(order);}System.out.println("查询结果:" + JSON.toJSONString(orders)); }private List arrayToList(Array array) throws Exception {String[] v = (String[]) array.getArray();return Lists.newArrayList(Arrays.asList(v)); }

输出结果:

查询结果:
[{"addressList": [{"city": "广州市","houseNumber": 10087,"province": "广东省","street": "X街道"}],"amount": 100.00,"createTime": "2020-11-17T23:53:34+08:00","customerName": "throwable","customerPhone": "12345678901","goodsIdList": [1, 1234, 999],"id": 1605628412414,"orderId": "00000000-0000-0000-0000-000000000000","orderStatus": 0
}]

客户端查询:

f5abc88ff7e4 :) SELECT * FROM ShoppingOrder;SELECT *
FROM ShoppingOrder┌────────────id─┬──────────────────────────────orderId─┬─amount─┬──────────createTime─┬─customerPhone─┬─customerName─┬─orderStatus─┬─goodsIdList──┬─address.province─┬─address.city─┬─address.street─┬─address.houseNumber─┐
│ 1605628412414 │ 00000000-0000-0000-0000-000000000000 │ 100.00 │ 2020-11-17 15:53:34 │ 12345678901   │ throwable    │ init        │ [1,999,1234] │ ['广东省']       │ ['广州市']   │ ['X街道']      │ [10087]             │
└───────────────┴──────────────────────────────────────┴────────┴─────────────────────┴───────────────┴──────────────┴─────────────┴──────────────┴──────────────────┴──────────────┴────────────────┴─────────────────────┘1 rows in set. Elapsed: 0.004 sec.

实践表明:

  • ClickHouseDataType中可以查看ClickHouse各种数据类型和Java数据类型以及SQLType之间的对应关系,如UInt64 => BigInteger

  • ClickHouseArray类型写入数据的时候可以使用[元素x,元素y]的格式,也可以使用java.sql.Array进行传递,具体是ClickHouseArray,读取数据也可以类似地操作

  • 枚举Enum会直接转换为Java中的String类型

小结

本文已经十分详细分析了ClickHouse的各种数据类型的功能和基本使用例子,下一篇文章将会分析DDL部分。ClickHouse中的很多DDL的用法比较独特,和传统关系型数据库的DDL区别比较大。

(本文完 c-7-d e-a-20201118 最近玩《王国守卫战-复仇》鸽了很久)


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部