なぜ媒体データとGA4を突き合わせるのか
広告運用のレポートで、媒体管理画面のCVとGA4のCVが一致しない。この経験は多くの運用者が持っているはずです。数字が合わないこと自体は異常ではありません。両者は測っている対象も定義も違うためです。
媒体管理画面のCVは、媒体が計上する申告値です。媒体ごとのアトリビューション設定が反映され、広告を見たものの直接クリックしていないビュースルーも含まれることがあります。一方でGA4のCVは、サイト側のタグで計測した実測値です。多くの場合ラストクリックが中心で、サイトに実際に着地したユーザーの行動を捉えています。
定義が違う数字を「どちらが正しいか」で比べても意味はありません。片方だけを見ていると、媒体の申告を鵜呑みにするか、サイト実測だけで媒体の貢献を過小評価するか、どちらかに偏ります。両方を同じ表に並べて差を見ることで、初めて「媒体の申告に対して、サイトでは実際にどう動いたか」が読み取れます。
具体例で考えます。あるキャンペーンの媒体CVが100件、GA4CVが60件だったとします。この40件の差を「媒体が盛っている」と切り捨てるのは早計です。ビュースルーの計上、アトリビューションの範囲、サイト側の計測欠損が混ざった結果だからです。差の中身を分けて考えられると、媒体の役割を正しく評価できます。
逆にGA4CVのほうが多いケースもあります。1回のセッションで複数回コンバージョンが起きる商材や、媒体側の計測タグが一部の成果を拾えていない場合です。どちらに振れているかは、媒体とサイトの計測設計に依存します。だからこそ、両方を並べて自社の傾向を掴むことに意味があります。
筆者も日次のレポートで、媒体CVとGA4CVを1枚に並べる運用をしています。差そのものよりも、差の大きさが平常時からどう動いたかを見るほうが、異変の発見には役立ちました。この記事では、BigQuery上で両者を突き合わせる具体的な手順を整理します。
突き合わせの前提を整える
突き合わせは、広告データとGA4データがどちらもBigQueryに揃っていることが前提です。片方でも欠けていると、JOINする対象そのものが存在しません。まず次の2つの土台を用意してください。
| 用意するもの | 内容 | 参照する記事 |
|---|---|---|
| 広告データのBQ集約 | 各媒体の費用・CVを日次でBigQueryに集める | BigQueryで広告データを集約する方法 |
| Google広告の連携 | Data Transfer Serviceで自動エクスポート | Google広告 BigQuery DTS連携 |
| Meta・Yahoo!の連携 | APIやGASでBigQueryへインポート | Meta・Yahoo!広告のBigQueryインポート |
| GA4データのエクスポート | イベントデータをBigQueryへ日次出力 | GA4 BigQueryエクスポート活用ガイド |
広告側は媒体ごとにテーブルの形が異なります。DTSやAPIで取り込んだ後、日付・キャンペーン名・費用・CVを共通の形にまとめたマートテーブルを作っておくと、後続のJOINが素直になります。この整形の考え方はBigQueryで広告データを集約する方法で扱っています。
GA4側は、BigQueryエクスポートで events_YYYYMMDD というテーブルが日次で生成されます。テーブル構造やネスト形式の扱いはGA4 BigQueryエクスポート活用ガイドで解説しているとおりです。この記事のSQLは、そこで説明したフィールド名と構文にそのまま合わせています。
突き合わせを始める前に、次の点が揃っているかを確認してください。ひとつでも欠けていると、後のJOINで期待した行が出てきません。
- 広告データが日次でBigQueryに入っている(費用・CV・キャンペーン名が揃う)
- GA4のBigQueryエクスポートが有効で
events_*テーブルが生成されている - 広告の遷移先URLにUTMが付いている(Google広告以外は特に必須)
-
utm_campaignの値と広告側のキャンペーン名の対応ルールが決まっている - 突き合わせに使う期間で、両テーブルにデータが存在している
補足 広告側のマートテーブルの列名(費用やCVのカラム名)は、取り込み方法によって変わります。この記事では説明のため
ads_martというテーブルにdatecampaign_namecostconversionsの列がある前提でSQLを書いています。実際の環境では、自分のマートの列名に読み替えてください。GA4側のフィールド名は、公式スキーマに沿った固定の名前を使います。
JOINキーを設計する
突き合わせの成否は、JOINキーの設計でほぼ決まります。基本は「日付 × ソース/メディア × キャンペーン」の3軸です。日付で期間を揃え、キャンペーン単位で媒体の管理粒度に合わせます。
GA4側は、セッションの獲得ディメンション(流入元)を使います。具体的には遷移先URLに付いた utm_source utm_medium utm_campaign の値です。広告側は、マートに入っているキャンペーン名を使います。この2つを噛み合わせるのがJOINの中心です。
ここで最も重要なのは、UTMパラメータの命名規則が揃っていないと、そもそもJOINが成立しないという点です。GA4側の utm_campaign の値と、広告側の campaign_name の値が文字列として一致しなければ、キーが噛み合いません。片方が「summer_sale」で、もう片方が「サマーセール」では、別のキャンペーンとして扱われます。
対策は、遷移先URLの utm_campaign に、広告側のキャンペーン名(またはキャンペーンIDなど一意に対応する値)を入れる設計に統一することです。運用の入口でこのルールを決めておくと、後からBigQuery側で無理なマッピングをする手間が減ります。逆にここが崩れていると、どれだけ精緻なSQLを書いても正しく突き合わせられません。
実践SQLで1枚のテーブルにまとめる
ここからは実際のSQLです。GA4側の集計、広告側の集計、両者のJOINの3段階で組み立てます。GA4側のフィールド名とネスト構文は、GA4 BigQueryエクスポート活用ガイドで説明したものをそのまま使っています。
クエリ①:GA4を日別・流入元別に集計する
まずGA4側です。セッションを user_pseudo_id と ga_session_id の組み合わせで識別し、各セッションの最初の page_view の page_location からUTMを取り出します。CVはここでは purchase イベントを1セッションに含むかで数えます。自社のCVイベントに合わせて purchase の部分を読み替えてください。
WITH events AS (
SELECT
user_pseudo_id,
event_date,
event_name,
event_timestamp,
(SELECT value.int_value
FROM UNNEST(event_params)
WHERE key = 'ga_session_id') AS ga_session_id,
(SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'page_location') AS page_location
FROM
`project.dataset.events_*`
WHERE
_TABLE_SUFFIX BETWEEN '20260601' AND '20260630'
),
sessions AS (
SELECT
user_pseudo_id,
ga_session_id,
MIN(event_date) AS session_date,
ARRAY_AGG(
IF(event_name = 'page_view', page_location, NULL)
IGNORE NULLS
ORDER BY event_timestamp
)[SAFE_OFFSET(0)] AS entry_url,
COUNTIF(event_name = 'purchase') AS purchase_count
FROM
events
WHERE
ga_session_id IS NOT NULL
GROUP BY
user_pseudo_id, ga_session_id
)
SELECT
PARSE_DATE('%Y%m%d', session_date) AS date,
REGEXP_EXTRACT(entry_url, r'utm_source=([^&]+)') AS source,
REGEXP_EXTRACT(entry_url, r'utm_medium=([^&]+)') AS medium,
REGEXP_EXTRACT(entry_url, r'utm_campaign=([^&]+)') AS campaign,
COUNT(*) AS ga4_sessions,
SUM(IF(purchase_count > 0, 1, 0)) AS ga4_conversions
FROM
sessions
GROUP BY
date, source, medium, campaign
ARRAY_AGG(... ORDER BY event_timestamp)[SAFE_OFFSET(0)] で、セッション内で最初に見た page_view のURLを取り出しています。UTMは着地ページのURLに付いているため、着地時点の流入元をセッションの獲得ディメンションとして扱う考え方です。REGEXP_EXTRACT でUTMを抜き出す方法は、GA4エクスポート記事のUTM分解の例と同じ構文です。
クエリ②:広告を日別・キャンペーン別に集計する
次に広告側です。こちらはGA4のような生イベントではなく、集約済みのマートテーブルを想定しています。日付とキャンペーン名で費用と媒体CVを集計します。
SELECT
date,
campaign_name,
SUM(cost) AS cost,
SUM(conversions) AS media_conversions
FROM
`project.dataset.ads_mart`
WHERE
date BETWEEN '2026-06-01' AND '2026-06-30'
GROUP BY
date, campaign_name
media_conversions が媒体の申告CVです。複数媒体を1つのマートにまとめている場合は、媒体を識別する列(例: platform)も残しておくと、後で媒体別に乖離を見られます。ここでは説明を簡潔にするため、キャンペーン単位に絞っています。
クエリ③:FULL OUTER JOINで1枚にまとめる
最後に両者を突き合わせます。JOINキーは日付とキャンペーンです。FULL OUTER JOIN を使う理由は、片側にしか存在しない行も落とさずに見たいためです。広告側にあってGA4側にない(サイト着地が計測されていない)、逆にGA4側にあって広告側にない(UTMの表記ズレなど)、どちらも重要な情報だからです。
WITH ga4 AS (
-- クエリ①の内容をそのまま入れる
SELECT
PARSE_DATE('%Y%m%d', session_date) AS date,
REGEXP_EXTRACT(entry_url, r'utm_campaign=([^&]+)') AS campaign,
REGEXP_EXTRACT(entry_url, r'utm_source=([^&]+)') AS source,
REGEXP_EXTRACT(entry_url, r'utm_medium=([^&]+)') AS medium,
COUNT(*) AS ga4_sessions,
SUM(IF(purchase_count > 0, 1, 0)) AS ga4_conversions
FROM sessions
GROUP BY date, campaign, source, medium
),
ads AS (
SELECT
date,
campaign_name,
SUM(cost) AS cost,
SUM(conversions) AS media_conversions
FROM `project.dataset.ads_mart`
WHERE date BETWEEN '2026-06-01' AND '2026-06-30'
GROUP BY date, campaign_name
)
SELECT
COALESCE(ads.date, ga4.date) AS date,
COALESCE(ads.campaign_name, ga4.campaign) AS campaign,
ads.cost,
ads.media_conversions,
ga4.ga4_sessions,
ga4.ga4_conversions
FROM
ads
FULL OUTER JOIN
ga4
ON
ads.date = ga4.date
AND ads.campaign_name = ga4.campaign
ORDER BY
date, cost DESC
上の例では WITH sessions AS (...) を省略しています。実行するときは、クエリ①の events と sessions のCTEを先頭に置いてから ga4 を続けてください。COALESCE で日付とキャンペーン名を補完しているのは、FULL OUTER JOINで片側がNULLになる行でもキー列を埋めるためです。
この1枚があれば、キャンペーンごとに「費用・媒体CV・GA4セッション・GA4CV」が横並びになります。媒体CVとGA4CVの列を見比べるだけで、申告と実測の差がその場で分かります。SQLをもう少し体系的に学びたい場合はBigQueryで広告データを分析するSQL実践が役立ちます。
乖離をどう読み解くか
数字が並んだら、次は差の解釈です。媒体CVとGA4CVがずれる理由は、いくつかの類型に整理できます。差を見たときに「どの理由で起きていそうか」を当てられると、対応の判断が早くなります。
| 乖離の主な理由 | 内容 | 傾向 |
|---|---|---|
| アトリビューション定義の差 | 媒体ごとに評価するクリックやコンバージョンの範囲が違う | 媒体CV > GA4CV になりやすい |
| ビュースルーの計上 | 広告を見たが未クリックの成果を媒体が計上することがある | 媒体CVが大きく出やすい |
| 計測方式の違い | 媒体はクリック基点、GA4はサイト側イベント基点で数える | 双方向にずれうる |
| 計測の欠損(ITP等) | ブラウザの制限やタグ未発火でGA4側が取りこぼす | GA4CV < 実態 になりやすい |
| 遷移時のパラメータ欠落 | リダイレクトなどでUTMが失われ流入元が判別できない | GA4側で該当行が分散・欠落 |
ここで大切なのは、これらは一般論として確立している傾向であって、どの理由が何パーセントを占めるかは環境ごとに違うという点です。「媒体CVは通常これくらい多い」といった固定の割合はありません。自社の値を継続して観測し、平常時の水準を把握することが出発点になります。
そこで有効なのが、乖離率を定点観測する運用です。日次または週次で「媒体CVに対するGA4CVの比率」を記録し、時系列で追います。比率そのものより、比率が急に動いたタイミングに注目します。急な変化は、タグの不具合、UTMの設定漏れ、媒体側の仕様変更など、調べるべき出来事のサインであることが多いためです。
乖離率を列として持たせる
定点観測をSQLに組み込むなら、クエリ③の結果に乖離率の列を足すのが手軽です。媒体CVを分母にGA4CVを割り、キャンペーン単位で並べます。媒体CVが0の行はゼロ除算になるため、SAFE_DIVIDE で回避します。
SELECT
date,
campaign,
cost,
media_conversions,
ga4_conversions,
ROUND(SAFE_DIVIDE(ga4_conversions, media_conversions), 2) AS ga4_ratio
FROM
joined
ORDER BY
date, cost DESC
ga4_ratio が1に近いほど、媒体の申告とサイト実測が近いことを示します。この列を日次で眺め、値が普段の水準から大きく外れたキャンペーンを拾えば、調査対象を素早く絞り込めます。joined の部分は、クエリ③の FULL OUTER JOIN の結果を指すCTEに置き換えてください。
つまずきやすいポイント
突き合わせを続けていると、いくつか決まった場所でつまずきます。事前に知っておくと、原因不明の乖離に悩む時間を減らせます。
第一に、キャンペーン名の表記ゆれと変更です。広告側でキャンペーン名を途中でリネームすると、過去のUTM(utm_campaign)と現在のキャンペーン名がずれます。全角と半角、スペースの有無、大文字と小文字の違いでも、文字列としては別物になります。JOINが急に噛み合わなくなったら、まず名前の変更履歴を疑ってください。
第二に、自動タグ(gclid)と手動UTMの関係です。Google広告は、遷移先URLに gclid という識別子を自動で付与し、これを通じてGA4に流入元が連携されます。手動でUTMを付けなくても、Google広告からの流入はGA4側で判別されます。
gclidと手動UTMのどちらが優先されるかといった細かい仕様は、この記事では深追いしません。実務で押さえるべきは、Google広告は自動タグで連携されうる一方、その他の媒体は手動UTMが判別の生命線になる、という守備範囲の違いです。突き合わせを設計するときは、媒体ごとに流入元がどう連携されるかを一度整理しておくと安全です。
第三に、GA4の (not set) です。流入元が判別できなかったセッションは、GA4上で (not set) として扱われます。UTMの付け忘れ、リダイレクトによるパラメータ欠落、直接流入などが原因です。上のSQLでは REGEXP_EXTRACT の結果がNULLになる行がこれに相当します。(not set) が多いときは、突き合わせの精度が下がっているサインとして、UTM設計を見直してください。
つまずきポイントを点検するチェックリストを用意しました。
- キャンペーン名を最近リネームしていないか(履歴を確認)
- 全角・半角・スペース・大文字小文字の表記が広告側とGA4側で揃っているか
- Google広告以外の媒体に、遷移先URLのUTMが付いているか
- リダイレクトでUTMが欠落していないか(着地URLを実機で確認)
- GA4側の
(not set)の割合が急に増えていないか
まとめ
媒体データとGA4をBigQueryで突き合わせる手順を、前提の整備からSQL、読み解き方まで整理しました。要点を振り返ります。
| ステップ | 内容 |
|---|---|
| 目的 | 定義の違う媒体CVとGA4CVを並べ、申告に対する実態を見る |
| 前提 | 広告データとGA4データの両方をBigQueryに集約しておく |
| キー設計 | 日付×キャンペーンでJOIN。UTMの命名規則を広告側と揃える |
| SQL | GA4集計・広告集計・FULL OUTER JOINの3段階で1枚にする |
| 読み解き | 乖離の理由を類型で押さえ、乖離率の変化を定点観測する |
| 注意 | 表記ゆれ・gclidとUTMの関係・(not set)に気をつける |
この突き合わせの価値は、媒体の申告を一方的に信じることでも否定することでもなく、両者の差を継続して見る目を持てることにあります。差は毎回ゼロにはなりません。だからこそ、平常時の水準を知り、変化に気づける状態を作ることが、実務での意思決定を支えます。
最初は媒体を1つに絞り、1本のキャンペーンで突き合わせの流れを通してみるのが近道です。そこで命名規則のズレや計測の癖を把握してから、媒体とキャンペーンを広げていくと無理がありません。一度SQLを組んでしまえば、日々の集計はスケジュールクエリやAIエージェントに任せられます。BigQueryをAIエージェントに接続して日本語で分析する方法はBigQueryをAIエージェントに接続するで解説しています。定型の突き合わせは自動化し、乖離が動いたときの深掘りに人の時間を使う。そんな運用に育てていくのがおすすめです。