楔子
在一般的關系型數據庫,相信很多人都不怎么使用數組這個結構,如果真的需要數組,那么會選擇將其變成數組格式的字符串進行存儲。但在 ClickHouse 中,數組的使用頻率是非常高的,因為它內置了大量和數組有關的函數。
SELECT version();
/*
┌─version()─┐
│ 21.7.3.14 │
└───────────┘
*/
SELECT count() FROM system.functions WHERE name LIKE '%array%';
/*
┌─count()─┐
│ 48 │
└─────────┘
*/
當前的 ClickHouse 是 21.7.3.14 版本,關於數組的函數有 48 個,通過這個 48 個函數,我們可以對數組進行各種騷操作。當然也有一些函數不是專門針對數組的,但是可以用在數組身上,我們就也放在一起說了,下面就來依次介紹相關函數的用法。
empty:判斷數組是否為空,如果一個數組不包含任何元素,返回 1;否則返回 0
SELECT empty([1, 2, 3]), empty([]);
/*
┌─empty([1, 2, 3])─┬─empty(array())─┐
│ 0 │ 1 │
└──────────────────┴────────────────┘
*/
empty 不僅可以檢測數組是否為空,還可以檢測字符串。
SELECT empty('satori'), empty('');
/*
┌─empty('satori')─┬─empty('')─┐
│ 0 │ 1 │
└─────────────────┴───────────┘
*/
notEmpty:判斷數組是否不為空,如果一個數組包含至少一個元素,返回 1;不包含任何元素,則返回 0
SELECT notEmpty([1, 2, 3]), notEmpty([]);
/*
┌─notEmpty([1, 2, 3])─┬─notEmpty(array())─┐
│ 1 │ 0 │
└─────────────────────┴───────────────────┘
*/
-- 同樣可以作用於字符串
SELECT notEmpty('satori'), notEmpty('')
/*
┌─notEmpty('satori')─┬─notEmpty('')─┐
│ 1 │ 0 │
└────────────────────┴──────────────┘
*/
length:返回數組的長度,該函數也可以返回字符串的長度
SELECT length([]), length([1, 2, 3]), length('satori'), length('');
/*
┌─length(array())─┬─length([1, 2, 3])─┬─length('satori')─┬─length('')─┐
│ 0 │ 3 │ 6 │ 0 │
└─────────────────┴───────────────────┴──────────────────┴────────────┘
*/
emptyArrayUInt8、emptyArrayUInt16、emptyArrayUInt32、emptyArrayUInt64、emptyArrayInt8、emptyArrayInt16、emptyArrayInt32、emptyArrayInt64、emptyArrayFloat32、emptyArrayFloat64、emptyArrayDate、emptyArrayDateTime、emptyArrayString:創建一個指定類型的空數組
-- 數組元素的類型為 nothing,因為沒有指定任何元素
SELECT [] v, toTypeName(v);
/*
┌─v──┬─toTypeName(array())─┐
│ [] │ Array(Nothing) │
└────┴─────────────────────┘
*/
-- 采用最小類型存儲,因為 1 和 2 都在 UInt8 的范圍內
SELECT [1, 2] v, toTypeName(v);
/*
┌─v─────┬─toTypeName([1, 2])─┐
│ [1,2] │ Array(UInt8) │
└───────┴────────────────────┘
*/
-- 但是我們可以創建指定類型的數組
SELECT emptyArrayDateTime() v, toTypeName(v);
/*
┌─v──┬─toTypeName(emptyArrayDateTime())─┐
│ [] │ Array(DateTime) │
└────┴──────────────────────────────────┘
*/
range:類似於 Python 中的 range,看測試用例
array:也是創建一個數組,和直接使用方括號類似。但是 array 函數要求必須至少傳遞一個常量,否則就不知道要創建哪種類型的數組。如果想創建指定類型的空數組,那么使用上面的 emptyArray* 系列函數即可
-- 不管是使用 array 創建,還是使用 [] 創建,里面的元素都必須具有相同的類型,或者能夠兼容
SELECT array(1, 2, 3), [1, 2, 3]
/*
┌─array(1, 2, 3)─┬─[1, 2, 3]─┐
│ [1,2,3] │ [1,2,3] │
└────────────────┴───────────┘
*/
arrayConat:將多個數組進行合並,得到一個新的數組
-- SELECT 中起的別名可以被直接其它字段所使用
SELECT [1, 2, 3] v1, [11, 22, 33] v2, [111, 222, 333] v3, arrayConcat(v1, v2, v3);
/*
┌─v1────┬─v2──────┬─v3────────┬─arrayConcat([1, 2], [11, 22], [111, 222])─┐
│ [1,2] │ [11,22] │ [111,222] │ [1,2,11,22,111,222] │
└───────┴─────────┴───────────┴───────────────────────────────────────────┘
*/
arrayElement:查找指定索引的元素,索引從 1 開始,也可以通過方括號直接取值;另外也支持負數索引,-1 代表最后一個元素
-- 索引從 1 開始,所以 arr[20] 就表示第 20 個元素,也就是 19
WITH range(100) AS arr SELECT arrayElement(arr, 20), arr[20];
/*
┌─arrayElement(arr, 20)─┬─arrayElement(arr, 20)─┐
│ 19 │ 19 │
└───────────────────────┴───────────────────────┘
*/
WITH range(100) AS arr SELECT arrayElement(arr, -1), arr[-50];
/*
┌─arrayElement(arr, -1)─┬─arrayElement(arr, -50)─┐
│ 99 │ 50 │
└───────────────────────┴────────────────────────┘
*/
has:判斷數組里面是否包含某個元素,如果包含,返回 1;不包含,返回0
WITH [1, 2, Null] AS arr SELECT has(arr, 2), has(arr, 0), has(arr, Null);
/*
┌─has(arr, 2)─┬─has(arr, 0)─┬─has(arr, NULL)─┐
│ 1 │ 0 │ 1 │
└─────────────┴─────────────┴────────────────┘
*/
-- 嵌套數組也是可以的
SELECT has([[1, 2]], [1, 2]);
/*
┌─has([[1, 2]], [1, 2])─┐
│ 1 │
└───────────────────────┘
*/
hasAll:判斷數組里面是否包含某個子數組,如果包含,返回 1;不包含,返回0
注意:空數組是任意數組的子集;Null 會被看成是普通的值;數組中的元素順序沒有要求;1.0 和 1 被視為相等
hasAll([], []):返回 1
hasAll([1, Null], [Null]):返回 1
hasAll([1.0, 2.0, 3.0], [2.0, 3.0, 1.0]):返回 1,因為元素順序無影響,並且 1.0 和 1 被視為相等
hasAll(['a', 'b'], ['a']):返回 1
hasAll(['a', 'b'], ['c']):返回 0
hasAll([[1, 2], [3, 4]], [[1, 2], [3, 4]]):返回 1,嵌套數組也是可以的
在 has 函數里面也有嵌套數組,但是維度不同。比如 has(a, b):如果 a 是維度為 N 的數組,那么 b 必須是維度為 N - 1 的數組;而 hasAll 則要求 a 和 b 的維度必須相同。
WITH [[1, 2], [11, 22]] AS arr, [[1, 2], [11, 22]] AS subset SELECT hasAll(arr, subset)
/*
┌─hasAll(arr, subset)─┐
│ 1 │
└─────────────────────┘
*/
-- 我們說 SELECT 里面別名可以給其它字段使用,因此下面這種做法也是合法的
WITH [[1, 2], [11, 22]] AS arr, arr AS subset SELECT hasAll(arr, subset)
/*
┌─hasAll(arr, subset)─┐
│ 1 │
└─────────────────────┘
*/
hasAny:判斷兩個數組里面是否有相同的元素,只要有 1 個相同的元素,返回 1;否則,返回 0
SELECT hasAny([1.0, 2.0], [1]), hasAny([Null], [1, Null])
/*
┌─hasAny([1., 2.], [1])─┬─hasAny([NULL], [1, NULL])─┐
│ 1 │ 1 │
└───────────────────────┴───────────────────────────┘
*/
SELECT hasAny([[1, 2], [3, 4]], [[3, 4]])
/*
┌─hasAny([[1, 2], [3, 4]], [[3, 4]])─┐
│ 1 │
└────────────────────────────────────┘
*/
hasSubstr:和 hasAll 類似,但是順序有要求,hasAll(arr, subset) 要求的是 subset 中的元素在 arr 中都出現即可;但是 hasSubstr 函數則不僅要求 subset 中的元素在 arr 中都出現,並且還要以相同的順序。舉個栗子:
hasSubstr([1, 2, 3], [2, 3]):返回 1
hasSubstr([1, 2, 3], [3, 2]):返回 0
hasSubstr([[1, 2], [2, 1], [3, 2]], [[3, 2]]):返回 1
-- 兩個數組的維度必須相同
SELECT hasSubstr([1, 2, 3], [3, 2]), hasSubstr([1, 2, 3], [2, 3]);
/*
┌─hasSubstr([1, 2, 3], [3, 2])─┬─hasSubstr([1, 2, 3], [2, 3])─┐
│ 0 │ 1 │
└──────────────────────────────┴──────────────────────────────┘
*/
indexOf:查找某個元素第一次在數組中出現的位置,索引從 1 開始;如果不存在,則返回 0
WITH [1, 2, 3, Null, 99] AS arr SELECT indexOf(arr, 100), indexOf(arr, 99), indexOf(arr, Null);
/*
┌─indexOf(arr, 100)─┬─indexOf(arr, 99)─┬─indexOf(arr, NULL)─┐
│ 0 │ 5 │ 4 │
└───────────────────┴──────────────────┴────────────────────┘
*/
arrayCount:查找一個數組中非 0 元素的個數,該數組類的元素類型必須是 UInt8,並且不能包含 Null 值。因為一旦包含 Null,那么類型就不是 UInt8 了,而是 Nullable(UInt8)
SELECT arrayCount([1, 2, 3]), arrayCount([1, 2, 3, 4, 0]);
/*
┌─arrayCount([1, 2, 3])─┬─arrayCount([1, 2, 3, 4, 0])─┐
│ 3 │ 4 │
└───────────────────────┴─────────────────────────────┘
*/
此外 arrayCount 還有一種用法,就是接收一個函數和一個數組:
WITH [1, 2, 3, 4, 0] AS arr
SELECT arrayCount(arr),
arrayCount(x -> cast(x + 1 AS UInt8), arr)
/*
┌─arrayCount(arr)─┬─arrayCount(lambda(tuple(x), CAST(plus(x, 1), 'UInt8')), arr)─┐
│ 4 │ 5 │
└─────────────────┴──────────────────────────────────────────────────────────────┘
*
ClickHouse 中的函數類似於 C++ 中的 lambda 表達式,x -> x + 1 相當於將 arr 中的每一個元素都加上 1,但結果得到整型是 UInt16,所以需要使用 cast 轉成 UInt8,否則報錯。另外,加上 1 之后就沒有為 0 的元素了,所以返回的結果是 5。
countEqual:返回某個元素在數組中出現的次數
WITH [1, 1, 1, 2, Null, Null] as arr SELECT countEqual(arr, 1), countEqual(arr, Null)
/*
┌─countEqual(arr, 1)─┬─countEqual(arr, NULL)─┐
│ 3 │ 2 │
└────────────────────┴───────────────────────┘
*/
arrayEnumerate:等價於先計算出數組的長度,假設為 N,然后返回 range(1, N + 1)
SELECT arrayEnumerate([2, 2, 2, 2]);
/*
┌─arrayEnumerate([2, 2, 2, 2])─┐
│ [1,2,3,4] │
└──────────────────────────────┘
*/
arrayEnumerateUniq:從數組的第一個元素開始,每重復一次就加 1
光說不好理解,直接看例子,然后畫圖說明:
SELECT arrayEnumerateUniq(['a', 'a', 'c', 'b', 'c', 'a', 'b', 'b']);
/*
┌─arrayEnumerateUniq(['a', 'a', 'c', 'b', 'c', 'a', 'b', 'b'])─┐
│ [1,2,1,1,2,3,2,3] │
└──────────────────────────────────────────────────────────────┘
*/
arrayEnumerateUniq 還可以接收多個數組,這些數據具有相同的長度,相信你已經知道它的作用了:
SELECT arrayEnumerateUniq(['a', 'a', 'b', 'a'], [1, 2, 2, 1]);
/*
┌─arrayEnumerateUniq(['a', 'a', 'b', 'a'], [1, 2, 2, 1])─┐
│ [1,1,1,2] │
└────────────────────────────────────────────────────────┘
*/
-- 舉個不恰當的例子
-- 你就可以理解為:arrayEnumerateUniq( [('a', 1), ('a', 2), ('b', 2), ('a', 1)] )
-- 此時會將多個數組作為一個整體來進行判斷,因此這些數組都必須有相同的長度
arrayPopBack:移除數組中的最后一個元素
SELECT arrayPopBack([1, 2, 3])
/*
┌─arrayPopBack([1, 2, 3])─┐
│ [1,2] │
└─────────────────────────┘
*/
顯然它是可以被嵌套的:
WITH [1, 2, 3] AS arr SELECT arrayPopBack(arrayPopBack(arr))
/*
┌─arrayPopBack(arrayPopBack(arr))─┐
│ [1] │
└─────────────────────────────────┘
*/
注意:對空數組使用 arrayPopBack 不會報錯,得到的還是空數組。
arrayPopFront:移除數組中的第一個元素
SELECT arrayPopFront([1, 2, 3]);
/*
┌─arrayPopFront([1, 2, 3])─┐
│ [2,3] │
└──────────────────────────┘
*/
和 arrayPopBack 一樣,也可以被嵌套,並且對空數組使用也不會報錯,還是得到空數組。
WITH [1, 2, 3] AS arr SELECT arrayPopFront(arrayPopFront(arr));
/*
┌─arrayPopFront(arrayPopFront(arr))─┐
│ [3] │
└───────────────────────────────────┘
*/
arrayPushBack:從數組的尾部塞進一個元素
SELECT arrayPushBack([1, 2, 3], 1);
/*
┌─arrayPushBack([1, 2, 3], 1)─┐
│ [1,2,3,1] │
└─────────────────────────────┘
*/
添加的時候記得類型要匹配,如果添加了 Null,那么數組會變成 Nullable。
arrayPushFront:從數組的頭部塞進一個元素
SELECT arrayPushFront(['a', 'b', 'c'], 'd');
/*
┌─arrayPushFront(['a', 'b', 'c'], 'd')─┐
│ ['d','a','b','c'] │
└──────────────────────────────────────┘
*/
添加的時候記得類型要匹配,如果添加了 Null,那么數組會變成 Nullable。
arrayResize:改變數組的長度
如果指定的長度比原來的長度大,那么會用零值從尾部進行填充
如果指定的長度比原來的長度大,那么會從尾部進行截斷
SELECT arrayResize(range(4), 7), arrayResize(range(4), 2);
/*
┌─arrayResize(range(4), 7)─┬─arrayResize(range(4), 2)─┐
│ [0,1,2,3,0,0,0] │ [0,1] │
└──────────────────────────┴──────────────────────────┘
*/
在填充的時候,也可以使用指定的值進行填充:
SELECT arrayResize(range(4), 7, 66), arrayResize(range(4), 7, Null);
/*
┌─arrayResize(range(4), 7, 66)─┬─arrayResize(range(4), 7, NULL)─┐
│ [0,1,2,3,66,66,66] │ [0,1,2,3,NULL,NULL,NULL] │
└──────────────────────────────┴────────────────────────────────┘
*/
arraySlice:返回數組的一個片段
arraySlice(arr, M):返回從索引為 M 開始以及之后的所有元素
arraySlice(arr, M, N):從索引為 M 的元素開始,總共返回 N 個元素
SELECT arraySlice(range(1, 10), 3), arraySlice(range(1, 10), 3, 4);
/*
┌─arraySlice(range(1, 10), 3)─┬─arraySlice(range(1, 10), 3, 4)─┐
│ [3,4,5,6,7,8,9] │ [3,4,5,6] │
└─────────────────────────────┴────────────────────────────────┘
*/
arraySort:對數據進行排序,然后返回
SELECT arraySort([2, 3, 1]), arraySort(['abc', 'ab', 'c']);
/*
┌─arraySort([2, 3, 1])─┬─arraySort(['abc', 'ab', 'c'])─┐
│ [1,2,3] │ ['ab','abc','c'] │
└──────────────────────┴───────────────────────────────┘
*/
字符串會按照字典序排序返回,整型、浮點型、日期都會按照大小返回。
問題來了,如果我們希望按照字符串的長度排序該怎么辦呢?所以 arraySort 還支持傳遞一個自定義函數:
-- 按照數組中元素的長度進行排序
SELECT arraySort(x -> length(x),['abc', 'ab', 'c']);
/*
┌─arraySort(lambda(tuple(x), length(x)), ['abc', 'ab', 'c'])─┐
│ ['c','ab','abc'] │
└────────────────────────────────────────────────────────────┘
*/
-- 先按照正負號排序,小於 0 的排在大於 0 的左邊,然后各自再按照絕對值進行排序
SELECT arraySort(x -> (x > 0, abs(x)), [-3, 1, 2, -1, -2, 3]);
/*
┌─arraySort(lambda(tuple(x), tuple(greater(x, 0), abs(x))), [-3, 1, 2, -1, -2, 3])─┐
│ [-1,-2,-3,1,2,3] │
└──────────────────────────────────────────────────────────────────────────────────┘
*/
我去,這 ClickHouse 也太強大了吧,這簡直不像是在寫 SQL 了,都有點像寫 Python 代碼了,所以 ClickHouse 這么火不是沒有原因的。
另外當出現空值或 NaN 的話,它們的順序如下:
-inf 普通數值 inf NaN Null
所以 arraySort 如果接收一個參數,那么該參數必須是一個數組,然后 ClickHouse 按照默認的規則進行排序;如果接收兩個參數,那么第一個參數是匿名函數,第二個參數是數組,此時 ClickHouse 會按照我們定義的函數來給數組排序;但其實 arraySort 還可以接收三個參數,第一個參數依舊是函數,然后第二個參數和第三個參數都是數組,此時會用數組給數組排序,舉個栗子:
-- 因為有兩個數組,所以匿名函數要有兩個參數,x 表示第一個數組、y 表示第二個數組
-- 首先不管排序規則是什么,最終輸出的都是第一個數組
-- x, y -> y 就表示按照第二個數組來給第一個數組進行排序輸出
SELECT arraySort(x, y -> y, [1, 2, 3], [22, 11, 33]);
/*
┌─arraySort(lambda(tuple(x, y), y), [1, 2, 3], [22, 11, 33])─┐
│ [2,1,3] │
└────────────────────────────────────────────────────────────┘
*/
-- 同理 x, y -> x 返回的還是 [1, 2, 3]、 x, y -> -x 返回的是 [3, 2, 1]
-- 只不過此時第二個數組就用不上了
arrayReverseSort:對數據進行逆序排序,然后返回
該函數你可以認為它是先按照 arraySort 排序,然后將結果再反過來,舉個栗子:
SELECT arraySort(x -> -x, [1, 2, 3]) sort, arrayReverseSort(x -> -x, [1, 2, 3]) reverse_sort;
/*
┌─sort────┬─reverse_sort─┐
│ [3,2,1] │ [1,2,3] │
└─────────┴──────────────┘
*/
指定了匿名函數,按照相反數進行排序,因為 -3 < -2 < -1,所示 arraySort 排序之后就是 [3, 2, 1],然后 arrayReverseSort 則是在其基礎上直接返回,所以得到的還是 [1, 2, 3]。
至於其它用法和 arraySort 都是一樣的,可以看做是在 arraySort 的基礎上做了一次反轉。不過有一點需要注意,那就是 Null 值和 NaN:
arraySort:-inf 普通數值 inf NaN Null
arrayReverseSort:inf 普通數值 -inf NaN Null
即使是 arrayReverseSort,NaN 和 Null 依然排在最后面。
arrayUniq:返回數組中不同元素的數量
SELECT arrayUniq([1, 2, 3, 1, 4]);
/*
┌─arrayUniq([1, 2, 3, 1, 4])─┐
│ 4 │
└────────────────────────────┘
*/
也可以傳遞多個長度相同的數組,會依次取出所有數組中相同位置的元素,然后拼成元組,並計算這些不重復的元組的數量,舉個栗子:
-- 相當於判斷 arrayUniq( [('a', 1, 3), ('a', 1, 3), ('b', 2, 3)] )
SELECT arrayUniq(['a', 'a', 'b'], [1, 1, 2], [3, 3, 3]);
/*
┌─arrayUniq(['a', 'a', 'b'], [1, 1, 2], [3, 3, 3])─┐
│ 2 │
└──────────────────────────────────────────────────┘
*
arrayJoin:將數組展開成多行
SELECT arrayJoin(range(1, 7));
/*
┌─arrayJoin(range(1, 7))─┐
│ 1 │
│ 2 │
│ 3 │
│ 4 │
│ 5 │
│ 6 │
└────────────────────────┘
*/
-- || 表示字符串拼接,當 arrayJoin 展開成多行的時候,會自動和其它字段組合
SELECT arrayJoin(range(1, 7)) AS v, 'A00' || cast(v AS String);
/*
┌─v─┬─concat('A00', CAST(arrayJoin(range(1, 7)), 'String'))─┐
│ 1 │ A001 │
│ 2 │ A002 │
│ 3 │ A003 │
│ 4 │ A004 │
│ 5 │ A005 │
│ 6 │ A006 │
└───┴───────────────────────────────────────────────────────┘
*/
如果出現了多個 arrayJoin ,那么會做笛卡爾積:
SELECT arrayJoin([1, 2, 3]), arrayJoin([11, 22, 33]);
/*
┌─arrayJoin([1, 2, 3])─┬─arrayJoin([11, 22, 33])─┐
│ 1 │ 11 │
│ 1 │ 22 │
│ 1 │ 33 │
│ 2 │ 11 │
│ 2 │ 22 │
│ 2 │ 33 │
│ 3 │ 11 │
│ 3 │ 22 │
│ 3 │ 33 │
└──────────────────────┴─────────────────────────┘
*/
提到了 arrJoin,那么就必須提一下 groupArray,這算是一個聚合函數,它和 arrayJoin 作用相反,將多行數據合並成數組。
SELECT number FROM numbers(5);
/*
┌─number─┐
│ 0 │
│ 1 │
│ 2 │
│ 3 │
│ 4 │
└────────┘
*/
SELECT groupArray(number) FROM numbers(5);
/*
┌─groupArray(number)─┐
│ [0,1,2,3,4] │
└────────────────────┘
*/
除了 groupArray,還有一個 groupUniqArray,從名字上看顯然多了一個去重的功能。
-- SELECT arrayJoin([1, 1, 2, 2, 3]) 會自動展開成多行
-- 當然我們也可以將它作為一張表
SELECT v FROM (SELECT arrayJoin([1, 1, 2, 2, 3]) v);
/*
┌─v─┐
│ 1 │
│ 1 │
│ 2 │
│ 2 │
│ 3 │
└───┘
*/
-- 通過 groupArray 再變成原來的數組
SELECT groupArray(v) FROM (SELECT arrayJoin([1, 1, 2, 2, 3]) v);
/*
┌─groupArray(v)─┐
│ [1,1,2,2,3] │
└───────────────┘
*/
-- 如果使用 groupUniqArray 的話
SELECT groupUniqArray(v) FROM (SELECT arrayJoin([1, 1, 2, 2, 3]) v);
/*
┌─groupUniqArray(v)─┐
│ [2,1,3] │
└───────────────────┘
*/
arrayDifference:計算數組中每相鄰的兩個元素的差值
-- 第一個元素固定為 0,第二個元素為 3 - 1,第三個元素為 4 - 3,以此類推
-- 相鄰元素相減
SELECT arrayDifference([1, 3, 4, 7, 10])
/*
┌─arrayDifference([1, 3, 4, 7, 10])─┐
│ [0,2,1,3,3] │
└───────────────────────────────────┘
*/
arrayDistinct:對數組中的元素進行去重
SELECT arrayDistinct([1, 1, 1, 2, 2, 3]);
/*
┌─arrayDistinct([1, 1, 1, 2, 2, 3])─┐
│ [1,2,3] │
└───────────────────────────────────┘
*/
arrayEnumerateDense:返回一個和原數組大小相等的數組,並指示每個元素在原數組中首次出現的位置(索引都是從 1 開始)
-- 22 首次出現在索引為 1 的位置、1 首次出現在索引為 2 的位置
-- 13 首次出現在索引為 4 的位置,因此結果為 [1, 2, 1, 3, 2, 3]
SELECT arrayEnumerateDense([22, 1, 22, 13, 1, 13]);
/*
┌─arrayEnumerateDense([22, 1, 22, 13, 1, 13])─┐
│ [1,2,1,3,2,3] │
└─────────────────────────────────────────────┘
*/
arrayIntersect:接收多個數組,並取它們的交集
SELECT arrayIntersect([1, 2], [2, 3], [3, 4]), arrayIntersect([1, 2], [2, 3], [2, 4]);
/*
┌─arrayIntersect([1, 2], [2, 3], [3, 4])─┬─arrayIntersect([1, 2], [2, 3], [2, 4])─┐
│ [] │ [2] │
└────────────────────────────────────────┴────────────────────────────────────────┘
*/
arrayReduce:將一個聚合函數作用在數組上,舉個栗子:
SELECT arrayReduce('max', [1, 23, 6]), arrayReduce('sum', [1, 23, 6]);
/*
┌─arrayReduce('max', [1, 23, 6])─┬─arrayReduce('sum', [1, 23, 6])─┐
│ 23 │ 30 │
└────────────────────────────────┴────────────────────────────────┘
*/
可能有人覺得直接用聚合函數不就好了,答案是不行的,因為這些聚合函數針對的都是多行結果集,而不是數組。
-- 相當於只有一行數據,所以返回其本身
-- 如果是 sum 就直接報錯了, 因為數組之間不能進行加法運算
SELECT max([11, 33, 22]);
/*
┌─max([11, 33, 22])─┐
│ [11,33,22] │
└───────────────────┘
*/
-- 如果想返回 33,我們應該將這個數組給展開,變成多行
SELECT max(arrayJoin([11, 33, 22]));
/*
┌─max(arrayJoin([11, 33, 22]))─┐
│ 33 │
└──────────────────────────────┘
*/
所以聚合函數針對的是多行,而不是數組,如果想用聚合函數,那么應該將數組給展開。或者使用這里的 arrayReduce,相當於將兩步合在一起了。當然我們也可以不用 arrayReduce,因為 ClickHouse 為了數組專門提供了相應的操作,比如求數組中最大的元素可以使用更強大的 arrayMax,后面說。
arrayReduceInRanges:對給定范圍內的數組元素應用聚合函數,光說不好解釋,直接看例子:
-- 會對數組中索引為 1 開始向后的 5 個元素進行 sum,結果為 15
-- 會對數組中索引為 2 開始向后的 4 個元素進行 sum,結果為 14
-- 會對數組中索引為 1 開始向后的 3 個元素進行 sum,結果為 6
SELECT arrayReduceInRanges(
'sum',
[(1, 5), (2, 4), (1, 3)],
[1, 2, 3, 4, 5]
)
/*
┌─arrayReduceInRanges('sum', array((1, 5), (2, 4), (1, 3)), [1, 2, 3, 4, 5])─┐
│ [15,14,6] │
└────────────────────────────────────────────────────────────────────────────┘
*/
-- 以上等價於
WITH [1, 2, 3, 4, 5] AS arr
SELECT [arrayReduce('sum', arraySlice(arr, 1, 5)),
arrayReduce('sum', arraySlice(arr, 2, 4)),
arrayReduce('sum', arraySlice(arr, 1, 3))] AS v
/*
┌─v─────────┐
│ [15,14,6] │
└───────────┘
*/
arrayReverse:對數據進行逆序,然后返回;我們之前還介紹了一個 arrayReverseSort,它在逆序之前會先排序,而這里的 arrayReverse 只是單純的逆序
-- arrayReverse 和 reverse 作用相同
SELECT arrayReverse([22, 33, 11]), reverse([22, 33, 11]);
/*
┌─arrayReverse([22, 33, 11])─┬─reverse([22, 33, 11])─┐
│ [11,33,22] │ [11,33,22] │
└────────────────────────────┴───────────────────────┘
*/
arrayFlatten:將數組扁平化
-- arrayFlatten 也可以使用 flatten 代替
SELECT arrayFlatten([[1, 2, 3], [11, 22, 33]]);
/*
┌─arrayFlatten([[1, 2, 3], [11, 22, 33]])─┐
│ [1,2,3,11,22,33] │
└─────────────────────────────────────────┘
*/
我們之前還介紹了一個 arrayConcat,可以對比一下兩者的區別
SELECT arrayConcat ([1, 2, 3], [11, 22, 33]);
/*
┌─arrayConcat([1, 2, 3], [11, 22, 33])─┐
│ [1,2,3,11,22,33] │
└──────────────────────────────────────┘
*/
arrayCompact:從數組中刪除連續重復的元素
SELECT arrayCompact([2, 2, 1, 1, 1, 3, 3, Null, Null]);
/*
┌─arrayCompact([2, 2, 1, 1, 1, 3, 3, NULL, NULL])─┐
│ [2,1,3,NULL] │
└─────────────────────────────────────────────────┘
*/
我們看到作用類似於之前介紹的 arrayDistinct,但兩者還是有區別的。
SELECT arrayDistinct([2, 2, 1, 1, 1, 3, 3, NULL, NULL])
/*
┌─arrayDistinct([2, 2, 1, 1, 1, 3, 3, NULL, NULL])─┐
│ [2,1,3] │
└──────────────────────────────────────────────────┘
*/
我們發現 arrayDistinct 不包含 Null 值。
arrayZip:類似於 Python 中的 zip,直接看示例:
SELECT arrayZip(['a', 'b', 'c'], [1, 2, 3], ['x', 'y', 'z']);
/*
┌─arrayZip(['a', 'b', 'c'], [1, 2, 3], ['x', 'y', 'z'])─┐
│ [('a',1,'x'),('b',2,'y'),('c',3,'z')] │
└───────────────────────────────────────────────────────┘
*/
arrayMap:對數組中每一個元素都作用相同的函數,根據函數的返回值創建一個新的數組,非常常用的一個功能。
SELECT arrayMap(x -> (x, 1), ['a', 'b', 'c']);
/*
┌─arrayMap(lambda(tuple(x), tuple(x, 1)), ['a', 'b', 'c'])─┐
│ [('a',1),('b',1),('c',1)] │
└──────────────────────────────────────────────────────────┘
*/
SELECT arrayMap(x -> x * 2, [1, 2, 3]) v1, sum(arrayJoin(v1)) v2, arrayReduce('sum', v1) v3;
/*
┌─v1──────┬─v2─┬─v3─┐
│ [2,4,6] │ 12 │ 12 │
└─────────┴────┴────┘
*/
當然也可以作用嵌套數組:
SELECT arrayMap(x -> arrayReduce('sum', x), [[1, 2, 3], [11, 22, 33], [33, 44, 55]]);
/*
┌─arrayMap(lambda(tuple(x), arrayReduce('sum', x)), [[1, 2, 3], [11, 22, 33], [33, 44, 55]])─┐
│ [6,66,132] │
└────────────────────────────────────────────────────────────────────────────────────────────┘
*/
SELECT arrayMap(x -> arrayReduce('max', x), [[1, 2, 3], [11, 22, 33], [33, 44, 55]]);
/*
┌─arrayMap(lambda(tuple(x), arrayReduce('max', x)), [[1, 2, 3], [11, 22, 33], [33, 44, 55]])─┐
│ [3,33,55] │
└────────────────────────────────────────────────────────────────────────────────────────────┘
*/
SELECT arrayMap(x -> arrayReduce('min', x), [[1, 2, 3], [11, 22, 33], [33, 44, 55]]);
/*
┌─arrayMap(lambda(tuple(x), arrayReduce('max', x)), [[1, 2, 3], [11, 22, 33], [33, 44, 55]])─┐
│ [1,11,33] │
└────────────────────────────────────────────────────────────────────────────────────────────┘
*/
也可以作用多個數組,這些數組的長度必須相等。此外,有多個數組,函數就要有多少個參數:
-- 得到的是 [1 + 11 + 33, 2 + 22 + 44, 3 + 33 + 55]
-- 如果是 arrayMap(x -> arrayReduce('sum', x), [[1, 2, 3], [11, 22, 33], [33, 44, 55]])
-- 那么得到的是 [1 + 2 + 3, 11 + 22 + 33, 33 + 44 + 55]
SELECT arrayMap(x, y, z -> arrayReduce('sum', [x, y, z]), [1, 2, 3], [11, 22, 33], [33, 44, 55]) AS v;
/*
┌─v──────────┐
│ [45,68,91] │
└────────────┘
*/
SELECT arrayMap(x, y, z -> (x + y, z), [1, 2, 3], [11, 22, 33], [33, 44, 55]) AS v;
/*
┌─v─────────────────────────┐
│ [(12,33),(24,44),(36,55)] │
└───────────────────────────┘
*/
arrayFilter:對數組中每一個元素都作用相同的函數,如果函數返回值為真(非 0),則該元素保留,否則不保留。
SELECT arrayFilter(x -> x > 5, [1, 4, 5, 7, 10]);
/*
┌─arrayFilter(lambda(tuple(x), greater(x, 5)), [1, 4, 5, 7, 10])─┐
│ [7,10] │
└────────────────────────────────────────────────────────────────┘
*/
SELECT arrayFilter(x -> length(x) > 1, ['a', 'aa', 'aaa']);
/*
┌─arrayFilter(lambda(tuple(x), greater(length(x), 1)), ['a', 'aa', 'aaa'])─┐
│ ['aa','aaa'] │
└──────────────────────────────────────────────────────────────────────────┘
*/
SELECT arrayFilter(x -> x LIKE 'sa%', ['satori', 'koishi']);
/*
┌─arrayFilter(lambda(tuple(x), like(x, 'sa%')), ['satori', 'koishi'])─┐
│ ['satori'] │
└─────────────────────────────────────────────────────────────────────┘
*/
arrayFill:對數組中每一個元素都作用相同的函數,如果函數返回值為真,則該元素保留,否則被替換為前一個元素。
-- 2 會被替換成 4,1 會被替換成 5
SELECT arrayFill(x -> x >= 3, [3, 4, 2, 5, 1]);
/*
┌─arrayFill(lambda(tuple(x), greaterOrEquals(x, 3)), [3, 4, 2, 5, 1])─┐
│ [3,4,4,5,5] │
└─────────────────────────────────────────────────────────────────────┘
*/
-- 第一個元素永遠不會被替換,2、3、4、5 都不滿足條件,因此都要換成前一個元素
-- 換 2 的時候,2 已經變成了 1,所以 3 的前面是 1,於是 3 也會變成 1
-- 4 和 5 也是同理,因此最終所有值都會變成 1
SELECT arrayFill(x -> x >= 6, [1, 2, 3, 4, 5]);
/*
┌─arrayFill(lambda(tuple(x), greaterOrEquals(x, 6)), [1, 2, 3, 4, 5])─┐
│ [1,1,1,1,1] │
└─────────────────────────────────────────────────────────────────────┘
*/
arrayReverseFill:對數組中每一個元素都作用相同的函數,如果函數返回值為真,則該元素保留,否則被替換為后一個元素。注意:此時數組是從后往前掃描的
-- 2 會被替換成 5,1 還是 1,最后一個元素不會被替換
SELECT arrayReverseFill(x -> x >= 3, [3, 4, 2, 5, 1]);
/*
┌─arrayReverseFill(lambda(tuple(x), greaterOrEquals(x, 3)), [3, 4, 2, 5, 1])─┐
│ [3,4,5,5,1] │
└────────────────────────────────────────────────────────────────────────────┘
*/
-- 因為數組從后往前掃描,所以 4 變成 5、3 也會變成 5,所有值都會變成 5
SELECT arrayReverseFill(x -> x >= 6, [1, 2, 3, 4, 5]);
/*
┌─arrayReverseFill(lambda(tuple(x), greaterOrEquals(x, 6)), [1, 2, 3, 4, 5])─┐
│ [5,5,5,5,5] │
└────────────────────────────────────────────────────────────────────────────┘
*/
arrayMin:返回數組中最小的元素
WITH [11, 22, 8, 33] AS arr SELECT arrayMin(arr) v1, min(arrayJoin(arr)) v2, arrayReduce('min', arr) v3;
/*
┌─v1─┬─v2─┬─v3─┐
│ 8 │ 8 │ 8 │
└────┴────┴────┘
*/
arrayMin 里面還可以傳遞一個匿名函數:
SELECT arrayMin(x -> -x, [11, 22, 8, 33])
/*
┌─arrayMin(lambda(tuple(x), negate(x)), [11, 22, 8, 33])─┐
│ -33 │
└────────────────────────────────────────────────────────┘
*/
會按照調用匿名函數的返回值進行判斷,選擇最小的元素,這里 33 在調用之后返回 -33,顯然是最小值。但是這里有一個需要注意的地方,就是它返回的也是匿名函數的返回值。個人覺得應該返回 33 才對,應為我們指定函數只是希望 ClickHouse 能夠按照我們指定的規則進行排序,而值還是原來的值,但 ClickHouse 這里設計有點莫測高深了。如果我們以字符串為例,那么會看的更加明顯:
SELECT arrayMin(x -> length(x), ['ab', 'abc', 'a']) v;
/*
┌─v─┐
│ 1 │
└───┘
*/
我們看到居然返回了一個 1,我們的本意是想選擇長度最短的字符串,但是返回的是最短字符串的長度,也就是返回的不是 'a',而是 length('a')。
arrayMax:返回數組中最大的元素
WITH [11, 22, 8, 33] AS arr SELECT arrayMax(arr) v1, max(arrayJoin(arr)) v2, arrayReduce('max', arr) v3;
/*
┌─v1─┬─v2─┬─v3─┐
│ 33 │ 33 │ 33 │
└────┴────┴────┘
*/
也可以加上一個匿名函數,作用和 arrayMin 完全一樣,並且返回的也是函數調用之后的結果。
arraySum:對數組求總和
WITH range(1, 101) AS arr SELECT arraySum(arr), arrayReduce('sum', arr), sum(arrayJoin(arr));
/*
┌─arraySum(arr)─┬─arrayReduce('sum', arr)─┬─sum(arrayJoin(arr))─┐
│ 5050 │ 5050 │ 5050 │
└───────────────┴─────────────────────────┴─────────────────────┘
*/
同樣可以加一個匿名函數:
WITH range(1, 101) AS arr SELECT arraySum(x -> x * 2, arr);
/*
┌─arraySum(lambda(tuple(x), multiply(x, 2)), arr)─┐
│ 10100 │
└─────────────────────────────────────────────────┘
*/
arrayProduct:對數組求總乘積
SELECT arrayProduct([1, 2, 3, 4, 5]);
/*
┌─arrayProduct([1, 2, 3, 4, 5])─┐
│ 120 │
└───────────────────────────────┘
*/
同樣可以加一個匿名函數:
SELECT arrayProduct(x -> x + 1, [1, 2, 3, 4, 5]);
/*
┌─arrayProduct(lambda(tuple(x), plus(x, 1)), [1, 2, 3, 4, 5])─┐
│ 720 │
└─────────────────────────────────────────────────────────────┘
*/
arrayAvg:對數組取平均值
WITH range(1, 101) AS arr SELECT arrayAvg(arr), arrayReduce('avg', arr), avg(arrayJoin(arr));
/*
┌─arrayAvg(arr)─┬─arrayReduce('avg', arr)─┬─avg(arrayJoin(arr))─┐
│ 50.5 │ 50.5 │ 50.5 │
└───────────────┴─────────────────────────┴─────────────────────┘
*/
同樣可以加一個匿名函數:
WITH range(1, 101) AS arr SELECT arrayAvg(x -> x * 2, arr);
/*
┌─arrayAvg(lambda(tuple(x), multiply(x, 2)), arr)─┐
│ 101 │
└─────────────────────────────────────────────────┘
*/
arrayCumSum:對數組進行累和
-- 第一個元素不變
SELECT arrayCumSum([1, 2, 3, 4, 5]);
/*
┌─arrayCumSum([1, 2, 3, 4, 5])─┐
│ [1,3,6,10,15] │
└──────────────────────────────┘
*/
同樣可以加一個匿名函數:
-- 第一個元素不變
SELECT arrayCumSum(x -> x * 2, [1, 2, 3, 4, 5]), arrayCumSum([2, 4, 6, 8, 10]);
/*
┌─arrayCumSum(lambda(tuple(x), multiply(x, 2)), [1, 2, 3, 4, 5])─┬─arrayCumSum([2, 4, 6, 8, 10])─┐
│ [2,6,12,20,30] │ [2,6,12,20,30] │
└────────────────────────────────────────────────────────────────┴───────────────────────────────┘
*/
小結
以上就是關於 ClickHouse 數組的一些函數操作,可以說是非常強大了,不光是功能強大,用起來也很舒服,仿佛有種在寫 Python 代碼的感覺。當然以上並不是關於數組的全部操作(絕大部分),但說實話已經夠用了,即使你當前的需求,某一個函數不能解決,那么也能多個函數組合來解決。比如我們想要計算兩個數組中相同位置的元素的差,那么就可以這么做:
-- 一個函數即可解決
SELECT arrayMap(x, y -> x - y, [1, 2, 3], [3, 2, 1]);
/*
┌─arrayMap(lambda(tuple(x, y), minus(x, y)), [1, 2, 3], [3, 2, 1])─┐
│ [-2,0,2] │
└──────────────────────────────────────────────────────────────────┘
*/
再比如,計算數組中每個元素減去上一個元素的值,由於第一個元素上面沒有值,那么設為空:
-- 我們只需要選擇 arr 的前 N - 1 個元素,然后再在頭部插入一個 Null,[Null, 11, 22, 33, 44, 55]
-- 最后讓 arr 和它的對應元素依次相減即可
WITH [11, 22, 33, 44, 55, 66] AS arr
SELECT arrayMap(
x, y -> x - y,
arr,
arrayPushFront(arraySlice(arr, 1, length(arr) - 1), Null)
) v;
/*
┌─v─────────────────────┐
│ [NULL,11,11,11,11,11] │
└───────────────────────┘
*/
顯然即使是復雜的需求,也可以通過多個函數組合完成,怎么樣,是不是有點酷呢?ClickHouse 內建了很多的函數,這些函數給我們一種仿佛在用編程語言寫代碼的感覺。