概要
INFORMATION_SCHEMA.JOBS でコスト犯人を特定する
region-asia-northeast1.INFORMATION_SCHEMA.JOBS の total_bytes_processed を集計すると「どのクエリが・誰が・何 TB スキャンしているか」を正確に特定できる。感覚ではなくデータで犯人を特定することが、コスト削減の第一歩。
Flat-Rate Slot の損益分岐点を計算する
Slot 予約が有利になるのは「月間スキャン量(TB) × $5 > Slot 月額」の条件を超えたとき。100 Slot/$1,700/月 なら 340 TB/月 が損益分岐点。現状 180 TB → 改善後 7.5 TB なら Slot は完全に割高。先に最適化、後でコスト構造を選ぶ順序が重要。
DATE パーティション + 30 日ルックバックで 94% 削減
非パーティションテーブルに WHERE created_at >= ... はフルスキャン確定。partition_by(event_date, 'date') + WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) で 900 GB → 50 GB(94.4%削減)。dbt incremental + insert_overwrite と組み合わせると差分のみ処理で更にコスト効率が上がる。
ROI をペイバック + 年間 ROI の 2 軸で伝える
「ペイバック 1.1 ヶ月・年間 ROI 977%」は証券マン視点で S&P 500 の約 100 倍の IRR。CFO が拒否できない数字。コスト削減提案は「クエリが速くなった」ではなく「$144,000 の投資で年間 $1,550 相当のリターン」という言語に変換することで意思決定を加速させる。
問題
ECサイト MOps チームは Argo Workflows + BigQuery による週次キャンペーン効果分析パイプラインを運用している。先月、SRE から「BigQuery のスキャン量が急増している」と報告があり、CTO から以下を求められた:
- INFORMATION_SCHEMA.JOBS を使って「スキャン量 TOP 5 クエリ」を特定し、コスト試算($換算)せよ
- 特定したクエリのうち最もコストが高い 1 本を Slot 予約(Flat-Rate)vs オンデマンド で比較し、損益分岐点(月間クエリ回数)を算出せよ
- BigQuery のコスト削減施策を 改善前 Bad SQL → 改善後 Good SQL で示し、スキャン削減率と月次コスト削減額を試算せよ
与件
| 項目 | 値 |
|---|---|
| オンデマンド料金 | $5/TB |
| Slot 予約(100 Slot) | $1,700/月(asia-northeast1) |
| 最重量クエリのスキャン量 | 1.2 TB/回、月 150 回実行 |
| テーブル全サイズ | 約 900 GB(18 ヶ月分・非パーティション) |
| 必要なデータ量 | 直近 30 日分 ≈ 50 GB |
| チーム工数上限 | 5 人日 |
| エンジニア単価 | 6,000 円/時間、1 USD = 150 円 |
Bad SQL 構造 — フルスキャンの罠
-- 問題①: パーティション未設定でフルスキャン
SELECT
order_id,
campaign_id,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM `project.mops_dataset.order_events`
-- ↑ 18ヶ月・900 GB のフルスキャン確定
WHERE created_at >= TIMESTAMP_SUB(
CURRENT_TIMESTAMP(), INTERVAL 30 DAY
)
-- ↑ TIMESTAMP型 + 非パーティション = 剪定不可
GROUP BY order_id, campaign_id
-- 問題②: INFORMATION_SCHEMA を見ていない
-- → 誰が・何TBスキャンしているかが不明
-- → コスト爆増に気づくのが遅れる
-- 問題③: Slot vs オンデマンドを評価していない
-- → 月 150 回 × 1.2 TB = 180 TB/月
-- → $5/TB × 180 = $900/月 が無診断で垂れ流し
created_at(TIMESTAMP型)にパーティション設定がないため、WHERE created_at >= ... は 18 ヶ月分(900 GB)をフルスキャン。必要なのは直近 30 日(50 GB)のみ。partition_by(event_date, 'date') 設定後は WHERE event_date >= ... で 94.4% 削減可能。INFORMATION_SCHEMA.JOBS の total_bytes_processed を集計すれば「どのクエリが・誰が・何 TB」を正確に特定できる。日次監視ジョブ化で早期警戒システムになる。ヒント(段階的開示)
ヒント1 — 方向性
total_bytes_processed。TB 換算(÷ 1e12)して $5 を掛けるだけで 1 ジョブあたりのコストが出る。GROUP BY query または user_email でランキングを作り「誰が・どのクエリが」コスト犯人かを特定することが出発点。感覚ではなくデータで診断する。
ヒント2 — アプローチ
- 損益分岐点の式:
オンデマンドコスト = Slot コスト→月間スキャン量(TB) × $5 = $1,700→月間スキャン量 = 340 TB→340 ÷ 1.2 TB/回 = 284 回/月が損益分岐。月 150 回 < 284 回 → オンデマンドが有利 - BigQuery パーティション削減:
partition_by(event_date, 'date')設定後、WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)で 50 GB/回 → 94.4% 削減 - ROI 計算: 月次削減 ≈ $862.5 → 年間 $10,350 → ペイバック = 実施コスト ÷ 月次削減
ヒント3 — 試算の骨格
-- INFORMATION_SCHEMA コスト診断
SELECT
SUBSTR(query, 1, 80) AS query_snippet,
COUNT(*) AS job_count,
ROUND(SUM(total_bytes_processed) / 1e12, 4) AS total_tb,
ROUND(SUM(total_bytes_processed) / 1e12 * 5, 2) AS cost_usd
FROM `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE DATE(creation_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
AND job_type = 'QUERY' AND state = 'DONE' AND error_result IS NULL
GROUP BY query_snippet
ORDER BY total_tb DESC LIMIT 5
-- 損益分岐点
-- Slot $1,700/月 ÷ $5/TB = 340 TB → ÷ 1.2 TB/回 = 284 回/月
-- パーティション削減試算
-- 現状: 900 GB × 150 回 × $5/TB = $675/月
-- 改善: 50 GB × 150 回 × $5/TB = $37.5/月
-- 削減: $637.5/月(94.4%削減)
-- ペイバック: 3人日 × 8h × 6,000円 = 144,000円 ÷ 95,625円/月 ≈ 1.5ヶ月
問題点分析(3点)
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | 非パーティションテーブルへの TIMESTAMP フィルタ(900 GB フルスキャン) | コスト/性能 | partition_by(event_date, 'date') + DATE フィルタで剪定 |
| 2 | INFORMATION_SCHEMA 未監視(コスト犯人を診断していない) | 可観測性 | 日次 CronWorkflow で TOP 10 クエリを Slack 通知 |
| 3 | Slot vs オンデマンド未評価(損益分岐点 284 回に対し 150 回で Slot 割高) | コスト判断 | 損益分岐点を算出してからコスト構造を選択する |
コスト構造図 — Bad vs Good + 損益分岐点
損益分岐点グラフ — Flat-Rate Slot vs オンデマンド
模範解答
-- ============================================================
-- INFORMATION_SCHEMA.JOBS でスキャン量 TOP 5 クエリを特定
-- region スコープ: region-asia-northeast1
-- 過去 30 日 / 完了ジョブ / エラー除外
-- ============================================================
SELECT
-- クエリ先頭 80 文字(長いクエリを読みやすく切り出す)
SUBSTR(query, 1, 80) AS query_snippet,
user_email,
COUNT(*) AS job_count,
-- スキャン量(TB 換算)
ROUND(SUM(total_bytes_processed) / 1e12, 4) AS total_tb,
-- オンデマンドコスト($5/TB)
ROUND(SUM(total_bytes_processed) / 1e12 * 5, 2) AS total_cost_usd,
-- 1 ジョブあたりの平均スキャン量
ROUND(AVG(total_bytes_processed) / 1e12, 4) AS avg_tb_per_job,
ROUND(AVG(total_bytes_processed) / 1e12 * 5, 4) AS avg_cost_usd_per_job,
-- 実行時間(秒)
ROUND(AVG(TIMESTAMP_DIFF(end_time, start_time, SECOND))) AS avg_duration_sec
FROM `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE
DATE(creation_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
AND state = 'DONE'
AND error_result IS NULL -- エラーになったジョブは除外(課金されないケース)
GROUP BY query_snippet, user_email
ORDER BY total_tb DESC
LIMIT 5
-- ============================================================
-- 結果例(最重量クエリ)
-- ============================================================
-- query_snippet | job_count | total_tb | total_cost_usd | avg_tb_per_job
-- SELECT order_id,… | 150 | 180.0 | $900.00 | 1.2 TB
-- SELECT campaign_id… | 720 | 86.4 | $432.00 | 0.12 TB
-- ...
-- ============================================================
-- 応用: 日次監視ジョブ用(前日のコスト TOP 10)
-- ============================================================
SELECT
user_email,
SUBSTR(query, 1, 100) AS query_snippet,
COUNT(*) AS job_count,
ROUND(SUM(total_bytes_processed) / 1e12, 4) AS total_tb,
ROUND(SUM(total_bytes_processed) / 1e12 * 5, 2) AS cost_usd
FROM `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE DATE(creation_time) = DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
AND job_type = 'QUERY' AND state = 'DONE' AND error_result IS NULL
GROUP BY user_email, query_snippet
ORDER BY total_tb DESC
LIMIT 10
============================================================
損益分岐点計算: Flat-Rate Slot vs オンデマンド
============================================================
【前提条件】
オンデマンド料金 : $5/TB
Slot 予約 100 Slot: $1,700/月(asia-northeast1・固定)
最重量クエリ : 1.2 TB/回(現状 150 回/月)
【オンデマンドコスト(現状)】
月間スキャン量: 1.2 TB/回 × 150 回 = 180 TB/月
月次コスト : 180 TB × $5/TB = $900/月
【損益分岐点(月間スキャン量)】
条件: オンデマンドコスト = Slot コスト
→ 月間スキャン量(TB) × $5 = $1,700
→ 月間スキャン量 = $1,700 ÷ $5 = 340 TB/月
【損益分岐点(月間クエリ回数)】
→ 340 TB ÷ 1.2 TB/回 = 283.3 回/月 ≈ 284 回/月
【現状の判断】
現状クエリ回数: 150 回/月
損益分岐点 : 284 回/月
→ 現状 150 回 < BEP 284 回
→ オンデマンド $900/月 < Slot $1,700/月
→ Slot 予約は割高: $800/月 余計にかかる
【改善後の判断(パーティション設定後)】
改善後スキャン: 0.05 TB/回 × 150 回 = 7.5 TB/月
改善後コスト : 7.5 TB × $5 = $37.5/月
→ Slot $1,700/月 と比べて 45 倍安い
→ パーティション改善後は Slot 予約を検討する余地なし
【Slot 予約が有利になる条件(今後の成長予測)】
月間スキャン量 > 340 TB/月 かつ
クエリパターンが予測可能で安定している場合
※ プロジェクト全体のスキャン量合計で判断すること
(単一クエリの最適化後はほぼ到達しない)
-- ============================================================
-- Bad: 非パーティションテーブルへの TIMESTAMP フィルタ
-- ============================================================
-- 問題点:
-- 1. order_events テーブルはパーティション未設定(18ヶ月・900 GB)
-- 2. WHERE created_at は TIMESTAMP 型関数フィルタ → 剪定不可
-- 3. 毎回 900 GB(実効 1.2 TB)フルスキャン確定
-- 4. コスト: 1.2 TB × $5 × 150 回/月 = $900/月
SELECT
order_id,
campaign_id,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM `project.mops_dataset.order_events` -- 非パーティション・900 GB
WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
-- ↑ TIMESTAMP型フィルタ → パーティション剪定不可
GROUP BY order_id, campaign_id
-- ============================================================
-- Good: dbt incremental + DATE パーティション + クラスタリング
-- ============================================================
-- models/mart/mart_order_campaign_monthly.sql
{{
config(
materialized='incremental',
incremental_strategy='insert_overwrite',
-- パーティション: event_date(DATE型)で日次分割
-- → 30 日フィルタで 50 GB/回(900 GB の 5.6%)のみスキャン
partition_by={
'field': 'event_date',
'data_type': 'date',
'granularity': 'day'
},
-- クラスタリング: WHERE / GROUP BY で使うカラムを指定
-- → パーティション剪定後さらにブロックスキャンを絞る
cluster_by=['campaign_id', 'order_id'],
on_schema_change='fail' -- スキーマ変更は明示的に対応(サイレント変更禁止)
)
}}
WITH source AS (
SELECT
-- パーティションキー(DATE型 → TIMESTAMP より確実に剪定が効く)
DATE(created_at) AS event_date,
order_id,
campaign_id,
amount,
status,
COALESCE(segment_id, 'unknown') AS segment_id -- NULL 保護
FROM {{ ref('stg_order_events') }}
{% if is_incremental() %}
-- パーティション剪定: 直近 30 日分のみスキャン
-- MAX(event_date) ベースにすることで追いつき遅延に対応
WHERE DATE(created_at) >= DATE_SUB(
(SELECT MAX(event_date) FROM {{ this }}),
INTERVAL 30 DAY
)
{% endif %}
)
SELECT
event_date, -- パーティションキー
campaign_id,
order_id,
segment_id,
SUM(amount) AS total_amount,
COUNT(*) AS order_count,
-- SAFE_DIVIDE: ゼロ除算ガード(全キャンセルで order_count=0 のケース対策)
SAFE_DIVIDE(SUM(amount), COUNT(*)) AS avg_order_value,
-- ステータス別カウント(キャンセル率の監視用)
COUNTIF(status = 'completed') AS completed_count,
COUNTIF(status = 'cancelled') AS cancelled_count,
CURRENT_TIMESTAMP() AS _loaded_at
FROM source
GROUP BY event_date, campaign_id, order_id, segment_id
スキャン量削減の確認方法
-- dbt 実行前: --dry-run でスキャン量を事前確認
-- (dbt v1.8+ で experimental サポート)
-- BigQuery Console で直接確認する場合:
-- クエリ右上の「処理されるデータ」に表示されるバイト数を確認
-- INFORMATION_SCHEMA で改善効果を検証(改善後 1 週間後に実行)
SELECT
DATE(creation_time) AS run_date,
ROUND(AVG(total_bytes_processed) / 1e12, 4) AS avg_tb_per_job,
ROUND(AVG(total_bytes_processed) / 1e12 * 5, 4) AS avg_cost_per_job
FROM `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE
DATE(creation_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 14 DAY)
AND LOWER(query) LIKE '%mart_order_campaign_monthly%'
AND state = 'DONE' AND error_result IS NULL
GROUP BY run_date
ORDER BY run_date
施策別コスト試算サマリー
| 施策 | 削減前コスト | 削減後コスト | 月次削減額 | 工数 | ROI |
|---|---|---|---|---|---|
| BigQuery パーティション設定(dbt incremental) | $900/月 | $37.5/月 | $862.5/月 | 3 人日 | 977%/年 |
| INFORMATION_SCHEMA 日次監視ジョブ化 | コスト爆増を発見遅れ | 翌日検知可能 | 早期介入効果(定量化難) | 1 人日 | 高(運用負荷削減) |
| Slot 予約 → オンデマンド継続 | (Slot だと $1,700) | $37.5/月 | $862.5/月(Slot 比) | 0(判断のみ) | ∞(不要な固定費回避) |
ROI 試算(全ステークホルダー向け)
【実施コスト(パーティション設定 + dbt 移行)】
3 人日 × 8h × 6,000円/h = 144,000円(≈ $960)
【月次削減額】
$862.5/月 × 150円/$ = 129,375円/月
【ペイバック期間】
144,000円 ÷ 129,375円/月 = 1.1 ヶ月
【年間 ROI】
年間削減額: $862.5 × 12 = $10,350(≈ 1,552,500円)
年間 ROI : ($10,350 - $960) / $960 = 977%
【ステークホルダー別の伝え方】
CFO 向け:
「$960 の実施コスト(3 人日)で年間 $10,350 の削減。
年間 ROI 977%。S&P 500 の約 97 倍の投資効果。
さらに Slot 予約($1,700/月・割高)の誤採択を防ぎ
年間追加 $10,200 の損失回避にもなる。」
CTO 向け:
「3 人日(dbt モデル追加 + --full-refresh)で月次 95.8%削減。
ペイバック 1.1 ヶ月。INFORMATION_SCHEMA 監視ジョブも
1 人日で追加でき、次回のコスト爆増を翌日に検知できる。」
エンジニア向け:
「dbt config に partition_by + cluster_by を追加し、
WHERE フィルタを created_at → event_date DATE型に変更するだけ。
--full-refresh 1 回でパーティション構造が再構築され
翌日から 94% コスト削減が確認できる。」
【スキャン削減率の内訳】
改善前スキャン: 900 GB(18 ヶ月分フルスキャン)
改善後スキャン: 50 GB(30 日分のみ)
削減率 : (900 - 50) / 900 = 94.4%
※ 1.2 TB/回 は BigQuery の実効スキャン量(
結合後・列プロジェクション前)のため
50 GB × (1.2/0.9) ≈ 67 GB が正確な改善後スキャン量
→ $5/TB × 0.067 TB × 150 回 = $50.3/月(94.4%削減)
リスクと対策
| リスク | 影響 | 対策 |
|---|---|---|
| dbt --full-refresh 中のダウンタイム | 分析クエリが旧テーブルを参照できなくなる | staging → production へのリネームが atomic なので実質ダウンタイムなし。ただし移行前後 30 分はクエリをホールド |
| パーティション設定後の遅延データ欠損 | 翌日処理のデータが 30 日ウィンドウ外に落ちる | dbt incremental の WHERE を MAX(event_date) ベースにし、最新パーティションから 30 日を確実にカバー |
| INFORMATION_SCHEMA クエリ自体のコスト | 監視ジョブが新たなコスト要因になる | INFORMATION_SCHEMA.JOBS はメタデータテーブルのためスキャン料金なし(課金対象外) |
ポイント解説
INFORMATION_SCHEMA.JOBS はユーザーテーブルではなくシステムメタデータテーブルのため、クエリしても スキャン料金は発生しない。コスト監視ジョブを毎日実行しても追加コストゼロ。ただし project.INFORMATION_SCHEMA.JOBS(プロジェクトスコープ)と region-xxx.INFORMATION_SCHEMA.JOBS(リージョンスコープ)では参照できる期間と対象ジョブが異なる(リージョンスコープの方が広い)。
BigQuery では
partition_by(event_date, 'date') のように DATE 型カラムをパーティションキーにした場合、WHERE event_date >= DATE_SUB(...) は確実にパーティション剪定が効く。一方、TIMESTAMP 型のパーティション(partition_by(created_at, 'timestamp'))に対して WHERE TIMESTAMP_TRUNC(created_at, DAY) >= ... などの関数をかませると剪定が効かないケースがある。dbt では DATE カラムを別途持たせ、WHERE フィルタは必ず DATE 型カラムに対して行う設計が安全。
Slot 予約は「月間スキャン量が固定かつ大量の場合」に有利。スタートアップ・成長期はクエリ量が変動し Slot 枠が余るため、変動費(オンデマンド)が合理的。損益分岐点を定期的に再計算し、「全プロジェクトの月間スキャン量 > Slot 費 ÷ $5/TB」を超えたタイミングで移行を検討する。パーティション最適化でスキャン量が激減した後は損益分岐点が上がるため、Slot が有利になる閾値は遠ざかる。
「ペイバック 1.1 ヶ月」は年率換算で約 977% ROI。S&P 500 の長期期待リターン 10% と比較すると約 97 倍。CFO に「3 人日の実施コスト $960 で年間 $10,350 のリターン」と伝えれば承認を得られない理由がない。コスト削減施策は「技術的改善」として報告するより「投資案件」として ROI・ペイバック・リスクの 3 点セットで提案すると意思決定が 10 倍速くなる。
今回のコスト爆増は「パーティション未設定のまま 18 ヶ月分データが蓄積したため」に発生した。日次監視ジョブ(
total_bytes_processed TOP 10 + 前日比アラート)があれば、スキャン量が増加した翌日に気づき 3 ヶ月前に介入できた。監視コスト: 0 円(INFORMATION_SCHEMA は課金なし)、予防効果: $862.5/月 × 3 ヶ月 = $2,587.5 の損失回避。
実務への応用
- INFORMATION_SCHEMA 日次監視ジョブ化: Argo Workflows の
CronWorkflow(毎朝 9:00 JST)で「前日スキャン量 TOP 10 クエリ」を集計し Slack に投稿する。total_bytes_processedの前日比 30% 以上増加をトリガーにして DataDog アラートと連携することで、BigQuery コスト爆増を早期検知する早期警戒システムになる。監視クエリ自体は INFORMATION_SCHEMA のためコスト無料。 - dbt INFORMATION_SCHEMA パーティション効率テスト:
dbt-bigqueryでINFORMATION_SCHEMA.PARTITIONSを参照し「直近 7 日分のパーティションのみスキャンしていること」を CI 検証する test を追加できる。dbt test実行時に自動チェックされ、パーティション剪定が壊れた変更を PR 段階で検知できる。 - 証券マン視点のコスト判断: Slot 予約は「固定費化 = リスクを取る」判断。変動費(オンデマンド)は「使った分だけ」の保守的な判断。成長フェーズではオンデマンドで変動費化し、クエリ量・パターンが安定してから Slot 移行を検討する。損益分岐点の定期的な再計算(四半期に 1 回)を習慣にすることで、常に最適なコスト構造を選択できる。
- BigQuery ラベル × コスト配賦: クエリに
labels = {'team': 'mops', 'use_case': 'campaign_analysis'}を設定するとINFORMATION_SCHEMA.JOBS.labelsでチーム別・用途別のコスト配賦レポートが作れる。Terraform で Cloud Billing の BigQuery エクスポートを設定し、ラベルごとの月次コストを自動集計する基盤を整えることで、チームの BigQuery コスト責任を明確化できる。
今日のまとめ
INFORMATION_SCHEMA.JOBS で「どのクエリが 1.2 TB スキャンしているか」を特定し(料金: $0)、partition_by(event_date, 'date') + 30 日ルックバックで 94.4% 削減($900 → $37.5/月)、Flat-Rate Slot は損益分岐点 284 回 に対し現状 150 回なので採択なし——「診断 → パーティション最適化 → Slot 判断」の順番で進めることで、3 人日・ペイバック 1.1 ヶ月・年間 ROI 977% という CFO が拒否できない投資案件を作れる。コスト削減の鉄則: 感覚で犯人を探さない(INFORMATION_SCHEMA で診断する)、最適化せずに構造を変えない(まず削減してから Slot を評価する)、ROI をペイバック + 年間 ROI の 2 軸で伝える。