二、ClickHouse 資料型別

YGY---一隻小碼農發表於2020-12-21


作為一款分析型資料庫,ClickHouse提供了許多資料型別,他們可以劃分三類: 基礎型別複合型別特殊型別。其中基礎型別使ClickHouse具備了描述資料的基本能力,而另外兩種型別則使ClickHouse的資料表達能力更加豐富立體。

YGY---一隻小碼農

1、基礎型別

基礎型別只有數值字串時間這三種型別。值得一提的是,與大多數普通的資料庫不一樣的是,ClickHouse沒有Boolean型別,但是呢,可以使用整型的0或1替代。

1.1、數值型別

數值型別分為整數浮點數定點數三類,接下來分別進行說明。

1.1.1、Int

在普通的觀念中,常用Tinyint、Smallint、Int和Bigint指代整數的不同取值範圍。而ClickHouse則是直接使用Int8、Int16、Int32和Int64這4種大小的Int型別,其末尾的數字正好表明了佔用位元組的大小(8位 = 1位元組)。詳細資訊如下表所示:

表1 有符號整數型別的具體資訊
名 稱大小(位元組)範圍普遍概念
Int81-128 ~ 127Tinyint
Int162-32768 ~ 32767Smallint
Int324-2147483648 ~ 2147483647Int
Int648-9223372036854775808~9223372036854775807BigInt

同樣的,ClickHouse也支援無符號的整數,使用字首U表示,具體資訊如下表所示:

表2 有符號整數型別的具體資訊
名 稱大小(位元組)範圍普遍概念
Int810 ~ 255Tinyint Unsigned
Int1620 ~ 65535Smallint Unsigned
Int3240 ~ 4294967295Int Unsigned
Int6480 ~ 18446744073709551615BigInt Unsigned

1.1.2、Float

與整數類似,ClickHouse直接使用Float32和Float64代表單精度浮點數以及雙精度浮點數,具體資訊如下表所示:

表3 浮點數型別的具體資訊
名稱大小(位元組)有效精度(位數)普遍概念
Float3247Float
Float816Double

在使用浮點數的時候,應當要意識到它是有限精度的。什麼意思呢?比如,我分別對Float32和Float64寫入超過有效精度的數值,下面讓我們看看會發生什麼事情。
例子:將擁有20位小數的數值分別寫入Float32和Float64,觀看他們會發生什麼?

select toFloat32('0.1234567890123456789') as result,toTypeName(result)
resulttoTypeName(toFloat32(‘0.1234567890123456789’))
0.12345679Float32
select toFloat64('0.1234567890123456789') as result,toTypeName(result)
resulttoTypeName(toFloat32(‘0.1234567890123456789’))
0.12345678901234568Float64

我們可以發現,Float32從小數點後第八位開始以及Float64從小數點後第十七位開始,都產生了資料溢位。所以,當你需要浮點數後幾位的時候,注意浮點數的型別,不超過8位的用Float32,超過8位而小於17位的用Float64,超過17位的如果需要,建議使用定點數(decimal)型別。

另外,ClickHouse的浮點數支援正無窮、負無窮以及非數字的表達方式。
正無窮:

select 0.1/0
divide(0.1,0)
select -0.1/0
divide(-0.1,0)
-∞
select 0/0
divide(0,0)
NaN

1.1.3、Decimal

如果想要使用更高精度的數值運算,則需要使用定點數!ClickHouse提供了Decimal32、Decimal64和Decimal128三種精度的定點數。可以通過兩種形式宣告定點:

  • 簡寫方式:Decinal32(S)、Decinal64(S)、Decinal128(S)
  • 原生方式:Decinal(P,S)

其中:

  • P代表精度,決定總位數(整數部分+小數部分),取值為1 ~ 38
  • S代表規模,決定小數位數,取值範圍是0 ~ P

簡寫方式和原生方式的對應方式如下表所示:

表4 定點數型別的具體資訊
名稱等效宣告範圍
Decinal32(S)Decimal(1~9,S)-10^(9-S) ~ 10^(9-S)
Decinal64(S)Decimal(10~18,S)-10^(18-S) ~ 10^(18-S)
Decinal128(S)Decimal(19~38,S)-10^(38-S) ~ 10^(38-S)

在使用兩個不同精度的定點數進行四則運算時,它們的小數點位數S會發生變化:

①在進行加法運算時,S取最大值(規模)。例如下面的查詢,toDecimal64(2,4)與toDecimal32(2,2)相加後S=4(也就是說,2.0000+2.00=4.0000):

select toDecimal64(2,4),toDecimal32(2,2),toDecimal64(2,4)+toDecimal32(2,2)
toDecimal64(2,4)toDecimal32(2,2)plus(toDecimal64(2,4)+toDecimal32(2,2))
2.00002.004.0000

②在進行減法運算時,其規則與加法運算相同,S同樣會取最大值(規模)。例如toDecimal32(4,4)與toDecimal64(2,2)相減後S=4(也就是說,4.0000 - 2.00=2.0000):

select toDecimal32(4,4),toDecimal64(2,2),toDecimal32(4,4)-toDecimal64(2,2)
toDecimal32(4,4)toDecimal64(2,2)minus(toDecimal32(4,4)-toDecimal64(2,2))
4.00002.002.0000

③在進行乘法運算時,S取兩者S之和。例如toDecimal64(2,4)與toDecimal32(2,2)相乘後S=4+2=6(也就是說,2.0000 * 2.00=4.000000):

select toDecimal64(2,4),toDecimal32(2,2),toDecimal64(2,4)*toDecimal32(2,2)
toDecimal64(2,4)toDecimal32(2,2)multiply(toDecimal64(2,4)*toDecimal32(2,2))
2.00002.004.000000

④在進行除法運算時,S取被除數的值,此時要求被除數的S必須大於除數的S,否則會報錯!例如toDecimal64(2,4)與toDecimal32(2,2)相除後S=4:

select toDecimal64(2,4),toDecimal32(2,2),toDecimal64(2,4)/toDecimal32(2,2)
toDecimal64(2,4)toDecimal32(2,2)divide(toDecimal64(2,4)+toDecimal32(2,2))
2.00002.001.0000

需要注意的是,如果被除數的S小於於除數的S,則會報錯!

select toDecimal32(2,2),toDecimal64(2,4),toDecimal32(2,2)/toDecimal64(2,4)

YGY---一隻小碼農

最後可以總結得出:對於不同精度的定點數之間的四則運算,其精度S的變化會遵循下表所示的規則。

表5 定點數四則運算後,精度變化的規則
名稱規則
加法S=max(S1,S2)
減法S=max(S1,S2)
乘法S = S1+S2(S1範圍>=S2範圍)
除法S=S1(S1為被除數,S1/S2)

還有,使用定點數時需要注意一點就是:由於現代計算器只支援32位和64位CPU,所以Decimal128是在軟體層面模擬實現的,它的速度會明顯慢於Decimal32與Decimal64。

1.2、字串型別

字串型別可以細分為String,FixedString和UUID三類。從命名來看彷佛不像是由一款資料庫提供的型別,反而像是一門程式語言的設計。

1.2.1、String

字串型別由String定義,長度不限。因此在使用String的時候無須宣告大小。它完全代替了傳統意義上資料庫的Varchar、Text、Clob和Blob等字元型別。String型別不限定字符集,因為它根本就沒有這個概念,所以可以將任意編碼的字串存入其中。但是為了程式的規範性和可維護性,在同一套程式中應該遵循使用統一的編碼,例如 “統一保持UTF-8編碼” 就是一種很好的約定。

1.2.2、FixedString

FixedString型別和傳統意義上的Char型別有些類似,對於一些字元有明確長度的場合,可以使用固定長度的字串。定長字串通過FixedString(N)宣告,其中N表示字串長度(N 必須是嚴格的正自然數)。但是和Char不同的是,FixedString使用null位元組填充末尾字元,而char通常使用空格填充。

select toFixedString('abc',5),length(toFixedString('abc',5)) as length
toFixedString(‘abc’,5)length
abc5

當服務端讀取長度大於 N 的字串時候,將返回錯誤訊息!

select toFixedString('abcdefg',5)

YGY---一隻小碼農
與 String 相比,極少會使用 FixedString,因為使用起來不是很方便。

1.2.3、UUID

UUID是一種資料庫常見的主鍵型別,在ClickHouse中,它卻被作為一種資料型別。UUID共有32位,它的格式為8-4-4-4-12。如果一個UUID型別的欄位在寫入資料時沒有被賦值,則會依照格式使用填充。比如:

create table UUID_TEST(
	c1 UUID,
	c2 String
) ENGINE = Memory;
--- 第一行UUID有值
insert into UUID_TEST SELECT generateUUIDv4(),'t1'
--- 第二行UUID沒有值
insert into UUID_TEST(c2) values('t2')
select * from UUID_TEST
c1c2
f378ea5b-5195-4987-a97f-2e5d5093f925t1
00000000-0000-0000-0000-000000000000t2

我們可以看到,第二行沒有被賦值的UUID被0填充了。所以當我們插入資料的時候,需要注意,如果沒有給UUID型別的列賦值,它會按照格式使用0填充而不是顯示空值!

1.3、時間型別

時間型別分為DateTime、DateTime64和Date三類。ClickHouse目前沒有時間戳型別。時間型別最高的精度是秒,也就是說,如果需要處理毫秒、微秒等大於秒解析度的時間,則只能藉助UInt型別實現。

1.3.1、DateTime

DateTime型別包含時、分、秒資訊,精確到秒,支援使用字串形式寫入:

create table Datetime_Test(
    t1 Datetime
) ENGINE = Memory
---以字串形式寫入
insert into Datetime_Test values('2020-12-21 00:00:00')
select t1,toTypeName(t1) From Datetime_Test
t1toTypeName(t1)
2020-12-21 00:00:00DateTime

1.3.2、DateTime64

DateTime64可以記錄亞秒,它在DateTime之上增加了精度的設定,例如:

create table Datetime64_TEST(
t1 Datetime64(2)
) ENGINE=Memory
--- 以字串的形式寫入
insert into Datetime64_TEST values('2020-12-21 00:00:00')
select t1,toTypeName(t1) from Datetime64_TEST
t1toTypeName(t1)
2020-12-21 00:00:00.00DateTime64(2)

1.3.3、Date

Date型別不包括具體的時間資訊,只精確到天,它同樣也支援字串形式寫入:

create table Date_TEST(
t1 Date
) ENGINE = Memory
---以字串形式寫入
insert into Date_TEST values('2020-12-21')
select t1,toTypeName(t1) from Date_TEST
t1toTypeName(t1)
2020-12-21Date

2、複合型別

除了基礎資料型別之外,ClickHouse還提供了陣列、元組、列舉和巢狀四類複合型別。這些型別通常是其他資料庫原生不具備的特性。擁有了複合型別之後,ClickHouse的資料模型表達能力更強了。

2.1、Array

陣列有兩種定義形式,常規方式array(T)。T 可以是任意型別,包含陣列型別。 但不推薦使用多維陣列,ClickHouse 對多維陣列的支援有限。例如,不能在 MergeTree 表中儲存多維陣列。:

select array(1,2) as a,toTypeName(a)
atoTypeName(array(1,2))
[1, 2]Array(UInt8)

或者可運用其簡寫方式[T]:

select [1,2]

通過上述的例子,我們可以發現:在查詢的時候並不需要主動宣告陣列的元素型別。因為ClickHouse的陣列擁有型別推斷的能力,推斷依據:以最小儲存代價為原則,即使用最小可表達的資料型別。例如在上面的例子中,array(1,2)會通過自動判斷將UInt8作為陣列型別。但是陣列元素中如果存在Null值,則元素型別將變成Nullable,例如:

select [1,2,null] as a,toTypeName(a)
atoTypeName([1,2,null])
[1,2,null]Array(Nullable(UInt8))

如果大家仔細觀察一下,那麼就可以看到:在同一個陣列內可以包含多種資料型別。例如陣列[1,2.0]也是可行的。但各型別之間必須相容,例如陣列[1,‘2’]則會報錯。

在定義表欄位時,陣列需要指定明確的元素型別,例如:

create table Array_TEST(
t1 Array(UInt8)
) engine=Memory

2.2、Tuple

元組型別由1~n個元素組成,每個元素之間允許設定不同的資料型別,且彼此之間不要求相容。元組同樣支援型別判斷,其推斷依據仍然以最小儲存代價為原則。與陣列類似,元組也可以使用兩種方式定義,常規方式tuple(T):

select tuple(1,'a',now()) as t,toTypeName(t)
ttoTypeName(tuple(1,‘a’,now()))
(1,‘a’,‘2020-12-18 01:07:15’)Tuple(UInt8, String, DateTime)

或者可以簡寫為:(T)

select (1,2.0,null) as t,toTypeName(t)
ttoTypeName(1,2.,NULL)
(1,2,NULL)Tuple(UInt8, Float64, Nullable(Nothing))

在定義表欄位時,元組也需要指定明確的元素型別:

create table Tuple_TEST(
c1 Tuple(Int8,String)
) engine=Memory;

元素型別和泛型的作用類似,可以進一步保障資料質量。在資料寫入的過程中會進行型別檢查。例如,寫入insert into Tuple_TEST values((1234,‘abcd’))是可行的,而寫入insert into Tuple_TEST values((‘abcd’,‘efgh’))則會報錯。

2.3、Enum

ClickHouse支援列舉型別,這是一種在定義常量時經常會使用的資料型別。ClickHouse提供了Enum8和Enum16兩種列舉型別,他們除了取值範圍不同之外,別無二致。列舉固定使用(String:Int)Key/Value鍵值對的形式定義資料,所以Enum8和Enum16分別會對應(String:Int8)和(String:Int16),例如:

create table Enum_TEST(
t1 Enum8('ready'=1,'start'=2,'success'=3,'error'=4)
) engine=Memory;

在定義列舉集合的時候,有幾點需要注意。首先,Key和Value是不允許重複的,要保證唯一性。其次,Key和Value的值都不能為Null,但Key允許是空字串。在寫入資料的時候,只會用到Key字串部分。這個 t1 列只能儲存型別定義中列出的值:‘ready’或’start’或’success’或’error’。如果嘗試儲存任何其他值,ClickHouse 丟擲異常。例如:

insert into Enum_TEST values('ready');
insert into Enum_TEST values('start');

資料在寫入的過程中,會對照列舉集合項的內容逐一檢查。如果Key字串不在集合範圍內則會丟擲異常,比如執行下面的語句就會報錯:

insert into Enum_TEST values('stop');

YGY---一隻小碼農
從表中查詢資料時,ClickHouse從Enum中輸出字串值。

select * from Enum_TEST
t1
ready
start

如果需要看到對應行的數值,則必須將 Enum 值轉換為整數型別。

SELECT CAST(t1, 'Int8') FROM Enum_TEST
CAST(t1, ‘Int8’)
1
2

可能有人會覺得,完全可以使用String替代列舉,為什麼還需要專門的列舉型別呢?這是出於效能的考慮。因為雖然列舉定義中的Key屬於String型別,但是在後續對列舉的所有操作中(包括排序、分組、去重、過濾等),會使用Int型別的Valuez=值。

2.4、Nested

巢狀型別,顧名思義是一種巢狀表結構。一張資料表,可以定義任意多個巢狀型別欄位,但每個欄位的巢狀層級只支援一級,即巢狀表內不能繼續使用巢狀型別。對於簡單場景的層級關係或關聯關係,使用巢狀型別也是一種不錯的選擇。例如,下面的nested_test是一張模擬的學生表,它的所屬部門欄位就使用了巢狀型別:

create table nest_test
(
    name String,
    age UInt8,
    teacher Nested(
        id UInt8,
        name String
    )
) ENGINE = Memory;

ClickHouse的巢狀型別和傳統的巢狀型別不相同,導致在初次接觸它的時候會讓人十分困惑。以上面的這張表為例,如果按照它的字面意思來理解,會很容易理解成nested_test與dept是一對一關係,其實這是錯誤的。不信可以執行下面的語句,看看會是什麼結果:

insert into nest_test values('張三',18,1,'語文老師')

YGY---一隻小碼農
注意上面的異常資訊,它提示期望寫入的是一個Array陣列型別。
現在大家應該明白了,巢狀型別本質是一種多維陣列的結構。巢狀表中的每個欄位都是一個陣列,並且行與行之間陣列的長度無須對齊。所以需要把剛才的insert語句調整成下面的形式:

insert into nest_test values('張三',18,[1,2,3],['語文老師','數學老師','英語老師'])

需要注意的是,在同一行資料內每個陣列欄位的長度必須相等。例如,在下面的示例中,由於行內陣列欄位的長度沒有對齊,所以會丟擲異常:

insert into nest_test values('張三',18,[1,2,3],['語文老師','數學老師'])

YGY---一隻小碼農

在訪問巢狀型別的資料時需要使用點符號,例如:

select name,teacher.id,teacher.name from nest_test
nameteacher.idteacher.name
張三[1, 2, 3][‘語文老師’, ‘數學老師’, ‘英語老師’]

3、特殊型別

ClickHouse還有一類不同尋常的資料型別,我將它們定義為特殊型別。

3.1、Nullable

準確來說,Nullable並不能算是一種獨立的資料型別,它更像是一種輔助的修飾符,需要與基礎資料型別一起搭配使用。Nullable型別與Java8的Optional物件有些相似,它表示某個基礎資料型別可以是Null值。其具體用法如下所示:

create table Null_test (
	t1 String,
	t2 Nullable(UInt8)
) ENGINE = TinyLog

通過Nullable修飾後c2欄位可以被寫入Null值:

insert into Null_test values ('張三',null)
insert into Null_test values('李四',18)
select t1,t2,toTypeName(t2) from Null_test
t1t2t3
張三NULLNullable(UInt8)
李四18Nullable(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.baidu.com','192.168.1.1');
select url,ip,toTypeName(ip) from IP4_test;
urliptoTypeName(ip)
www.baidu.com192.168.1.1IPv4

細心的人可能會問,直接用字串不就行了嗎?為何多此一舉呢?我想至少有如下兩個原因。
(1)出於便捷性的考量,例如IPv4型別支援格式檢查,格式錯誤的IP資料是無法被寫入的,例如

insert into IP4_test values ('www.baidu.com','192.168.1')

YGY---一隻小碼農

(2)出於效能的考量,同樣以IPv4為例,IPv4使用UInt32儲存,相比String更加緊湊,佔用的空間更小,查詢效能更快。IPv6型別是基於FixedString(16)封裝的,它的使用方法與IPv4別無二致,此處不再多說。
在使用Domain型別的時候還有一點需要注意的是,雖然它從表象上看起來與String一樣,但Domain並不是字串,所以它不支援隱式的自動型別轉換。如果需要返回IP的字串形式,則需要顯示呼叫IPv4NumToString或IPv6NumToString函式進行轉換。

相關文章