楔子
作為一款分析型數據庫,ClickHouse 提供了許多數據類型,它們可以划分為基礎類型、復合類型和特殊類型。其中基礎類型使 ClickHouse 具備了描述數據的基本能力,而另外兩種類型則使 ClickHouse 的數據表達能力更加的豐富立體。
下面就來分門別類的介紹一下。
基礎類型
基礎類型只有數值、字符串和時間三種類型,注:准確來說,還有布爾類型(Bool),但由於沒有 true、false,所以一般都用整型(UInt8)表示布爾類型,1 為真 0 為假。
數值類型
數值類型分為整數、浮點數和 Decimal 三類,接下來分別進行說明。
1)Int
在普遍觀念中,常用 Tinyint、Smallint、Int 和 Bigint 指代整數的不同取值范圍,而 ClickHouse 則直接使用 Int8、Int16、Int32、Int64 來指代 4 種大小的 Int 類型,其末尾的數字則表示該類型的整數占多少位。可以認為:Int8 等價於 Tinyint、Int16 等價於 Smallint、Int32 等價於 Int、Int64 等價於 Bigint。
ClickHouse 也支持無符號的整數,使用前綴 U 表示,比如:UInt8、UInt16、UInt32、UInt64。
2)Float
與整數類似,ClickHouse 直接使用 Float32 和 Float64 代表單精度浮點數和雙精度浮點數,可以看成是 float 和 double。
ClickHouse 的浮點數支持正無窮、負無窮以及非數字的表達方式。
satori :) select 1 / 0, -1 / 0, 0 / 0
SELECT
1 / 0,
-1 / 0,
0 / 0
Query id: e3b3712c-0506-4b3b-b2d8-7f936c548740
┌─divide(1, 0)─┬─divide(-1, 0)─┬─divide(0, 0)─┐
│ inf │ -inf │ nan │
└──────────────┴───────────────┴──────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
3)Decimal
如果要求高精度的數值運算,則需要使用Decimal、即定點數(類似於浮點數),ClickHouse 提供了 Decimal32、Decimal64 和 Decimal128 三種精度的Decimal。在定義表字段的類型時,可以通過兩種形式聲明:簡寫方式有 Decimal32(S)、Decimal64(S)、Decimal128(S) 三種,原生方式為 Decimal(P, S),表示該定點數的整數位加上小數位的總長度最大為 P,其中小數位長度最多為 S。Decimal32 的 P 為 10、Decimal64 的 P 為 19、Decimal128 的 P 為 39。比如某個字段類型是 Decimal32(3),那么表示該字段存儲的定點數,其整數位加上小數位的總長度不超過 10,其中小數部分如果超過 3 位則只保留 3 位。
而在 SQL 中我們可以通過 toDecimal32 或 toDecimal64 將一個整數或浮點數變成定點數,比如:toDecimal32(2, 5) 得到的結果就是 2.00000。另外使用兩個不同精度的 Decimal 進行四則遠算的時候,它們的小數點位數會 S 發生變化。在進行加法和減法運算時,S 取最大值。
satori :) select toDecimal32(22, 3) + toDecimal32(33, 2)
SELECT toDecimal32(22, 3) + toDecimal32(33, 2)
Query id: a223565d-e6ba-4db9-aa7d-1cae37424128
┌─plus(toDecimal32(22, 3), toDecimal32(33, 2))─┐
│ 55.000 │
└──────────────────────────────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
在進行乘法運算時,S 取兩者之和:
satori :) select toDecimal32(22, 3) * toDecimal32(33, 2)
SELECT toDecimal32(22, 3) * toDecimal32(33, 2)
Query id: 0f1bd695-9f37-43b4-a6d1-b2a62a1dd6c3
┌─multiply(toDecimal32(22, 3), toDecimal32(33, 2))─┐
│ 726.00000 │
└──────────────────────────────────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
在進行除法運算時,S 取被除數的值,此時要求被除數的 S 必須大於除數 S,否則報錯。
satori :) select toDecimal64(6, 3) / toDecimal64(3, 2)
SELECT toDecimal64(6, 3) / toDecimal64(3, 2)
Query id: 55b4a58f-b722-4487-8391-2c08e6c764ca
┌─divide(toDecimal64(6, 3), toDecimal64(3, 2))─┐
│ 2.000 │
└──────────────────────────────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :) select toDecimal64(6, 3) / toDecimal64(3, 4) -- 這里會報錯,因為被除數的 S 小於除數的 S
SELECT toDecimal64(6, 3) / toDecimal64(3, 4)
Query id: 6f782411-0d15-4ab8-adab-be19c9f6b0e2
0 rows in set. Elapsed: 0.003 sec.
Received exception from server (version 21.7.3):
Code: 69. DB::Exception: Received from localhost:9000. DB::Exception:
Decimal result's scale is less than argument's one: While processing toDecimal64(6, 3) / toDecimal64(3, 4).
satori :)
另外還有一點需要注意:由於現代計算機系統只支持 32 或者 64 位,所以 Decimal128 是在軟件層面模擬出來的,它的速度會比 Decimal32、Decimal64 要慢。
字符串類型
字符串類型可以細分為 String、FixedString 和 UUID 三類,從命名來看仿佛不像是一款數據庫提供的類型,反倒像一門編程語言的設計。
1)String
字符串由 String 定義,長度不限,因為在使用 String 的時候無需聲明大小。它完全代替了傳統意義上的 Varchar、Text、Clob 和 Blob 等字符類型。String 類型不限定字符集,因為它根本沒有這個概念,所以可以將任意編碼的字符串存入其中。但是為了程序的規范性和可維護性,在同一套程序中使用統一的編碼,比如 utf-8,就是一種很好的約定。
2)FiexedString
FixedString 類型和傳統意義上的 Char 類型有些類似,對於一些有着明確長度的場合,可以使用 FixedString(N) 來聲明固定長度的字符串。但與 char 不同的是,FixedString 使用 NULL 字節來填充末尾字符,而 char 通常使用空格填充。
可以使用 toFixedString 生成 FixedString。
satori :) select toFixedString('satori', 7), length(toFixedString('satori', 7))
SELECT
toFixedString('satori', 7),
length(toFixedString('satori', 7))
Query id: 45fe6e26-0540-40de-9eaf-aeefd12a3a1b
┌─toFixedString('satori', 7)─┬─length(toFixedString('satori', 7))─┐
│ satori │ 7 │
└────────────────────────────┴────────────────────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
3)UUID
UUID 是一種數據庫常見的主鍵類型,在 ClickHouse 中直接把它作為一種數據類型。UUID 共有 32 位,它的格式為 8-4-4-4-12。如果一個 UUID 類型的字段在寫入數據的時候沒有被賦值,那么它會按照相應格式用 0 填充。
時間類型
時間類型分為 DateTime、DateTime64 和 Date三類。ClickHouse 目前沒有設置時間戳類型,時間類型最高的精度是秒,也就是說如果要處理毫秒、微妙等大於秒分辨率的時間,則只能借助UInt實現。
1)DateTime
DateTime 類型包含年、月、日、時、分、秒信息,精確到秒,支持使用字符串的方式寫入;
2)DateTime64
DateTime64 可以記錄亞秒,它在 DateTime 之上增加了精度的設置。舉個栗子:DateTime64 類型的時間可以是 2018-01-01 12:12:32.22;但如果是 DateTime 的話,則是2018-01-01 12:12:32,也就是說最后的 .22 沒了。
3)Date
Date 類型不包含具體的時間信息,只精確到天,並且和 DateTime、DateTime64 一樣,支持字符串寫入。
復合類型
除了基礎數據類型之外,ClickHouse 還提供了數組、元組、枚舉和嵌套,總共四種復合類型。這些類型通常都是其他數據庫原生不具備的特性,擁有了復合類型之后,ClickHouse 的數據模型表達能力就更強了。
Array
數據有兩種定義形式,常規方式 Array(T),比如某個字段是包含 UInt8 的數組,那么就可以聲明為 Array(UInt8);需要說明的是,ClickHouse 中的類型是區分大小寫的,比如這里的 Array 就不可以寫成 array,UInt8 不可以寫成 uint8。
當然在查詢的時候,我們可以通過 array 函數創建一個數組。注意:ClickHouse 中的絕大部分函數也是區分大小寫的,只要是你在其它關系型數據庫中沒有見過的函數,基本上都區分大小寫。
satori :) select array(1, 2) as a, toTypeName(a) -- toTypeName 表示獲取字段的類型
SELECT
[1, 2] AS a,
toTypeName(a)
Query id: b22e371f-d5fc-4245-b299-a79a344fc7ea
┌─a─────┬─toTypeName(array(1, 2))─┐
│ [1,2] │ Array(UInt8) │
└───────┴─────────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
這里只是演示,關於具體的語法后面說,
在查詢的時候簡寫成 [v1, v2, v3, ...] 也是可以的;
satori :) select [1, 2] as a, toTypeName(a)
SELECT
[1, 2] AS a,
toTypeName(a)
Query id: 50d4e367-cd1b-4826-bca8-eb913e6fbaeb
┌─a─────┬─toTypeName([1, 2])─┐
│ [1,2] │ Array(UInt8) │
└───────┴────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
從上述的例子中可以發現,在查詢時並不需要主動聲明數據的元素類型,因為 ClickHouse 的數組擁有類型推斷的能力,推斷的依據是:以最小存儲代價為原則,即使用最小可表達的數據類型。比如:array(1, 2)
會使用 UInt8 作為數組類型。但如果數組中存在 NULL 值,元素類型將變為 Nullable。
satori :) select [1, 2, NULL] as a, toTypeName(a)
SELECT
[1, 2, NULL] AS a,
toTypeName(a)
Query id: 95eb5bfa-5008-4bd0-ac94-bd2e39de6485
┌─a──────────┬─toTypeName([1, 2, NULL])─┐
│ [1,2,NULL] │ Array(Nullable(UInt8)) │
└────────────┴──────────────────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
數組里面的元素可以有多種,但前提是它們必須能夠兼容,比如:[1, 2.13]
可以,但是 [1, 'ABC']
則不行。而在定義表字段的時候,如果使用 Array 類型,則需要指定明確的元素類型,比如:
CREATE TABLE table_name (
arr Array(String) --指定明確類型
) ENGINE = Memory
Tuple
元組由 1 ~ n 個元素組成,每個元素之間允許設置不同的數據類型,且彼此之間不要求兼容。元組同樣支持類型推斷,其推斷依據仍然是以最小存儲代價為原則。與數組類似,在 SQL 中我們可以通過 Tuple(T) 來定義。
類似數組,我們可以使用 tuple 函數在查詢的時候創建元組:
satori :) SELECT tuple(1, 'a', now()) as a, (1, 3, '666') as b
SELECT
(1, 'a', now()) AS a,
(1, 3, '666') AS b
Query id: ab97ea39-accd-4eaa-85b0-c1728fde57e4
┌─a─────────────────────────────┬─b───────────┐
│ (1,'a','2021-08-05 01:10:07') │ (1,3,'666') │
└───────────────────────────────┴─────────────┘
1 rows in set. Elapsed: 0.002 sec.
satori :)
關於數組和元組的區別,熟悉 Python 的話應該很清楚,答案是元組不可變。在定義表字段時,元組也需要指定明確的元素類型。
CREATE TABLE table_name (
-- 可以指定多個類型
-- 但是注意:Tuple(String, Int8) 表示 tpl 字段的值只能是含有兩個元素的元組
-- 並且第一個元素為 String,第二個元素為 Int8
tpl Tuple(String, Int8)
) ENGINE = Memory
而在數據寫入的過程中會進行類型檢查。例如,寫入 ('abc', 123)
是可行的,但是 ('abc', 'def')
則報錯。
Enum
ClickHouse 支持枚舉類型,這是一種在定義常量時經常會使用的數據類型。ClickHouse 提供了 Enum8 和 Enum16 兩種枚舉類型,它們之間除了取值范圍不同之外,別無二致。枚舉固定使用 (String:Int)
鍵值對的形式定義數據,所以 Enum8 和 Enum16 分別會對應 (String:Int8) 和 (String:Int16),例如:
CREATE TABLE table_name(
e Enum('ready'=1, 'start'=2, 'success'=3, 'error'=4)
) ENGINE = Memory
在定義枚舉集合的時候,有幾點需要注意。首先,Key 和 Value 是不允許重復的,要保證唯一性。其次,Key 和 Value 的值都不能為 Null,但 Key 允許為空字符串。在寫入枚舉數據的時候,只會用到 Key 字符串部分,例如:
INSERT INTO table_name VALUES('ready')
另外在數據寫入的時候,會對照枚舉集合項的內容進行逐一檢查,如果 Key 字符串不存在集合范圍內則會拋出異常,比如執行下面的語句就會報錯:
INSERT INTO table_name VALUES('abc') -- 會報錯
可能有人覺得,完全可以使用 String 代替枚舉,為什么還需要專門實現枚舉類型呢?答案是出於對性能的考慮。因為雖然枚舉中定義的 Key 是屬於 String 類型,但是在后續對枚舉的所有操作中(包括排序、分子、去重、過濾等),會使用 Int 類型的 Value 值。
Nested
嵌套類型,顧名思義是一種嵌套表結構。一張數據表,可以定義任意多個嵌套類型字段,但每個字段的嵌套層級只支持一級,即嵌套表內不能繼續使用嵌套類型。對於簡單場景的層級關系或關聯關系,使用嵌套類型也是一種不錯的選擇。例如,我們下面創建一張表 nested_test,具體的建表邏輯后面會說,當然本身也不是特別難的東西。
CREATE TABLE nested_test (
name String,
age UInt8,
dept Nested(
id UInt32,
name String
)
) ENGINE = Memory;
ClickHouse 的嵌套類型和傳統的嵌套類型不相同,導致在初次接觸它的時候會讓人十分困惑。以上面這張表為例,如果按照它的字面意思來理解,會很容易理解成 nested_test 與 dept 是一對一的包含關系,其實這是錯誤的。不信可以執行下面的語句,看看會是什么結果:
我們看到報錯了,現在大家應該明白了,嵌套類型本質是一種多維數組的結構。嵌套表中的每個字段都是一個數組,並且行與行之間數組的長度無須對齊。所以需要把剛才的 INSERT 語句調整成下面的形式:
INSERT INTO nested_test VALUES('nana', 20, [10000, 10001, 10002], ['唐辛子', 'ななかぐら', 'ゴウマ']);
-- 行與行之間,數組長度無需對齊。
INSERT INTO nested_test VALUES('nana', 20, [10000, 10001], ['唐辛子', 'ななかぐら']);
需要注意的是,在同一行數據內每個數組字段的長度必須相等。例如,在下面的示例中,由於行內數組字段的長度沒有對齊,所以會拋出異常:
提示我們長度不一樣。
在訪問嵌套類型的數據時需要使用點符號,例如:
特殊類型
ClickHouse 還有一類不同尋常的數據類型,將它們定義為特殊類型。
Nullable
准確來說,Nullable 並不能算是一種獨立的數據類型,它更像是一種輔助的修飾符,需要與基礎數據類型一起搭配使用。Nullable 類型與 Python 類型注解里面的 Optional 有些相似,它表示某個基礎數據類型可以是 NULL 值。其具體用法如下所示:
CREATE TABLE null_test (
col1 String,
col2 Nullable(UInt8)
) ENGINE = Memory
通過 Nullable 修飾后 col2 字段可以被寫入 NULL 值:
INSERT INTO null_test VALUES ('nana', NULL);
在使用 Nullable 類型的時候還有兩點值得注意:首先,它只能和基礎類型搭配使用,不能用於數組和元組這些復合類型,也不能作為索引字段;其次,應該慎用 Nullable 類型,包括 Nullable 的數據表,不然會使查詢和寫入性能變慢。因為在正常情況下,每個列字段的數據會被存儲在對應的 [Column].bin 文件中。如果一個列字段被 Nullable 類型修飾后,會額外生成一個 [Column].null.bin 文件專門保存它的 NULL 值。這意味着在讀取和寫入數據時,需要一倍的額外文件操作。
Domain
域名類型分為 IPv4 和 IPv6 兩類,本質上它們是對整型和字符串的進一步封裝。IPv4 類型是基於 UInt32 封裝的,它的具體用法如下所示:
CREATE TABLE ip4_test (
url String,
ip IPv4
) ENGINE = Memory;
INSERT INTO ip4_test VALUES ('www.nana.com', '127.0.0.1');
細心的人可能會問,直接使用字符串不就行了嗎?為何多此一舉呢?至少有如下兩個原因:
1)出於便捷性的考量,例如IPv4類型支持格式檢查,格式錯誤的IP數據是無法被寫入的,例如:
INSERT INTO ip4_test VALUES('www,nana.com', '192.0.0')
Exception on client:
Code: 441. DB::Exception: Invalid IPv4 value.: data for INSERT was parsed from query
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 21.7.3 revision 54449.
2)出於性能的考量,同樣以 IPv4 為例,IPv4 使用 UInt32 存儲,相比 String 更加緊湊,占用的空間更小,查詢性能更快。IPv6 類型是基於 FixedString(16) 封裝的,它的使用方法與 IPv4 別無二致,此處不再贅述。
在使用 Domain 類型的時候還有一點需要注意,雖然它從表象上看起來與 String 一樣,但 Domain 類型並不是字符串,所以它不支持隱式的自動類型轉換。如果需要返回 IP 的字符串形式,則需要顯式調用 IPv4NumToString 或 IPv6NumToString 函數進行轉換。