在 Postgres 上使用 HyperLogLog 進行去重計數

原做者/ BURAK YUCESOY
翻譯&編輯 / 馬叔javascript

在數據庫上運行 SELECT COUNT(DISTINCT)是很是常見的。在應用程序中,一般有一些分析大屏能夠突出顯示去重項的數量,例如去重的用戶,去重的產品或去重的訪問。雖然傳統的 SELECT COUNT(DISTINCT)查詢在單機設置中運行良好,但在分佈式系統中卻很難解決。當你有這種類型的查詢時,你不能只是將查詢推送到從屬節點而後把結果加起來,由於極可能,在不一樣的從節點中存在重疊的記錄。相反,你能夠這樣作:java

  • 將全部不一樣的數據拉到一臺機器上並在那裏計數。(伸縮性差)git

  • 作 map / reduce。(有伸縮性,但很慢)github

這就是能夠用到近似算法或草算(sketches)的地方。草算(sketches)是機率算法,能夠在數學可證實的偏差範圍內有效地生成近似結果。這樣的草圖有不少,但今天咱們只專門講一個:HyperLogLog (HLL)。HLL 很是擅於估算列表中的去重元素計數。首先,咱們來看一下 HLL 的內部結構,來幫助咱們瞭解爲何 HLL 算法能夠伸縮地解決去重計數問題,並解決如何將其以分佈式的方式應用。而後,咱們來看一些使用 HLL 的例子。算法

HLL 在後面都在作什麼?

對全部元素求散列(哈希)

HLL 和幾乎全部其餘機率計數算法都依賴於數據的均勻分佈。可是,由於在現實世界中,咱們的數據通常不是均勻分佈的,因此 HLL 首先就會對每一個元素進行散列,使數據分佈更均勻。這裏說到的均勻分佈,其意思是:元素的每一位都有 0.5 的機率成爲 0 或 1.咱們很快就會看到爲何這是有用的。除了均勻性,散列讓 HLL 能夠用相同的方式處理全部數據類型。只要你的數據類型有散列函數(哈希函數),你就可使用 HLL 進行基數估算。sql

觀察罕見的數據模式

在散列了全部元素後,HLL 將查找每一個散列元素的二進制形式。它主要是看這裏是否存在不太可能發生的比特模式(bit patterns)。若是存在這種罕見的模式,就意味着咱們正在處理大數據集。數據庫

爲此,HLL 在每一個元素的散列值中找首位零比特,並找到首位零比特的長度。基本上,爲了可以觀察到 k 個首位零,咱們須要 2k + 1 次試驗(即,散列數)。所以,若是數據集中,首位零的最大數目爲 k,HLL 就得出結論,這裏存在大約 2k + 1 個不一樣的元素。json

這是很是直接、簡單的估算方法。然而,它具備一些重要的特性,在分佈式環境中尤爲明顯。併發

  • HLL 的內存佔用很是低。對於最大數 n,咱們只須要存儲 log log n 比特。例如,若是咱們將元素散列成 64 比特整數,咱們只須要存儲 6 比特來進行估算。這比起樸素法(須要記住全部值),大量節省了內存。分佈式

  • 咱們只須要對數據進行一次遍歷(掃描),找到首位零的最大值。

  • 咱們可使用流數據。在計算了首位零的最大值以後,若是這時來了一些新的數據,咱們就能夠不用再過一遍整個數據集,能夠直接將它們包含進去進行計算。咱們只須要查找每一個新元素的首位零的數,將它們與整個數據集裏首位零的最大數進行比較,若是須要的話,就能夠更新首位零的最大值。

  • 咱們能夠有效地合併兩個單獨數據集的估算結果。咱們只須要選擇首位零數值更大的那個做爲合併數據集首位零的最大值。這就讓咱們能夠將數據分片,估算它們的基數,以及合併結果。這被稱爲可加性,可加性讓咱們能夠在分佈式系統中使用 HLL。

隨機平均

若是你認爲上述的估算還不是很好,你是對的。首先,咱們的預測老是以 2k 的形式。其次,若是數據分佈沒有足夠統一,咱們可能會得出誤差特別大的估算。

一個可能解決這些問題的方案是,用不一樣的散列函數來重複這個過程,而後取平均值。這應該是可行的,可是,對全部的數據進行屢次散列十分昂貴。 HLL 解決了這個問題,這個方法叫作隨機平均。基本上就是,咱們將數據分紅桶,並分別對每一個桶使用上述算法。而後咱們就取這些結果的平均值。咱們用哈希值的前幾個比特來肯定某個元素屬於哪一個存儲桶,而後使用剩餘的比特來計算首位零的最大值。

此外,咱們還能夠選擇一些桶來劃分數據,以此來定製 / 調整精度。咱們須要爲每一個存儲桶存儲 log log n 個比特。因爲咱們能夠將 log log n 比特中的每一個估算值都存儲起來,因此就算咱們建立大量的存儲桶,最終也只會使用很是少的內存。在進行大規模數據操做時,這麼小的內存佔用十分重要。要合併兩個估算,咱們會合並每一個桶,而後取平均值。所以,若是咱們打算進行合併操做,咱們應該保留每一個數據桶中首位零的最大值。

還有什麼?

爲了提升估算的準確性,HLL 還作了一些其餘的事情,不過觀察比特模式和隨機平均仍然是 HLL 的關鍵點。在這些優化以後,HLL 可使用 1.5 kB 的內存來估算數據集的基數,其典型錯誤率爲2%。固然,若是用更多的內存,能夠提升準確度。咱們不會詳細介紹其餘步驟,網上有關 HLL 的內容有不少不少。

分佈式系統中的HLL

正如咱們提到的,HLL 具備可加性的特質,這意味着,你能夠將數據集分爲幾個部分,使用 HLL 算法對其分別操做,查找每一個部分的去重元素數量。而後,不用回顧原始數據,你也能夠有效地合併中間的 HLL 結果、查找全部數據的去重元素數量。

若是處理大規模數據,並將數據保存在不一樣的物理機器中,那麼,無需將整個數據拖放到一個位置,你就可使用 HLL 來計算全部數據的去重計數。實際上,Citus 能夠幫你作這個操做。有一個爲 PostgreSQL 開發的 HLL 擴展包,它與 Citus 徹底兼容。若是你已經安裝了 HLL 擴展包,而且想在分佈式數據表上運行 COUNT(DISTINCT)查詢,Citus 會自動啓用 HLL。配置後,你不須要額外作任何事情。

使用 HLL

創建

要使用 HLL,咱們將使用 Citus Cloud 和 GitHub 事件數據集。你能夠從這裏看到並瞭解更多有關 Citus Cloud 的信息。假設你建立了你的 Citus Cloud 實例,並經過 psql 與它鏈接,你能夠經過下面這個建立 HLL 擴展:

CREATE EXTENSION hll;複製代碼

你應該在主節點和從節點建立擴展。 而後經過設置 citus.count_distinct_error_rate 配置值來啓用計數不一樣的近似值。當配置值設置的較低時,能夠提供更準確的結果,但須要更長的時間和更多的內存進行計算。 咱們建議將其設置爲0.005。

SET citus.count_distinct_error_rate TO 0.005;複製代碼

與以前在博客中用到的不一樣,咱們將只使用 github_events 表 和 large_events.csv 數據集;

CREATE TABLE github_events
(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    user_id bigint,
    org jsonb,
    created_at timestamp 
);

SELECT create_distributed_table('github_events', 'user_id');

\COPY github_events FROM large_events.csv CSV複製代碼

例子

在分發了數據表格後,咱們可使用常規的 COUNT(DISTINCT)查詢來找出多少去重用戶建立了事件:

SELECT
    COUNT(DISTINCT user_id)
FROM
    github_events;複製代碼

它應該返回相似這樣的東西:

count
--------
 264227

(1 row)複製代碼

看起來,這個查詢與 HLL 沒有任何關係。 可是,若是你將 citus.count_distinct_error_rate 設置爲大於0,併發出 COUNT(DISTINCT)查詢,Citus 就會自動使用 HLL。對於這種簡單的用例,你甚至不須要更改查詢。建立事件的用戶,準確的去重計數是 264198,因此咱們的錯誤率略大於 0.0001。

咱們也可使用約束條件來過濾掉一些結果。 例如,咱們能夠查詢建立 PushEvent 的去重用戶數量:

SELECT
    COUNT(DISTINCT user_id)
FROM
    github_events
WHERE
    event_type = ‘PushEvent'::text;複製代碼

它會返回:

count
--------
 157471

(1 row)複製代碼

相似地,該查詢的準確去重計數是157154,咱們的錯誤率略大於0.002。

結論

若是在 Postgres 中,你有關於count (distinct)伸縮性的問題,能夠看一下 HLL,若是足夠近似的計數對你來講可行,這就可能會頗有用。 關於「使用 Citus 進一步擴展計數事件」,若是你有任何問題,請與咱們聯繫


翻譯若有不當之處,歡迎指正。歡迎探討技術問題。

原文連接:citusdata

相關文章
相關標籤/搜索