ふくふくHukuhuku Inc.
EP.03Algorithms 10分公開: 2026-05-11

HyperLogLog:BigQuery が 1 兆件のユニーク数を 16KB で数えるアルゴリズム

COUNT(DISTINCT) は本来メモリ大食い、1 兆件のユニーク数なら数百 GB 必要。これを 16KB・1% 誤差で実現するのが HyperLogLog。Python で実装し、実機で 1.01% 誤差を確認。

#HyperLogLog#確率的アルゴリズム#BigQuery#COUNT DISTINCT
CO📔 Google Colab で開く(上から順にセルを実行)
シェア

「今月のユニークユーザ数は?」「過去 1 年のユニーク商品数は?」「広告クリックのユニーク IP は?」。これらの COUNT(DISTINCT) クエリ は、データ規模が大きくなるとメモリと時間の暴れ馬になる。1 兆件のユニーク数を 16KB のメモリで 1% 誤差で求める 方法が HyperLogLog (HLL)

1. これで何が動いているか

  • Google BigQuery: `APPROX_COUNT_DISTINCT()` の中身が HLL
  • Snowflake: `APPROX_COUNT_DISTINCT()`
  • Amazon Redshift: `APPROXIMATE COUNT(DISTINCT ...)`
  • PostgreSQL: `postgresql-hll` 拡張、Citus / Timescale
  • Redis: `PFADD / PFCOUNT` (HyperLogLog コマンド)
  • Druid / Clickhouse: 内蔵
  • 広告計測 (Google Analytics 等): 大規模ユニーク数集計

2. なぜ正確な COUNT(DISTINCT) はダメなのか

手法1 億ユニーク1 兆ユニーク
ハッシュセット (正確)メモリ 2-4GBメモリ 20-40TB (非現実的)
HyperLogLog16KB / 誤差 1%16KB / 誤差 1%

3. 仕組みの直感

  • 各要素を ハッシュ関数で固定長 (例: 64 bit) にする
  • ハッシュ値の 末尾に 0 が連続する数 (ρ) を観測
  • 「ハッシュ値の末尾に 0 が k 個連続する確率は 1/2^k」、よって「最大 k 個の連続 0 を観測 = 約 2^k ユニーク要素」
  • 1 個のレジスタだとブレが大きいので、ハッシュの先頭ビットで複数レジスタに分散、各レジスタで最大 ρ を保持
  • 全レジスタの調和平均で集計、α (補正項) を掛けて推定値を出す
なぜ末尾 0 で数が分かる?

: コインを連続して投げて表が k 回連続する確率は 1/2^k。10 回連続表が出たら、約 1024 回試行している と推定できる。HLL はこれをハッシュ値で実現。

4. Python 実装 (動作確認済)

純粋 Python の HyperLogLog 実装
Python
import hashlibimport math
class HLL:    def __init__(self, b=12):        """b=12 → m=4096 レジスタ → 誤差 約 1.6%"""        self.b = b        self.m = 1 << b   # レジスタ数 (2^b)        self.registers = [0] * self.m
    def add(self, item):        # 64 bit ハッシュ        h = int(hashlib.sha256(str(item).encode()).hexdigest(), 16)        # 先頭 b bit でレジスタ index、残り bits で ρ (末尾 0 の連続数+1)        idx = h & (self.m - 1)        w = h >> self.b        rho = (w & -w).bit_length() if w else 64        # 各レジスタは観測した最大 ρ を保持        self.registers[idx] = max(self.registers[idx], rho)
    def count(self):        # 調和平均で推定        alpha = 0.7213 / (1 + 1.079 / self.m)        z = sum(2 ** -r for r in self.registers)        e = alpha * self.m * self.m / z        # 小規模補正 (linear counting)        if e <= 2.5 * self.m:            zeros = self.registers.count(0)            if zeros:                e = self.m * math.log(self.m / zeros)        return int(e)
    def merge(self, other):        """2 つの HLL を結合 (UNION の正確な実装)"""        assert self.m == other.m        self.registers = [max(a, b) for a, b in zip(self.registers, other.registers)]
# 動作確認hll = HLL(b=12)true_unique = 100_000for i in range(true_unique):    hll.add(f"user_{i}")
estimated = hll.count()print(f"True: {true_unique}, Estimated: {estimated}")print(f"Error: {abs(estimated - true_unique) / true_unique * 100:.2f}%")print(f"Memory: {hll.m * 4 / 1024:.1f} KB")
# 実機実行結果:# True: 100000, Estimated: 98985# Error: 1.01%# Memory: 16.0 KB

5. 実用ライブラリ datasketch

datasketch (本番運用向け)
Python
# pip install datasketchfrom datasketch import HyperLogLog
hll = HyperLogLog(p=12)for i in range(100_000):    hll.update(f"user_{i}".encode("utf-8"))
print(f"Estimated: {int(hll.count())}")
# 結合 (異なるサーバの集計を統合)hll1 = HyperLogLog(p=12)hll2 = HyperLogLog(p=12)for i in range(50_000):     hll1.update(f"u{i}".encode())for i in range(30_000, 80_000):  hll2.update(f"u{i}".encode())
merged = HyperLogLog(p=12)merged.merge(hll1)merged.merge(hll2)# True unique = 80,000 (重複部分を考慮)print(f"Merged estimate: {int(merged.count())}")

6. 精度とメモリのトレードオフ

b (レジスタ指数)m (レジスタ数)メモリ標準誤差
82561 KB6.5%
1010244 KB3.3%
12 (推奨)409616 KB1.6%
141638464 KB0.8%
1665536256 KB0.4%

7. 重要な特徴:マージ可能

HLL の最大の魅力は「結合 (マージ) 可能」レジスタ単位で max を取るだけ で、正確な誤差で複数の HLL を統合できる。これにより:

  • サーバ別の HLL → 全体の HLL (分散集計)
  • 日次 HLL → 月次 HLL (時系列集計)
  • サイト別 HLL → 全社 HLL (組織横断集計)
  • 「フィルタ A 適用済み HLL」と「フィルタ B 適用済み HLL」を後で統合

8. 業務での使い方

BigQuery / Snowflake での使用例
SQL
-- BigQuery: APPROX_COUNT_DISTINCTSELECT  date,  APPROX_COUNT_DISTINCT(user_id) AS unique_usersFROM eventsWHERE date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)GROUP BY date;
-- Snowflake / Redshift も同様の関数-- 通常の COUNT(DISTINCT) の 10-100 倍速い
-- HLL_SKETCH を使った高度な集計 (BigQuery)WITH daily AS (  SELECT    date,    HLL_COUNT.INIT(user_id) AS sketch  FROM events  GROUP BY date)SELECT HLL_COUNT.MERGE(sketch) AS unique_30daysFROM daily;

9. メリットとデメリット

観点HLL正確な COUNT(DISTINCT)
メモリ固定 (16KB)ユニーク数に比例 (GB 単位)
速度O(1) per add、並列化容易ハッシュテーブル管理コスト
精度1.6% 誤差 (b=12)100%
マージ可能 (高速)全データ再走査
追加削除削除は不可可能
用途大規模ユニーク数推定小規模・正確性必須

10. 代替・関連アルゴリズム

  • LogLog: HLL の前身、精度は劣るが直感的
  • Linear Counting: 小規模で精度高い (HLL は小規模時に LC に切替)
  • Theta Sketch (Yahoo): HLL より精度高め、マージ可能
  • KMV (k-Minimum Values): 別のアプローチ、HLL より直感的

11. 関連 OSS / 製品

  • datasketch (Python): HLL / MinHash / Theta Sketch
  • postgresql-hll: PostgreSQL 拡張
  • Apache DataSketches (Yahoo): Java / C++、本番向け
  • stream-lib (Java): HLL / CMS / Bloom

12. 参考論文

  • Flajolet et al. (2007) "HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm" — 元論文
  • Heule et al. (2013, Google) "HyperLogLog in Practice" — 実用化のための補正、BigQuery の元

13. 次の話

EP.04 では Count-Min Sketch を扱います。Twitter のリアルタイムトレンド・Google Trends の中で動く確率的頻度推定アルゴリズム。HLL と並ぶ「Streaming Algorithm」の代表作。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp