- クエリがステージ(stage)に分割され、スロットとシャッフルを使って分散実行される仕組み
- 実行グラフ(execution graph)を読み、どのステージがボトルネックかを特定する方法
- JOINの肥大化・スキュー・SELECT * といった典型的なアンチパターンの見分け方と対処
- クエリキャッシュ・マテリアライズドビュー・BI Engine・検索インデックスの使い分け
- スロット使用量とキャッシュヒットを INFORMATION_SCHEMA.JOBS で確認する方法
前提: 本章のハンズオンはすべてサンドボックスで完走可(BI Engine と検索インデックスは予約・課金が前提の機能のため、本章では読み物として扱います)
クエリ実行の内部構造 — ステージ・スロット・シャッフル
BigQuery はクエリを受け取ると、SQL を実行グラフ(execution graph)に変換します。実行グラフは複数のステージ(stage)で構成され、各ステージは READ・COMPUTE・FILTER・SORT・AGGREGATE・JOIN・WRITE などのステップ(step)の並びでできています。BigQuery は分散並列アーキテクチャで複数ステージを並列実行し、遅延を減らします。
各ステージの内部では、スロット(slot)という仮想的な計算単位が作業単位(work unit)を並列に処理します。スロットは CPU・メモリ・I/O を抽象化した会計単位で、ジョブ全体のコストは totalSlotMs(スロットミリ秒の合計)として集計されます。第 7 章の on-demand(処理バイト課金)と capacity-based(スロット予約課金)の違いは、このスロットの調達方法の違いに過ぎません。
ステージ同士はシャッフル(shuffle)という高速な分散メモリ機構でデータをやり取りします。GROUP BY や JOIN のように同じキーの行を同じワーカーに集める処理では、シャッフルの WRITE ステップで HASH パーティショニング(キー列のハッシュ値でデータを再配置すること)が行われます。転送量が大きくシャッフルメモリに収まらないと、ディスクへのフォールバック(shuffleOutputBytesSpilled)が発生し性能が劣化します。BigQuery は実行中でもプランを動的に調整でき、偏りを平準化する REPARTITION ステージを追加することもあります。
実行グラフの読み方
BigQuery Studio でクエリを実行すると、結果の下に「実行の詳細」ボタンが表示されます。「実行グラフ」タブを開くと、中央にステージをノード・シャッフル転送量をエッジとするグラフが描かれます。ノードはスロット時間で色分けされ、色が濃い赤ほど相対的に多くのスロット時間を消費していることを示します。左パネルはクエリ本文のヒートマップ、右パネルは選択したステージ・ステップの詳細統計です。
各ステージには、平均ワーカーと最も遅いワーカー(worst-case)の両方について、次の 4 種類の時間比率が記録されます。
| 指標 | 意味 | 見るポイント |
|---|---|---|
| Wait Ratio | スケジューリング待機に費やした時間の割合 | 高い場合はスロット不足・他ジョブとの競合を疑う |
| Read Ratio | 入力データ(テーブルまたはシャッフル)の読み取りに費やした時間の割合 | 未パーティションの大規模スキャンやフィルタの効きの悪さを疑う |
| Compute Ratio | CPU で式評価・関数実行・フィルタ判定を行った時間の割合 | 平均(avg)に対して最大(max)が突出していればスキューを疑う |
| Write Ratio | 出力(次のステージへのシャッフル、または最終結果)の書き込みに費やした時間の割合 | シャッフル出力バイト数(shuffleOutputBytes)が大きすぎないか確認 |
このほか recordsRead / recordsWritten(入出力レコード数)、parallelInputs(並列作業単位数)、inputStages(依存する先行ステージの ID。JOIN ステージは左右を準備する 2 つの依存ステージを持つのが典型的)も記録されます。ステップ詳細には読み取った列名や結合パターン(後述のブロードキャスト / ハッシュ)が表示され、どの SQL 記述がどの負荷を生んでいるかを直接たどれます。
ボトルネックの型とベストプラクティス
実行グラフを読めるようになったら、次は典型的なボトルネックのパターンを覚え、実行グラフ上でそれを見分けられるようにします。
SELECT * と過剰な列読み込み
BigQuery はカラムナストレージなので、参照した列だけが課金対象になります。SELECT * は I/O と結果の具体化(マテリアライゼーション)の両方に無駄を生みます。必要な列だけを選ぶか、大半が必要なら SELECT * EXCEPT (col1, col2) で除外列を指定します。
JOIN の肥大化 — ブロードキャスト vs ハッシュ
片方のテーブル(通常は小さい方)が 1 つのスロットのメモリに収まる場合、BigQuery はそのテーブルを全ワーカーに複製するブロードキャスト JOINを選びます(ステップ詳細では JOIN EACH WITH ALL)。ブロードキャストできないほど大きい場合は、両方のテーブルを結合キーでシャッフルして同じスロットに集めるハッシュ JOINになります(JOIN EACH WITH EACH)。ハッシュ JOIN はデータ移動を伴うぶん高コストです。テーブルは大きい順に並べるのがベストプラクティスです。
スキュー(データ偏り)
結合キーやグループ化キーの値の分布が偏っていると、特定のワーカーだけが大量のデータを処理する「スキュー」が起きます。あるステージの compute の最大時間が平均時間を大きく上回る場合にスキューを疑い、キーの分布を確認した上で早期フィルタやクエリ分割で対処します。
ORDER BY・DISTINCT 集計・近似集計
ORDER BY は外側のクエリかウィンドウ句の中でのみ使い、複雑な演算はクエリの最後に回します。大規模なソートには LIMIT を併用します。AGGREGATE ステップでは COUNT(DISTINCT expression) は計算コストが高く、巨大な入力では性能問題になりがちです。正確な値が不要なら、統計的な推定値を返す代わりに巨大な入力で高いパフォーマンスを発揮する APPROX_COUNT_DISTINCT(expression) を検討してください。
キャッシュ・マテリアライズドビュー・BI Engine・検索インデックスの使い分け
BigQuery には、クエリの再計算そのものを避ける、または減らすための機構が階層的に用意されています。
| 機構 | 効く場面 | 費用 | 備考 |
|---|---|---|---|
| クエリキャッシュ | 同一クエリ文字列を約 24 時間以内に再実行 | 無料(ヒット時は課金なし) | 非決定的関数・変更されたテーブル参照・ワイルドカードテーブル等を使うと無効化 |
| マテリアライズドビュー | 同じ集計パターンを繰り返しクエリ | ストレージ+増分更新の計算コスト | スマートチューニングでベーステーブルへのクエリを自動的に書き換え |
| BI Engine | ダッシュボード等からの反復的な対話的クエリ | 予約容量の課金(課金プロジェクト必須) | ベクトル化処理とインメモリキャッシュで高速化 |
| 検索インデックス | 非構造化テキスト・JSON のポイントルックアップ | リージョンの上限内は無料(ストレージ課金あり) | ビュー・マテリアライズドビュー・外部テーブルには作成不可 |
クエリキャッシュは匿名データセットに保存された結果テーブルを再利用する仕組みで、約 24 時間ベストエフォートで保持されます。非決定的関数の使用、参照先テーブルの変更、出力先テーブルの明示指定、クエリ文字列の変更(空白やコメントを含む)があると無効になります。ヒット時はジョブ統計の cacheHit が true になり課金は発生しません。
マテリアライズドビューは SQL の結果を定期的に事前計算して保存するビューです。ベーステーブルの変更は差分だけを取り込んで自動的にバックグラウンド更新されます。クエリの一部がビューで解決できる場合、BigQuery はクエリをビュー参照に自動的に書き換えます(スマートチューニング。増分更新可能なビューのみ対応)。使える集計関数は ANY_VALUE・APPROX_COUNT_DISTINCT・ARRAY_AGG・AVG・BIT_AND/OR/XOR・COUNT・COUNTIF・HLL_COUNT.INIT・LOGICAL_AND/OR・MAX/MIN・MAX_BY/MIN_BY・SUM に限られます(2026-08 時点)。JOIN は利用できますが制約があり、自己結合と RIGHT / FULL OUTER JOIN は非対応、LEFT OUTER JOIN と UNION ALL は増分更新ビューでのプレビュー対応です(マテリアライズドビューの入れ子や論理ビュー参照にも制約・プレビュー扱いがあります)。更新の鮮度とコストは max_staleness オプション(30 分〜3 日)で調整できます。
BI Engineはよく使うデータをベクトル化・辞書圧縮エンコーディングでメモリに保持し、コンソール・Data Studio(旧 Looker Studio)等の BI ツール・REST API・JDBC/ODBC 経由のクエリを高速化する一般提供サービスです。エディションの予約容量が必要で、予約に対して課金されます。検索インデックスは SEARCH 関数や = / IN / LIKE によるポイントルックアップを高速化し、あいまい検索やワイルドカードには対応しません。どちらもサンドボックスの制約や予約が絡むため、本章では位置づけの理解にとどめます。
公開データセットの Austin Bikeshare の乗車履歴を駅・利用者種別ごとに集計し、実行グラフでステージ構成を確認します。費用目安: サンドボックスで可・無料枠内(スキャン量はごく小さく、目安は数十 MB。必ず dry run で確認してください)。
- BigQuery Studio のクエリエディタで、まず dry run(右上の見積もり表示、または
bq query --dry_run)でスキャン量を確認します。 - 問題なければ次のクエリを実行します。
SELECT
start_station_name,
subscriber_type,
COUNT(*) AS trip_count,
ROUND(AVG(duration_minutes), 1) AS avg_duration_minutes
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`
GROUP BY start_station_name, subscriber_type
ORDER BY trip_count DESC
LIMIT 20;
「実行の詳細」→「実行グラフ」タブを開くと、READ → AGGREGATE(GROUP BY のシャッフル)→ SORT(ORDER BY)→ LIMIT → WRITE の並びのステージが確認できます。各ステージの Wait/Read/Compute/Write 比率と shuffleOutputBytes を見比べ、最も色濃い(スロット時間を多く消費する)ステージを確認してください。
後片付け: このハンズオンは既存テーブルへの参照のみで、作成したリソースはありません。
SELECT * のコストを dry run で比較したあと、自前の小さな駅マスタを作ってブロードキャスト JOIN の実行グラフを観察します。費用目安: サンドボックスで可・無料枠内(数十 MB程度の見込み。必ず dry run で確認)。
- 列を絞った場合と絞らない場合を dry run だけで比較します(実行はしません)。
-- dry run のみで比較する。実行しない。
SELECT * FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`;
SELECT start_station_name, duration_minutes
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`;
差を確認したら、自分のプロジェクトへデータセットを作り、駅ごとの乗車回数を集計した小さなマスタテーブルを用意します。
CREATE SCHEMA IF NOT EXISTS `PROJECT_ID.handson_ch08`
OPTIONS (location = 'US');
CREATE OR REPLACE TABLE `PROJECT_ID.handson_ch08.station_lookup` AS
SELECT
start_station_name AS station_name,
COUNT(*) AS station_trip_count
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`
GROUP BY start_station_name;
PROJECT_ID は自分のプロジェクト ID に置き換えてください。station_lookup は駅の数だけの小さなテーブルです。これを乗車履歴の大テーブルと結合します。
SELECT
t.subscriber_type,
s.station_name,
s.station_trip_count,
COUNT(*) AS trips_from_here
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips` AS t
JOIN `PROJECT_ID.handson_ch08.station_lookup` AS s
ON t.start_station_name = s.station_name
WHERE t.duration_minutes < 60
GROUP BY t.subscriber_type, s.station_name, s.station_trip_count
ORDER BY trips_from_here DESC
LIMIT 20;
JOIN ステージのステップ詳細を開くと、station_lookup 側が小さいため JOIN EACH WITH ALL(ブロードキャスト JOIN)が選ばれていることを確認できます。
後片付け: station_lookup は次のハンズオンでも使うため、章末でまとめて削除します。
同じ列への厳密な distinct 集計と近似集計のスロット消費を INFORMATION_SCHEMA.JOBS で確認します。費用目安: サンドボックスで可・無料枠内(数十 MB 程度)。
SELECT COUNT(DISTINCT duration_minutes) AS exact_distinct
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`;
SELECT APPROX_COUNT_DISTINCT(duration_minutes) AS approx_distinct
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`;
2 つのクエリを実行したら、直近のジョブ履歴を確認します。INFORMATION_SCHEMA.JOBS(別名 JOBS_BY_PROJECT)はリージョン修飾子が必須です。
SELECT
creation_time,
ROUND(total_slot_ms / 1000, 2) AS slot_seconds,
total_bytes_processed,
cache_hit,
query
FROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 10 MINUTE)
AND query LIKE '%duration_minutes%'
ORDER BY creation_time DESC
LIMIT 10;
このテーブルは行数が少なく差は小さく出ますが、total_slot_ms と total_bytes_processed はテーブルが巨大になったときの差を見積もる際にそのまま使えます。creation_time で絞り込むとパーティションプルーニングが効きます。
後片付け: このハンズオンは既存テーブル・システムビューの参照のみで、作成したリソースはありません。
小さなベーステーブルとマテリアライズドビューを作り、集計クエリが自動的にビューへ書き換えられる様子を dry run のバイト数の変化で確認します。費用目安: サンドボックスで可・無料枠内。
CREATE OR REPLACE TABLE `PROJECT_ID.handson_ch08.trips_base` AS
SELECT start_station_name, subscriber_type, duration_minutes
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`;
まずこの集計クエリを dry run し、処理見込みバイト数を控えておきます。
-- dry run で処理バイト数を控える
SELECT
start_station_name,
subscriber_type,
COUNT(*) AS trip_count,
AVG(duration_minutes) AS avg_duration_minutes
FROM `PROJECT_ID.handson_ch08.trips_base`
GROUP BY start_station_name, subscriber_type;
次にマテリアライズドビューを作成します。COUNT と AVG はどちらもマテリアライズドビューでサポートされる集計関数です。
CREATE MATERIALIZED VIEW `PROJECT_ID.handson_ch08.station_summary_mv`
OPTIONS (
enable_refresh = true,
refresh_interval_minutes = 30
) AS
SELECT
start_station_name,
subscriber_type,
COUNT(*) AS trip_count,
AVG(duration_minutes) AS avg_duration_minutes
FROM `PROJECT_ID.handson_ch08.trips_base`
GROUP BY start_station_name, subscriber_type;
初回リフレッシュ完了まで数分待ち、同じ集計クエリをもう一度 dry run してください。スマートチューニングが働けば、処理見込みバイト数がベーステーブル直読みのときより大きく減っているはずです。
後片付け: このハンズオンで作成したデータセットとハンズオン 8-2 のテーブルをまとめて削除します。
bq rm -r -f -d PROJECT_ID:handson_ch08
つまずきポイント
dry run や結果がキャッシュから返されたジョブでは、実行グラフの診断情報が含まれません。見たいときはキャッシュを使わせない(クエリ本文を変える、キャッシュ利用を外す)か、実際に処理が走ったジョブを選んでください。
クエリを実行するロケーションと INFORMATION_SCHEMA ビューのリージョンが一致していないとエラーになります。本書は US 前提のため
region-us を使いますが、別リージョンならそのリージョン名に読み替えてください。creation_time で絞り込まないと無駄なスキャンとクォータ消費につながります。
ベーステーブルへの変更が大きすぎたり、ビューの定義がサポート範囲外(JOIN を含む等)だったりすると、BigQuery はスマートチューニングを諦めます。dry run のバイト数が減っていなければ、ビューの定義が増分更新可能か、リフレッシュが完了しているかを確認してください。
JOIN の実行計画はテーブルの実際のサイズで自動的に決まるため、想定と異なる JOIN パターンが選ばれることがあります。JOIN ステップ詳細で
JOIN EACH WITH ALL(ブロードキャスト)か JOIN EACH WITH EACH(ハッシュ)かを確認し、想定と違えば結合キーの型(STRING より INT64 が高速)やテーブルの並び順を見直してください。
理解度チェック
Q1. 実行グラフであるステージの Compute Ratio について、max が avg を大きく上回っている。これは何のサインか?
データのスキュー(偏り)のサインです。一部のワーカーに極端に多くのデータや計算が集中しており、そのワーカーの処理完了を他のワーカーが待つ形になっています。結合キーやグループ化キーの値の分布を確認し、必要なら早期フィルタやクエリ分割を検討します。
Q2. ブロードキャスト JOIN とハッシュ JOIN の違いを説明してください。
ブロードキャスト JOIN は、片方のテーブル(通常は小さい方)を全ワーカーに複製して各ワーカーでローカルに結合します(JOIN EACH WITH ALL)。ハッシュ JOIN は両方のテーブルを結合キーでシャッフルし、同じキーが同じスロットに集まるように再配置してから結合します(JOIN EACH WITH EACH)。ハッシュ JOIN はデータ移動を伴うぶん高コストです。
Q3. クエリキャッシュが無効になる条件を 2 つ挙げてください。
例: CURRENT_TIMESTAMP() のような非決定的関数を使っている/参照先テーブルが変更された/出力先テーブルを明示指定した/ワイルドカードテーブルを参照した/クエリ文字列が空白やコメントも含めて 1 文字でも変わった、など。いずれか 2 つで正解です。
Q4.(シナリオ)同じ GROUP BY 集計クエリが 1 日に数百回、ダッシュボードから実行される。クエリ文字列は毎回同じで、ベーステーブルは 1 日 1 回だけ更新される。最もコスト効率よく高速化する手段は?
A. クエリキャッシュに任せる B. マテリアライズドビューを作る C. BI Engine を予約する D. APPROX_COUNT_DISTINCT に書き換える
B。 集計パターンが固定でベーステーブルの更新頻度も低いため、マテリアライズドビューによる事前計算が最も費用対効果が高い選択です。A のクエリキャッシュは約 24 時間で失効し必ずヒットする前提では頼れません。C は予約課金が発生し過剰投資です。D は distinct 集計のコストを下げるだけで、繰り返し実行の無駄自体は減りません。
Q5.(シナリオ)2 つの大テーブル(どちらも数百 GB)を結合キーで JOIN したところ、実行グラフで JOIN ステージの Compute Ratio の max が avg の 10 倍以上あり、そのステージだけ極端に時間がかかっている。最初に確認すべきことは?
A. BI Engine を予約する B. 結合キーの値の分布に極端な偏りがないか確認する C. クエリキャッシュを無効化する D. ORDER BY を追加する
B。 Compute の max が avg を大きく上回るのはスキューの典型的なサインです。まず結合キーの分布を確認し、偏りがあれば早期フィルタやクエリ分割を検討します。A・C は根本原因と無関係でコストだけがかかり、D はソートを追加するだけで JOIN のスキュー自体は解消しません。
まとめと次章へ
- クエリは実行グラフ(ステージの集合)に変換され、ステージ間はシャッフルで通信する。スロットはステージ内の作業単位を並列処理する仮想的な計算単位
- 実行グラフの Wait/Read/Compute/Write 比率(avg/max)と
shuffleOutputBytesを読めば、どのステージがボトルネックか、スキューが起きていないかを特定できる - JOIN はテーブルサイズに応じてブロードキャスト(
JOIN EACH WITH ALL)かハッシュ(JOIN EACH WITH EACH)が自動選択される。大きいテーブルを先に、小さいテーブルを後に置く - 最適化の優先順位は パーティション/クラスタ(第 6 章)→ クエリの書き方 → マテリアライズドビュー → BI Engine の順で検討する
- クエリキャッシュ(約 24 時間・無料)、マテリアライズドビューのスマートチューニング、BI Engine、検索インデックスはそれぞれ効く場面が異なる
- スロット消費とキャッシュヒットは
INFORMATION_SCHEMA.JOBSのtotal_slot_ms/cache_hit/total_bytes_processedで確認できる
次の第 9 章「DML・タイムトラベル・スナップショット」では、これまで避けてきた行単位の UPDATE / DELETE / MERGE と、誤って書き換えたデータを過去の状態に戻すタイムトラベル・スナップショットの仕組みを扱います。
- Query plan and timeline
- Introduction to optimizing query performance
- Optimize query computation
- Introduction to materialized views
- Create and use materialized views
- Cached query results
- Introduction to BI Engine
- Introduction to search indexes
- Introduction to slots
- INFORMATION_SCHEMA.JOBS view
- Try BigQuery using the sandbox