本文へ移動
SQL道場 更新日: 2026年5月2日 約21分で読めます

セッションIDなしのアクセスログをSQLでセッション化する方法


「1回の訪問」はどこからどこまでか

Webサイトの行動ログを分析するとき、「1回のセッション(訪問)の中でユーザーは何ページ見たか」「カートに入れてから購買まで何分かかったか」を調べたい場面は多い。

しかし現実のログデータには、セッションIDが存在しないことがある。システムの設計上セッションを記録していない、古いデータでセッション情報が欠落している、複数のデータソースを統合した結果セッション情報が消えたなど理由はさまざまだ。

そのとき使うのが「30分ギャップルール」だ。

同一ユーザーのイベントログの中で、前のイベントから30分以上間隔が空いた場合、そこを「セッションの区切り」とみなす

これは Google Analytics をはじめ多くのアナリティクスツールが採用している業界標準的な定義だ。

今回はこのルールをSQLで実装し、行動ログに「セッションID」を割り当てるクエリを段階的に作る。GAP and ISLAND 問題と本質的に同じ発想で、応用の幅が非常に広いテクニックだ。


使用するテーブル

-- user_events(ユーザー行動ログ)
-- event_id      : イベントID
-- user_id       : ユーザーID
-- event_type    : イベント種別('page_view', 'add_to_cart', 'purchase' など)
-- page_name     : ページ名
-- occurred_at   : イベント発生日時(TIMESTAMP型・JST)
event_iduser_idevent_typepage_nameoccurred_at
E001U001page_viewトップ2024-12-01 10:00:00
E002U001page_viewシャンプー商品ページ2024-12-01 10:03:00
E003U001add_to_cartシャンプー商品ページ2024-12-01 10:05:00
E004U001page_viewトップ2024-12-01 14:00:00
E005U001page_viewコンディショナー商品ページ2024-12-01 14:04:00
E006U001purchase購入完了2024-12-01 14:12:00
E007U002page_viewトップ2024-12-01 11:00:00

U001 の 10:05 から 14:00 の間は168分空いている。30分ルールでは E003 と E004 の間がセッションの区切りだ。


STEP 1 ― 前のイベントからの経過時間を計算する

まず各イベントに「同じユーザーの直前のイベントからの経過分数」を付与する。LAG を使う。

-- STEP1: 前イベントからの経過時間を計算する

WITH event_with_prev AS (
    SELECT
        event_id,
        user_id,
        event_type,
        page_name,
        occurred_at,

        -- 同じユーザーの1つ前のイベント発生時刻
        LAG(occurred_at) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
        )  AS prev_occurred_at
    FROM user_events
),
with_gap AS (
    SELECT
        event_id,
        user_id,
        event_type,
        page_name,
        occurred_at,
        prev_occurred_at,

        -- 前イベントからの経過分数(初回イベントは NULL)
        DATE_DIFF('minute', prev_occurred_at, occurred_at)  AS minutes_since_prev
    FROM event_with_prev
)
SELECT *
FROM with_gap
ORDER BY user_id, occurred_at;

出力イメージ(U001)

event_iduser_idevent_typeoccurred_atprev_occurred_atminutes_since_prev
E001U001page_view10:00:00NULLNULL
E002U001page_view10:03:0010:00:003
E003U001add_to_cart10:05:0010:03:002
E004U001page_view14:00:0010:05:00235
E005U001page_view14:04:0014:00:004
E006U001purchase14:12:0014:04:008

E004 の前イベントから235分空いており、明らかに「セッションの区切り」だと分かる。


STEP 2 ― セッション開始フラグを立てる

「前のイベントから30分以上空いた行」または「そのユーザーの最初のイベント」を「セッション開始」としてフラグを立てる。

-- STEP2: セッション開始フラグを付与する

WITH event_with_prev AS (
    SELECT
        event_id, user_id, event_type, page_name, occurred_at,
        LAG(occurred_at) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
        )  AS prev_occurred_at
    FROM user_events
),
with_gap AS (
    SELECT
        *,
        DATE_DIFF('minute', prev_occurred_at, occurred_at)  AS minutes_since_prev
    FROM event_with_prev
),
with_session_start AS (
    SELECT
        *,
        -- セッション開始フラグ
        -- 条件1: ユーザーの最初のイベント(prev が NULL)
        -- 条件2: 前イベントから30分以上空いた
        CASE
            WHEN prev_occurred_at IS NULL        THEN 1  -- 最初のイベント
            WHEN minutes_since_prev >= 30        THEN 1  -- 30分ルール
            ELSE                                      0
        END  AS is_session_start
    FROM with_gap
)
SELECT *
FROM with_session_start
ORDER BY user_id, occurred_at;

出力イメージ(U001)

event_iduser_idoccurred_atminutes_since_previs_session_start
E001U00110:00:00NULL1
E002U00110:03:0030
E003U00110:05:0020
E004U00114:00:002351
E005U00114:04:0040
E006U00114:12:0080

E001 と E004 が「セッション開始」とマークされた。


STEP 3 ― セッション番号を割り当てる(核心ロジック)

ここが今回の核心だ。

is_session_start の累積和(SUM() OVER (ORDER BY ...))を使う。

セッション開始フラグを上から順に足し込んでいくと、セッションが変わるたびに累積和が1増える。同じセッション内のイベントはすべて同じ累積和になる。これをセッション番号として使う。

E001: is_session_start=1 → 累積和=1(セッション1)
E002: is_session_start=0 → 累積和=1(セッション1)
E003: is_session_start=0 → 累積和=1(セッション1)
E004: is_session_start=1 → 累積和=2(セッション2)
E005: is_session_start=0 → 累積和=2(セッション2)
E006: is_session_start=0 → 累積和=2(セッション2)
-- STEP3: セッション番号を割り当てる

WITH event_with_prev AS (
    SELECT
        event_id, user_id, event_type, page_name, occurred_at,
        LAG(occurred_at) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
        )  AS prev_occurred_at
    FROM user_events
),
with_gap AS (
    SELECT *,
        DATE_DIFF('minute', prev_occurred_at, occurred_at)  AS minutes_since_prev
    FROM event_with_prev
),
with_session_start AS (
    SELECT *,
        CASE
            WHEN prev_occurred_at IS NULL THEN 1
            WHEN minutes_since_prev >= 30 THEN 1
            ELSE 0
        END  AS is_session_start
    FROM with_gap
),
with_session_num AS (
    SELECT
        event_id,
        user_id,
        event_type,
        page_name,
        occurred_at,
        minutes_since_prev,
        is_session_start,

        -- セッション番号 = ユーザー内でのセッション開始フラグの累積和
        SUM(is_session_start) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )  AS session_num
    FROM with_session_start
)
SELECT *
FROM with_session_num
ORDER BY user_id, occurred_at;

出力イメージ(U001)

event_iduser_idevent_typeoccurred_atis_session_startsession_num
E001U001page_view10:00:0011
E002U001page_view10:03:0001
E003U001add_to_cart10:05:0001
E004U001page_view14:00:0012
E005U001page_view14:04:0002
E006U001purchase14:12:0002
E007U002page_view11:00:0011

セッション番号が割り当てられた。


STEP 4 ― セッションIDを生成して完成させる(完成版)

user_id + session_num の組み合わせを「セッションID」として使いやすい文字列に変換する。

-- STEP4: セッションID付きのログテーブル(完成版)

WITH event_with_prev AS (
    SELECT
        event_id, user_id, event_type, page_name, occurred_at,
        LAG(occurred_at) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
        )  AS prev_occurred_at
    FROM user_events
),
with_gap AS (
    SELECT *,
        DATE_DIFF('minute', prev_occurred_at, occurred_at)  AS minutes_since_prev
    FROM event_with_prev
),
with_session_start AS (
    SELECT *,
        CASE
            WHEN prev_occurred_at IS NULL THEN 1
            WHEN minutes_since_prev >= 30 THEN 1
            ELSE 0
        END  AS is_session_start
    FROM with_gap
),
with_session_num AS (
    SELECT
        event_id, user_id, event_type, page_name, occurred_at,
        SUM(is_session_start) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )  AS session_num
    FROM with_session_start
)
SELECT
    event_id,
    user_id,
    event_type,
    page_name,
    occurred_at,
    session_num,

    -- セッションIDを文字列として生成(user_id + '_' + session_num)
    user_id || '_' || CAST(session_num AS VARCHAR)  AS session_id
FROM with_session_num
ORDER BY user_id, occurred_at;

STEP 5 ― セッション単位のサマリを出す

セッションIDが付いたので、セッション単位の集計ができる。

-- STEP5: セッション単位の行動サマリ

WITH event_with_prev AS (
    SELECT
        event_id, user_id, event_type, page_name, occurred_at,
        LAG(occurred_at) OVER (
            PARTITION BY user_id ORDER BY occurred_at ASC
        )  AS prev_occurred_at
    FROM user_events
),
with_gap AS (
    SELECT *,
        DATE_DIFF('minute', prev_occurred_at, occurred_at)  AS minutes_since_prev
    FROM event_with_prev
),
with_session_num AS (
    SELECT
        event_id, user_id, event_type, page_name, occurred_at,
        SUM(CASE
            WHEN prev_occurred_at IS NULL THEN 1
            WHEN minutes_since_prev >= 30 THEN 1
            ELSE 0
        END) OVER (
            PARTITION BY user_id
            ORDER BY occurred_at ASC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )  AS session_num
    FROM with_gap
)
SELECT
    user_id,
    session_num,
    user_id || '_' || CAST(session_num AS VARCHAR)  AS session_id,

    -- セッション開始・終了日時
    MIN(occurred_at)                                AS session_start,
    MAX(occurred_at)                                AS session_end,

    -- セッション継続時間(分)
    DATE_DIFF('minute', MIN(occurred_at), MAX(occurred_at))
                                                    AS session_duration_min,

    -- セッション内のイベント数(=ページビュー数など)
    COUNT(event_id)                                 AS event_count,

    -- カートへの追加があったか
    MAX(CASE WHEN event_type = 'add_to_cart' THEN 1 ELSE 0 END)
                                                    AS has_add_to_cart,

    -- 購買があったか
    MAX(CASE WHEN event_type = 'purchase'    THEN 1 ELSE 0 END)
                                                    AS has_purchase,

    -- 閲覧したページ一覧(カンマ区切り)
    ARRAY_JOIN(
        ARRAY_AGG(page_name ORDER BY occurred_at ASC),
        ' → '
    )                                               AS page_path

FROM with_session_num
GROUP BY user_id, session_num
ORDER BY user_id, session_num;

完成した出力イメージ

session_idsession_startsession_endduration_minevent_counthas_add_to_carthas_purchasepage_path
U001_110:00:0010:05:005310トップ → シャンプー商品ページ → シャンプー商品ページ
U001_214:00:0014:12:0012301トップ → コンディショナー商品ページ → 購入完了
U002_111:00:0011:00:000100トップ

U001 のセッション1はカートに追加したが購買せずに離脱(カゴ落ち)。セッション2で戻ってきて別商品を購買している。このように「1ユーザーの複数セッションをまたいだ行動」が追跡できる。


応用:カゴ落ち分析

セッションサマリがあれば、「カートに入れたが購買しなかったセッション(カゴ落ち)」の抽出も一行だ。

-- カゴ落ちセッションの抽出

SELECT
    user_id,
    session_id,
    session_start,
    session_duration_min,
    event_count,
    page_path
FROM session_summary  -- 上記のSTEP5のクエリをCTEまたはビューとして使う
WHERE has_add_to_cart = 1
AND has_purchase      = 0
ORDER BY session_start DESC;

このリストに対してカゴ落ちメール(「忘れ物はありませんか?」系のリマインドメール)の配信対象を作ることができる。


実務での運用ヒント

① 30分という閾値は調整できる

通販ECでは30分が一般的だが、熟慮型の高単価商品(家電・家具)では「60分」を使うこともある。コンテンツサイトやアプリでは「15分」と短く設定する場合もある。自社データで「実際にどのくらいの間隔が多いか」を確認してから決めるのが正確だ。STEP1 の minutes_since_prev の分布をヒストグラムで見れば、自然な区切りが見えてくる。

② 同一秒に複数イベントが発生する場合

イベントが同一秒に複数記録されるケースがある(ページ遷移と同時にトラッキングピクセルが発火するなど)。ORDER BY occurred_at ASC だけでは順序が不定になるため、ORDER BY occurred_at ASC, event_id ASC のようにタイブレーカーを加えると安定する。

③ データ量が大きい場合の分割実行

行動ログは注文ログより桁違いに行数が多い。数億行のログに対して全件 LAG を計算するとクエリが重くなる。Treasure Data の場合は WHERE occurred_at >= DATE_ADD('day', -30, CURRENT_DATE) で期間を絞るか、月次パーティションに分けて実行する設計が現実的だ。


まとめ ― ストリーク分析との共通点

今回のセッション再構築とストリーク分析は、本質的に同じパターンで解ける。

ストリーク分析セッション再構築
区切りの定義連続しない月(購買のない月)30分以上の間隔
グループキーの作り方月番号 − ROW_NUMBERis_session_start の累積 SUM
結果連続購買区間(Island)セッション

どちらも「連続しているグループを見つけて番号を振る」という GAP and ISLAND の考え方だ。区切りの定義だけが異なる。このパターンを一度習得すると、「週次ログイン連続記録」「連続レビュー投稿」「一定間隔のクリック列」など、さまざまな「連続・区切り」分析に使い回せる。


次のアクション

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

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

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

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

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

実績・支援内容を見る →

MarTech Farmをもっと見る

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

続きを読む