【実務・中級編】 分散集約処理 – Cloud Spanner

Cloud Spannerの分散集約処理:大規模データに対するGROUP BYを極限まで速くする設計と実装

こんにちは。テクニカルリードの私だ。
今日のコードレビューで、あるジュニアエンジニアが書いたクエリを見て少し気になった。数千万行のトランザクションテーブルに対して、平然と次のような集約クエリを投げていたのだ。

— ⚠️ 典型的なアンチパターン:全件を単一ノードに集約しようとする愚行
SELECT
merchant_id,
SUM(amount)
FROM transactions
GROUP BY ` merchant_id;

彼らは「Cloud Spannerはリレーショナルデータベースだから、普通に書けば勝手に最適化してくれる」と思っている。だが、それは半分正しくて半分危険だ。Spannerは分散データベースであり、物理的なデータは数千、数万のノード(スプリット)に水平分散されている。このアーキテクチャの裏側を理解せずにお気楽にクエリを書くと、「ホットスポット」「ネットワーク帯域の飽和」「CoordinatorノードのCPU枯渇」という、分散DBの三大悪夢を踏み抜くことになる。

今回は、Cloud Spannerが内部でどのように「分散集約(Distributed Aggregation)」を行っているのか、そのアルゴリズムの核心と、我々エンジニアが実務でどう設計に落とし込むべきかを叩き込む。

—

1. Cloud Spannerの分散集約アルゴリズム:2段階プレアグリゲーションの真実

まず、Spannerのクエリエンジンが裏側で何をやっているのかを解剖しよう。
Spannerの分散集約は、単にデータを一箇所にかき集めて計算するようなナイーブなものではない。基本思想は「MapReduce的な思想の、極限までレイテンシを削ぎ落としたインメモリ実装」だ。

Spannerのクエリプランナーは、集約クエリ(`GROUP BY` や `SUM` など)を検知すると、多くの場合「2段階集約(Two-phase Aggregation)」の実行計画を組み立てる。

[クライアント]
▲ (最終集約結果のみを受信:通信量最小)
│
[Root / Coordinator ノード] (最終的な統合: SUMの合算、AVGの再計算など)
▲
│ (部分集約された軽量な中間データ)
┌─────┴─────────────────────────┐
│ スプリットA スプリットB スプリットC
│ (部分集約) (部分集約) (部分集約)
└───────────────────────────────┘

フェーズ1:部分集約(Partial Aggregation / Map側)

データが物理的に保存されている各スプリット(またはそれを担当するリーダーノード)上で、ローカルに保持するデータに対してまず `GROUP BY` と集約関数の部分計算を行う。

  • `SUM(amount)` なら、スプリット内での部分和を計算。
  • `COUNT()` なら、スプリット内での部分カウントを計算。
  • 効果: ここでデータ量が劇的に圧縮される(例えば、数百万行のレコードが、ユニークな `merchant_id` の数だけの軽量な中間行に縮小される)。

フェーズ2:最終集約(Final Aggregation / Reduce側)

各スプリットから送られてきた部分集約結果を、クエリのコーディネーター役となるノード(Root)が受け取り、最終的な統合を行う。

  • `SUM` は部分和同士を足し合わせる。
  • `COUNT` も同様に足し合わせる。
  • `AVG` のような非線形な集約関数であっても、各ノードから `SUM` と `COUNT` のペアを送らせることで、正確な全体平均をコーディネーター側で算出する。

この仕組みのおかげで、「数テラバイトの生データそのものをネットワーク経由でシャッフルする」という最悪の事態が回避される。ネットワークを流れるのは、常に「集約済み・圧縮済みの中間データ」なのだ。

—

2. 実務で直面する罠:なぜあなたの集約クエリは遅いのか?

この美しい分散集約メカニズムも、設計を誤ると完全に機能不全に陥る。実務でよくある「やってはいけない設計」を挙げておこう。

罠1:インターリーブ(Interleave)構造を無視した主キー設計

もし `transactions` テーブルの主キーが単に `transaction_id` だけだったらどうなるか。`merchant_id` ごとの集約を行う際、同一マーチャントのデータが全スプリットにバラバラに散らばることになる。結果として、フェーズ1での部分集約の効率が落ち、中間データのネットワーク転送量が増大する。

【解法】
テナントやマーチャント単位で集約を頻繁に行うなら、インターリーブ構造またはプレフィックス主キーの設計が必須だ。

— 模範的なテーブル定義(パーティション化の最適化)
CREATE TABLE merchants (
merchant_id INT64,
— その他のカラム
) PRIMARY KEY(merchant_id);

CREATE TABLE transactions (
merchant_id INT64,
transaction_id INT64,
amount INT64,
created_at TIMESTAMP,
) PRIMARY KEY(merchant_id, transaction_id),
INTERLEAVE IN PARENT merchants ON DELETE CASCADE;

この設計であれば、同一 `merchant_id` のデータは物理的に同一のスプリット(または近傍)にコロケーションされやすくなり、部分集約の局所性が飛躍的に向上する。

罠2:高カーディナリティすぎる GROUP BY

`GROUP BY` の対象カラムのカーディナリティ(ユニーク値の数)が、テーブルの総行数とほぼ同等である場合(例:ミリ秒単位のタイムスタンプや、UUIDでグループ化するなど)、フェーズ1でのデータ圧縮が全く効かない。
結果として、全スプリットから膨大な中間データがコーディネーターノードに送られ、Memory Limit Exceeded や CPUスパイイク を引き起こす。

—

3. 実践:パフォーマンスを極限まで引き出すクエリ設計パターン

では、数億行規模のデータから安全かつ高速に集約を行うためのプラクティスを示そう。

パターンA:コストベースオプティマイザ(CBO)を味方につける書き方

SpannerのCBOに正しい統計情報を与え、確実に2段階集約を選択させるためには、無駄な関数をラップさせないことだ。

— 良い例:オプティマイザが容易に部分集約を適用できるシンプルさ
SELECT
merchant_id,
SUM(amount) AS total_amount,
COUNT(1) AS txn_count
FROM transactions
WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY merchant_id;

ここで、もし `WHERE` 句の条件がインデックスのプレフィックスと一致していれば、スキャン自体が高速化され、その後の分散集約フェーズへ極めてクリーンに移行する。

パターンB:マテリアライズドビュー(MV)による事前集約

リアルタイム性がそこまでシビアではなく(数秒〜数分の遅延が許容される)、ダッシュボード等で常に重い集約結果を返す必要がある場合、アドホックなクエリに頼るべきではない。マテリアライズドビューを使うのが、Spannerにおける最高峰の解だ。

— マテリアライズドビューの定義
CREATE MATERIALIZED VIEW MerchantDailySales
AS
SELECT
merchant_id,
DATE(created_at) AS sale_date,
SUM(amount) AS daily_sum,
COUNT() AS daily_count
FROM transactions
GROUP BY merchant_id, sale_date;

なぜこれが最強なのか?
分散集約処理そのものを「バックグラウンドのデータ更新時(あるいは非同期のビューメンテナンス時)」に事前完了させておくアプローチだ。クライアントからクエリが飛んできた時、Spannerは分散集約の計算をリアルタイムで行う必要すらなく、単に事前計算済みのビューからデータを一撃で引いてくるだけで済む。レイテンシはミリ秒単位に落ちる。

—

4. チーフアーキテクトからの最終インスペクション

Cloud Spannerにおける分散集約は、データベースエンジンが裏側でどれだけ頑張ってくれても、「元データの物理配置(主キー設計)」と「クエリの絞り込み(WHERE句の効率)」が間違っていれば破綻する。

設計レビューにおいて、以下のチェックリストをクリアしているか確認してほしい。

1. GROUP BY の対象カラムは、物理データ配置(主キー・インターリーブ)と調和しているか?
2. 集約前のフィルタリング(WHERE)によって、スキャン対象データが十分に削ぎ落とされているか?
3. アドホックな集約クエリの乱用になっていないか?高頻度で実行される重い集約はマテリアライズドビューにオフロードされているか?

分散データベースの本質は「隠蔽」ではなく「協調」だ。Spannerの分散集約アルゴリズムの挙動を脳内にインストールし、エンジンが最も効率よく並列計算できる舞台を整えてやるのが、我々シニアエンジニアの仕事である。

次のコードレビューでは、無駄な全件シャッフルを引き起こすクエリがないか、厳しく見させてもらう。健闘を祈る。

コメント

タイトルとURLをコピーしました