ClickHouse读书笔记(三)—数据定义
一、基础类型
1、数值类型
- 整型:Int8、Int16、Int32、Int64
- 无符号整型:UInt8、UInt16、UInt32、UInt64
- 浮点数:Float32(7位精度)、Float64(16位精度)
- 定点数:Decimal32、Decimal64、Decimal128三种精度,简写方式有Decimal32(S)、Decimal64(S)、Decimal128(S)。原生方式为Decimal(P,S),P代表总位数,范围是1~38;S代表小数位数,范围是0~P。加减乘除运算会导致S发生变化,加减法S=max(S1,S2);乘法S=S1+S2(S1范围>=S2范围);除法S=S1(S1是被除数,S1/S2);
2、字符串类型
- String:不限长度,不限定字符集,同一套程序应该遵循使用统一的编码,例如“统一保持UTF-8编码”
- FixedString:定长字符串,类似传统意义的Char类型,定长字符串通过FixedString(N)声明,N表示字符串长度,末尾不足补null字节
- UUID:数据库常见的主键类型,共32位,格式是8-4-4-4-12,如果写入时没赋值会按照格式用0填充
3、时间类型
支持字符串写入,没有时间戳,最高精度是秒,毫秒微秒需要借助UInt类型。
- DateTime:年月日时分秒,例如2019-06-22 00:00:00
- DateTime64:支持亚秒,例如2019-06-22 00:00:00.00
- Data:年月日,例如2019-06-22
二、复合类型
1、Array
数组,写法有两种:array(T)和[T],例如array(1, 2) 和 [1, 2],通过toTypeName()函数获取类型名称,例如Array(UInt8)。CK的数组拥有类型推断能力,以最小存储代价为原则,使用最小可表达的数据类型。多种数据类型之间可以兼容,例如[1, 1.2]
定义表字段时,需要明确元素类型,c1 Array(String)。
2、Tuple
元组类型,由1~n个元素组成,每个元素之间允许设置不同数据类型,不要求兼容。写法有两种:tuple(T)和(T),tuple(1,'a',now())或者(1,2.0,null),同样支持最小存储代价原则的类型推断。
定义表字段时,需要明确元素类型,c2 Tuple(String, Int8),写入时会进行类型检查。
3、Enum
枚举类型,定义常量时经常会使用到的数据类型。Enum8和Enum16只有取值范围不同。枚举固定使用(String:Int)Key/Value键值对的形式定义数据。Enum8对应(String:Int8),Enum16对应(String:Int16)。
定义枚举集合【c1 Enum8('ready' = 1, 'start' = 2, 'success' = 3, 'error' = 4)】,注意:Key和Value都不允许重复,都不允许为Null,但Key允许为空字符串,写入枚举数据时,只会用到Key字符串部分。例如【INSERT INTO Enum_TEST VALUES('ready');】
为什么不直接用String替代枚举,还要枚举类型?因为后续对枚举的所有操作,包括排序、分组、去重、过滤等都会使用Int类型的Value值。
4、Nested
嵌套类型,一张表可以有多个嵌套类型字段,但只能嵌套一层。嵌套里面的字段期望写入的是Array数组类型。并且同一行数据每个数组字段的长度必须相等。
CREATE TABLE nested_test ( name String, age UInt8 , dept Nested( id UInt8, name String ) ) ENGINE = Memory; INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001,10002], ['研发部','技 术支持中心','测试部']); -- 正确,3个id对应3个name INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001], ['研发部','技术支持中心','测试部']); -- 错误,2个id对应3个name SELECT name, dept.id, dept.name FROM nested_test; -- 访问嵌套类型的数据需要使用点符号
三、特殊类型
1、Nullable
准确来说,Nullable并不能算是一种独立的数据类型,它更像是 一种辅助的修饰符,需要与基础数据类型一起搭配使用。被修饰的字段可以写入null值。有两个注意点:
- 只能和基础类型搭配使用,不能用于复合类型,也不能作为索引字段。
- 慎用,会使查询和写入性能变慢。正常字段的数据会被存储在对应的[Column].bin文件中,如果用Nullable类型修饰,会额外生成一个[Column].null.bin文件专门保存它的Null值,这意味着读取和写入数据时需要一倍的额外文件操作。
2、Domain
域名类型,分为Ipv4和Ipv6,本质是对整型和字符串的进一步封装。有三个注意点
- 相比String,有格式校验。
- IPv4使用UInt32存 储,相比String更加紧凑,占用的空间更小,查询性能更快。IPv6类 型是基于FixedString(16)封装的。
- 看起来与String一样,但不支持隐式的自动类型转换,如果需要返回IP的字符串形式,需要显式调 用IPv4NumToString或IPv6NumToString函数。
四、关于最小存储代价原则的疑问(21.9.4.35版本)
SELECT [1, 2.1, null] as a , toTypeName(a) -- Array(Nullable(Float64)),2.1用Float32就足够存储了,为什么最小存储代价的类型是Float64? SELECT [1, 2, null] as a , toTypeName(a) -- Array(Nullable(UInt8)),这个没有疑问,两个小正数用UInt8 SELECT [1, -2, null] as a , toTypeName(a) -- Array(Nullable(Int16)),-2用Int8应该是够存了,为什么最小存储代价的类型是Int16?
盲猜:在最小存储代价之上还有一个默认规则,浮点数一律用Float64,正负小整数一律用Int16