はじめに:セールは売れて当然、でも本当に得だったのか

セールやクーポンを打つと、売上は伸びます。これはほぼ確実です。値引きをすれば、買い控えていた人が動き、まとめ買いも増えます。だから「セール期間の売上が前月より増えた」という事実だけを見て、施策が成功したと判断してしまいがちです。

しかし、ECを運営していると、こんなモヤモヤを感じたことはないでしょうか。

  • セール中は確かに売れたが、終わった直後の数週間がいつもより静かだった
  • 値引きしなくても買ってくれたはずの常連客にまで、割引を配ってしまった気がする
  • 売上は増えたのに、月末に残った利益はそれほど変わっていない

これらは、セールの「売れた」という見かけの成果と、「本当に得をしたか」という実質の成果がズレているサインです。セールの効果測定で本当に知りたいのは、売上が増えたかどうかではなく、普段どおりに営業していた場合と比べて、どれだけ上乗せできたかです。

この記事では、GA4のデータをBigQueryに集約し、セール期間とそうでない期間をbefore/afterで比較する分析テンプレートを紹介します。売上・客数・客単価・粗利を並べて見ることで、「売れて当然」の先にある実質的な効果を測れるようにします。SQLのサンプルも載せるので、自社のデータに当てはめながら読み進めてみてください。

なお、本記事はGA4からBigQueryへのエクスポートが設定済みで、events_YYYYMMDD テーブルが揃っていることを前提にしています。エクスポートやEC計測の実装、受注データとの突合については、以下の記事も合わせて参考にしてください。

効果測定の落とし穴:需要先食いと通常価格の機会損失

before/afterの比較式を作る前に、セールの効果を見誤らせる2つの落とし穴を押さえておきます。ここを意識しないと、SQLで数字を出しても解釈を間違えます。

落とし穴1:需要の先食い

セールで安くなると、本来であれば数週間後・数か月後に買うはずだった人が、前倒しで買います。これが需要の先食いです。

先食いが起きると、セール期間の売上は確かに増えます。しかしその増加分の一部は、未来の売上を前借りしているだけです。セール直後に売上が落ち込むのは、この前借りの反動であることが少なくありません。

したがって、セール期間だけを見て「増えた」と判断するのは危険です。セール直後の期間まで含めて、トータルでどう動いたかを見る必要があります。これがbefore/afterの「after」をセール期間そのものではなく、セール終了後の期間として設計する理由です。

落とし穴2:通常価格で売れたはずの売上の機会損失

もうひとつの落とし穴は、値引きしなくても買ってくれた客に対しても割引してしまうことです。

セールがなくても、その商品を定価で買う予定だった人は一定数います。その人たちにまで割引を適用すると、本来得られたはずの利益を自ら削っていることになります。これが機会損失です。

セールで増えた売上のうち、「セールがなければ発生しなかった純増分」と「セールがなくても発生していたもともとの売上」を切り分けないと、施策の本当の貢献は見えてきません。完全に切り分けるのは難しいのですが、後述する比較期間の設計とbaselineの考え方で、ある程度の近似はできます。

比較期間の設計

before/after比較の精度は、ほぼ「期間の取り方」で決まります。雑に「前月」と「セール月」を比べると、季節要因や曜日構成の違いに引きずられて、施策の効果なのかただの季節変動なのか分からなくなります。

ここでは、3つの期間を定義します。

  • before(基準期間):セール前の通常営業期間。客数や売上の平常時の水準(baseline)を測る
  • during(施策期間):セール・クーポンを実施した期間
  • after(反動観測期間):セール終了直後の通常営業期間。需要先食いの反動を観測する

期間設計で気をつけたいポイントは次のとおりです。

曜日構成をそろえる

ECは曜日によって売上が大きく変わります。給料日後や週末に売上が伸びる店舗も多いはずです。beforeとduringの長さがバラバラだったり、片方が週末を多く含んでいたりすると、比較が歪みます。できるだけ同じ曜日数・同じ日数で区切るのが基本です。たとえば「セール7日間」と比べるなら、beforeも直前の7日間、afterも直後の7日間にそろえます。

季節要因を避ける

年末商戦やセール常連月など、もともと需要が高い時期にbeforeを置くと、平常時より高い水準を基準にしてしまいます。可能なら、前年同期や、季節イベントの影響がない通常週をbeforeに選びます。

外部要因をメモしておく

同時期に広告を増やした、SNSで取り上げられた、競合がセールをやめた、といった外部要因があると、セール単独の効果と区別がつきません。SQLでは拾えない情報なので、分析メモとして必ず残しておきます。

before/after比較のSQLテンプレート

ここから実際のSQLに入ります。GA4のBigQueryエクスポートから purchase イベントを取り出し、期間ごとに売上・客数・客単価・粗利を集計します。

まず、期間ラベルを付けながら注文単位のデータを整形します。GA4の purchase イベントでは、売上が ecommerce.purchase_revenue、注文番号が ecommerce.transaction_id に入っている前提です。

-- 期間ラベルを付けた注文単位データを作る
WITH orders AS (
  SELECT
    -- 期間の区分け(日付はサイトの実施期間に合わせて書き換える)
    CASE
      WHEN PARSE_DATE('%Y%m%d', event_date) BETWEEN DATE '2026-06-23' AND DATE '2026-06-29' THEN 'before'
      WHEN PARSE_DATE('%Y%m%d', event_date) BETWEEN DATE '2026-06-30' AND DATE '2026-07-06' THEN 'during'
      WHEN PARSE_DATE('%Y%m%d', event_date) BETWEEN DATE '2026-07-07' AND DATE '2026-07-13' THEN 'after'
      ELSE NULL
    END AS period,
    user_pseudo_id,
    ecommerce.transaction_id AS transaction_id,
    ecommerce.purchase_revenue AS revenue
  FROM `your_project.analytics_XXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260623' AND '20260713'
    AND event_name = 'purchase'
    AND ecommerce.transaction_id IS NOT NULL
)

SELECT
  period,
  COUNT(DISTINCT transaction_id) AS orders,           -- 注文件数
  COUNT(DISTINCT user_pseudo_id) AS buyers,           -- 購入者数(客数)
  SUM(revenue) AS total_revenue,                       -- 売上合計
  SAFE_DIVIDE(SUM(revenue), COUNT(DISTINCT transaction_id)) AS avg_order_value  -- 客単価
FROM orders
WHERE period IS NOT NULL
GROUP BY period
ORDER BY
  CASE period WHEN 'before' THEN 1 WHEN 'during' THEN 2 WHEN 'after' THEN 3 END;

このクエリで、3期間それぞれの注文件数・客数・売上・客単価が1行ずつ並びます。客単価は「セールで安く売った結果、1注文あたりの金額がどう動いたか」を見る重要な指標です。セール中は客数が増える一方で客単価が下がることが多く、その綱引きの結果として売上が決まります。

1日あたりに換算して比較する

beforeとduringの日数が違う場合や、afterまで含めて公平に比べたい場合は、合計値ではなく1日あたりに割り戻すと比較しやすくなります。なお、このクエリは前項の orders CTE を参照するので、前項のCTEに続けて書く前提です(単体では orders が未定義で動きません)。

-- 各期間を1日あたりに換算して比較する
WITH daily AS (
  SELECT
    period,
    SUM(revenue) AS total_revenue,
    COUNT(DISTINCT transaction_id) AS orders,
    -- 期間ごとの日数(実施期間に合わせて書き換える)
    CASE period
      WHEN 'before' THEN 7
      WHEN 'during' THEN 7
      WHEN 'after'  THEN 7
    END AS days
  FROM orders
  WHERE period IS NOT NULL
  GROUP BY period
)

SELECT
  period,
  SAFE_DIVIDE(total_revenue, days) AS revenue_per_day,   -- 1日あたり売上
  SAFE_DIVIDE(orders, days) AS orders_per_day            -- 1日あたり注文件数
FROM daily
ORDER BY
  CASE period WHEN 'before' THEN 1 WHEN 'during' THEN 2 WHEN 'after' THEN 3 END;

beforeの1日あたり売上をbaseline(平常時の水準)とみなすと、duringでどれだけ上乗せできたか、afterでどれだけ反動が出たかを、同じ物差しで読めるようになります。

粗利を加えて「本当に得だったか」を見る

ここが今回の記事の肝です。売上だけでなく粗利を出します。セールは値引きで売っているぶん、売上が増えても粗利が増えていないことがあるからです。

GA4の purchase イベントだけでは原価が分からないので、商品ごとの原価マスタを別テーブルとして用意し、結合します。ここでは商品単位の購入データ(purchase イベントの items 配列)を展開して原価を突き合わせます。

-- 商品単位に展開して粗利を計算する
WITH item_level AS (
  SELECT
    CASE
      WHEN PARSE_DATE('%Y%m%d', event_date) BETWEEN DATE '2026-06-23' AND DATE '2026-06-29' THEN 'before'
      WHEN PARSE_DATE('%Y%m%d', event_date) BETWEEN DATE '2026-06-30' AND DATE '2026-07-06' THEN 'during'
      WHEN PARSE_DATE('%Y%m%d', event_date) BETWEEN DATE '2026-07-07' AND DATE '2026-07-13' THEN 'after'
      ELSE NULL
    END AS period,
    item.item_id AS item_id,
    item.quantity AS quantity,
    item.item_revenue AS item_revenue   -- 値引き後の実売上
  FROM `your_project.analytics_XXXXXX.events_*`,
    UNNEST(items) AS item
  WHERE _TABLE_SUFFIX BETWEEN '20260623' AND '20260713'
    AND event_name = 'purchase'
)

SELECT
  il.period,
  SUM(il.item_revenue) AS revenue,                              -- 売上
  SUM(il.quantity * cost.unit_cost) AS total_cost,              -- 原価合計(原価未登録ぶんは含まれない)
  SUM(il.item_revenue) - SUM(il.quantity * cost.unit_cost) AS gross_profit,  -- 粗利
  SAFE_DIVIDE(
    SUM(il.item_revenue) - SUM(il.quantity * cost.unit_cost),
    SUM(il.item_revenue)
  ) AS gross_margin_rate,                                       -- 粗利率
  -- 原価マスタに存在しなかった商品の売上を別途集計して検知する
  SUM(IF(cost.item_id IS NULL, il.item_revenue, 0)) AS revenue_without_cost,
  COUNTIF(cost.item_id IS NULL) AS items_without_cost
FROM item_level AS il
LEFT JOIN `your_project.master.item_cost` AS cost
  ON il.item_id = cost.item_id
WHERE il.period IS NOT NULL
GROUP BY il.period
ORDER BY
  CASE il.period WHEN 'before' THEN 1 WHEN 'during' THEN 2 WHEN 'after' THEN 3 END;

item_cost テーブルは、item_idunit_cost(1個あたりの原価)を持つ自前のマスタです。スプレッドシートで管理しているなら、BigQueryに外部テーブルとして読み込んでおくと運用が楽になります。

⚠️ 注意

原価マスタとの結合は、必ず LEFT JOIN にしてください。INNER JOIN にすると、原価が未登録の商品が売上からまるごと欠落します。すると売上も粗利も実態より小さくなり、しかも粗利率は「原価が分かっている商品だけ」で計算されるため、実態より良く見えてしまいます。新商品やマスタ登録漏れがあると、この欠落に気づかないまま「粗利率が高い」と誤判断する危険があります。LEFT JOIN にしたうえで、上のクエリの revenue_without_cost(原価不明商品の売上)と items_without_cost(原価不明の明細数)を毎回確認し、これらが0でなければマスタを補ってから集計し直すか、欠落金額を注記したうえで結果を読みます。

このクエリで、期間ごとの売上・原価・粗利・粗利率がそろいます。ここまで来て初めて、「売上は増えたが粗利は薄くなった」のか「売上も粗利もちゃんと伸びた」のかを判断できます。

カニバリ・利益で見るときの注意

数字が出たら、解釈のときに次の点に注意してください。表面的な増減だけで結論を出さないためのチェックリストです。

afterの反動まで合算して評価する

beforeを基準に、duringで増えた売上から、afterで落ち込んだぶんを差し引いて考えます。during期間だけ見れば大成功でも、afterの落ち込みを足し戻すと、トータルではほぼ横ばいだった、というケースは珍しくありません。これが需要先食いの正体です。before・during・afterを必ずセットで見てください。

粗利率の低下幅を確認する

セール中は粗利率が下がるのが普通です。問題は下がり幅です。客数が大きく増えて、粗利率の低下を量でカバーできていれば、粗利額は増えます。逆に、客数の増加がわずかなのに粗利率だけ大きく下がっていれば、値引きしすぎのサインです。粗利「率」ではなく粗利「額」が、beforeの水準に対してどう動いたかで判断します。

カニバリ(売上の食い合い)を疑う

セール対象商品が売れた裏で、定価のままの類似商品や、利益率の高い商品の売上が落ちていることがあります。これがカニバリです。期間ごとの集計を商品カテゴリ別にも出して、「セール商品は伸びたが、別カテゴリが落ちていないか」を確認すると、店舗全体での実質的な影響が見えてきます。

在庫の動きと合わせて見る

セールで一気に在庫を掃けたのか、それとも売れ筋を安売りして利益を削っただけなのかは、在庫の回転と合わせて見ると判断しやすくなります。滞留在庫の整理という目的なら、粗利が薄くても成功と評価できる場面もあります。在庫の観点からの分析は、以下の記事で詳しく扱っています。

まとめ

セールは値引きをすれば売れます。だからこそ、「売れた」という事実だけで効果を判断してはいけません。本当に知りたいのは、平常時と比べてどれだけ上乗せできたか、そしてその上乗せが利益に結びついているかです。

この記事で紹介した分析テンプレートのポイントを整理します。

  • before / during / after の3期間で、曜日構成と日数をそろえて比較する
  • 売上・客数・客単価・粗利を並べ、見かけの売上だけで判断しない
  • afterの反動まで合算して、需要先食いを見抜く
  • 粗利率ではなく粗利額の動きで、値引きが利益に見合っていたかを判断する
  • カテゴリ別や在庫の動きと合わせて、カニバリと実質効果を確認する

GA4とBigQueryがあれば、これらは自前で測れます。最初は期間の日付を書き換えるだけでも構いません。一度テンプレートを作っておけば、次のセールから同じ物差しで振り返れるようになり、「なんとなく売れた気がする」から「いくら上乗せできて、いくら利益が残ったか」へと、施策の評価が一段クリアになります。