BigQuery ハンズオン教科書|2026-08 版
第 17 章 サンドボックスで完走可

運用と監視 — INFORMATION_SCHEMA を武器にする

ジョブ・ストレージ・スロットをメタデータで観測し、クォータと上手に付き合う

この章で学ぶこと

前提: 全ハンズオンはサンドボックスで完走可・無料枠内。INFORMATION_SCHEMA クエリは 1 回あたり最低 10 MB の課金判定を受けますが、無料枠(クエリ月 1 TiB・ストレージ 10 GiB、2026-08 時点)に対して無視できる量です。

ジョブという実行単位とライフサイクル

BigQuery でクエリ・ロード・エクスポート・テーブルコピーを実行すると、そのたびにジョブが自動的に作成されます。ジョブは非同期に実行され、進捗をポーリングできる管理単位です。ジョブには 4 つの種類があります。

ジョブは常に次の 3 状態のいずれかにあります。

PENDING 待機中 実行開始 RUNNING 実行中 終了 DONE 完了 成功 失敗 bq cancel / BQ.JOBS.CANCEL PENDING・RUNNING 中のみ有効 reason: stopped
図 17-1 ジョブは PENDING → RUNNING → DONE と進み、DONE の中身は error_result の有無で成功・失敗に分かれる。キャンセルは PENDING・RUNNING 中だけ有効で、キャンセルされたジョブは error_result.reason = 'stopped' として DONE になる。

ジョブは <project_id>:<region>.<job_id> という完全修飾 ID を持ちます。BigQuery Studio に表示される ID はこの完全形式ですが、bq コマンドの出力は短縮形式(project と location を含まない)です。ジョブの追跡は bq コマンドで一通り行えます。

# 自分のジョブを一覧(bigquery.jobs.list 権限)
bq ls --jobs=true --max_results=10

# 全ユーザーのジョブを一覧(bigquery.jobs.listAll 権限が必要)
bq ls --jobs=true --all=true --max_results=10 PROJECT_ID

# ジョブの詳細を確認
bq show --format=prettyjson --job=true JOB_ID

# 実行中・待機中のジョブをキャンセル(PENDING/RUNNING のみ対象)
bq --location=US cancel JOB_ID

ジョブ履歴はプロジェクトの全リージョン分が 6 か月間保存されます。bq ls --jobs=true は既定で自分のジョブだけを返し、--all=true(bigquery.jobs.listAll 権限が必要)を付けると全ユーザーのジョブが対象になります。

実務の視点: ジョブ操作に関わる IAM ロールは目的で使い分けます。開発者には自分のジョブの実行・閲覧・キャンセルだけができる BigQuery Job User(roles/bigquery.jobUser)で十分なことが多く、運用担当が全ジョブを横断的に見る必要があるなら BigQuery Resource Admin(roles/bigquery.resourceAdmin、全ジョブの一覧・メタデータ取得)、他人のジョブを止める権限まで要るなら BigQuery Admin(roles/bigquery.admin)まで踏み込みます。「実行できる」と「他人のジョブが見える」は別の権限段階だと意識してください。

INFORMATION_SCHEMA を武器にする

INFORMATION_SCHEMA は BigQuery の全リソースのメタデータを SQL で問い合わせられる、読み取り専用のシステム定義ビュー群です。課金は次の通りです。

INFORMATION_SCHEMA ビューを問い合わせるには、データセット・リージョン・プロジェクトの 3 種類の修飾子のいずれかでスコープを指定する必要があります。

修飾子 対応ビュー(一部) `region-us`.INFORMATION_SCHEMA プロジェクト全体・非階層的 JOBS / TABLE_STORAGE 等 mydataset.INFORMATION_SCHEMA 単一データセットのみ TABLES / COLUMNS 等
図 17-2 リージョン修飾子はプロジェクト全体(JOBS・TABLE_STORAGE など)、データセット修飾子は単一データセット(TABLES・COLUMNS など)に範囲を絞る。TABLE_STORAGE はデータセット修飾子に対応していない点に注意。

リージョン修飾子は region-us のように region- の後にロケーション名を続けます。非階層的で、US マルチリージョンの修飾子は us-central1 などの単一リージョンを含みません。プロジェクト修飾子はどのビューでも併用でき、省略時はクエリを実行しているプロジェクトが対象になります。

-- プロジェクト修飾子 + リージョン修飾子 + JOBS ビュー
SELECT job_id, total_bytes_billed, total_slot_ms
FROM `PROJECT_ID`.`region-us`.INFORMATION_SCHEMA.JOBS
WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY);

JOBS ビュー — コスト・スロット・エラーの分析

INFORMATION_SCHEMA.JOBS(別名 JOBS_BY_PROJECT。両者は同じ結果を返す同義語)は、現在のプロジェクトの全ジョブのメタデータをほぼリアルタイムで提供します。creation_time でパーティション化され、project_id と user_email でクラスタリングされているため、creation_time に絞り込むほど効率よくプルーニングされます。主なカラムは次の通りです。

このビューを問い合わせるには、プロジェクトに BigQuery User(roles/bigquery.user)と BigQuery Resource Viewer(roles/bigquery.resourceViewer)ロールが必要です(実体は bigquery.jobs.listAll と bigquery.jobs.create 権限)。全ユーザーのジョブを横断的に見る、管理者寄りのビューです。

自分のジョブだけを見るなら INFORMATION_SCHEMA.JOBS_BY_USER を使います。必要なロールは BigQuery User だけ(bigquery.jobs.list 権限)で、JOBS ビューよりずっと低い権限で済みます。

実務の視点: 「自分のクエリ履歴を振り返りたい開発者」には JOBS_BY_USER、「プロジェクト全体のコストを横断分析したい運用担当」には JOBS(JOBS_BY_PROJECT)と、権限の強さに応じてビューを選び分けます。JOBS は組織・フォルダ単位にも JOBS_BY_FOLDER・JOBS_BY_ORGANIZATION として拡張でき、スコープが広がるほど必要な IAM も組織階層の上位に上がります。
ORGANIZATION(..._BY_ORGANIZATION) FOLDER(..._BY_FOLDER) PROJECT JOBS = JOBS_BY_PROJECT JOBS_BY_USER 自分のジョブのみ どのスコープでも
図 17-3 JOBS 系ビューは ORGANIZATION・FOLDER・PROJECT という入れ子のスコープを持つ(外側ほど広い IAM 権限が必要)。JOBS_BY_USER はこの階層と直交し、どのスコープに居ても常に「自分が実行したジョブだけ」に絞り込む。

TABLE_STORAGE ビュー — ストレージ肥大の棚卸し

INFORMATION_SCHEMA.TABLE_STORAGE(別名 TABLE_STORAGE_BY_PROJECT)は、テーブル・マテリアライズドビューごとの現在のストレージ使用量を 1 行ずつ返します。数秒〜数分の遅延はありますが、ほぼ現在値のスナップショットです。必要なロールは BigQuery Metadata Viewer(roles/bigquery.metadataViewer、実体は bigquery.tables.get / bigquery.tables.list)で、JOBS ビューよりも軽い権限で読めます。主なカラムは次の通りです。

データセットが論理課金モデルなら active_logical_bytes と long_term_logical_bytes、物理課金モデルなら active_physical_bytes + fail_safe_physical_bytes と long_term_physical_bytes を使うと、月次ストレージ課金のおおまかな見積もりができます。

クォータの構造とエラーの読み方

クォータには「引き上げ可能なもの」と「変更できないシステム上限」の 2 種類があります。代表的な値は次の通りです(2026-08 時点。プロジェクト単位が既定)。

項目既定値備考
オンデマンドの最大同時スロット2,000/プロジェクト、20,000/組織組織内で複数プロジェクトが同時に高負荷だと按分される
ペンディングジョブ数50,000/プロジェクト・リージョン変更不可のシステム上限
キュー待ちインタラクティブクエリ1,000 件超過分は quota エラーで弾かれる
キュー待ちバッチクエリ20,000 件-
クエリ / マルチステートメントクエリの実行時間6 時間最大 3 回までリトライされるため合計は 6 時間を超え得る
オンデマンドの CPU 使用量上限256 CPU 秒 / スキャン MiB超過で billingTierLimitExceeded
ロードジョブ1,500/テーブル/日、100,000/プロジェクト/日失敗したロードも回数に含む
コピージョブ100,000/プロジェクト/日リージョン跨ぎは 2,000/日、100/宛先テーブル/日
抽出(エクスポート)ジョブ100,000/プロジェクト/日無料枠は 50 TiB/日(共有スロットプール利用時)
mutating DML(UPDATE・DELETE・MERGE)の同時実行2 件/テーブル超過分はテーブルごとに最大 20 件までキュー、最大待機 7 時間

クォータ・上限の実測値は Google Cloud コンソールの「IAM と管理」→「割り当て」ページ、または各ジョブ種別のドキュメントに記載の「現在の使用量を表示」リンクから確認できます。上限が近いか不安なら、まずは INFORMATION_SCHEMA.JOBS で自分たちの実際のジョブ量を数えるのが早道です。

エラーが発生したら error_result.reason を見て種類を切り分けます。

error_result.reason は? quotaExceeded (403) 詳細確認のうえ増枠を申請 rateLimitExceeded (403) 間隔を空けてリトライ resourcesExceeded (400) クエリを最適化する billingTierLimit Exceeded (400) クロスジョインを見直す
図 17-4 error_result.reason の値ごとに対処が異なる。quotaExceeded と rateLimitExceeded はどちらも HTTP 403 だが、前者は割り当て超過、後者は短時間の過剰リクエストという別の原因を持つ。
reasonHTTP意味対処
quotaExceeded403プロジェクトのクォータ・カスタムクォータ・無料枠の超過message プロパティで詳細確認、サポートへ増枠依頼、カスタムクォータの見直し
rateLimitExceeded403短時間に送信したリクエスト数が過剰指数バックオフでリクエスト頻度を落とす
resourcesExceeded400ジョブがメモリ等のシステムリソースを過剰消費JOIN 順序・中間結果サイズなどクエリ自体を見直す
billingTierLimitExceeded400オンデマンドクエリの CPU 使用量がスキャン量に対し過大(billingTier > 100)非効率なクロスジョインを最適化するか、容量ベース(editions)へ移行
responseTooLarge403クエリ結果が最大レスポンスサイズを超過LIMIT の追加、ORDER BY の見直し、宛先テーブルへの書き出し

リザベーションと組織運用の型

継続的に大きなワークロードを抱えるなら、オンデマンド課金ではなくリザベーション(スロットの予約)を使い、プロジェクトを予約にassignment(割り当て)します。assignment には job_type を指定でき、同じ予約でも用途別に振る舞いを分けられます。代表的な job_type は QUERY(既定値)・CONTINUOUS(継続クエリ)・PIPELINE(ロード・エクスポート・コピー等のバックグラウンドジョブ)・BACKGROUND・ML_EXTERNAL です。

-- プロジェクトを予約に割り当てる(job_type で用途を分ける)
CREATE ASSIGNMENT
  `ADMIN_PROJECT_ID.region-LOCATION.RESERVATION_NAME.ASSIGNMENT_ID`
OPTIONS (
  assignee = 'projects/PROJECT_ID',
  job_type = 'PIPELINE'
);

割り当て直後にクエリを実行すると、反映まで最大 5 分ほどオンデマンド課金のまま実行される場合があります。またプレビュー機能として、予約に対するプロジェクトキャップ(scheduling_policy_max_slots / scheduling_policy_concurrency)を CREATE ASSIGNMENT の追加オプションで設定でき、同じ予約を使うプロジェクトごとにスロット消費や同時実行数を個別に制限できます。

リザベーション 購入済みスロット assignment ETL プロジェクト job_type=PIPELINE BI プロジェクト job_type=QUERY ML プロジェクト job_type=ML_EXTERNAL 検証用プロジェクト 未割り当て=オンデマンド
図 17-5 1 つのリザベーションから複数プロジェクトへ job_type 別に assignment することで、ETL・BI・ML のワークロードが互いのスロットを奪い合わないよう分離できる。割り当てのないプロジェクトはオンデマンド課金のまま残る。

組織運用では、この技術要素を次のような型に落とし込みます。

ラベルはキー・値ともに小文字英数字とアンダースコア・ダッシュのみ(国際文字も可)で、キーは 1〜63 文字、値は 0〜63 文字、1 リソースにつき最大 64 個まで付けられます。キーはリソース内で一意である必要があります。

ハンズオン 17-1|INFORMATION_SCHEMA.JOBS で高額クエリ Top10 を特定する

公開データセットに数回クエリを実行し、そのジョブ履歴を INFORMATION_SCHEMA.JOBS で分析します。サンドボックスで完走可・無料枠内(合計でも数 MB 程度)。

  1. 演習用データセットを作成し、Shakespeare の公開データセットに 2 種類のクエリを投げます。
bq mk --location=US --dataset PROJECT_ID:handson_ch17

-- 処理: 約 6 MB
SELECT corpus, SUM(word_count) AS total_words
FROM `bigquery-public-data.samples.shakespeare`
GROUP BY corpus
ORDER BY total_words DESC
LIMIT 5;

-- 処理: 約 6 MB
SELECT word, word_count
FROM `bigquery-public-data.samples.shakespeare`
WHERE corpus = 'hamlet'
ORDER BY word_count DESC
LIMIT 10;
  1. 直近のジョブを課金バイト数の多い順に並べます(INFORMATION_SCHEMA クエリ自体は最低 10 MB 課金)。
SELECT
  job_id,
  creation_time,
  total_bytes_billed,
  total_slot_ms,
  cache_hit
FROM `region-us`.INFORMATION_SCHEMA.JOBS
WHERE job_type = 'QUERY'
  AND state = 'DONE'
  AND creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
ORDER BY total_bytes_billed DESC
LIMIT 10;

期待される出力: 直前に実行した 2 件のクエリが total_bytes_billed の大きい順に並びます(このビューには自分の全履歴が対象期間分含まれるため、過去に実行したジョブが多いほど行数も増えます)。cache_hit が true の行は再実行時の 2 回目以降で見られます。

後片付け:

bq rm -r -f -d PROJECT_ID:handson_ch17
ハンズオン 17-2|TABLE_STORAGE でストレージ肥大テーブルと未使用テーブルを棚卸しする

サイズの異なる 2 つのテーブルを作り、TABLE_STORAGE でストレージ使用量と最終更新時刻を確認します。サンドボックスで完走可・無料枠内(合計でも数 MB 程度)。ハンズオン 17-1 のデータセットを削除済みの場合は再作成してください。

  1. 大きさの異なる 2 つのテーブルを作成します。
bq mk --location=US --dataset PROJECT_ID:handson_ch17

CREATE OR REPLACE TABLE `PROJECT_ID.handson_ch17.hamlet_words` AS
SELECT word, word_count
FROM `bigquery-public-data.samples.shakespeare`
WHERE corpus = 'hamlet';

CREATE OR REPLACE TABLE `PROJECT_ID.handson_ch17.all_words_snapshot` AS
SELECT word, corpus, word_count
FROM `bigquery-public-data.samples.shakespeare`;
  1. TABLE_STORAGE でサイズと最終更新時刻を確認します(TABLE_STORAGE はデータセット修飾子に対応していないため、リージョン修飾子 + table_schema での絞り込みが必要です)。
SELECT
  table_name,
  total_rows,
  ROUND(total_logical_bytes / POW(1024, 2), 3) AS logical_mb,
  ROUND(active_logical_bytes / POW(1024, 2), 3) AS active_mb,
  storage_last_modified_time
FROM `region-us`.INFORMATION_SCHEMA.TABLE_STORAGE
WHERE table_schema = 'handson_ch17'
ORDER BY total_logical_bytes DESC;

期待される出力: all_words_snapshot が hamlet_words より大きい 2 行。未使用テーブルを検出する定番パターンは、storage_last_modified_time を現在時刻と比較する条件を WHERE に足すだけです(例: AND storage_last_modified_time < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY))。

後片付け:

bq rm -r -f -d PROJECT_ID:handson_ch17
ハンズオン 17-3|失敗ジョブをわざと作り、傾向とエラーの読み方を確認する

存在しないテーブルを参照してジョブを 1 件失敗させ、INFORMATION_SCHEMA.JOBS で失敗傾向を分析します。サンドボックスで完走可・無料枠内(失敗クエリはスキャンなしで課金対象外)。ハンズオン 17-1/17-2 のデータセットを削除済みの場合は再作成してください。

  1. 存在しないテーブルを参照し、意図的にジョブを失敗させます。
bq mk --location=US --dataset PROJECT_ID:handson_ch17

-- 存在しないテーブルを参照するため notFound で失敗する(スキャンなし・課金なし)
SELECT * FROM `PROJECT_ID.handson_ch17.does_not_exist_yet`;
  1. 失敗ジョブの傾向を error_result.reason でグルーピングし、直近の失敗の詳細も確認します。
SELECT
  job_type,
  state,
  error_result.reason AS error_reason,
  COUNT(*) AS job_count
FROM `region-us`.INFORMATION_SCHEMA.JOBS
WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
GROUP BY job_type, state, error_reason
ORDER BY job_count DESC;

-- 直近の失敗ジョブの詳細(reason と message)
SELECT job_id, error_result.reason, error_result.message
FROM `region-us`.INFORMATION_SCHEMA.JOBS
WHERE state = 'DONE' AND error_result IS NOT NULL
ORDER BY creation_time DESC
LIMIT 5;

期待される出力: 1 つ目のクエリで error_reason = 'notFound' の行が 1 件以上表示されます。2 つ目のクエリでは、そのジョブの message に「Not found: Table ...」という具体的な原因が入っています。quotaExceeded や resourcesExceeded が出た場合も同じ 2 段構え(まず reason で分類 → message で詳細確認)で読み解きます。

後片付け:

bq rm -r -f -d PROJECT_ID:handson_ch17
ハンズオン 17-4|ラベルでジョブにタグ付けし、コスト配賦を集計する

データセットとクエリジョブにラベルを付け、ラベル別にコストを集計します。サンドボックスで完走可・無料枠内。ハンズオン 17-1〜17-3 のデータセットを削除済みの場合は再作成してください。

  1. データセットにラベルを付け、ラベル付きのクエリジョブを実行します。
bq mk --location=US --dataset PROJECT_ID:handson_ch17
bq update --set_label team:analytics PROJECT_ID:handson_ch17

bq query --use_legacy_sql=false \
  --label team:analytics \
  --label env:handson \
  'SELECT corpus, COUNT(*) AS row_count
   FROM `bigquery-public-data.samples.shakespeare`
   GROUP BY corpus'

ジョブのラベルは 実行時にしか付けられません。実行済みのジョブに後からラベルを追加・変更することはできない点に注意してください(データセットやテーブルのラベルは後から bq update --set_label で変更できます)。

  1. ラベルをキーに、ジョブのコストを集計します。
SELECT
  label.value AS team,
  COUNT(*) AS job_count,
  SUM(total_bytes_billed) AS total_bytes_billed
FROM `region-us`.INFORMATION_SCHEMA.JOBS,
  UNNEST(labels) AS label
WHERE label.key = 'team'
  AND creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
GROUP BY team
ORDER BY total_bytes_billed DESC;

期待される出力: team = analytics の行に、先ほど実行したジョブの total_bytes_billed が集計されます。同じパターンで department や cost_center ラベルを使えば、組織のコスト配賦の単位でそのまま集計できます。

後片付け:

bq rm -r -f -d PROJECT_ID:handson_ch17

つまずきポイント

region 修飾子を付け忘れて「must specify a location」で失敗する: INFORMATION_SCHEMA ビューはデータセット・リージョン・プロジェクトのいずれかの修飾子が必須です。素の INFORMATION_SCHEMA.JOBS だけを書くとエラーになります。また、クエリの実行ロケーションと修飾子のロケーションが一致していないと Table myproject:region-us.INFORMATION_SCHEMA.[VIEW] not found in location US のようなエラーになります。
JOBS ビューの dry run 見積もりが実際よりかなり大きく出る: JOBS ビューへの dry run は creation_time(パーティショニングカラム)による絞り込みしか見積もりに反映せず、project_id や user_email(クラスタリングカラム)の絞り込みは加味されません。実際の処理バイト数は見積もりよりかなり少ないことが多いので、慌てて代替クエリを探す前に実際に実行して確認してください。
JOBS ではなく JOBS_BY_USER を使ってしまい、他人のジョブが見えない: JOBS(JOBS_BY_PROJECT)はプロジェクト全体を横断するために bigquery.jobs.listAll が必要ですが、JOBS_BY_USER は自分のジョブしか返さない代わりに bigquery.jobs.list だけで済みます。「権限エラーは出ていないのに他人のジョブが 1 件も出てこない」ときは、ビュー選択を疑ってください。
TABLE_STORAGE をデータセット修飾子で問い合わせようとして構文エラーになる: TABLES や COLUMNS と違い、TABLE_STORAGE はデータセット修飾子に対応していません。必ずリージョン修飾子を使い、絞り込みは WHERE table_schema = 'データセット名' で行ってください。

理解度チェック

Q1. INFORMATION_SCHEMA ビューを問い合わせるとき、必ず指定しなければならないものは何ですか?

データセット修飾子・リージョン修飾子・プロジェクト修飾子のいずれかによるスコープ指定です。どれも付けずに素の INFORMATION_SCHEMA.VIEW だけを書くとエラーになります。プロジェクト修飾子は他の 2 つと併用でき、省略時は実行中のプロジェクトが対象になります。

Q2. INFORMATION_SCHEMA.JOBS と JOBS_BY_USER の違いを、必要な権限の観点から説明してください。

JOBS(JOBS_BY_PROJECT の同義語)はプロジェクト内の全ユーザーのジョブを対象とし、bigquery.jobs.listAll を含む BigQuery User + BigQuery Resource Viewer ロールが必要です。JOBS_BY_USER は実行者自身のジョブだけを返す代わりに、BigQuery User ロール(bigquery.jobs.list)だけで問い合わせられます。

Q3. quotaExceeded と resourcesExceeded の違いは何ですか?

quotaExceeded(HTTP 403)はプロジェクトのクォータ・カスタムクォータ・無料枠を使い切ったことを示し、message プロパティで超過した項目を確認してから増枠やカスタムクォータの調整で対処します。resourcesExceeded(HTTP 400)はクォータとは無関係に、個々のジョブがメモリなどのシステムリソースを使いすぎたことを示し、クエリ自体(JOIN 順序や中間結果のサイズ)を見直して対処します。

Q4.(シナリオ)ETL 用の大量ロードジョブが夜間バッチで走ると、日中に動かしているダッシュボードの BI クエリの応答が遅くなる、というスロット競合が起きている。最も効果的な対策は?
A. BI クエリのクォータを引き上げる B. ETL 用プロジェクトと BI 用プロジェクトを別のリザベーションに assignment し job_type でも分離する C. INFORMATION_SCHEMA.JOBS で監視だけ強化する D. ETL のロードジョブの実行時間帯を記録する

B。 リザベーションを分け、ETL プロジェクトには job_type=PIPELINE、BI プロジェクトには job_type=QUERY で assignment すれば、スロットプールそのものが分離され競合が起きなくなります。A はオンデマンドのクォータを増やすだけで根本のスロット共有は解消しません。C・D は現状把握にはなりますが、競合そのものへの対策にはなりません。

Q5.(シナリオ)オンデマンドプロジェクトで実行したクエリが billingTierLimitExceeded で失敗した。ジョブの詳細を見ると、行数の少ない複数テーブルを条件を絞らずに JOIN していた。最適な対処は?
A. maximum_bytes_billed を上げる B. カスタムクォータを緩和する C. 非効率なクロスジョインになっていないか JOIN 条件を見直す D. LIMIT 句を追加する

C。 billingTierLimitExceeded はスキャン量に対して CPU 使用量が過大なときに発生し、典型的な原因は不正確な JOIN 条件による意図しないクロスジョインです。A・B はバイト数やクォータの上限緩和であり CPU 使用量の問題には無関係です。D は結果行数を減らすだけで、JOIN 自体の計算量は変わりません。

まとめと次章へ