「今月のユニークユーザ数は?」「過去 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 (非現実的) |
| HyperLogLog | 16KB / 誤差 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 KB5. 実用ライブラリ 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 (レジスタ数) | メモリ | 標準誤差 |
|---|---|---|---|
| 8 | 256 | 1 KB | 6.5% |
| 10 | 1024 | 4 KB | 3.3% |
| 12 (推奨) | 4096 | 16 KB | 1.6% |
| 14 | 16384 | 64 KB | 0.8% |
| 16 | 65536 | 256 KB | 0.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 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。