ClickHouse 中的常用聚合函數


楔子

這次來說一下 ClickHouse 中的聚合函數,因為和關系型數據庫的相似性,本來聚合函數不打算說的,但是 ClickHouse 提供了很多關系型數據庫中沒有的函數,所以我們還是從頭了解一下。

 

count:計算數據的行數,有以下幾種方式:

  • count(字段):計算該字段中不為 Null 的元素數量
  • count()、count(*):計算數據集的總行數

所有如果某個字段中不包含 Null,那么對該字段進行 count 得到的結果和 count()、count(*) 是相等的。

SELECT count(), count(*), count(product) FROM sales_data;
/*
┌─count()─┬─count()─┬─count(product)─┐
│    1349 │    1349 │           1349 │
└─────────┴─────────┴────────────────┘

這里再提一下聚合函數,聚合函數針對的是多行結果集,而不是數組。

-- 這里得到的是 1,原因在於這里只有一行數據
SELECT count([1, 2, 3]);
/*
┌─count()─┐
│       1 │
└─────────┘
*/

-- 如果將其展開的話,那么會得到 3,因為展開之后變成了 3 行數據
SELECT count(arrayJoin([1, 2, 3]));
/*
┌─count(arrayJoin([1, 2, 3]))─┐
│                           3 │
└─────────────────────────────┘
*/

當然使用 count 計算某個字段的元素數量時,還可以進行去重。

SELECT count(DISTINCT product) FROM sales_data;
/*
┌─uniqExact(product)─┐
│                  3 │
└────────────────────┘
*/

-- 根據返回的字段名,我們發現 ClickHouse 在底層實際上調用的是 uniqExact 函數
SELECT uniqExact(product) FROM sales_data;
/*
┌─uniqExact(product)─┐
│                  3 │
└────────────────────┘
*/
-- 也就是 count(DISTINCT) 等價於 uniqExact
-- 不過還是建議像關系型數據庫那樣使用 count(DISTINCT) 比較好,因為更加習慣

 

min、max、sum、avg:計算每組數據的最小值、最大值、總和、平均值

SELECT min(amount), max(amount), sum(amount), avg(amount) 
FROM sales_data GROUP BY product, channel;
/*
┌─min(amount)─┬─max(amount)─┬─sum(amount)─┬────────avg(amount)─┐
│         547 │        2788 │      248175 │ 1643.5430463576158 │
│         658 │        2805 │      252148 │ 1669.8543046357615 │
│         613 │        2803 │      246198 │ 1652.3355704697988 │
│         709 │        2870 │      256602 │ 1699.3509933774835 │
│         599 │        2869 │      245029 │ 1601.4967320261437 │
│         511 │        2673 │      252908 │ 1686.0533333333333 │
│         564 │        2710 │      252057 │ 1714.6734693877552 │
│         621 │        2832 │      251795 │ 1701.3175675675675 │
│         642 │        2803 │      245904 │ 1650.3624161073826 │
└─────────────┴─────────────┴─────────────┴────────────────────┘
*/

除此之外還有兩個非聚合函數 least、greatest 也比較實用,那么這兩個函數是干什么的呢?看一張圖就明白了。

我們可以測試一下:

SELECT least(A, B), greatest(A, B) FROM test_1;
/*
┌─least(A, B)─┬─greatest(A, B)─┐
│          11 │             13 │
│           7 │              8 │
│           5 │              8 │
│          11 │             15 │
│           9 │             13 │
└─────────────┴────────────────┘
*/

問題來了,如果 ClickHouse 沒有提供 least 和 greatest 這兩個函數,那么我們要如何實現此功能呢?首先我們可以使用 arrayMap:

-- 由於 arrayMap 針對的是數組,不是多行結果集,所以需要借助 groupArray 將多行結果集轉成數組
-- 另外在比較大小的時候也要將兩個元素組合成數組 [x, y],然后使用 arrayMin 比較
-- 或者使用 least(x, y) 也可以對兩個標量進行比較,不過這里我們是為了實現 least,所以就不用它了
SELECT arrayMap(x, y -> arrayMin([x, y]), groupArray(A), groupArray(B)) arr FROM test_1;
/*
┌─arr───────────┐
│ [11,7,5,11,9] │
└───────────────┘
*/

-- 結果確實實現了,但結果是數組,我們還要再將其展開成多行
-- 這里我們使用 WITH,注意 WITH 子句里的查詢只可以返回一行結果集
WITH (
    SELECT arrayMap(x, y -> arrayMin([x, y]), groupArray(A), groupArray(B)) FROM test_1
) AS arr SELECT arrayJoin(arr);
/*
┌─arrayJoin(arr)─┐
│             11 │
│              7 │
│              5 │
│             11 │
│              9 │
└────────────────┘
*/

以上就實現了 least,至於 greatest 也是同理。那么除了使用數組的方式,還可以怎么做呢?如果將這個問題的背景再改成關系型數據庫的話,你一定能想到,沒錯,就是 CASE WHEN。

SELECT CASE WHEN A < B THEN A ELSE B END FROM test_1;
/*
┌─multiIf(less(A, B), A, B)─┐
│                        11 │
│                         7 │
│                         5 │
│                        11 │
│                         9 │
└───────────────────────────┘
*/

整個過程顯然變得簡單了,所以也不要忘記關系型數據庫的語法在 ClickHouse 中也是可以使用的,另外我們看到返回的結果集的字段名叫 multiIf...,雖然我們使用的是 CASE WHEN,但是 ClickHouse 在底層會給語句進行優化,在功能不變的前提下,尋找一個在 ClickHouse 中效率更高的替代方案。因此你直接使用 multiIf... 也是可以的,比如:

SELECT multiIf(less(A, B), A, B) FROM test_1

而至於上面的 multiIf,它的功能和 CASE WHEN 是完全類似的。只不過這里個人有一點建議,既然 ClickHouse 會進行語句的優化,那么能用關系型數據庫語法解決的問題,就用關系型數據庫語法去解決。這么做的原因主要是為了考慮 SQL 語句的可讀性,因為相比 ClickHouse,大部分人對關系型數據庫語法顯然更熟悉一些。如果使用這里的 mulitIf...,那么當別人閱讀時,可能還要查閱一下 multiIf 函數、或者 mulitIf 里面又調用的 less 函數是做什么的;但如果使用 CASE WHEN,絕對的一目了然。

當然以上只是個人的建議,如果你對 ClickHouse 的函數用的非常 6,那么完全可以不優先使用關系型數據庫的語法,不然這些函數不是白掌握了嗎。

 

any:選擇每組數據中第一個出現的值

-- 按照 product, channel 進行分組之后,我們可以求每組的最小值、最大值、平均值等等
-- 而這里的 any 則表示獲取每組第一個出現的值
SELECT any(amount) FROM sales_data GROUP BY product, channel;
/*
┌─any(amount)─┐
│        1864 │
│        1573 │
│         847 │
│        1178 │
│        1736 │
│         511 │
│         568 │
│        1329 │
│        1364 │
└─────────────┘
*/

當然 any 看起來貌似沒有實際的意義,因為聚合之后每組第一個出現的值並不一定能代表什么。那么問題來了,如果想選擇分組中的任意一個值,該怎么辦呢?

-- 使用 groupArray 變成一個數組,然后再通過索引選擇即可
-- 因為我們選擇的是第 1 個元素,所以此時等價於 any
SELECT groupArray(amount)[1] FROM sales_data 
GROUP BY product, channel;
/*
┌─arrayElement(groupArray(amount), 1)─┐
│                                1864 │
│                                1573 │
│                                 847 │
│                                1178 │
│                                1736 │
│                                 511 │
│                                 568 │
│                                1329 │
│                                1364 │
└─────────────────────────────────────┘
*/

如果想分組之后選擇,選擇每個組的最小值該怎么做呢?

-- 在上面的基礎上再調用一下 arrayMin 即可
SELECT arrayMin(groupArray(amount)) FROM sales_data
GROUP BY product, channel;
/*
┌─arrayMin(groupArray(amount))─┐
│                          547 │
│                          658 │
│                          613 │
│                          709 │
│                          599 │
│                          511 │
│                          564 │
│                          621 │
│                          642 │
└──────────────────────────────┘
*/

如果想分組之后選擇,選擇每個組的第 N 大的值該怎么做呢?比如我們選擇第 3 大的值。

-- 從小到大排個序即可,然后選擇索引為 -3 的元素
-- 或者從大到小排個序,然后選擇索引為 3 的元素
SELECT arraySort(groupArray(amount))[-2] rank3_1, arrayReverseSort(groupArray(amount))[2] rank3_2
FROM sales_data GROUP BY product, channel;
/*
┌─rank3_1─┬─rank3_2─┐
│    2784 │    2784 │
│    2804 │    2804 │
│    2650 │    2650 │
│    2856 │    2856 │
│    2865 │    2865 │
│    2610 │    2610 │
│    2632 │    2632 │
│    2754 │    2754 │
│    2694 │    2694 │
└─────────┴─────────┘
*/

確實給人一種 pandas 的感覺,之前做數據分析主要用 pandas。但是 pandas 有一個致命的問題,就是它要求數據能全部加載到內存中,所以在處理中小型數據集的時候確實很方便,但是對於大型數據集就無能為力了,只能另辟蹊徑。但是 ClickHouse 則是通過分片機制支持分布式運算,所以個人覺得它簡直就是分布式的 pandas。

 

varPop:計算方差,\({\frac{\sum(x - \hat{x})^2} n}\);stddevPop:計算標准差,等於方差開根號

SELECT varPop(amount) v1, stddevPop(amount) v2, v2 * v2 
FROM sales_data;
/*
┌───────v1─┬───────v2─┬─multiply(stddevPop(amount), stddevPop(amount))─┐
│ 269907.7 │ 519.5264 │                              269907.7096217908 │
└──────────┴──────────┴────────────────────────────────────────────────┘
*/

問題來了,如果我們想手動實現方差的計算該怎么辦?試一下:

-- 將結果集轉成數組,並先計算好平均值
WITH (SELECT groupArray(amount) FROM sales_data) AS arr, 
      arraySum(arr) / length(arr) AS amount_avg
-- 通過 arrayMap 將數組中的每一個元素都和平均值做減法,然后再平方,得到新數組
-- 最后再用 arrayAvg 對新數組取平均值,即可計算出方差
SELECT arrayAvg(
    arrayMap(x -> pow(x - amount_avg, 2), arr)
) 

 

covarPop:計算協方差,\({\frac{\sum(x - \hat{x})(y - \hat{y})} n}\)

比較少用,這里不演示的了,可以自己測試一下。

 

anyHeavy:使用  heavy hitters 算法選擇每組中出現頻率最高的值

SELECT anyHeavy(amount) FROM sales_data;
/*
┌─anyHeavy(amount)─┐
│             2369 │
└──────────────────┘
*/

 

anyLast:選擇每組中的最后一個值

SELECT anyLast(amount) FROM sales_data GROUP BY product, channel;
/*
┌─anyLast(amount)─┐
│            1679 │
│            1767 │
│            2369 │
│            2660 │
│            2865 │
│            2422 │
│            1481 │
│            1439 │
│            2443 │
└─────────────────┘
*/

-- 同樣可以借助數組實現
SELECT groupArray(amount)[-1] FROM sales_data GROUP BY product, channel;
/*
┌─arrayElement(groupArray(amount), -1)─┐
│                                 1679 │
│                                 1767 │
│                                 2369 │
│                                 2660 │
│                                 2865 │
│                                 2422 │
│                                 1481 │
│                                 1439 │
│                                 2443 │
└──────────────────────────────────────┘
*/

 

argMin:接收兩個列,根據另一個列選擇當前列的最小值,我們畫一張圖,通過和 min 進行比對,就能看出它的用法了

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 │
└───────────────────────┘
*/

首先 min(A) 和 min(B) 分別返回 5 和 7 無需解釋,而 argMin(A, B) 表示根據 B 的最小值選擇 A,B 的最小值是 7,對應 A 就是 8;同理 argMin(B, A) 表示根據 A 的最小值選擇 B,A 的最小值是 5,對應 B 就是 8。

以上就是 argMin,同理還有 argMax。

 

topK:選擇出現頻率最高的 K 個元素

-- 這里選擇出現頻率最高的兩個元素
SELECT topK(2)(arrayJoin([1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3]));
/*
┌─topK(2)(arrayJoin([1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3]))─┐
│ [1,3]                                                    │
└──────────────────────────────────────────────────────────┘
*/

我們看到以數組的形式返回,因為聚合函數最終每個組只會對應一行數據,所以得到的是數組。

topK 也是非常常見的,如果讓我們自己實現,雖然可以做到,但會比較麻煩,ClickHouse 替我們考慮的還是很周到的。

 

groupArrayMovingSum:滑動窗口,每個窗口內的數據進行累和。

SELECT groupArray(number), groupArrayMovingSum(4)(number)
FROM (SELECT number FROM numbers(10));
/*
┌─groupArray(number)────┬─groupArrayMovingSum(4)(number)─┐
│ [0,1,2,3,4,5,6,7,8,9] │ [0,1,3,6,10,14,18,22,26,30]    │
└───────────────────────┴────────────────────────────────┘
*/

畫一張圖,來解釋一下:

首先 groupArrayMovingSum(4) 表示窗口的長度為 4,然后不斷的向下滑動,計算包含當前元素在內往上的四個元素之和。如果元素的個數不夠窗口的長度,那么有幾個算幾個,比如前三個元素。

那么試想一下,如果窗口長度等於數組的長度,那么會發生什么呢?

-- 不指定窗口長度,那么窗口長度就等於數組長度
SELECT groupArray(number), groupArrayMovingSum(number)
FROM (SELECT number FROM numbers(10));
/*
┌─groupArray(number)────┬─groupArrayMovingSum(number)─┐
│ [0,1,2,3,4,5,6,7,8,9] │ [0,1,3,6,10,15,21,28,36,45] │
└───────────────────────┴─────────────────────────────┘
*/
-- 顯然相當於進行了累和

這就是 ClickHouse 提供的窗口函數,但關系型數據庫中的窗口函數語法在 ClickHouse 還沒有得到完美的支持,但很明顯通過這些強大的函數我們也可以實現相應的功能。

除了 groupArrayMovingSum 之外,還有一個 groupArrayMovingAvg,用法完全一樣,只不過計算的是平均值,這里就不單獨說了。

 

groupArraySample:隨機選擇 N 個元素

-- 隨機選擇 3 個元素
SELECT groupArraySample(3)(amount) FROM sales_data;
/*
┌─groupArraySample(3)(amount)─┐
│ [1268,2246,1606]            │
└─────────────────────────────┘
*/

我們還可以綁定一個隨機種子,如果種子一樣,那么每次隨機選擇的數據也是一樣的。

SELECT groupArraySample(3, 666)(amount) FROM sales_data;
/*
┌─groupArraySample(3, 666)(amount)─┐
│ [635,1290,1846]                  │
└──────────────────────────────────┘
*/

SELECT groupArraySample(3, 666)(amount) FROM sales_data;
/*
┌─groupArraySample(3, 666)(amount)─┐
│ [635,1290,1846]                  │
└──────────────────────────────────┘
*/

SELECT groupArraySample(3, 661)(amount) FROM sales_data;
/*
┌─groupArraySample(3, 661)(amount)─┐
│ [2011,2125,1542]                 │
└──────────────────────────────────┘
*/

 

deltaSum:對相鄰的行進行做差,然后求和,注意:小於 0 不會計算在內

-- 3 - 1 = 2
-- 4 - 3 = 1
-- 1 - 4 = -3
-- 8 - 1 = 7
-- 所以結果為 2 + 1 + 7
SELECT deltaSum(arrayJoin([1, 3, 4, 1, 8]));
/*
┌─deltaSum(arrayJoin([1, 3, 4, 1, 8]))─┐
│                                   10 │
└──────────────────────────────────────┘
*/

 

小結

以上就是關於 ClickHouse 的一些聚合函數,還有相當一部分沒有介紹到,主要是覺得應用的場景非常少見,后續再補充。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM