【テクニカル・上級編】 クエリ実行計画の分析 – Cloud Spanner

Cloud Spanner:実行計画の「深淵」を読み解く ― 分散データベースの挙動を支配せよ

Cloud Spannerを「ただのマネージドRDBMS」だと考えているなら、それは大きな誤解だ。Spannerは巨大な分散システムであり、そのクエリ実行計画は単なるSQLの翻訳結果ではない。それは、地理的に分散された数千のノード間で、いかにして最小のレイテンシでデータをマージし、一貫性を担保するかという「戦術の設計図」である。

本稿では、Google Cloudコンソールの実行計画ビューを、ただ眺めるのではなく「解剖」する視点を授ける。

—

1. 実行計画は「コストの見積もり」ではない。「データの旅路」である

多くのエンジニアが犯す間違いは、クエリ実行計画を「どこにインデックスが効いているか」を確認するためだけのツールとして使うことだ。否、真に見るべきは「データがどこで物理的に結合され、どのノードがボトルネックになっているか」である。

Spannerのクエリ実行計画において、最も注意すべきは以下の2つのオペレータだ。

Distributed Union

これは「スプリット(Split)を跨いだ並列処理」の司令塔である。

  • 深淵の知見: `Distributed Union` が頻出する場合、クエリが複数のスプリットに散らばっていることを意味する。データが適切にシャーディングされていない、あるいはWHERE句の絞り込みが甘い証拠だ。もし `Distributed Union` の子ノードとして `Full Table Scan` が現れたら、そのクエリは数百万行のレコードをネットワーク越しに引きずり回している。即座にインデックス戦略を見直せ。

Distributed Cross Apply

ネステッドループ結合の「分散版」だ。

  • 深淵の知見: 外部テーブルの各行に対して、内部テーブルを検索しに行く。これが実行計画の深い階層に出現すると、指数関数的にネットワークI/Oが増大する。「N+1問題」の分散DB版と言えば分かりやすいだろう。可能な限り `Hash Join` や `Apply` の回数を減らすデータモデル設計(インターリーブテーブルの活用など)が必須となる。

—

2. スキャンタイプを支配する:Index Scan vs. Table Scan

Spannerにおいて、「なぜ全件スキャンが走るのか」を理解するには、「ストリームの物理的レイアウト」を想像する必要がある。

  • Index Scan: インデックスはそれ自体が単独のテーブルとして物理的に格納されている。インデックスを選択するということは、必要なカラムが全てインデックス内に含まれている(Covering Index)状態を目指すことだ。もしIndex Scanの後に `Backfetch` が発生しているなら、インデックスは役に立っていない。インデックスに足りないカラムを追加し、I/Oをインデックス内だけで完結させろ。
  • Table Scan: 避けるべき悪ではない。しかし、頻出する場合は「データモデリングの敗北」を示唆する。Spannerは主キー順に物理ソートされている。`ORDER BY` を伴うクエリで `Sort` オペレータが計画に現れるなら、それは主キー設計がクエリのアクセスパターンと不整合を起こしているサインだ。

—

3. メモリ最適化:`Memory Limit Exceeded` を防ぐ極意

Spannerのクエリ実行エンジンは、各ノードのメモリ制約内で動作する。大規模なハッシュ結合やソートが発生すると、即座にメモリ溢れ(またはレイテンシの急騰)を引き起こす。

実行計画の読み方(Memory観点)

— 実行計画の例
+– SerializeResult
+– Hash Join (Build: Table B, Probe: Table A)
+– Filter (Table A)
+– Filter (Table B)

  • 知見: `Hash Join` の `Build` 側が大規模なデータセットである場合、それはメモリの地雷だ。Spannerはハッシュテーブルをメモリ上に構築する。このサイズがノードの許容値を超えると、パフォーマンスは劇的に悪化する。
  • 対策: 実行計画上で「どのテーブルがBuild側になっているか」を確認せよ。サイズが小さい方をBuild側に持ってくるよう、クエリのヒント(`JOIN_METHOD`)を強制することも、時にはシニアエンジニアの務めだ。

—

4. 伝説のアーキテクトが教える「チューニングの哲学」

最後に、チューニングにおいて最も重要な原則を伝える。

1. 「ローカル性」を最大化せよ:
可能な限り、同じスプリット内、あるいは同じサーバ内での処理で完結させろ。インターリーブテーブルを駆使し、親子関係にあるデータを物理的に隣接させる。これがSpannerにおける最強のパフォーマンスチューニングである。

2. 「実行計画の差分」を見ろ:
クエリを少し変えた時、実行計画のどのノードが `Distributed` から `Local` に変わったか。あるいは `Hash Join` が `Merge Join` に変わったか。この変化の理由を論理的に説明できないコードは、本番に投入するな。

3. 過剰な正規化を疑え:
RDBMSの教科書的な正規化は、分散DBにおいては「ネットワークの分断」を意味する。計算コストとストレージコスト、どちらを支払うか。Spannerのスケール性を活かすなら、時には冗長性を持たせた非正規化(列の重複保持)を恐れるな。

結び

Spannerの実行計画は、隠された「物理的な真実」を饒舌に語りかけてくる。エラーログや遅延通知を眺めるだけではなく、実行計画のツリー構造を読み解き、ノード間を駆け巡るバイト列の重みを想像せよ。

その先にあるのは、ただ動くシステムではない。「極限まで最適化された、止まることのない分散エンジン」である。

アーキテクトとして、君たちが書くそのクエリが、巨大なSpannerの海で優雅に泳ぐことを期待している。

コメント

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