本文へ移動
効果検証SQL 更新日: 2026年5月2日 約30分で読めます

ファネル分析をSQLで行う方法|30分以内のカゴ投入から購買を正確に測る

「ファネル通過」の定義が甘いと施策が狂う

ECサイトのコンバージョン分析で「カート追加率」「購買転換率」という指標をよく目にする。しかしこれらを正確に計算しているサイトは意外と少ない。

よくある誤りがこれだ。

今月カートに追加したユーザー:8,000人
今月購買したユーザー:3,200人
 ⇒ 転換率:40%

これは転換率ではない。「今月カートに追加したユーザー」と「今月購買したユーザー」は別の集合で、順序も時間関係も考慮されていない。カートに追加してから3ヶ月後に購買した人も「転換した」としてカウントされる。

本当の転換率を測るには2つの条件が必要だ。

  1. 順序の条件:「カゴ追加 → 購買」の順序を正しく守っている
  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_iduser_idsession_idevent_typeproduct_idoccurred_at
E001U001S001page_viewP0102024-12-01 14:00:00
E002U001S001add_to_cartP0102024-12-01 14:05:00
E003U001S001begin_checkoutNULL2024-12-01 14:18:00
E004U001S001purchaseNULL2024-12-01 14:24:00
E005U002S002page_viewP0102024-12-01 15:00:00
E006U002S002add_to_cartP0102024-12-01 15:08:00
E007U002S002begin_checkoutNULL2024-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_idsession_idstep1_timestep2_timestep3_timestep4_time
U001S00114:00:0014:05:0014:18:0014:24:00
U002S00215:00:0015:08:0015:55:00NULL
U003S00310:00:00NULLNULLNULL
U004S00411:00:0011:12:00NULLNULL

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_idsession_idpassed_step1passed_step2passed_step3passed_step4min_s1_to_s2min_s2_to_s3min_s3_to_s4
U001S00111115136
U002S0021100847NULL
U003S0031000NULLNULLNULL
U004S004110012NULLNULL

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_stepusersstep_rate_pctdrop_rate_pctavg_time_to_next_min
STEP1_商品ページ閲覧48,412100.0NULLNULL
STEP2_カート追加14,52430.070.08.4
STEP3_チェックアウト開始9,14018.937.111.2
STEP4_購買完了7,31215.120.06.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_outcomesession_countpctavg_minutesmedian_minutesp90_minutes
カゴ落ち7,84154.0
購買完了5,12435.310.28.021
チェックアウト止まり1,55910.716.814.028

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_typesessionscart_add_ratepurchase_ratevs_avg_pct
desktop18,41238.422.1+7.0
tablet6,84131.216.8+1.7
mobile23,15924.810.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 実装のポイントをまとめる。

  1. 各ユーザー×セッションで「各ステップの最初の発生時刻」を MIN(CASE WHEN ...) で取得する
  2. DATE_DIFF('minute', prev_step_time, next_step_time) BETWEEN 0 AND 30 で時間制約を適用する
  3. passed_step2 * raw_step3 の乗算で「前のステップを通過していなければ後続も未通過」という連鎖を表現する
  4. ステップ間の所要時間分布(avg_time_to_next_min)からカゴ落ちメールの最適タイミングを設計する
  5. デバイス別・時間帯別に比較することで「どこがボトルネックか」を特定する

「カゴ落ち率70%」という数字は、対策なしでは「70%の売上を取りこぼしている」ことを意味する。このファネルを正確に計測し、最大ドロップポイント(今回は商品ページ→カート追加)に対して施策を集中させることが、EC改善の最短ルートだ。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む