15

11-22ClickHouse数据库数据定义手记之数据类型

 3 years ago
source link: http://www.throwable.club/2020/11/22/click-house-data-types/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前提

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

73y2MrE.jpg!mobile

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

数据类型

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

  • 数值类型
  • 字符串类型
  • 日期时间类型
  • 复合类型
  • 特殊类型

这里做一份汇总的表格:point_down:

大类 类型 类型名称 一般概念 JavaType 备注 数值类型 Int8 8bit 整型 TINYINT Byte|Integer - 数值类型 Int16 16bit 整型 SMALLINT Short|Integer - 数值类型 Int32 32bit 整型 INT Integer - 数值类型 Int64 64bit 整型 BIGINT Long - 数值类型 Int128 128bit 整型 `- - - 数值类型 Int256 256bit 整型 - - - 数值类型 UInt8 无符号 8bit 整型 TINYINT UNSIGNED - Java 中不存在无符号整数类型,选择类型时只要不溢出就行 数值类型 UInt16 无符号 16bit 整型 SMALLINT UNSIGNED - Java 中不存在无符号整数类型,选择类型时只要不溢出就行 数值类型 UInt32 无符号 32bit 整型 INT UNSIGNED - Java 中不存在无符号整数类型,选择类型时只要不溢出就行 数值类型 UInt64 无符号 64bit 整型 BIGINT UNSIGNED - Java 中不存在无符号整数类型,选择类型时只要不溢出就行 数值类型 Float32 32bit 单精度浮点数 FLOAT Float - 数值类型 Float64 64bit 双精度浮点数 DOUBLE Double - 数值类型 Decimal(P,S) 高精度数值, P 为总位长, S 为小数位长 DECIMAL BigDecimal - 数值类型 Decimal32(S) 高精度数值, P 总位长属于 [1,9]S 为小数位长 DECIMAL BigDecimal Decimal(P,S) 特化类型 数值类型 Decimal64(S) 高精度数值, P 总位长属于 [10,18]S 为小数位长 DECIMAL BigDecimal Decimal(P,S) 特化类型 数值类型 Decimal128(S) 高精度数值, P 总位长属于 [19,38]S 为小数位长 DECIMAL BigDecimal Decimal(P,S) 特化类型 字符串类型 String 不定长字符串,长度随意不限 广义上类似 LONGTEXT String 替代了传统 DBMS 中的 VARCHARBLOBCLOBTEXT 等类型 字符串类型 FixedString(N) 定长字符串,使用 null 字节填充末尾字符 有点类似 VARCHAR String - 字符串类型 UUID 特殊字符串, 32 位长度,格式为: 8-4-4-4-4-12 - String 一般使用内置函数生成 日期时间类型 Date 日期 DATE LocalDate - 日期时间类型 DateTime 日期时间 类似 DATE_TIME LocalDateTime | OffsetDateTime 只精确到秒,不包含毫秒 日期时间类型 DateTime64 日期时间 类似 DATE_TIME LocalDateTime | OffsetDateTime 只精确到秒,不包含毫秒,但是包含亚秒,即 10 ^ (-n) 秒 复合类型 Array(T) 数组 - 类似 T[] - 复合类型 Tuple(S,T...R) 元组 - - - 复合类型 Enum 枚举 - - - 复合类型 Nested 嵌套 - - - 特殊类型 Nullable NULL 修饰类型,不是独立的数据类型 - - - 特殊类型 Domain 域名 - - 存储 IPV4IPV6 格式的域名

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

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

数值类型

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

整型

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

带符号整型数值:

类型 字节(byte)数 范围 Int8 1 [-128, 127] Int16 2 [-32768, 32767] Int32 4 [-2147483648, 2147483647] Int64 8 [-9223372036854775808, 9223372036854775807] Int128 16 [-170141183460469231731687303715884105728, 170141183460469231731687303715884105727] Int256 32 [-57896044618658097711785492504343953926634992332820282019728792003956564819968,57896044618658097711785492504343953926634992332820282019728792003956564819967]

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

无符号整型数值:

类型 字节(byte)数 范围 UInt8 1 [0, 255] UInt16 2 [0, 65535] UInt32 4 [0, 4294967295] UInt64 8 [0, 18446744073709551615] UInt256 32 [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;

输出结果:

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(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 :point_down::point_down:

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

可以做一个小测试:

f5abc88ff7e4 :) SELECT toFloat32('0.1234567890') AS a,toTypeName(a);

SELECT
    toFloat32('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);

SELECT
    toFloat32('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);

SELECT
    toFloat64('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);

SELECT
    toFloat64('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;

SELECT
    toDecimal32(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/x

SELECT
    toDecimal32(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*x

SELECT
    toDecimal32(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 = Memory

Ok.

0 rows in set. Elapsed: 0.018 sec.

f5abc88ff7e4 :) INSERT INTO test_u VALUES (1,generateUUIDv4());

INSERT INTO test_u VALUES

Ok.

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 = Memory

Ok.

0 rows in set. Elapsed: 0.025 sec.

f5abc88ff7e4 :) INSERT INTO dt VALUES(1),(2),('0000-00-00'),('2020-11-11');

INSERT INTO dt VALUES


Received 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 VALUES

Ok.

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 = Memory

Ok.

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 VALUES

Ok.

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;

SELECT
    toDateTime(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;

SELECT
    toDateTime64(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 = Memory

Ok.

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 VALUES

Ok.

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 = Memory

Ok.

0 rows in set. Elapsed: 0.017 sec.

f5abc88ff7e4 :) INSERT INTO test_arr VALUES([1,2,3],['throwable','doge']);

INSERT INTO test_arr VALUES

Ok.

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 = Memory

Ok.

0 rows in set. Elapsed: 0.018 sec.

f5abc88ff7e4 :) INSERT INTO test_tp VALUES(1,(999,'throwable')),(2,(996,'doge'));

INSERT INTO test_tp VALUES

Ok.

2 rows in set. Elapsed: 0.003 sec.

f5abc88ff7e4 :) INSERT INTO test_tp VALUES(1,('doge','throwable'));

INSERT INTO test_tp VALUES


Exception 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 = Memory

Ok.

0 rows in set. Elapsed: 0.021 sec.

f5abc88ff7e4 :) INSERT INTO test_e VALUES(1),(2),('other');

INSERT INTO test_e VALUES

Ok.

3 rows in set. Elapsed: 0.004 sec.

f5abc88ff7e4 :) SELECT sex,CAST(sex,'Int8') FROM test_e

SELECT
    sex,
    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<Major> majors;
}

class Major {
    
    Long id;
    String name;
}

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

行号 major.id major.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 = Memory

Ok.

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 VALUES

Ok.

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;

SELECT
    n.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 = Memory

Ok.

0 rows in set. Elapsed: 0.022 sec.

f5abc88ff7e4 :) INSERT INTO test_null VALUES(1,'throwable'),(2,NULL);

INSERT INTO test_null VALUES

Ok.

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 = Memory

Ok.

0 rows in set. Elapsed: 0.029 sec.

f5abc88ff7e4 :) INSERT INTO test_d VALUES(1,'192.168.1.0');

INSERT INTO test_d VALUES

Ok.

1 rows in set. Elapsed: 0.003 sec.

f5abc88ff7e4 :) SELECT ip,IPv4NumToString(ip) FROM test_d;

SELECT
    ip,
    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 依赖:

<dependency>
    <groupId>ru.yandex.clickhouse</groupId>
    <artifactId>clickhouse-jdbc</artifactId>
    <version>0.2.4</version>
</dependency>

编写测试案例:

@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<Long> goodsIdList;

    /**
     * 这里实际上只有一个元素
     */
    private List<Address> 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<ShoppingOrder> 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) -> Array<BigInteger>
        Array goodsIdList = rs.getArray("goodsIdList");
        BigInteger[] goodsIdListValue = (BigInteger[]) goodsIdList.getArray();
        Set<Long> goodsIds = Sets.newHashSet();
        for (BigInteger item : goodsIdListValue) {
            goodsIds.add(item.longValue());
        }
        order.setGoodsIdList(goodsIds);
        List<Address> addressList = Lists.newArrayList();
        // Array(String) -> Array<String>
        Array province = rs.getArray("address.province");
        List<String> provinceList = arrayToList(province);
        // Array(String) -> Array<String>
        Array city = rs.getArray("address.city");
        List<String> cityList = arrayToList(city);
        // Array(String) -> Array<String>
        Array street = rs.getArray("address.street");
        List<String> streetList = arrayToList(street);
        // UInt64 -> Array<BigInteger>
        Array houseNumber = rs.getArray("address.houseNumber");
        BigInteger[] houseNumberValue = (BigInteger[]) houseNumber.getArray();
        List<Long> 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<String> 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 最近玩《王国守卫战-复仇》鸽了很久)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK