MENU

ClickHouse 数据类型

May 8, 2023 • Read: 2040 • 编码,ClickHouse

对于一款支持海量数据分析系统而言,能支持 DML 查询实属难得可贵。在海量的数据场景下,很多看似简单的操作也会变得举步维艰,所以一些系统会选择做减法来规避一些难题。而 ClickHouse 支持完备的 DML 命令,包括 INSERT、SELECT、UPDATE 和 DELETE。虽UPDATE 和 DELETE 可能会存在一些性能问题,但这些能力的提供确实丰富了架构师的筹码,在架构设计师可以多几个选择。

作为一款完备的 DBMS,ClickHouse 提供了 DDL 和 DML 功能,并支持大部分标准的SQL。这也是 ClickHouse 相对容易上手的原因。但作为一款异军突起的 OLAP 数据库黑马,ClickHouse 有着属于自己的设计目标,高性能才是它的根本。比如 ClickHouse 在基础数据方面相比于其他常规数据库会少一些,但它又同时提供了实用的复合数据类型,而这些复合类型是常规数据库所不具备的。再比如,ClickHouse 所提供的 DDL 和 DML 查询,在部分细节上和其他数据库也是不同的(例如 UPDATE、DELETE 是借助 ALTER 的变种实现的)。

所以掌握 ClickHouse 中的数据定义方法是很重要的,这能帮助我们深刻理解和使用 ClickHouse。

ClickHouse 数据类型

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

基础类型

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

数值类型

整型

名称大小(字节)范围普遍概念
Int81-128 到 127Tinyint
Int162-32768 到 32767Smallint
Int324-2147483648 到 2147483647Int
Int648-9223372036854775808 到 9223372036854775807Bigint
UInt810 到 255Tinyint Unsigned
UInt1620 到 65535Smallint Unsigned
UInt3240 到 4294967295Int Unsigned
UInt6480 到 18446744073709551615Bigint Unsigned

在传统观念中,常用 Tinyint、Smallint、Int 和 Bigint 指代整数的不同取值范围。而 ClickHouse 则直接用Int8、Int16、Int32、Int64 来指代四种大小的 Int 类型,其末尾的数字用来表明占用字节的大小(8位 = 1 字节)。无符号则使用前缀 U 来表示(Unsigned)。

浮点型

名称大小(字节)有效精度(位数)普遍概念
Float3247Float
Float64816Double

在使用浮点数的时候,要意识到他的精度是有限的。例如,Float32 的有效长度为小数点后 7 位,实际存储可以存储 8 位,但第八位的精度不准,Float64 则为是从第十七位开始,精度不准。

dbkuaizi-clickhouse :) SELECT toFloat32('0.123456789') as f32, toTypeName(f32);

SELECT
    toFloat32('0.123456789') AS f32,
    toTypeName(f32)

Query id: 502f6090-cc47-41d7-819e-37cf6b90b0aa

┌────────f32─┬─toTypeName(toFloat32('0.123456789'))─┐
│ 0.12345679 │ Float32                              │
└────────────┴──────────────────────────────────────┘

1 row in set. Elapsed: 0.001 sec. 

ClickHouse 的浮点型支持正无穷、负无穷、以及 非数字三种表达方式:

dbkuaizi-clickhouse :) SELECT 0.8/0,-0.8/0,0/0;

SELECT
    0.8 / 0,
    -0.8 / 0,
    0 / 0

Query id: 789b7cc8-df3d-49eb-ad18-de1aec7867b4

┌─divide(0.8, 0)─┬─divide(-0.8, 0)─┬─divide(0, 0)─┐
│            inf │            -inf │          nan │
└────────────────┴─────────────────┴──────────────┘

1 row in set. Elapsed: 0.001 sec. 

Decimal

如果要求更高精度的运算,则需要使用定点数。ClickHouse 提供了 Decimal32、Decimal64 和 Decimal128 三种精度的定点数。可以通过两种方式声明定点数:简写方式有 Decimal32(S)、Decimal64(S)、Decimal128(S) 三种,原生方式为 Decimal(P,S),其中:

  • P 代表精度,决定总位数(整数位数+小数位数),取值范围是 1 ~ 38;
  • S 代表规模,代表小数位数,取值范围是 0 ~ P。

简写方式和原生方式对比如图:

名称等效声明范围
Decimal32(S)Decimal(1 ~ 9,S)-1 10^(9-S) 到 1 10^(9-S)
Decimal64(S)Decimal(10 ~ 18,S)-1 10^(18-S) 到 1 10^(18-S)
Decimal128(S)Decimal(19 ~ 38,S)-1 10^(38-S) 到 1 10^(38-S)

在使用两个不同精度的定点数进行四则运算时,他们的小数点位数 S 会发生变化。规则如下:

  • 加法运算:S 取最大值,例如 toDecimal64(2,4)toDecimal32(2,2) 相加后 S = 4
  • 减法运算:S 取最大值,与加法相同
  • 乘法运算:S 取两者之和,例如 toDecimal64(2,4)toDecimal32(2,2) 相乘后 S = 4+2=6
  • 除法运算:S 取被除数的值,此时要求被除数的 S 必须大于除数的 S,否则就会报错。例如 toDecimal64(2,4)toDecimal32(2,2) 相除后 S = 4

最后需要注意的是,因为现代计算机系统只支持 32 和 64 位系统的 CPU,所以 Decimal128 是在软件层面模拟实现,速度会明显慢于 Decimal32 和 Decimal64。

字符串类型

字符串类型可以细分为 StringFixedStringUUID 三类。

String

字符串由 String 定义,长度不限。因此在使用此类型时无需声明大小,完全代替了传统数据库中的 VarChar、Text、Clob 和 Blob 类型。同时也没字符集的概念,可以将任意编码的字符存入其中。但是可维护性,需要在同一套程序中遵循使用统一的编码,例如“统一使用 UTF-8”编码这样的约定。

FixedString

FixedString 类型和传统意义上的 Char 类型有些类似,对于一些字符有明确长度的场合,可以使用固定长度的字符串。定长字符串通过 FixedString(N) 来声明,N 表示字符串长度。与 Char 类型不同的是 FixedString 使用 null 填充末尾字符,而 Char 通常使用空格填充。

UUID

UUID 是一种常见的数据库主键类型,在 ClickHouse 中被直接当作一种数据类型。UUID 有 32 位。它的格式为 8-4-4-4-12。如果一个 UUID 类型在写入时没有赋值,就会按照格式使用 0 填充,即 00000000-0000-0000-0000-000000000000

时间类型

时间类型分为 DateTime、DateTime64 和 Date 三类。

DateTime

时间戳类型,用四个字节的(无符号)整数型存储 Unix 时间戳,允许存储与日期类型相同范围内的值。值的范围: [1970-01-01 00:00:00, 2106-02-07 06:28:15]。

INSERT INTO Datetime_TEST VALUES('2023-04-22 00:00:00')

DateTime64

此类型允许以 日期加时间的形式来存储一个时间值,具有定义的亚秒精度,语法如下:

DateTime64(precision, [timezone])
  • precision 表示时间刻度(精度)大小,最大 8 位
  • timezone 表示时区,此信息存储在列的元数据中

值的范围: [1900-01-01 00:00:00, 2299-12-31 23:59:59.99999999]

Date

日期类型,用两个字节存储,表示从 1970-01-01 (无符号) 到当前的日期值。值的范围: [1970-01-01, 2149-06-06],日期类型没有时区概念。

Date32

一种日期表示方式,支持与 DateTime64 相同的日期范围,值代表自 1970-01-01 以来的天数(可以理解为输入一个时间戳,但只存储年月日)。值的范围: [1970-01-01, 2299-12-31]

复合类型

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

Array 数组

ClickHouse 的数组与其他强类型语言原理类似,同一类型的数据组合。

创建数组

有两种方式创建数组,常规方式为 array(T) 或简写方式 [T]

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

┌─a─────┬─toTypeName([1, 2])─┐
│ [1,2] │ Array(UInt8)       │
└───────┴────────────────────┘

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

┌─a─────┬─toTypeName([1, 2])─┐
│ [1,2] │ Array(UInt8)       │
└───────┴────────────────────┘

数组类型

在定义数组时,不需要主动声明数组的元素类型(动态创建数组)。因为 ClickHouse 的数组拥有类型推断的功能,推断依据为:以最小的存储代价为原则,使用最小可表达的数据类型。例如上面的 array(1,2) 会自动推断为 Uint8 作为数组的存储类型。但是数组元素如果存在 Null 值,元素类型将会变为 Nullabel (此类型后面会讲), 例如:

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

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

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

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

  • 如果 ClickHouse 无法确定数据类型,就会报错,例如:array(1,'a') 这样的数组无法推断使用什么类型来存储
  • 数组元素的最大数量限制为一百万个

数组大小

如果想知道数组的元素数量,而不想读取整个字段时,可以通过 sizeN 来实现,对于多维数组,可以使用 sizeN-1,其中 N 代表查询层级:

CREATE TABLE t_arr (`arr` Array(Array(Array(UInt32)))) ENGINE = MergeTree ORDER BY tuple();

INSERT INTO t_arr VALUES ([[[12, 13, 0, 1],[12]]]);

SELECT arr.size0, arr.size1, arr.size2 FROM t_arr;

┌─arr.size0─┬─arr.size1─┬─arr.size2─┐
│         1 │ [2]       │ [[4,1]]   │
└───────────┴───────────┴───────────┘

Tuple 元组

元组由 1 ~ N 个元素组成,每个元素都可以设置不同的数据类型,且不要求兼容。同时,元组也支持类型推断,依据依然是以最小存储代价为原则。

创建元组

元组的创建方式与数组类似,元组也可以使用两种方式定义,常规方式为 tuple(T) 或简写方式 (T)

SELECT tuple(1,'a') AS x, toTypeName(x)

┌─x───────┬─toTypeName(tuple(1, 'a'))─┐
│ (1,'a') │ Tuple(UInt8, String)      │
└─────────┴───────────────────────────┘

-- 简写示例
SELECT (1, 'a') AS x, (today(), rand(), 'someString') y, ('a') not_a_tuple;

┌─x───────┬─y──────────────────────────────────────┬─not_a_tuple─┐
│ (1,'a') │ ('2022-09-21',2006973416,'someString') │ a           │
└─────────┴────────────────────────────────────────┴─────────────┘

元素类型

元组会自动检测每个元素最小可存储类型,如果类型为 Null 则元组元素的类型为 Nullabel

SELECT tuple(1, NULL) AS x, toTypeName(x)

┌─x────────┬─toTypeName(tuple(1, NULL))──────┐
│ (1,NULL) │ Tuple(UInt8, Nullable(Nothing)) │
└──────────┴─────────────────────────────────┘

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

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

获取元组

可以使用索引或名称读取命名元组的元素:

CREATE TABLE named_tuples (`a` Tuple(s String, i Int64)) ENGINE = Memory;

INSERT INTO named_tuples VALUES (('y', 10)), (('x',-10));

SELECT a.s FROM named_tuples;

┌─a.s─┐
│ y   │
│ x   │
└─────┘

SELECT a.2 FROM named_tuples;

┌─tupleElement(a, 2)─┐
│                 10 │
│                -10 │
└────────────────────┘

Enum 枚举

ClickHouse 支持由命名值组成的枚举类型,命名值声明为 'string' = int'string' 名称,ClickHouse 只存储数字,但支持通过名称对值进行操作。

ClickHouse 支持两种 Enum 类型:

  • 8 位 Enum,最多可以存储 [-128 ~ 127],范围内枚举的 256 个值,
  • 16 位 Enum ,最多可以存储 [-32768 ~ 32767] 范围内枚举的 65535 个值。

ClickHouse 会在定义表结构时,根据设定的命名值自动推断 Enum 类型。也可以使用 Enum8Enum16 来指定存储的类型。

使用 Enum 的优点

或许有的人会疑惑,使用 String 完全可以代替掉枚举,为什么还需要专门的枚举类型呢?

这么做的原因是处于性能的考虑,虽然枚举定义中 Key 仍然为 String 类型,但数据库底层操作中实际使用的是 Int 类型的 Value 值,效率比String 高得多。

Enum 创建

创建 Enum 需要注意以下几点:

  • Key 和 Value 不允许重复,要保证唯一性
  • Key 和 Value 的值都不允许为 Null, 但 Key 允许是空字符串
  • Value 值的数字可以是任意顺序,无关紧要

创建一个带有 Enum8('hello' = 1, 'world' = 2) 类型的字段:

CREATE TABLE t_enum
(
    x Enum('hello' = 1, 'world' = 2)
)
ENGINE = TinyLog

也可以省略数字,ClickHouse 默认会自动从 1 开始分配编号。

CREATE TABLE t_enum
(
    x Enum('hello', 'world')
)
ENGINE = TinyLog

当然也可以手动指定一个合法起始编号:

CREATE TABLE t_enum
(
    x Enum('hello' = -128, 'world')
)
ENGINE = TinyLog

Enum 插入

Enum 列定义完成后,只能存储定义字段时列出的值,如果想尝试保存其他值,ClickHouse 会引发异常。

例如,以上列示例中表为例:

-- 正常写入 
INSERT INTO t_enum VALUES ('hello'), ('world'), ('hello')

-- 报错
INSERT INTO t_enum values('abc')

Enum 查询

直接查询 Enum 列时,会显示对应的字符串,若要查看对应的 Int值,则可以使用 CAST() 函数 实现。

dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum

┌─x─────┬─CAST(x, 'Int8')─┐
│ hello │               1 │
│ world │               2 │
│ hello │               1 │
└───────┴─────────────────┘

3 rows in set. Elapsed: 0.010 sec. 

Enum Null

Enum 中 Key 和 Value 不允许为字符串,但可以通过 Nullable 的方式实现,将 NULL 包含在 Nllable 类型中:

CREATE TABLE t_enum_nullable
(
    x Nullable( Enum8('hello' = 1, 'world' = 2) )
)
ENGINE = TinyLog;

这也就可以同时存储 字符串和 Null 了:

INSERT INTO t_enum_nullable Values('hello'),('world'),(NULL)

Enum 读取

  • 当以文本形式(Value)读取时,ClickHouse 将值被解析为字符串,并从 Enum 的集合中搜索相应的字符串,若未找到则会报错。
  • 当以文本格式(Key)读取时,读取字符串并查找对应的的数值,若未找到则会报错。
  • 当以文本形式(Value)写入时,它将数值写成相应的字符串。如果列数据包含垃圾(不属于有效集合的数字),就会抛出一个异常。
  • 当以二进制的形式读取和写入的方式与 Int8 和 Int16 类型一致。

-- 个人理解以文本形式写入
dbkuaizi-clickhouse :) INSERT INTO t_enum VALUES (2);

-- 转换成字符串了
dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum

┌─x─────┬─CAST(x, 'Int8')─┐
│ hello │               1 │
│ world │               2 │
│ hello │               1 │
│ world │               2 │
└───────┴─────────────────┘

dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x =1

┌─x─────┬─CAST(x, 'Int8')─┐
│ hello │               1 │
│ hello │               1 │
└───────┴─────────────────┘

2 rows in set. Elapsed: 0.145 sec. 

dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x = 'hello'

┌─x─────┬─CAST(x, 'Int8')─┐
│ hello │               1 │
│ hello │               1 │
└───────┴─────────────────┘

2 rows in set. Elapsed: 0.049 sec. 

Enum 其他

  • Order ByGroup ByINDISTINCT 等操作中, Enum 的行为与对应的数字相同,例如 按数字排序。
  • 在等式运算和比较运算时,Enum 的工作机制与它们底侧的那个数值的工作机制相同。
  • 枚举值(字符串)不能直接与数字比较,被拒之可以与常量字符串比较。 如果比较的字符串不在 Enum 有效值中,将会引发异常。

    SELECT x,CAST(x, 'Int8') FROM t_enum where x = 'hello'; -- 正常运行
    SELECT x,CAST(x, 'Int8') FROM t_enum where x = 'abc'; -- 会报错
  • 可以使用 IN 运算符来判断一个 Enum 是否存在于某个 Enum 集合中,其中 IN 集合中字符串,也必须是 Enum 的有效值,否则将会引发异常。

    dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x IN('hello'); -- 正常
    dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x IN('hello','abc'); -- 会报错
  • 枚举值也可以通过 toT 函数转为数字类型,其中 T 是数字类型,当 T 对应的是枚举的基础数字类型时,这种转换是零成本的。
-- enum8 int类型底层是 int8,可以直接这样零损耗转换
dbkuaizi-clickhouse :) SELECT x,toInt8(x) FROM t_enum;

┌─x─────┬─toInt8(x)─┐
│ hello │         1 │
│ world │         2 │
│ hello │         1 │
│ world │         2 │
└───────┴───────────┘
  • Enum 类型可以被 ALTER 无成本的修改对应合集的值,可以通过 ALTER 操作来增加或删除 Enum 的成员(只要该表没有用到此值,直接删除都是安全的),如果改变已经被使用的值,将会抛出异常
  • 通过 ALTER 操作可以将 Enum8 转为 Enum16,反之毅然。与 Int8 转 Int16 一样。

Nested 嵌套

嵌套类型的数据结构就像是单元格中的表格,嵌套数据结构在声明时,与创建表时声明字段的方式完全一样。每行数据中的嵌套字段,可以包含任意数量的行。

换一个说法,如果将 ClickHouse 的表看作是一个列式存储的 JSON 对象数组,那么嵌套类型就是某个字段里面又嵌入了一个 列式存储的 JSON 对象数组。

Nested 创建

下面创建一个员工表,每个员工负责 N 项目,我们把项目id 和名称作为一个嵌套结构来存储(感觉有点像多对多中间表的用法)

CREATE TABLE employee (
    name String,
    project Nested(
        id UInt8,
        name String
    )
) ENGINE = Memory;

Nested 插入

需要注意的是:每个嵌套类型的单元格中所有的列长度必须相同

-- 使用数组的方式插入数据
INSERT INTO employee VALUES ('张三' ,[100,101], ['项目1','项目2']);

-- 如果两个列长度不一致,就会报错。
INSERT INTO employee VALUES ('张三' ,[100,101,104], ['项目1','项目2']);

-- 长度一致,可以插入
INSERT INTO employee VALUES ('张三' ,[100,101,104], ['项目1','项目2','项目3']);

Nested 查询

大多情况下,当使用嵌套结构时,其中列是以点分割的列名称指定的,这些列组成了一个匹配类型的数组,单个嵌套结构的所有列数组都具有相同长度。

最容易理解的方式是将嵌套数据结构视为一组相同长度的多个列数组。

dbkuaizi-clickhouse :) SELECT name,project.id,project.name FROM employee;

┌─name─┬─project.id─┬─project.name──────┐
│ 张三 │ [100,101]  │ ['项目1','项目2'] │
└──────┴────────────┴───────────────────┘
┌─name─┬─project.id─┬─project.name──────┐
│ 李四 │ [103,105]  │ ['项目3','项目4'] │
└──────┴────────────┴───────────────────┘

可以使用 ARRAY JOIN 字句将嵌套结构展开:

dbkuaizi-clickhouse :) SELECT name,p.id,p.name FROM employee  ARRAY JOIN project AS p ;

┌─name─┬─p.id─┬─p.name─┐
│ 张三 │  100 │ 项目1  │
│ 张三 │  101 │ 项目2  │
└──────┴──────┴────────┘
┌─name─┬─p.id─┬─p.name─┐
│ 李四 │  103 │ 项目3  │
│ 李四 │  105 │ 项目4  │
└──────┴──────┴────────┘

补充:只是命令行这么显示,实际查询出来是一个列表

name|p.id|p.name|
----+----+------+
李四  | 103|项目3   |
李四  | 105|项目4   |
张三  | 100|项目1   |
张三  | 101|项目2   |

嵌套结构的字段不支持查询操作,只能在 SELELCT 中列出来,例如下面的 SQL 就无法执行:

SELECT name,project.id,project.name FROM employee WHERE project.id = 100;

ALTER 操作

需要注意的是,如果要对嵌套结构里的字段进行 ALTER 操作,会具有一定的局限性。

Map 类型

Map(Key,Value) 类型存储 Key-Value 键值对,Key,Value 类型要求如下:

  • Key 可选类型为:String、Integer、LowCardinality、FixedString、UUID、Date、DateTime、Date32、Enum
  • Value 可选类型为:任何类型,甚至可以是 Map 和 Array 类型

Map 创建

创建 Map 字段时,需要明确指定 Key 和 Value 类型

CREATE TABLE table_map (a Map(String, UInt64)) ENGINE=Memory;

Map 插入

插入数据时要使用 要使用花括号的格式插入,类似于 JOSN 对象的写法

INSERT INTO table_map VALUES ({'key1':1, 'key2':10}), ({'key1':2,'key2':20}), ({'key1':3,'key2':30});

Map 查询

查询 Map 类型时,如果只获取某个Value,需要用中括号的方式取值:

MiWiFi-RA69-srv :) SELECT a['key2'] FROM table_map;

┌─arrayElement(a, 'key2')─┐
│                      10 │
│                      20 │
│                      30 │
└─────────────────────────┘

当获取不存在的 Key 时,会返回零:

MiWiFi-RA69-srv :) SELECT a['key3'] FROM table_map;

┌─arrayElement(a, 'key3')─┐
│                       0 │
│                       0 │
│                       0 │
└─────────────────────────┘

将 Tuple 转为 Map

MiWiFi-RA69-srv :) SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;

┌─map───────────────────────────┐
│ {1:'Ready',2:'Steady',3:'Go'} │
└───────────────────────────────┘

Map.keys 和 Map.values

如果只想获取所有的 Key 或者所有的 Value,可以这样查询

MiWiFi-RA69-srv :) SELECT a.keys,a.values FROM table_map;

┌─a.keys──────────┬─a.values─┐
│ ['key1','key2'] │ [1,10]   │
│ ['key1','key2'] │ [2,20]   │
│ ['key1','key2'] │ [3,30]   │
└─────────────────┴──────────┘
┌─a.keys──────────┬─a.values─┐
│ ['key1','key3'] │ [1,40]   │
└─────────────────┴──────────┘

特殊类型

ClickHouse 还有一些特殊类型。

LowCardinality 低基数

将其他数据类型在存储时转为字典编码。

LowCardinality(data_type)

LowCardinality 改变了数据的存储方式和数据的处理规则,ClickHouse 将 字典编码 应用于LowCardinality 列,使用字典编码数据操作可以显著提高 SELECT 查询性能。

使用 LowCardinality 的效率取决于数据的多样性,如果字典包含的值多样性少于 10000 个,那么 ClickHouse 会表现出更高的数据读取或存储效率,如果字典值的多样性超过 10000 个,那么与普通的数据类型相比,ClickHouse 的效率可能会更差。

在处理字符串时,应该优先考虑使用 LowCardinality 而不是枚举,因为 LowCardinality 拥有更大的灵活性以及更高的效率。

LowCardinality 创建

CREATE TABLE lc_t
(
    `id` UInt16,
    `strings` LowCardinality(String)
)
ENGINE = MergeTree()
ORDER BY id

LowCardinality 实践

Nullable 可为空

在设定字段类型时使用 Nullabel 修饰,将允许存储 Null 来代表值缺失。例如 Nullabel(Int8) 类型, 除了存储 Int8 标准数值之外还允许存储 Null

Nullabel(typeName)
  • typeName 不能是复合数据类型,例如 Array、Map、Tuple,复合数据类型可以包含 Nullable 类型值,例如:Array(Nullable(Int8)) ,

  • Nullable 类型不能包含在表索引中。
  • 设计表结构时应慎用 Nullable 类型,Nullable 类型对查询和写入性能都有负面影响。

使用


-- 创建表
dbkuaizi :) CREATE TABLE nullable (`n` Nullable(UInt32)) ENGINE = MergeTree ORDER BY tuple();

-- 写入数据
dbkuaizi :) INSERT INTO nullable VALUES (1) (NULL) (2) (NULL);

-- 查询数据
dbkuaizi :) SELECT n.null FROM nullable;

┌─n.null─┐
│      0 │
│      1 │
│      0 │
│      1 │
└────────┘

-- 查询不是 null 的记录

dbkuaizi :) SELECT * FROM nullable where n.null == 0;

Query id: bb83f038-408c-4e51-9db8-7d507741be1d

┌─n─┐
│ 1 │
│ 2 │
└───┘

通过 字段.null 的方法可以返回会值是否是 null,如果是 null 则返回 1 否则返回 0。

Nullabel 运算

当 Null 值与其他值进行运算时,结果为 Null。

CREATE TABLE t_null(x Int8, y Nullable(Int8)) ENGINE TinyLog;

INSERT INTO t_null VALUES (1, NULL), (2, 3);

SELECT x + y FROM t_null;

┌─plus(x, y)─┐
│       ᴺᵁᴸᴸ │
│          5 │
└────────────┘

Domains 域

域类型分为 IPv4 和 IPv6 两类,本质上是对整型和字符串类型进一步封装,并且在写入时对格式进行相应的校验,不符合规范的数据无法被写入。

无论是 IPv4 还是 IPv6 底层存储是都是转为紧凑的二进制进行存储

IPv4

IPv4 是一个基于 Uint32 类型的域,用作存储 IPv4 值的类型代替。它提供了人性化的输入校验,以及更小的存储占用。

使用

CREATE TABLE hits (url String, from IPv4) ENGINE = MergeTree() ORDER BY from;

INSERT INTO hits (url, from) VALUES ('https://wikipedia.org', '116.253.40.133')('https://clickhouse.com', '183.247.232.58')('https://clickhouse.com/docs/en/', '116.106.34.242');

SELECT * FROM hits;

┌─url────────────────────────────────┬───────────from─┐
│ https://clickhouse.com/docs/en/ │ 116.106.34.242 │
│ https://wikipedia.org              │ 116.253.40.133 │
│ https://clickhouse.com          │ 183.247.232.58 │
└────────────────────────────────────┴────────────────┘

虽然查询时 IPv4 会自动格式化显示,但我们需要清楚的是 IPv4 不能隐式转换为除 Uint32 类型以外的类型。所以不能直接当字符串用,必须使用 IPv4NumToString() 函数进行转换操作。

SELECT toTypeName(s), IPv4NumToString(from) as s FROM hits LIMIT 1;

┌─toTypeName(IPv4NumToString(from))─┬─s──────────────┐
│ String                            │ 183.247.232.58 │
└───────────────────────────────────┴────────────────┘

或转为 Uint32 类型的值:

SELECT toTypeName(i), CAST(from as UInt32) as i FROM hits LIMIT 1;

┌─toTypeName(CAST(from, 'UInt32'))─┬──────────i─┐
│ UInt32                           │ 3086477370 │
└──────────────────────────────────┴────────────┘

IPv6

IPv6 是一个基于 FixedString(16) 类型的域,用作存储 IPv6 值的类型替代。与 IPv4 一样提供了输入校验和小的存储占用。

使用

CREATE TABLE hits (url String, from IPv6) ENGINE = MergeTree() ORDER BY from;

INSERT INTO hits (url, from) VALUES ('https://wikipedia.org', '2a02:aa08:e000:3100::2')('https://clickhouse.com', '2001:44c8:129:2632:33:0:252:2')('https://clickhouse.com/docs/en/', '2a02:e980:1e::1');

SELECT * FROM hits;

┌─url────────────────────────────────┬─from──────────────────────────┐
│ https://clickhouse.com          │ 2001:44c8:129:2632:33:0:252:2 │
│ https://clickhouse.com/docs/en/ │ 2a02:e980:1e::1               │
│ https://wikipedia.org              │ 2a02:aa08:e000:3100::2        │
└────────────────────────────────────┴───────────────────────────────┘

值不能隐式的转为除 FixedString(16) 意外的类型,如果需要转为字符串,必须使用 IPv6NumToString() 函数进行转换。

地理位置类型

这玩意用的实在太少了,后面用到了再整理。

官方文档链接地址:https://clickhouse.com/docs/en/sql-reference/data-types/geo


  • 笔记少部分内容来自 《ClickHouse 原理解析与应用实践》第四章 第一小节 ClickHouse 的数据类型
  • 大部分内容来自 ClickHouse 英文版官方文档(说一嘴,不同语言的文档详细程度是不同的)