二、ClickHouse 資料型別
學習筆記(二)ClickHouse的資料型別
作為一款分析型資料庫,ClickHouse提供了許多資料型別,他們可以劃分三類: 基礎型別、 複合型別和 特殊型別。其中基礎型別使ClickHouse具備了描述資料的基本能力,而另外兩種型別則使ClickHouse的資料表達能力更加豐富立體。
1、基礎型別
基礎型別只有數值、字串和時間這三種型別。值得一提的是,與大多數普通的資料庫不一樣的是,ClickHouse沒有Boolean型別,但是呢,可以使用整型的0或1替代。
1.1、數值型別
數值型別分為整數、浮點數和定點數三類,接下來分別進行說明。
1.1.1、Int
在普通的觀念中,常用Tinyint、Smallint、Int和Bigint指代整數的不同取值範圍。而ClickHouse則是直接使用Int8、Int16、Int32和Int64這4種大小的Int型別,其末尾的數字正好表明了佔用位元組的大小(8位 = 1位元組)。詳細資訊如下表所示:
名 稱 | 大小(位元組) | 範圍 | 普遍概念 |
---|---|---|---|
Int8 | 1 | -128 ~ 127 | Tinyint |
Int16 | 2 | -32768 ~ 32767 | Smallint |
Int32 | 4 | -2147483648 ~ 2147483647 | Int |
Int64 | 8 | -9223372036854775808~9223372036854775807 | BigInt |
同樣的,ClickHouse也支援無符號的整數,使用字首U表示,具體資訊如下表所示:
名 稱 | 大小(位元組) | 範圍 | 普遍概念 |
---|---|---|---|
Int8 | 1 | 0 ~ 255 | Tinyint Unsigned |
Int16 | 2 | 0 ~ 65535 | Smallint Unsigned |
Int32 | 4 | 0 ~ 4294967295 | Int Unsigned |
Int64 | 8 | 0 ~ 18446744073709551615 | BigInt Unsigned |
1.1.2、Float
與整數類似,ClickHouse直接使用Float32和Float64代表單精度浮點數以及雙精度浮點數,具體資訊如下表所示:
名稱 | 大小(位元組) | 有效精度(位數) | 普遍概念 |
---|---|---|---|
Float32 | 4 | 7 | Float |
Float | 8 | 16 | Double |
在使用浮點數的時候,應當要意識到它是有限精度的。什麼意思呢?比如,我分別對Float32和Float64寫入超過有效精度的數值,下面讓我們看看會發生什麼事情。
例子:將擁有20位小數的數值分別寫入Float32和Float64,觀看他們會發生什麼?
select toFloat32('0.1234567890123456789') as result,toTypeName(result)
result | toTypeName(toFloat32(‘0.1234567890123456789’)) |
---|---|
0.12345679 | Float32 |
select toFloat64('0.1234567890123456789') as result,toTypeName(result)
result | toTypeName(toFloat32(‘0.1234567890123456789’)) |
---|---|
0.12345678901234568 | Float64 |
我們可以發現,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
簡寫方式和原生方式的對應方式如下表所示:
名稱 | 等效宣告 | 範圍 |
---|---|---|
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.0000 | 2.00 | 4.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.0000 | 2.00 | 2.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.0000 | 2.00 | 4.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.0000 | 2.00 | 1.0000 |
需要注意的是,如果被除數的S小於於除數的S,則會報錯!
select toDecimal32(2,2),toDecimal64(2,4),toDecimal32(2,2)/toDecimal64(2,4)
最後可以總結得出:對於不同精度的定點數之間的四則運算,其精度S的變化會遵循下表所示的規則。
名稱 | 規則 |
---|---|
加法 | 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 |
---|---|
abc | 5 |
當服務端讀取長度大於 N 的字串時候,將返回錯誤訊息!
select toFixedString('abcdefg',5)
與 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
c1 | c2 |
---|---|
f378ea5b-5195-4987-a97f-2e5d5093f925 | t1 |
00000000-0000-0000-0000-000000000000 | t2 |
我們可以看到,第二行沒有被賦值的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
t1 | toTypeName(t1) |
---|---|
2020-12-21 00:00:00 | DateTime |
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
t1 | toTypeName(t1) |
---|---|
2020-12-21 00:00:00.00 | DateTime64(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
t1 | toTypeName(t1) |
---|---|
2020-12-21 | Date |
2、複合型別
除了基礎資料型別之外,ClickHouse還提供了陣列、元組、列舉和巢狀四類複合型別。這些型別通常是其他資料庫原生不具備的特性。擁有了複合型別之後,ClickHouse的資料模型表達能力更強了。
2.1、Array
陣列有兩種定義形式,常規方式array(T)。T 可以是任意型別,包含陣列型別。 但不推薦使用多維陣列,ClickHouse 對多維陣列的支援有限。例如,不能在 MergeTree 表中儲存多維陣列。:
select array(1,2) as a,toTypeName(a)
a | toTypeName(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)
a | toTypeName([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)
t | toTypeName(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)
t | toTypeName(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');
從表中查詢資料時,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,'語文老師')
注意上面的異常資訊,它提示期望寫入的是一個Array陣列型別。
現在大家應該明白了,巢狀型別本質是一種多維陣列的結構。巢狀表中的每個欄位都是一個陣列,並且行與行之間陣列的長度無須對齊。所以需要把剛才的insert語句調整成下面的形式:
insert into nest_test values('張三',18,[1,2,3],['語文老師','數學老師','英語老師'])
需要注意的是,在同一行資料內每個陣列欄位的長度必須相等。例如,在下面的示例中,由於行內陣列欄位的長度沒有對齊,所以會丟擲異常:
insert into nest_test values('張三',18,[1,2,3],['語文老師','數學老師'])
在訪問巢狀型別的資料時需要使用點符號,例如:
select name,teacher.id,teacher.name from nest_test
name | teacher.id | teacher.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
t1 | t2 | t3 |
---|---|---|
張三 | NULL | Nullable(UInt8) |
李四 | 18 | Nullable(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;
url | ip | toTypeName(ip) |
---|---|---|
www.baidu.com | 192.168.1.1 | IPv4 |
細心的人可能會問,直接用字串不就行了嗎?為何多此一舉呢?我想至少有如下兩個原因。
(1)出於便捷性的考量,例如IPv4型別支援格式檢查,格式錯誤的IP資料是無法被寫入的,例如
insert into IP4_test values ('www.baidu.com','192.168.1')
(2)出於效能的考量,同樣以IPv4為例,IPv4使用UInt32儲存,相比String更加緊湊,佔用的空間更小,查詢效能更快。IPv6型別是基於FixedString(16)封裝的,它的使用方法與IPv4別無二致,此處不再多說。
在使用Domain型別的時候還有一點需要注意的是,雖然它從表象上看起來與String一樣,但Domain並不是字串,所以它不支援隱式的自動型別轉換。如果需要返回IP的字串形式,則需要顯示呼叫IPv4NumToString或IPv6NumToString函式進行轉換。
相關文章
- clickhouse資料型別簡介資料型別
- ClickHouse資料庫資料定義手記之資料型別資料庫資料型別
- 【clickhouse專欄】基礎資料型別說明資料型別
- SSIS 資料型別 第二篇:變數的資料型別資料型別變數
- 二、變數與資料型別變數資料型別
- python基礎(二)—-資料型別Python資料型別
- 第二章:資料型別(續)資料型別
- 關於 PHP 的資料型別 (二)PHP資料型別
- js資料型別之基本資料型別和引用資料型別JS資料型別
- 資料型別: 資料型別有哪些?資料型別
- JavaScript 基礎 (二) - 引用資料型別 (物件)JavaScript資料型別物件
- 從規範看ECMAScript(二):資料型別資料型別
- 基本資料型別及其包裝類(二)資料型別
- Redis入門教程(二)— 基本資料型別Redis資料型別
- 第二週學習--基本資料型別資料型別
- 區別值型別資料和引用型別資料型別
- 資料型別,型別轉換資料型別
- Redis學習筆記(二)——Redis資料型別Redis筆記資料型別
- java之二進位制與資料型別Java資料型別
- 自學PHP筆記(二)PHP資料型別PHP筆記資料型別
- js學習 第二篇資料型別JS資料型別
- 資料型別資料型別
- JAVA中基本資料型別和引用資料型別Java資料型別
- 3. php資料型別、資料型別轉換PHP資料型別
- 基本資料型別與字串型別資料型別字串
- 二進位制安全_C語言中資料型別C語言資料型別
- 二、python的邏輯運算與資料型別Python資料型別
- MySQL基礎之----資料型別篇(常用資料型別)MySql資料型別
- Java中的基本資料型別與引用資料型別Java資料型別
- Clickhouse Engine kafka 將kafka資料同步clickhouseKafka
- JavaScript - 資料型別JavaScript資料型別
- Symbol資料型別Symbol資料型別
- 資料型別2資料型別
- JavaScript 資料型別JavaScript資料型別
- js資料型別JS資料型別
- TypeScript資料型別TypeScript資料型別
- Oracle 資料型別Oracle資料型別
- SQL 資料型別SQL資料型別