確率的データ構造(Bloom・HLL・Count-Min)
テラバイトの基数や頻度を、正確な集計の数千分の一のメモリで概算できます。誤り率を数式で保証しつつ、分散パーティションをそのまま足し合わせられる集約スケッチの原理を、ビッグデータ集計の観点でつかめます。
- 1.確率的データ構造(スケッチ)は、正確な集計に必要な線形メモリを、制御された誤り率と引き換えに定数〜対数サイズへ圧縮する。Bloomは存在判定(偽陽性のみ)、HyperLogLogは基数(ユニーク数)推定、Count-Minは頻度推定を担い、いずれも巨大データを1パスかつ有界メモリで概算する。
- 2.分散集計での本質的な利点は加法性(mergeability)。各パーティションで独立に作ったスケッチを、シャッフルで大きな中間データを運ばずに定数サイズのまま結合できる。HLLはレジスタごとの最大値、Count-Minはセルごとの加算、Bloomはビット単位のORで、部分集約の統合が損失なく成立する。
- 3.誤りは片側かつ定量化できる。Bloomは偽陰性を出さず偽陽性率は約 (1 - e^(-kn/m))^k、HLLの相対標準誤差は約 1.04/√m、Count-Minは幅wと深さdで過大方向の誤差を ε=e/w・確率 1-δ(δ=e^(-d))で抑える。メモリを増やすほど誤差が縮む交換関係を、用途ごとに設計値として選ぶ。
なぜ「正確」を捨てると桁で軽くなるのか
大規模データの集計には、避けがたいメモリの下限があります。「このデータセットに含まれるユニークなユーザー数は」を厳密に答えるには、原理的に見た要素をすべて覚える必要があり、要素数nに対しメモリはΩ(n)で増えます。10億ユニークを正確に数えれば、それだけで数十GBのハッシュ集合を抱えることになる。分散基盤ではこれが二重に痛い。各ノードのメモリを食うだけでなく、部分結果を統合するとき巨大な集合をシャッフルで運ぶ羽目になるからです。
確率的データ構造(sketch)は、この下限を答えを近似に緩めることで回避します。厳密な集合の代わりに、値のハッシュから抽出した統計量だけを固定サイズの配列に畳み込む。得られるのは正確な数ではなく、誤差が数式で保証された推定値です。ダッシュボードの「アクティブユーザー数」や「上位検索語」に、小数点以下の正確さは要りません。数%の相対誤差を許すだけで、メモリがKB〜数十KBの定数に落ちる。これがスケッチの取引です。
本稿が扱う三つのスケッチは、ビッグデータ集計で効く性質を共有します。(1) 1パス:ストリームを一度なめるだけで更新でき、データを保持し直さない。(2) 有界メモリ:入力量に関係なくサイズが固定(HLLは対数的に微増)。(3) 加法性(mergeable):部分ごとに作ったスケッチを結合でき、しかも結合結果が「全体を1つのスケッチで作ったもの」と一致する。この三点が、バッチとストリームの双方で同じ集約器を使い回せる理由です。
ブルームフィルタ:ビット配列に「集合の影」を焼く
ブルームフィルタは存在判定に特化した集合です。「この要素は集合に入っているか」に、入っていない(確実)またはたぶん入っている(偽陽性あり)で答えます。実体はmビットの配列とk個の独立なハッシュ関数だけ。
- 追加:要素xについて
h1(x)..hk(x) mod mのk箇所のビットを1に立てる。 - 問い合わせ:同じk箇所を見て、すべて1なら「たぶん有る」、1つでも0なら「確実に無い」。
要素そのものは一切保存しないので、偽陰性は原理的に起きません(追加時に立てたビットが勝手に0へ戻ることはない)。一方、無関係な要素群がたまたまk箇所すべてを他の要素で埋めていれば、偽陽性が出ます。
m=16, k=3 のビット配列
add("A"): h→ 2, 7,13 を 1 に
add("B"): h→ 5, 7, 9 を 1 に
query("A"): 2,7,13 すべて1 → たぶん有る(真陽性)
query("Z"): 5,9,13 が偶然すべて1 → たぶん有る(偽陽性! Zは未追加)
query("Q"): 4 が 0 → 確実に無い
偽陽性率は解析的に出ます。n要素を入れた後、あるビットが0のままの確率は (1 - 1/m)^(kn) ≈ e^(-kn/m)。問い合わせのk箇所がすべて1になる確率、すなわち偽陽性率pは概ね p ≈ (1 - e^(-kn/m))^k。ここから設計式が導けます。目標pと想定nに対し、必要ビット数は m ≈ -n·ln(p)/(ln 2)^2、最適なハッシュ本数は k ≈ (m/n)·ln 2。例えば偽陽性1%なら1要素あたり約9.6ビット、0.1%でも約14.4ビットで足り、要素の実サイズと無関係に一定なのが要点です。
分析基盤でブルームフィルタが効くのは、高価なI/Oの前段フィルタとしてです。LSM系ストレージやParquetは、各データブロックに含まれるキーのブルームフィルタを添えておき、点問い合わせで「このブロックに絶対に無い」と即断できればブロックのGET自体を省きます。分散結合でも、片側テーブルのキー集合をブルームフィルタ(Bloom join / runtime filter)にして相手側へ配り、結合相手になり得ない行を読む前に落とす。偽陰性が無いので枝刈りが安全(消してよい行だけを消す)なのが決定的で、偽陽性はたまに無駄読みが混じるだけで正しさは壊しません。複数パーティションのフィルタはビットORで結合できます。
HyperLogLog:基数を「最長の先頭ゼロ列」から当てる
「ユニーク数(基数, cardinality)」の推定はHyperLogLog(HLL)の担当です。発想の核は確率です。各要素をハッシュして一様なビット列にすると、先頭に連続する0がρ個続く確率は 2^(-(ρ+1))。多数の異なる要素を流せば、いつかは長い先頭ゼロ列が現れる。観測した最長の先頭ゼロ列がρ_maxなら、見たユニーク数はおおよそ 2^ρ_max と当たりを付けられます。1つのカウンタは分散が大きすぎるので、HLLはハッシュの先頭数ビットで要素を m = 2^p 個のレジスタへ振り分け、各レジスタが担当分の最長先頭ゼロ列を保持し、全レジスタの推定を調和平均でならして分散を潰します。
m = 4 レジスタ(先頭2ビットでバケット選択、残りで先頭ゼロ数ρを測る)
要素ごとに: b = 先頭2ビット, ρ = 残りビットの (先頭0連続数 + 1)
register[b] = max(register[b], ρ) ← 最大値だけ更新
推定基数 ≈ α_m · m^2 / Σ_j 2^(-register[j]) (調和平均ベース)
精度はレジスタ数だけで決まり、相対標準誤差は約 1.04/√m。p=14(m=16384レジスタ)なら誤差は約0.8%で、各レジスタは6ビットあれば足りるので全体で約12KB。これで基数が数十億でも同じサイズで概算できます。基数がmに比して小さい領域では推定が偏るため、実務のHLL++は小基数域を線形カウンティングへ切り替え、疎表現でメモリをさらに節約します。
MPPデータウェアハウスやTrino/BigQueryの APPROX_COUNT_DISTINCT の実体はHLLです。正確な COUNT(DISTINCT) は、各ノードが見たユニーク値の集合を最終段へ集めて重複排除する必要があり、高カーディナリティ列ではこの集合が巨大化してシャッフルとメモリを圧迫します(MPPの集約が詰まる典型)。HLLなら各ノードは12KB程度のレジスタ列を持つだけで、統合は次項の通り定数サイズ。「日次UUを何%かの誤差で、桁違いに安く」という要件に噛み合います。厳密な課金・監査用途には正確なDISTINCTを、探索的な集計にはHLLを、と用途で分けます。
Count-Min:頻度を「複数ハッシュの最小値」で読む
Count-Minスケッチは各要素の出現頻度(および上位頻出=ヘビーヒッター)を推定します。構造はd行×w列のカウンタ二次元配列と、行ごとに1つずつ、計d個の独立なハッシュ関数。
- 更新:要素xの出現時、各行iで
count[i][hi(x) mod w]を1増やす(d箇所を加算)。 - 問い合わせ:同じd箇所を見て、その最小値を頻度推定とする。
なぜ最小値か。ハッシュ衝突は必ずカウンタを増やす方向にしか働かない(他の要素が同じセルに乗ると値が水増しされる)ため、各行の推定は真の頻度以上になります。d個の独立な推定のうち最小のものが、最も衝突汚染の少ない値。ゆえにCount-Minの誤差は**常に過大側(片側)**で、過小評価は起きません。
d=2 行 × w=5 列
add("A") 3回, add("B") 1回, hがぶつかると同一セルを共有
行0: [ .. A+B=4 .. ] ← A,B が同じ列に衝突
行1: [ .. A=3 .. B=1 .. ] ← 別の列に分離
query("A") = min(4, 3) = 3 ← 正しい(過大側の4を最小で回避)
誤差保証は明快です。幅wと深さdに対し、加法的な過大誤差は総更新回数Nに比例して 推定 ≤ 真値 + ε·N、ここで ε = e/w、この保証が破れる確率は δ = e^(-d)。つまり幅wが誤差の大きさを、深さdが失敗確率を独立に制御します。例えば w=2000, d=5 なら約40KBで、ε≈0.14%・信頼度 1 - e^(-5)≈99.3%。頻度の高い要素ほど相対誤差は小さく、ヘビーヒッター検出に向く一方、低頻度要素は衝突ノイズに埋もれやすい点が構造的な限界です。
加法性:スケッチが分散集計に噛み合う本当の理由
三つのスケッチが分析基盤で価値を持つ決め手は、精度以上に加法性(mergeability)です。分散集計は「各パーティションで部分集約し、結果を統合する」形を取りますが、素朴な正確集計では中間状態(ユニーク値集合など)が大きく、統合段のシャッフルが律速になります。スケッチは中間状態が定数サイズで、しかも単純な要素ごとの演算で損失なく結合できます。
| スケッチ | 結合演算 | 結合の可換・結合則 | 分散集計での意味 |
|---|---|---|---|
| Bloom | ビット単位のOR | 成立(同じ m,k が前提) | パーティションのフィルタを統合してキー集合を近似 |
| HyperLogLog | レジスタごとの max | 成立(同じ p が前提) | 部分基数を12KB定数のまま全体基数へ |
| Count-Min | セルごとの加算 | 成立(同じ w,d,ハッシュが前提) | 部分頻度表を足すだけで全体頻度・上位語へ |
この加法性は代数的にはMapReduceのcombinerが要求する結合則・交換則そのものです。だからスケッチは、map側の事前集約でも、reduce側の統合でも、同じ演算で安全に畳み込める。Sparkのパーティションごとに部分スケッチを作り treeAggregate で二分木状に統合しても、結果は全データを1つのスケッチに通したものと一致します。さらに定数サイズゆえに保存して後から結合できる。日次のHLLを保存しておけば、任意期間の週次・月次UUを、生データを読み直さずレジスタのmaxだけで算出できます。これが正確なDISTINCTでは(日次のユニーク集合和が必要で)成り立たない、スケッチ特有の運用上の強みです。
加法性には前提があります。同じ構造パラメータで作られたスケッチ同士しか結合できません。ビット幅mの違うブルームフィルタ、精度pの違うHLL、幅・深さ・ハッシュ関数の違うCount-Minは、ビットやセルの意味がずれるため単純結合が成り立ちません。基盤全体でパラメータを固定するのが鉄則です。加えて誤り解析はいずれもハッシュが理想的に一様という仮定に乗っています。質の悪いハッシュや、Count-Minで独立でないハッシュ族を使うと、衝突が偏って誤差の理論保証(Count-Minならε=e/w、δ=e^(-d))が崩れます。実装では暗号学的でなくとも分布の良い高速ハッシュ(MurmurHash・xxHash等)と、Count-Minでは互いに独立なシードを用います。
トレードオフの設計:どの誤りを、いくら許すか
三つは「近似で軽くする」点は同じでも、答える問い・誤りの向き・サイズの決定要因が異なります。設計とは、用途ごとに「許せる誤りの種類と量」を選び、それに対応するパラメータを逆算する作業です。
| 観点 | ブルームフィルタ | HyperLogLog | Count-Min |
|---|---|---|---|
| 答える問い | 存在するか(集合の所属) | ユニーク数(基数) | 各要素の頻度・上位頻出 |
| 誤りの向き | 偽陽性のみ(偽陰性なし) | 両側の相対誤差 | 過大のみ(過小なし) |
| 主な誤差式 | p≈(1-e^(-kn/m))^k | 相対誤差≈1.04/√m | 誤差≤ε·N, ε=e/w, δ=e^(-d) |
| サイズの決定要因 | 想定要素数nと目標p | レジスタ数 m=2^p のみ | 幅w(誤差)×深さd(失敗確率) |
| 削減する対象 | 無駄なI/O・結合の枝刈り | DISTINCTの集合とシャッフル | 頻度表の全カウンタ保持 |
いずれも「メモリを増やすほど誤差が縮む」交換関係を持ちますが、縮み方が違います。ブルームとCount-Minは誤差が要素数・更新回数に相対的で、想定規模nやNを見積もってサイズを取る必要があります(見積もりを超えると劣化する)。対してHLLは入力規模に依存せず、レジスタ数mだけで相対精度が決まる——数千でも数十億でも同じ12KBで同じ0.8%というスケール非依存性が、基数推定でHLLが定番化した理由です。ストリーミングで長時間走らせるなら、規模想定が要らないHLLの安定性は運用上も効きます。
押さえどころ。(1) 問いと担当の対応:存在=Bloom、基数=HLL、頻度=Count-Min。(2) 誤りの片側性:Bloomは偽陰性なし(枝刈りが安全)、Count-Minは過小評価なし(最小値を取る理由)、HLLは両側の相対誤差。(3) 精度式:Bloom p≈(1-e^(-kn/m))^k、HLL 1.04/√m、Count-Min ε=e/w・δ=e^(-d)。(4) 加法性:BloomはOR、HLLはmax、Count-Minは加算で結合でき、これがcombiner/treeAggregateと分散集計に噛み合う核心。(5) 前提:同一パラメータ同士しか結合できず、理論保証は一様ハッシュに依存する。「近似で線形メモリの下限を回避し、定数サイズの中間状態をシャッフルせず結合する」が全体の一文要約。
まとめ
- 確率的データ構造は、正確な集計に要る線形メモリの下限を、定量化された誤り率と引き換えに定数〜対数サイズへ落とす。1パス・有界メモリ・加法性の三点で、ビッグデータの近似集計に噛み合う。
- ブルームフィルタは存在判定に特化し、偽陰性を出さず偽陽性率は約
(1-e^(-kn/m))^k。無駄なI/Oや結合相手を読む前に安全に枝刈りする前段フィルタとして効く。 - HyperLogLogは先頭ゼロ列の確率から基数を当て、相対誤差は約
1.04/√m。約12KBで数十億ユニークを概算でき、APPROX_COUNT_DISTINCTの実体として正確なDISTINCTのシャッフル爆発を回避する。 - Count-Minはd×wカウンタと最小値読みで頻度を推定し、誤差は過大側のみ(
ε=e/w、失敗確率δ=e^(-d))。ヘビーヒッター検出に向くが低頻度要素は衝突に埋もれる。 - 分散集計での決め手は加法性:BloomはOR、HLLはmax、Count-Minは加算で、部分スケッチを損失なく結合できる。これはcombinerやtreeAggregateが要求する結合則そのもので、保存して任意期間を後から結合する運用も成り立つ。ただし同一パラメータ同士しか結合できず、保証は一様ハッシュに依存する。
データ工学 Article
確率的データ構造(Bloom・HLL・Count-Min)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
確率的データ構造
比較で見る軸
難易度: advanced / カテゴリ: データ工学 / タグ数: 6
導入後に効く点
分散集計での本質的な利点は加法性(mergeability)。各パーティションで独立に作ったスケッチを、シャッフルで大きな中間データを運ばずに定数サイズのまま結合できる。HLLはレジスタごとの最大値、Count-Minはセルごとの加算、Bloomはビット単位のORで、部分集約の統合が損失なく成立する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データ工学
- タグ数
- 6
判断チェックリスト
- 自社の用途が「確率的データ構造 / HyperLogLog」に近いか確認する。
- 強みである「確率的データ構造(スケッチ)は、正確な集計に必要な線形メモリを、制御された誤り率と引き換えに定数〜対数サイズへ圧縮する。Bloomは存在判定(偽陽性のみ)、HyperLogLogは基数(ユニーク数)推定、Count-Minは頻度推定を担い、いずれも巨大データを1パスかつ有界メモリで概算する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。