「ファネル通過」の定義が甘いと施策が狂う
ECサイトのコンバージョン分析で「カート追加率」「購買転換率」という指標をよく目にする。しかしこれらを正確に計算しているサイトは意外と少ない。
よくある誤りがこれだ。
今月カートに追加したユーザー:8,000人
今月購買したユーザー:3,200人
⇒ 転換率:40%
これは転換率ではない。「今月カートに追加したユーザー」と「今月購買したユーザー」は別の集合で、順序も時間関係も考慮されていない。カートに追加してから3ヶ月後に購買した人も「転換した」としてカウントされる。
本当の転換率を測るには2つの条件が必要だ。
- 順序の条件:「カゴ追加 → 購買」の順序を正しく守っている
- 時間の条件:カゴ追加から購買まで「N分/時間以内」という制約
これを満たす分析を時間制約付きファネル分析と呼ぶ。今回はSQLで精確に実装する。
ファネルの定義
今回のファネルは次の4ステップだ。
STEP 1: 商品ページ閲覧(page_view)
STEP 2: カート追加(add_to_cart)
STEP 3: チェックアウト開始(begin_checkout)
STEP 4: 購買完了(purchase)時間制約:各ステップから次のステップまで30分以内に進んだ場合のみ「ファネル通過」とみなす。
使用するテーブル
-- user_events(ユーザー行動ログ)
-- event_id : イベントID
-- user_id : ユーザーID(顧客ID)
-- session_id : セッションID(No.5で再構築したもの)
-- event_type : 'page_view' / 'add_to_cart' / 'begin_checkout' / 'purchase'
-- product_id : 商品ID(NULLの場合あり)
-- occurred_at : 発生日時(TIMESTAMP・JST)| event_id | user_id | session_id | event_type | product_id | occurred_at |
|---|---|---|---|---|---|
| E001 | U001 | S001 | page_view | P010 | 2024-12-01 14:00:00 |
| E002 | U001 | S001 | add_to_cart | P010 | 2024-12-01 14:05:00 |
| E003 | U001 | S001 | begin_checkout | NULL | 2024-12-01 14:18:00 |
| E004 | U001 | S001 | purchase | NULL | 2024-12-01 14:24:00 |
| E005 | U002 | S002 | page_view | P010 | 2024-12-01 15:00:00 |
| E006 | U002 | S002 | add_to_cart | P010 | 2024-12-01 15:08:00 |
| E007 | U002 | S002 | begin_checkout | NULL | 2024-12-01 15:55:00 |
U001 は4ステップを30分以内に完走した。U002 は begin_checkout が add_to_cart から47分後(30分超)のためこのファネルではカウントしない。
STEP 1 ― 各ユーザー×セッションの各ステップ到達時刻を取得する
まず各ユーザー×セッションで「最初に各イベントタイプが発生した時刻」を取得する。
-- STEP1: ユーザー×セッション×ステップの最初の到達時刻
WITH first_events AS (
SELECT
user_id,
session_id,
-- 各ステップの最初の発生時刻(セッション内で複数回発生する場合は最初)
MIN(CASE WHEN event_type = 'page_view' THEN occurred_at END)
AS step1_time,
MIN(CASE WHEN event_type = 'add_to_cart' THEN occurred_at END)
AS step2_time,
MIN(CASE WHEN event_type = 'begin_checkout' THEN occurred_at END)
AS step3_time,
MIN(CASE WHEN event_type = 'purchase' THEN occurred_at END)
AS step4_time
FROM user_events
WHERE occurred_at >= TIMESTAMP '2024-12-01 00:00:00'
AND occurred_at < TIMESTAMP '2025-01-01 00:00:00'
GROUP BY user_id, session_id
)
SELECT *
FROM first_events
ORDER BY user_id, session_id;出力イメージ
| user_id | session_id | step1_time | step2_time | step3_time | step4_time |
|---|---|---|---|---|---|
| U001 | S001 | 14:00:00 | 14:05:00 | 14:18:00 | 14:24:00 |
| U002 | S002 | 15:00:00 | 15:08:00 | 15:55:00 | NULL |
| U003 | S003 | 10:00:00 | NULL | NULL | NULL |
| U004 | S004 | 11:00:00 | 11:12:00 | NULL | NULL |
STEP 2 ― 時間制約を適用してファネルの各ステップを判定する
「前のステップから30分以内に次のステップが発生しているか」を CASE WHEN で判定する。
-- STEP2: 時間制約(30分)を適用したファネル通過判定
WITH first_events AS (
SELECT
user_id, session_id,
MIN(CASE WHEN event_type = 'page_view' THEN occurred_at END) AS step1_time,
MIN(CASE WHEN event_type = 'add_to_cart' THEN occurred_at END) AS step2_time,
MIN(CASE WHEN event_type = 'begin_checkout' THEN occurred_at END) AS step3_time,
MIN(CASE WHEN event_type = 'purchase' THEN occurred_at END) AS step4_time
FROM user_events
WHERE occurred_at >= TIMESTAMP '2024-12-01 00:00:00'
AND occurred_at < TIMESTAMP '2025-01-01 00:00:00'
GROUP BY user_id, session_id
),
funnel_flags AS (
SELECT
user_id,
session_id,
step1_time,
step2_time,
step3_time,
step4_time,
-- STEP1通過:page_view があれば通過(起点なので時間制約なし)
CASE WHEN step1_time IS NOT NULL THEN 1 ELSE 0 END
AS passed_step1,
-- STEP2通過:step1の後、30分以内にadd_to_cartが発生
CASE
WHEN step2_time IS NOT NULL
AND step1_time IS NOT NULL
AND DATE_DIFF('minute', step1_time, step2_time) <= 30
AND step2_time > step1_time -- 必ず順序を守る
THEN 1 ELSE 0
END AS passed_step2,
-- STEP3通過:step2の後、30分以内にbegin_checkoutが発生
CASE
WHEN step3_time IS NOT NULL
AND step2_time IS NOT NULL
AND DATE_DIFF('minute', step2_time, step3_time) <= 30
AND step3_time > step2_time
THEN 1 ELSE 0
END AS passed_step3_raw, -- step2通過を前提とする前の中間値
-- STEP4通過:step3の後、30分以内にpurchaseが発生
CASE
WHEN step4_time IS NOT NULL
AND step3_time IS NOT NULL
AND DATE_DIFF('minute', step3_time, step4_time) <= 30
AND step4_time > step3_time
THEN 1 ELSE 0
END AS passed_step4_raw,
-- ステップ間の所要時間(分)
DATE_DIFF('minute', step1_time, step2_time) AS min_step1_to_2,
DATE_DIFF('minute', step2_time, step3_time) AS min_step2_to_3,
DATE_DIFF('minute', step3_time, step4_time) AS min_step3_to_4
FROM first_events
)
SELECT
user_id,
session_id,
passed_step1,
passed_step2,
-- 連鎖的な通過判定:前のステップを通過していない場合は後続も0
passed_step2 * passed_step3_raw AS passed_step3,
passed_step2 * passed_step3_raw * passed_step4_raw AS passed_step4,
min_step1_to_2,
min_step2_to_3,
min_step3_to_4
FROM funnel_flags
ORDER BY user_id, session_id;出力イメージ
| user_id | session_id | passed_step1 | passed_step2 | passed_step3 | passed_step4 | min_s1_to_s2 | min_s2_to_s3 | min_s3_to_s4 |
|---|---|---|---|---|---|---|---|---|
| U001 | S001 | 1 | 1 | 1 | 1 | 5 | 13 | 6 |
| U002 | S002 | 1 | 1 | 0 | 0 | 8 | 47 | NULL |
| U003 | S003 | 1 | 0 | 0 | 0 | NULL | NULL | NULL |
| U004 | S004 | 1 | 1 | 0 | 0 | 12 | NULL | NULL |
U002 は step2→step3 が47分かかったため step3 は「未通過」となり、step4 も連鎖的に未通過だ。
STEP 3 ― ファネルサマリと各ステップのドロップオフ率を計算する(完成版)
-- STEP3: ファネルサマリ(完成版)
WITH first_events AS (
SELECT
user_id, session_id,
MIN(CASE WHEN event_type = 'page_view' THEN occurred_at END) AS step1_time,
MIN(CASE WHEN event_type = 'add_to_cart' THEN occurred_at END) AS step2_time,
MIN(CASE WHEN event_type = 'begin_checkout' THEN occurred_at END) AS step3_time,
MIN(CASE WHEN event_type = 'purchase' THEN occurred_at END) AS step4_time
FROM user_events
WHERE occurred_at >= TIMESTAMP '2024-12-01 00:00:00'
AND occurred_at < TIMESTAMP '2025-01-01 00:00:00'
GROUP BY user_id, session_id
),
funnel_flags AS (
SELECT
user_id, session_id,
step1_time, step2_time, step3_time, step4_time,
1 AS passed_step1,
CASE
WHEN step2_time IS NOT NULL AND step1_time IS NOT NULL
AND DATE_DIFF('minute', step1_time, step2_time) BETWEEN 0 AND 30
THEN 1 ELSE 0
END AS passed_step2,
CASE
WHEN step3_time IS NOT NULL AND step2_time IS NOT NULL
AND DATE_DIFF('minute', step2_time, step3_time) BETWEEN 0 AND 30
THEN 1 ELSE 0
END AS raw_step3,
CASE
WHEN step4_time IS NOT NULL AND step3_time IS NOT NULL
AND DATE_DIFF('minute', step3_time, step4_time) BETWEEN 0 AND 30
THEN 1 ELSE 0
END AS raw_step4,
DATE_DIFF('minute', step1_time, step2_time) AS min_1_2,
DATE_DIFF('minute', step2_time, step3_time) AS min_2_3,
DATE_DIFF('minute', step3_time, step4_time) AS min_3_4
FROM first_events
),
funnel_final AS (
SELECT
user_id, session_id,
passed_step1,
passed_step2,
passed_step2 * raw_step3 AS passed_step3,
passed_step2 * raw_step3 * raw_step4 AS passed_step4,
min_1_2, min_2_3, min_3_4
FROM funnel_flags
)
SELECT
'STEP1_商品ページ閲覧' AS funnel_step,
SUM(passed_step1) AS users,
100.0 AS step_rate_pct,
NULL AS drop_rate_pct,
NULL AS avg_time_to_next_min
UNION ALL
SELECT
'STEP2_カート追加',
SUM(passed_step2),
ROUND(SUM(passed_step2) * 100.0 / NULLIF(SUM(passed_step1), 0), 1),
ROUND(100.0 - SUM(passed_step2) * 100.0 / NULLIF(SUM(passed_step1), 0), 1),
ROUND(AVG(CASE WHEN passed_step2 = 1 THEN min_1_2 END), 1)
FROM funnel_final
UNION ALL
SELECT
'STEP3_チェックアウト開始',
SUM(passed_step3),
ROUND(SUM(passed_step3) * 100.0 / NULLIF(SUM(passed_step1), 0), 1),
ROUND(
SUM(passed_step2 - passed_step3) * 100.0 / NULLIF(SUM(passed_step2), 0)
, 1),
ROUND(AVG(CASE WHEN passed_step3 = 1 THEN min_2_3 END), 1)
FROM funnel_final
UNION ALL
SELECT
'STEP4_購買完了',
SUM(passed_step4),
ROUND(SUM(passed_step4) * 100.0 / NULLIF(SUM(passed_step1), 0), 1),
ROUND(
SUM(passed_step3 - passed_step4) * 100.0 / NULLIF(SUM(passed_step3), 0)
, 1),
ROUND(AVG(CASE WHEN passed_step4 = 1 THEN min_3_4 END), 1)
FROM funnel_final;完成した出力イメージ
| funnel_step | users | step_rate_pct | drop_rate_pct | avg_time_to_next_min |
|---|---|---|---|---|
| STEP1_商品ページ閲覧 | 48,412 | 100.0 | NULL | NULL |
| STEP2_カート追加 | 14,524 | 30.0 | 70.0 | 8.4 |
| STEP3_チェックアウト開始 | 9,140 | 18.9 | 37.1 | 11.2 |
| STEP4_購買完了 | 7,312 | 15.1 | 20.0 | 6.8 |
- 商品ページ閲覧 → カート追加のドロップが最大(70%が離脱)
- カート → チェックアウト開始で37%が離脱(カゴ落ちの本丸)
- チェックアウト → 購買完了は20%の離脱(比較的スムーズ)
- 全体転換率は15.1%(商品閲覧から購買完了まで)
STEP 4 ― 離脱時間の分布を出す(どの時間帯で諦めるか)
「カート追加後、何分で諦めるか」の分布を出す。これはカゴ落ちメールのタイミング設計に直結する。
-- STEP4: カート追加後の離脱時間分布(カゴ落ちタイミング)
WITH first_events AS (
SELECT
user_id, session_id,
MIN(CASE WHEN event_type = 'add_to_cart' THEN occurred_at END) AS step2_time,
MIN(CASE WHEN event_type = 'begin_checkout' THEN occurred_at END) AS step3_time,
MIN(CASE WHEN event_type = 'purchase' THEN occurred_at END) AS step4_time
FROM user_events
WHERE occurred_at >= TIMESTAMP '2024-12-01 00:00:00'
AND occurred_at < TIMESTAMP '2025-01-01 00:00:00'
GROUP BY user_id, session_id
),
cart_abandonment AS (
SELECT
user_id,
session_id,
step2_time,
step3_time,
step4_time,
-- カート後の行動
CASE
WHEN step4_time IS NOT NULL
AND DATE_DIFF('minute', step2_time, step4_time) <= 30
THEN '購買完了'
WHEN step3_time IS NOT NULL
AND DATE_DIFF('minute', step2_time, step3_time) <= 30
THEN 'チェックアウト止まり'
ELSE 'カゴ落ち'
END AS cart_outcome,
-- カート後の最終行動までの時間(分)
COALESCE(
CASE WHEN step4_time IS NOT NULL THEN DATE_DIFF('minute', step2_time, step4_time) END,
CASE WHEN step3_time IS NOT NULL THEN DATE_DIFF('minute', step2_time, step3_time) END,
NULL -- 記録なし(セッション終了)
) AS minutes_after_cart
FROM first_events
WHERE step2_time IS NOT NULL -- カート追加があるセッションのみ
)
SELECT
cart_outcome,
COUNT(*) AS session_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS pct,
ROUND(AVG(minutes_after_cart), 1) AS avg_minutes,
ROUND(APPROX_PERCENTILE(minutes_after_cart, 0.5), 0) AS median_minutes,
ROUND(APPROX_PERCENTILE(minutes_after_cart, 0.9), 0) AS p90_minutes
FROM cart_abandonment
GROUP BY cart_outcome
ORDER BY pct DESC;出力イメージ
| cart_outcome | session_count | pct | avg_minutes | median_minutes | p90_minutes |
|---|---|---|---|---|---|
| カゴ落ち | 7,841 | 54.0 | — | — | — |
| 購買完了 | 5,124 | 35.3 | 10.2 | 8.0 | 21 |
| チェックアウト止まり | 1,559 | 10.7 | 16.8 | 14.0 | 28 |
STEP 5 ― デバイス別・時間帯別のファネル比較
「スマートフォンとPCでファネルの通過率が違うか」「時間帯によってカゴ落ちが増えるか」を比較する。
-- STEP5: デバイス別ファネル転換率の比較
WITH first_events AS (
SELECT
e.user_id,
e.session_id,
e.device_type, -- 'mobile' / 'desktop' / 'tablet'
MIN(CASE WHEN e.event_type = 'page_view' THEN e.occurred_at END) AS s1,
MIN(CASE WHEN e.event_type = 'add_to_cart' THEN e.occurred_at END) AS s2,
MIN(CASE WHEN e.event_type = 'begin_checkout' THEN e.occurred_at END) AS s3,
MIN(CASE WHEN e.event_type = 'purchase' THEN e.occurred_at END) AS s4
FROM user_events e
WHERE e.occurred_at >= TIMESTAMP '2024-12-01 00:00:00'
AND e.occurred_at < TIMESTAMP '2025-01-01 00:00:00'
GROUP BY e.user_id, e.session_id, e.device_type
),
funnel AS (
SELECT
device_type,
1 AS p1,
CASE WHEN s2 IS NOT NULL AND s1 IS NOT NULL
AND DATE_DIFF('minute', s1, s2) BETWEEN 0 AND 30 THEN 1 ELSE 0 END AS p2,
CASE WHEN s2 IS NOT NULL AND s1 IS NOT NULL
AND DATE_DIFF('minute', s1, s2) BETWEEN 0 AND 30
AND s3 IS NOT NULL
AND DATE_DIFF('minute', s2, s3) BETWEEN 0 AND 30 THEN 1 ELSE 0 END AS p3,
CASE WHEN s2 IS NOT NULL AND s1 IS NOT NULL
AND DATE_DIFF('minute', s1, s2) BETWEEN 0 AND 30
AND s3 IS NOT NULL
AND DATE_DIFF('minute', s2, s3) BETWEEN 0 AND 30
AND s4 IS NOT NULL
AND DATE_DIFF('minute', s3, s4) BETWEEN 0 AND 30 THEN 1 ELSE 0 END AS p4
FROM first_events
)
SELECT
device_type,
COUNT(*) AS sessions,
SUM(p2) AS cart_adds,
ROUND(SUM(p2) * 100.0 / NULLIF(COUNT(*), 0), 1) AS cart_add_rate,
SUM(p4) AS purchases,
ROUND(SUM(p4) * 100.0 / NULLIF(COUNT(*), 0), 1) AS purchase_rate,
-- モバイルとの差分を出すためのリファレンス値(WINDOWで計算)
ROUND(
SUM(p4) * 100.0 / NULLIF(COUNT(*), 0)
- AVG(SUM(p4) * 100.0 / NULLIF(COUNT(*), 0)) OVER ()
, 1) AS vs_avg_pct
FROM funnel
GROUP BY device_type
ORDER BY purchase_rate DESC;出力イメージ
| device_type | sessions | cart_add_rate | purchase_rate | vs_avg_pct |
|---|---|---|---|---|
| desktop | 18,412 | 38.4 | 22.1 | +7.0 |
| tablet | 6,841 | 31.2 | 16.8 | +1.7 |
| mobile | 23,159 | 24.8 | 10.4 | -4.7 |
モバイルの購買転換率(10.4%)はデスクトップ(22.1%)の半分以下だ。カート追加率の差(24.8% vs 38.4%)に加え、チェックアウトの入力ハードルがモバイルで高いことが推測される。モバイルUXの改善・Apple Pay等のワンタップ決済の導入が優先施策として浮かび上がる。
実務での運用ヒント
① 「セッションまたぎ」のファネルをどう扱うか
今回は「同一セッション内での30分制約」を使ったが、「カートに追加して翌日購買した」という跨ぎセッション購買も現実には多い。これを捉えるには「カート追加イベントから最大24時間以内の購買」という緩い時間制約を別途定義し、STEP2 の session_id 条件を外すことで対応できる。ただしその場合は「セッション内30分転換率」と「跨ぎ転換率」を区別してレポートする必要がある。
② ファネルの各ステップに「最初のイベント」を使う理由
同一セッションで同じ商品を複数回カートに追加したり、チェックアウトページを行き来するケースがある。今回は MIN(occurred_at) で最初のイベント時刻を使っているため、このような繰り返しイベントがあっても正確にファネルを追跡できる。
③ product_id でファネルを商品別に追跡する
「商品Aのページを見てカートに入れて購買した」という商品単位の追跡は、product_id を GROUP BY に加えることで実現できる。ただし「ページ閲覧はP010だがカートに入れたのはP011(別商品)」という代替購買のケースをどう扱うかを事前に定義しておく必要がある。
まとめ
時間制約付きファネル分析の SQL 実装のポイントをまとめる。
- 各ユーザー×セッションで「各ステップの最初の発生時刻」を
MIN(CASE WHEN ...)で取得する DATE_DIFF('minute', prev_step_time, next_step_time) BETWEEN 0 AND 30で時間制約を適用するpassed_step2 * raw_step3の乗算で「前のステップを通過していなければ後続も未通過」という連鎖を表現する- ステップ間の所要時間分布(
avg_time_to_next_min)からカゴ落ちメールの最適タイミングを設計する - デバイス別・時間帯別に比較することで「どこがボトルネックか」を特定する
「カゴ落ち率70%」という数字は、対策なしでは「70%の売上を取りこぼしている」ことを意味する。このファネルを正確に計測し、最大ドロップポイント(今回は商品ページ→カート追加)に対して施策を集中させることが、EC改善の最短ルートだ。