· 19分で読める

広告データとGA4をBigQueryで突き合わせる|媒体報告とサイト実測のギャップを分析する

目次閉じる開く

なぜ媒体データとGA4を突き合わせるのか

広告運用のレポートで、媒体管理画面のCVとGA4のCVが一致しない。この経験は多くの運用者が持っているはずです。数字が合わないこと自体は異常ではありません。両者は測っている対象も定義も違うためです。

媒体管理画面のCVは、媒体が計上する申告値です。媒体ごとのアトリビューション設定が反映され、広告を見たものの直接クリックしていないビュースルーも含まれることがあります。一方でGA4のCVは、サイト側のタグで計測した実測値です。多くの場合ラストクリックが中心で、サイトに実際に着地したユーザーの行動を捉えています。

同じ「CV」でも媒体とGA4では定義が違う媒体管理画面の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 というテーブルに date campaign_name cost conversions の列がある前提でSQLを書いています。実際の環境では、自分のマートの列名に読み替えてください。GA4側のフィールド名は、公式スキーマに沿った固定の名前を使います。

JOINキーを設計する

突き合わせの成否は、JOINキーの設計でほぼ決まります。基本は「日付 × ソース/メディア × キャンペーン」の3軸です。日付で期間を揃え、キャンペーン単位で媒体の管理粒度に合わせます。

GA4側は、セッションの獲得ディメンション(流入元)を使います。具体的には遷移先URLに付いた utm_source utm_medium utm_campaign の値です。広告側は、マートに入っているキャンペーン名を使います。この2つを噛み合わせるのがJOINの中心です。

JOINキーは「日付 × ソース/メディア × キャンペーン」GA4側(セッション獲得)utm_sourceutm_mediumutm_campaign遷移先URLのパラメータから取得広告側(マート)date(日付)campaign_namecost / conversions媒体の管理粒度で集計JOIN最重要:命名規則が揃っていないとJOINできないutm_campaign の値が広告側のキャンペーン名と一致する設計にする

ここで最も重要なのは、UTMパラメータの命名規則が揃っていないと、そもそもJOINが成立しないという点です。GA4側の utm_campaign の値と、広告側の campaign_name の値が文字列として一致しなければ、キーが噛み合いません。片方が「summer_sale」で、もう片方が「サマーセール」では、別のキャンペーンとして扱われます。

対策は、遷移先URLの utm_campaign に、広告側のキャンペーン名(またはキャンペーンIDなど一意に対応する値)を入れる設計に統一することです。運用の入口でこのルールを決めておくと、後からBigQuery側で無理なマッピングをする手間が減ります。逆にここが崩れていると、どれだけ精緻なSQLを書いても正しく突き合わせられません。

UTM命名規則のそろえ方噛み合わない例GA4: utm_campaign = summer_sale広告: サマーセール_7月→ 別キャンペーン扱いでJOIN不成立噛み合う例GA4: utm_campaign = summer_sale_07広告: summer_sale_07→ 値が一致してJOIN成立

実践SQLで1枚のテーブルにまとめる

ここからは実際のSQLです。GA4側の集計、広告側の集計、両者のJOINの3段階で組み立てます。GA4側のフィールド名とネスト構文は、GA4 BigQueryエクスポート活用ガイドで説明したものをそのまま使っています。

突き合わせSQLの3ステップクエリ①GA4を日別×流入元でセッション・CVを集計クエリ②広告を日別×キャンペーン別に集計クエリ③FULL OUTER JOINで1枚に統合費用・媒体CV・GA4セッション・GA4CVを1行で並べる

クエリ①:GA4を日別・流入元別に集計する

まずGA4側です。セッションを user_pseudo_idga_session_id の組み合わせで識別し、各セッションの最初の page_viewpage_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 (...) を省略しています。実行するときは、クエリ①の eventssessions の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の設定漏れ、媒体側の仕様変更など、調べるべき出来事のサインであることが多いためです。

乖離率は「水準」より「変化」を見る乖離率 = GA4CV ÷ 媒体CV を日次・週次で記録する同じ集計方法で継続して並べることが前提平常時水準が安定 → そのまま様子見急な変化タグ・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広告自動タグ(gclid)でGA4に流入元を連携手動UTMがなくても判別される※優先関係の詳細は本文では深追いしないMeta・Yahoo!など遷移先URLに手動でUTMを付与UTMがないと流入元が判別できない命名規則の統一が特に重要

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の命名規則を広告側と揃える
SQLGA4集計・広告集計・FULL OUTER JOINの3段階で1枚にする
読み解き乖離の理由を類型で押さえ、乖離率の変化を定点観測する
注意表記ゆれ・gclidとUTMの関係・(not set)に気をつける

この突き合わせの価値は、媒体の申告を一方的に信じることでも否定することでもなく、両者の差を継続して見る目を持てることにあります。差は毎回ゼロにはなりません。だからこそ、平常時の水準を知り、変化に気づける状態を作ることが、実務での意思決定を支えます。

最初は媒体を1つに絞り、1本のキャンペーンで突き合わせの流れを通してみるのが近道です。そこで命名規則のズレや計測の癖を把握してから、媒体とキャンペーンを広げていくと無理がありません。一度SQLを組んでしまえば、日々の集計はスケジュールクエリやAIエージェントに任せられます。BigQueryをAIエージェントに接続して日本語で分析する方法はBigQueryをAIエージェントに接続するで解説しています。定型の突き合わせは自動化し、乖離が動いたときの深掘りに人の時間を使う。そんな運用に育てていくのがおすすめです。

関連記事

この記事をAIと深掘りする

要約・疑問の解消に。記事のタイトル・URL・参照元を入れた質問文が自動で入力されます。

| 共有 はてブ

SIGNALZ メルマガ

厳選した実践ナレッジを週1回お届けします。

SIGNALZ

SIGNALZ

運用型広告の実務経験をもとに、体系的なナレッジを発信しています。

SIGNALZの記事はAIを活用して作成し、10年以上の運用型広告の実務経験をもとに内容を確認・監修しています。制作方針の詳細はサイトについてをご覧ください。

この記事について感想やご質問を送れます

誤りの指摘、補足情報、ご質問など、お気軽にどうぞ。