【BigQueryでCVの裏側を読む】コンテンツSEOは無駄か? ブログ経由の後日CVを可視化する

この記事は、ブログやオウンドメディアを運用しているのに「で、これ売上につながってるの?」と詰められて答えに困っているマーケター・SEO担当者に向けたものです。GA4の標準レポートでブログのCVを見ると、たいてい悲しいほど数字が小さい。それを根拠に「コンテンツSEOは無駄」と判断されそうになっている人に、読んでほしい内容です。

結論から書きます。ブログのCV貢献が小さく見えるのは、多くの場合、ブログの実力ではなく測り方の問題です。ブログ記事は、その場で契約を決めさせるページというより、検討のきっかけを作る入口です。読んだ人の多くは、その日には契約せず、数日から数週間の検討期間を挟んで、別の入口から戻ってきて契約します。ところがGA4の標準的なCV集計は「最後に接触したチャネル(last-click)」に成果を全部つけるので、最初にきっかけを作ったブログの貢献は、別チャネルでクローズしたという理由だけでゼロとして消えます。

実際にあるサービスのデータをBigQueryで調べたところ、ブログで直接クローズしたCVは5件しかありませんでした。でも経路のどこかでブログに触れてから契約したCVは24件。last-clickの集計では、ブログの貢献は実態の約5分の1しか見えていませんでした。本記事では、この「消えているブログの貢献」をBigQueryでどう可視化するかを、クエリ付きで解説します。

なお、登場するプロジェクトID・データセット・サービス名・URL・件数の一部は、クライアント情報を伏せるためサンプル値に置き換えています。クライアントは仮にサンプル社(サブスク型サービスを運営)とします。

なぜGA4標準レポートではブログのCVが過小に見えるのか

GA4のCV集計は、原則として「最後にCVに繋がったセッションの参照元」に成果を割り当てます。これがlast-clickです。最後の一押しを評価する考え方として、それ自体は間違っていません。

問題は、ブログの役割が「最後の一押し」ではないことです。ブログ記事は、検討の入口で読まれます。何か困りごとを調べていて検索からブログ記事に着地し、知識を得て、その日は契約しない。数日後に料金やプランを比較し、また数日後に申込ページから契約する。このとき契約セッションの入口は広告だったり指名検索だったりするので、last-clickの集計では、そのCVは広告や指名の手柄になります。きっかけを作ったブログは、経路に確かに存在していたのに、成果ゼロと記録される。

これがGA4標準レポートで「ブログのCVが少ない」と見える正体です。ブログが効いていないのではなく、ブログが効く位置(経路の前半)を、last-clickという測り方が拾えていないだけです。

測り方を変える:last-clickではなく「経路にブログが含まれるか」

ではどう測るか。発想は単純で、CVしたユーザーの行動を時系列でさかのぼり、CVに至るまでの経路のどこかにブログ接触が含まれていたかを数えます。last-click(CVしたまさにそのセッションでブログを見たか)と、アシスト基準(CV前の経路のどこかでブログを見たか)の2つを並べて比べる。この差が、last-clickでは見えていなかったブログの貢献です。

GA4のデータをBigQueryにエクスポートしておけば、これはクエリで出せます。標準レポートのUIではできない、行動ログを直接たどる分析です。

ユーザーの行動を時系列で追う準備

まず、全イベントから必要な情報を取り出します。ユーザーID、セッションID、イベント名、発生時刻、閲覧ページのパス、そしてセッションの参照元です。ブログかどうかはページのパスが /blog/ で始まるかで判定します。

CVは申込完了イベント(ここでは仮に purchase とします)とし、対象期間のユーザーごと最初の1件に絞ります。ブログ接触は、そのCVの時点から90日さかのぼった範囲で拾います。検討期間が数週間に及ぶことを考えると、CV月だけを見ると経路の前半が切れてしまうので、さかのぼって読むのが肝心です。

ブログ貢献を last-click と アシストで数えるクエリ

次のクエリは、CVごとに2つのフラグを立てます。ひとつはlast-click(CVしたそのセッションでブログを見たか)、もうひとつはアシスト(CV前90日のどこかのセッションでブログを見たか)。最後にそれぞれを合計して、件数と倍率を出します。

WITH

ev AS (

  SELECT

    user_pseudo_id,

    (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id,

    event_name,

    TIMESTAMP_MICROS(event_timestamp) AS event_time,

    REGEXP_REPLACE(

      REGEXP_EXTRACT(

        (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location'),

        r'(https?://[^?#]+)'),

      r'^https?://[^/]+', '') AS path

  FROM `sample-project-000000.analytics_000000000.events_*`

  WHERE _TABLE_SUFFIX BETWEEN '20260311' AND '20260531'

),

sess AS (

  SELECT

    user_pseudo_id, session_id,

    MIN(event_time) AS session_start,

    MAX(REGEXP_CONTAINS(IFNULL(path,''), r'^/blog/')) AS has_blog

  FROM ev

  WHERE session_id IS NOT NULL

  GROUP BY 1, 2

),

cv AS (

  SELECT user_pseudo_id, cv_time, cv_session_id

  FROM (

    SELECT user_pseudo_id, event_time AS cv_time, session_id AS cv_session_id,

      ROW_NUMBER() OVER (PARTITION BY user_pseudo_id ORDER BY event_time) AS rn

    FROM ev

    WHERE event_name = 'purchase'

      AND event_time >= TIMESTAMP('2026-04-01', 'Asia/Tokyo')

      AND event_time <  TIMESTAMP('2026-06-01', 'Asia/Tokyo')

  )

  WHERE rn = 1

),

flags AS (

  SELECT

    c.user_pseudo_id,

    MAX(CASE WHEN s.session_id = c.cv_session_id AND s.has_blog THEN 1 ELSE 0 END) AS blog_lastclick,

    MAX(CASE WHEN s.session_start BETWEEN TIMESTAMP_SUB(c.cv_time, INTERVAL 90 DAY) AND c.cv_time

                  AND s.has_blog THEN 1 ELSE 0 END) AS blog_involved

  FROM cv c

  JOIN sess s ON s.user_pseudo_id = c.user_pseudo_id

  GROUP BY c.user_pseudo_id

)

SELECT

  (SELECT COUNT(*) FROM cv)                              AS cv_total,

  SUM(blog_lastclick)                                   AS blog_lastclick,

  SUM(blog_involved)                                    AS blog_involved,

  SUM(blog_involved) - SUM(blog_lastclick)              AS hidden_diff,

  ROUND(SAFE_DIVIDE(SUM(blog_involved), SUM(blog_lastclick)), 1) AS ratio,

  ROUND(100 * SAFE_DIVIDE(SUM(blog_involved), (SELECT COUNT(*) FROM cv)),1) AS involved_share_pct

FROM flags;

結果:ブログの貢献は last-click では5分の1しか見えていなかった

このクエリの出力がこうなりました。

CV総数は311件。ブログで直接クローズ(last-click)は5件。経路にブログを含むCV(アシスト基準)は24件。差し引き19件が、last-clickでは消えていたブログの貢献です。倍率は4.8倍。

指標件数
CV総数311件
ブログで直接クローズ(last-click)5件
経路にブログを含む(アシスト基準)24件
last-clickで見えていなかった貢献(差分)19件
倍率(アシスト ÷ last-click)4.8倍
全CVに占めるブログ関与の割合約8%

読み替えると、こうなります。ブログ経由で契約した人は24人いるのに、last-clickの集計ではそのうち5人しかブログの手柄になりません。残り19人ぶんのブログの貢献は、別チャネルでクローズしたという理由だけで、ゼロとして記録されています。GA4標準レポートでブログのCVを見て「5件か、少ないな」と判断していた人は、実態の約5分の1だけを見て判断していたことになります。

検討期間を可視化する:ブログは「後日CV」の入口だった

なぜこれだけ差が出るのか。理由は検討期間にあります。ブログ接触からCVまでの日数を出すと、ブログ経由で契約した人の大半が、その場では契約せず、日数を置いてから戻ってきていました。

ブログ接触からCVまでの経過日数で分けると、ブログ経由CVのうち後日CV(1日以上空けて契約)が大多数を占め、検討日数の中央値は10日、1週間以上空けたケースも相当数ありました。同じセッション内で即契約した「即決」は少数派です。

これが、last-clickがブログを取りこぼす仕組みそのものです。同じ日にブログを読んでそのまま契約するなら、契約セッションの中にブログ接触が含まれるので、last-clickでもブログが見えます。ところが検討期間が空くと、ブログを読んだ日と契約した日が別のセッションに分かれます。そして契約する日は、たいてい広告や指名検索など別の入口から入り直してくる。last-clickが見るのは、この最後に入り直したセッションの参照元だけなので、10日前にきっかけを作ったブログは、構造的に拾えません。検討期間が長い後日CVほど、ブログがlast-clickから抜け落ちやすい、ということです。

入口になった記事には性格の違いがある

どのブログ記事がCVの入口になっていたかを集計すると、記事ごとに役割が違うことも見えました。

検討日数が長い記事は、長期検討の入口として機能しています。製品タイプや選び方を解説する記事などは、入口になったCVがすべて後日CVで、検討日数の中央値も10日前後。じっくり比較検討する人の最初の一歩になっているタイプです。

一方、検討日数が短い記事もあります。解約金や料金、特定の利用シーンへの向き不向きといった、具体的な不安や条件を調べに来る記事は、検討日数が1日程度と短い。読みに来た人が、答えを得たら早く決める。背中を押すタイプの記事です。

同じ「CVの入口になった記事」でも、認知・教育の入口になる記事と、最後の不安を解消して即決を促す記事では、果たす役割が違います。これは、どの記事をリライトで強化するか、どこに内部リンクを引くかの判断材料になります。

一点、計測上の限界も補足しておきます。今回の数字は計測を開始した日以降のデータしか追えていないため、それ以前のブログ接触は経路に含められていません。つまりブログのアシストは、ここで出た数字よりさらに過小に出ている可能性が高く、実態はもっと効いている方向の誤差です。

まとめ:コンテンツSEOは無駄か?

無駄かどうかは、GA4標準レポートのCV数だけでは判断できません。ブログは検討の入口で効き、人は後日・別チャネルで契約するため、last-clickの集計ではブログの貢献が構造的に消えるからです。

確かめる手順は3つです。1つ目、CVしたユーザーの経路を時系列でさかのぼる。2つ目、ブログで直接クローズした件数(last-click)と、経路にブログを含む件数(アシスト)を並べて倍率を出す。3つ目、ブログ接触からCVまでの検討日数を見て、後日CVがどれだけあるかを確かめる。

この3つをBigQueryで出すと、「GA4では5件にしか見えなかったブログのCVが、実際には24件に効いていた」といった、標準レポートでは見えない貢献が数字で立ち上がります。コンテンツSEOを続けるか辞めるかを判断する前に、まず正しく測ることをおすすめします。