BigQueryのテーブルにLooker Studioを直結すると、グラフを1枚開くたびにクエリが走ります。設計を省いたダッシュボードほど、請求書で気づく構造です。この記事では、Apple Watchの健康データをサンプルに、取り込み・Dataformでの変換・ダッシュボード更新までを1本のパイプラインとして組む手順を、動くコードと公式ドキュメントの数値で整理します。なおLooker Studioは2026年4月にData Studio(日本語表記はデータポータル)へ改称され、製品URLも datastudio.google.com へ移りました。検索での通りは今もLooker Studioのため、本記事は併記で進めます。
まとめ:Dataformを挟む構成で最初に決めること
結論から示します。Looker Studioに見せるのは生データではなく、Dataformが日次で作る集計済みマートにします。可視化ツールから生ログを直接叩く構成は、閲覧人数に比例してスキャン量が増えるため、最初から採りません。
もうひとつ、更新の粒度を先に決めてください。データポータルのBigQueryコネクタは既定で12時間ごとにキャッシュが切れる設定で、ここを分単位に詰めるとクエリ回数がそのまま増えます。日次で貯まるデータに分単位の更新は要りません。
費用の話は単純です。Dataform自体は無料で、課金はDataformが発行するBigQueryのクエリとCloud Loggingから出ます。BigQueryにはクエリ月1TiB・ストレージ月10GiBの無料枠があり、個人スケールのデータなら枠内に収まる規模。なお、GA4のエクスポートを起点にする経路はGA4とBigQuery、Looker Studioの連携手順|Search Console統合と費用の実額で扱っているため、本記事はGA4以外の自前データを変換する側に絞ります。
3サービスの責務分担とDataform固有の課金構造
変換をDataformへ寄せる判断とサービスごとの守備範囲
BigQueryは保管とクエリ実行、Dataformはその上で走る変換の定義とスケジュール、Looker Studio(データポータル)は表示だけを担います。公式ドキュメントはDataformを「BigQueryにおけるデータ変換の複雑なワークフローを開発・テスト・バージョン管理・スケジュールする、データアナリスト向けのサービス」と定義しており、ELTのT(変換)に位置づけています。
ここで押さえるべきは、実際にSQLを実行しているのは常にBigQueryだという点。Dataformは実行計画とバージョン管理を持つ側にすぎません。この分担を取り違えると、費用の発生源を探すときに見当違いの場所を調べることになります。
Dataform無料・課金元はBigQueryとCloud Logging
Dataformの料金ページは「Dataform is a free service.」と明言しています。課金はDataformがBigQueryで実行するクエリと、ワークフロー実行の監視に使うCloud Loggingから発生します。Cloud Loggingは既定で有効かつすべてのDataformワークフロー実行に必須と明記されているため、無効化して費用を減らす選択肢はありません。
| サービス | 責務 | 課金 |
|---|---|---|
| BigQuery | 保管とクエリ実行 | スキャン量とストレージ |
| Dataform | 変換の定義・依存解決・スケジュール | 無料(実行分はBigQueryへ転嫁) |
| Cloud Logging | ワークフロー実行ログ | 取り込み量に応じて課金 |
| Looker Studio | 表示のみ | 無料(表示のたびにBigQueryへ課金) |
オンデマンドとEditionsの分岐点、リージョンごとの単価やコスト監視の設定はBigQueryの料金:オンデマンドとEditionsの分岐点・ストレージ課金・コスト監視にまとめています。
Apple Watchデータの取り込み経路とパーティション設計
ヘルスケアアプリのXML書き出しとJSONL整形
取り込み元はCSVでもスプレッドシートでも構いません。ここではサンプルとしてApple Watchの健康データを使います。iOSアプリを作ってHealthKitから読む方法もありますが、ダッシュボードを組むだけならアプリ開発は不要です。Appleのユーザガイドは、「ヘルスケア」>「概要」>右上の写真またはイニシャル>「すべてのヘルスケアデータを書き出す」の操作で、ヘルスケアおよびフィットネスのすべてのデータをXMLフォーマットで書き出せると案内しています。
書き出されるXMLは1レコード1要素の平たい構造で、心拍数・歩数・消費カロリー・睡眠といった種別が属性で区別されます。BigQueryへ入れる前に改行区切りJSON(JSONL)へ変換して型を確定させておくと、スキーマ推定の揺れに悩まされません。このとき、時刻列はタイムゾーンオフセット付きの文字列にしてください。パーティションはUTC基準で切られるため、ここを曖昧にすると後の集計が日付境界でずれます。
パーティション付きテーブルの作成とbq loadでの投入
bqはBigQuery用のPython製CLIです。--locationはサブコマンドの前に置くグローバルフラグで、指定を省くと既定のUSマルチリージョンにデータセットが作られます。なおこのフラグが効くのはmk --datasetなど一部のコマンドだけで、mk --tableでは無視されテーブルはデータセットのロケーションを継承します。テーブルは最初から日付パーティションとクラスタリングを付けて作ってください。あとから付け替えると再作成が必要になります。
gcloud config set project PROJECT_ID
bq --location=asia-northeast1 mk --dataset health_raw
bq mk \
--table \
--schema 'recorded_at:TIMESTAMP,record_type:STRING,unit:STRING,value:FLOAT,source_name:STRING' \
--time_partitioning_field recorded_at \
--time_partitioning_type DAY \
--clustering_fields record_type \
--require_partition_filter=true \
health_raw.records
bq --location=asia-northeast1 load \
--source_format=NEWLINE_DELIMITED_JSON \
health_raw.records \
./records.jsonl
--require_partition_filter=trueを付けると、パーティション列を絞らないクエリがエラーで弾かれます。全期間スキャンを事故として止める仕組みで、費用の歯止めとしては最も確実です。ただしこの設定は、次章のDataform側にも制約として効いてきます。パーティション指定の書き分けはBigQueryのCREATE TABLE:パーティション指定とMERGEの実装指針に整理しています。
Dataformによる変換層の構成とスケジュール設定
declarationとrefによる依存関係の宣言
DataformはSQLXという形式でテーブル定義を書きます。ファイル先頭のconfigブロックで出力形態(table、view、incremental、declaration、assertion)を宣言し、本体のSELECT文では参照先を${ref()}で書く形です。この書き方をした分だけDataformが依存グラフを組み立て、実行順を自動で決めてくれます。
ここに初回で必ず踏む落とし穴があります。${ref()}が解決できるのはDataformプロジェクト内で定義されたテーブルだけで、前章のbq mkのようにDataformの外で作ったテーブルは対象外です。外部のテーブルを参照するには、type: "declaration"のファイルを1本置いて存在を宣言する必要があります。
-- definitions/health_raw_records.sqlx
config {
type: "declaration",
database: "PROJECT_ID",
schema: "health_raw",
name: "records"
}
あわせて、リポジトリの設定ファイルに書く処理ロケーションをBigQueryのデータセットと揃えてください。公式は、クエリがどのデータセットも参照しない場合の既定ロケーションをUSマルチリージョンとしています。東京にデータセットを作ったならdefaultLocationをasia-northeast1にしないと、データセットが見つからず失敗します。
incrementalとuniqueKeyでの日次追記設計
健康データは日々増える時系列です。毎回テーブル全体を作り直すとスキャン量が積み上がるため、増分更新のincremental型で組みます。config直下のuniqueKeyを指定すると、Dataformは同じキーの既存行をMERGEで差し替えてから追記します。バッチを二重実行しても日付ごとの行が重複しません。
混同しやすいのが、assertionsブロック内にも同名のuniqueKeyが存在すること。こちらは重複行があれば検証を失敗させる別プロパティです。config直下に書いたuniqueKeyはMERGEの結合キーであり、重複の検査はしません。重複そのものを検知したいならassertions側にも明示してください。
pre_operationsでの定数化によるフルスキャン回避
増分の境界値をWHERE句へ直接埋め込む書き方には、2つの問題が同時に起きます。ひとつは、${when(incremental(), ...)}の第3引数を省略すると初回実行と全更新時に空文字へ展開され、前章で--require_partition_filter=trueを付けたテーブルに対して述語なしのクエリが飛んでエラーになること。もうひとつは、境界値をサブクエリで動的に求めると、公式が明記しているとおりパーティションの絞り込みが効かず毎回フルスキャンになることです。
公式が示す回避策は、pre_operationsブロックでBigQueryスクリプトの変数として値を宣言し、それを定数式としてWHERE句で使う形です。
config {
type: "incremental",
schema: "health_mart",
uniqueKey: ["activity_date", "record_type"],
assertions: {
nonNull: ["activity_date", "record_type"],
rowConditions: ["daily_value >= 0"]
},
bigquery: {
partitionBy: "activity_date",
clusterBy: ["record_type"],
updatePartitionFilter:
"activity_date >= DATE_SUB(CURRENT_DATE('Asia/Tokyo'), INTERVAL 3 DAY)"
}
}
pre_operations {
DECLARE from_ts DEFAULT (
${when(incremental(),
`SELECT TIMESTAMP(COALESCE(MAX(activity_date), DATE "2020-01-01"),
"Asia/Tokyo") FROM ${self()}`,
`SELECT TIMESTAMP("2020-01-01 00:00:00+09")`)}
)
}
SELECT
DATE(recorded_at, "Asia/Tokyo") AS activity_date,
record_type,
SUM(value) AS daily_value
FROM ${ref("health_raw", "records")}
WHERE recorded_at >= from_ts
GROUP BY 1, 2
${self()}はこのSQLXが作るテーブル自身への参照です。境界値を取り出すTIMESTAMP()にタイムゾーンを渡している点が要になります。activity_dateは日本時間で切った日付なのに、TIMESTAMP(activity_date)と1引数で書くとUTCの0時と解釈され、日本時間の0時から9時までのレコードが毎日境界日から漏れます。しかもMERGEなので、前日まで正しかった行が欠損値で上書きされ、エラーも出ません。集計値が静かに目減りする事故になります。COALESCEを挟んでいるのは、出力テーブルが空の状態で増分実行に入るとMAX()がNULLを返し、以降ずっと0行のまま止まるためです。
updatePartitionFilterはMERGE側の手当てです。指定しないと、Dataformは一致する行を探すために出力テーブル全体を走査します。直近数日だけを更新対象と決め打てば、こちらのスキャン量も抑えられます。
assertionsによる可視化前のデータ検証
Dataformのassertionsは、テーブル作成後に自動で走るデータ品質テストです。configブロックに書ける条件は4種類あります。nonNullは指定列にNULLが無いこと、uniqueKeyは指定列の値に重複が無いこと、uniqueKeysは複数の列組み合わせでの一意性、rowConditionsは任意のSQL式を全行が満たすことを検証し、1行でもfalseになれば失敗。結果はワークスペースのワークフロー実行ログで確認できます。
ここを省くと、書き出しXMLの単位がkcalとkJで混ざった日に、ダッシュボードの数値だけが跳ねます。可視化の手前に検証を置く価値は、グラフを直す時間の節約ではありません。間違ったグラフを信じて判断しない点にあります。
リリース構成とワークフロー構成でのスケジュール設定
Dataformのスケジュール実行は2段構えです。まずリリース構成(release configuration)でコンパイル設定のテンプレートを作り、次にワークフロー構成(workflow configuration)でそのリリース構成と実行対象アクション、実行スケジュールを指定します。スケジュールはunix-cron形式で入力し、タイムゾーンを選べます。
公式ドキュメントが明記している落とし穴が2つ。既定のDataformサービスエージェントではワークフローを実行できず、カスタムサービスアカウントか自分のGoogleアカウント認証情報を指定する必要があります。もう1点、最新のコンパイル結果を確実に実行させるには、コンパイル結果の作成時刻とスケジュール実行時刻の間を最低1時間空けます。どちらも画面上で警告が出ないため、初回設定でそのまま踏む箇所です。
ダッシュボードの更新頻度とBigQuery費用の抑え方
データの更新速度とレポート自動更新の違い
データソース設定の「データの更新速度」が、キャッシュを保持する時間です。コネクタごとに選べる値が決まっており、BigQueryは分単位で1〜50分ごと、時間単位で1〜12時間ごとから選べて、既定は12時間ごとになっています。
混同しやすいのがレポート側の「自動更新」設定で、公式ヘルプはこれがデータソースの更新速度には影響しないと明記しています。統合(ブレンド)データソースの場合は、統合したデータソースのうち最も短い更新間隔が採用される点にも注意が要ります。15分のスプレッドシートと4時間のBigQueryを混ぜれば全体が15分になり、BigQueryへの問い合わせも15分ごとに走る計算です。
費用を抑える打ち手の優先順位
最初に効くのは更新速度の延長です。既定の12時間から詰める合理的な理由は、元データが12時間より短い間隔で更新される場合だけ。日次バッチのマートを1分更新で見に行く設定は、同じ数字を取りに行くクエリを何十回も発行しているのと変わりません。
次が抽出(extract)データソースです。集計済みマートをLooker Studio側へ静的に取り込んでしまえば、閲覧のたびのBigQueryクエリはゼロになります。制約は明確で、抽出済みデータソースは100MBを超えるとデータが抽出されずエラーになり、750,000行を超えると切り捨てられます。日次粒度まで落とした健康データなら余裕で収まりますが、生ログ相当のサイズでは使えません。
BI Engineは条件付きです。BI Engineが高速化したクエリではテーブルデータを読むステージが無料になり、オンデマンド料金ではそのステージが0スキャンバイトとして課金される、と公式は説明しています。ただしBI Engineはメモリ容量を予約する有料サービスで、既定表示のus-central1では1GiB時あたり0.0416USDの課金。しかもEditionsのコミットメントに付く無償容量について、公式は「オンデマンド分析のプロジェクトにあるBI Engineの予約は無償容量に算入されない」と明記しています。無料枠に収まる個人スケールでは、まず入れない判断が正解です。
INFORMATION_SCHEMAによるダッシュボード発クエリの特定
どのレポートが費用を食っているかは、推測せずBigQuery側で特定できます。Data Studioが発行するジョブにはrequestor、looker_studio_report_id、looker_studio_datasource_idのラベルが自動で付き、INFORMATION_SCHEMA.JOBSから追跡できると公式ドキュメントが説明しています。
SELECT
(SELECT l.value FROM UNNEST(labels) l
WHERE l.key = "looker_studio_report_id") AS report_id,
COUNT(*) AS jobs,
ROUND(SUM(total_bytes_billed) / POW(1024, 4), 3) AS tib_billed
FROM `region-asia-northeast1`.INFORMATION_SCHEMA.JOBS
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND (SELECT l.value FROM UNNEST(labels) l
WHERE l.key = "requestor") = "looker_studio"
GROUP BY report_id
ORDER BY tib_billed DESC
レポートIDはData StudioのURL末尾に出るIDと一致するため、上位に出たIDをそのままURLへ当てれば犯人のレポートが開きます。費用が想定を超えたとき、ここを見れば「どのグラフを直すか」まで一手で降りられます。実行時の前提は2つ。このビューはプロジェクト内の全ユーザーのジョブを返すためroles/bigquery.resourceViewerが必要で、クエリの実行ロケーションもリージョン修飾子と揃える必要があります。
Dataformを挟まないほうがよい条件
いちばん大きな分岐は変換の規模です。マートが2、3本しかないなら、リポジトリ・ワークスペース・リリース構成・ワークフロー構成という4層の概念は明らかに過剰で、BigQueryのスケジュールされたクエリで十分に足ります。Dataformが効き始めるのは、依存関係が枝分かれして実行順を人が覚えられなくなってから。「あるテーブルの更新漏れが、下流の集計を静かに壊す」状況が出たら移行時期だと考えてください。この判断を先送りして先に道具を増やすと、動く仕組みではなく管理対象が増えます。
もうひとつ、秒単位の鮮度が要件に書かれている場合はこの構成ごと不適です。BigQueryコネクタの最短更新は1分ごとで、そもそも準リアルタイム監視の道具ではありません。ここを無理に詰めても費用が増えるだけです。基盤全体の規模別の判断はデータ分析基盤の構築とは?5層アーキテクチャとBigQuery実装手順を技術視点で解説で層ごとに整理しています。
よくある質問
BigQueryサンドボックスだけでこの構成を試せますか?
試せません。サンドボックスはクレジットカードなしで始められますが、公式のLimitationsがDML文の非対応を明記しています。本記事のuniqueKey付きincrementalテーブルはMERGE、つまりDMLで動くため実行できません。加えて、すべてのテーブル・ビュー・パーティションが60日で自動的に失効します。クエリの動作確認までなら使えますが、パイプラインとして回すなら課金アカウントが必要です。
Looker StudioからBigQueryにつなぐと料金はどのくらいかかりますか?
Looker Studio(データポータル)本体とBigQueryコネクタの利用は無料で、費用はBigQuery側のクエリ課金として発生します。オンデマンドではスキャンしたデータ量に応じた課金で、月1TiBまでは無料枠です。日次集計のマートを参照する個人スケールなら枠内に収まりますが、部署全員が生ログ直結のダッシュボードを開く構成では簡単に超えます。単価はリージョンで変わるため、料金ページのリージョン切り替えで自分のロケーションの値を確認してください。
Dataformを使わずBigQueryのスケジュールされたクエリだけで組めますか?
組めます。変換が数本で依存関係が単純なうちは、そのほうが構成要素が少なく運用も軽くなります。判断材料は本数ではなく依存の複雑さです。${ref()}による依存解決とassertionsによる自動検証は、スケジュールされたクエリの側では自前で作り込むしかない部分なので、そこに手間を感じ始めたら移行のタイミングになります。
Looker StudioがData Studioに改称されたと聞きました。作ったレポートは作り直しですか?
不要です。公式ドキュメントは「この変更に合わせてレポートを更新する必要はありません」と明記しており、lookerstudio.google.com は datastudio.google.com へ自動的にリダイレクトされます。ひとつだけ実務上の注意があり、社内でプロキシによる外部サイト制限をかけている場合は、IT管理者が新しいドメインをACLへ追加する必要があります。ドキュメントの所在も docs.cloud.google.com/data-studio 配下へ移りました。
ダッシュボードの数値が古いまま更新されません。どこから見ればよいですか?
Dataformのワークフロー実行ログで、スケジュール実行が成功しているかとassertionsが失敗していないかを確認するのが先です。次にBigQueryのテーブルで最新の日付まで行が入っているかを見ます。ここまで正常ならLooker Studio側のキャッシュが原因で、データソースの更新速度が既定の12時間のままか、レポート側の自動更新をデータソースの更新と取り違えていないかを疑ってください。