0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SQLで行う連続判定

Last updated at Posted at 2025-12-07

SQLで行う連続判定

概要

SQLの連続判定の方法を自分なりに思い付いたので備忘のためメモ

前提知識

SQLの基本文法(集約関数、分析関数)

前提データと求めたい結果

以下のように、customer_id(顧客番号)とym(注文年月)の列を持つOrderHistory(注文履歴)テーブルがある。
customer_idごとに、最大何か月連続で注文しているか求めたい。

注文履歴テーブル

スクリーンショット 2025-12-07 225223.jpg

求めたいデータ

スクリーンショット 2025-12-07 225321.jpg

どうやって求めるか?

自分か書いたSQL

顧客ごとの最大連続購入月数を求めるSQL
WITH tmp1 AS (
    -- 注文年月と、2000年1月からの通し月数(monthly_seq)を集計
    SELECT customer_id
           , ym
           -- 2000年1月からの通し月数を計算 (連続性判定の基礎)
           , PERIOD_DIFF(DATE_FORMAT(order_date, '%Y%m'), 200001) AS monthly_seq
      FROM OrderHistory
),
tmp2 AS (
    -- 各レコードの前後の通し月数を取得
    SELECT  customer_id
            ,ym
            ,monthly_seq
            ,LAG(monthly_seq) OVER (PARTITION BY customer_id ORDER BY monthly_seq) AS lag_seq
            ,LEAD(monthly_seq) OVER (PARTITION BY customer_id ORDER BY monthly_seq) AS lead_seq
    FROM tmp1
),
tmp3 AS (
    -- 各レコードについて、1つ前の差分と1つ後の差分を出す
    SELECT
            customer_id,
            ,ym
            ,monthly_seq
            ,lag_seq
            ,lead_seq
            -- 前の月との差分。連続なら必ず -1
            ,COALESCE(lag_seq - monthly_seq, 0) AS lag_diff
            -- 次の月との差分。連続なら必ず +1
            ,COALESCE(lead_seq - monthly_seq, 0) AS lead_diff 
      FROM tmp2
),
tmp4a AS (
    -- 連続期間の先頭レコードを絞る (直前の月との差が -1 ではない)
    SELECT customer_id
           ,ym AS bgn_ym
           ,monthly_seq AS bgn_seq
           -- 各グループの開始点に連番を振る (後の結合用)
           ,ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ym) AS rn
     FROM tmp3
    WHERE lag_diff <> -1
),
tmp4b AS (
    -- 連続期間の最終レコードを絞る (直後の月との差が +1 ではない)
    SELECT customer_id
           ,ym AS end_ym
           ,monthly_seq AS end_seq
           -- 最終レコードにも同じく連番を振る
           ,ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ym) AS rn
      FROM tmp3
     WHERE lead_diff <> 1
),
tmp5 AS (
    -- 開始点と終了点を、連番 (rn) をキーに結合
    SELECT ta.customer_id
           ,ta.bgn_ym
           ,tb.end_ym
           -- 期間の長さを通し月数で計算 (end_seq - bgn_seq + 1)
           ,tb.end_seq - ta.bgn_seq + 1 AS consecutive_months
      FROM tmp4a ta
     INNER JOIN tmp4b tb
        ON ta.customer_id = tb.customer_id
       AND ta.rn = tb.rn
)
, tmp6 as (
    -- 最終的な結果:顧客IDごとに最長の連続期間を求める
    SELECT customer_id
           , MAX(consecutive_months) AS max_consecutive_months
      FROM tmp5
     GROUP BY customer_id
)
select *
  from tmp6;

SQLは以上の通りですが、with句の連発で何をしているかサッパリなので、次の項でそれぞれのWITH句で何をしているか一つずつ解説していきます。

詳細解説

WITH句1つ目(tmp1)

各行のymに対して、2001年1月1日を起点として経過月数を求めています。
これは次以降のWITH句で生きてきます。
image.png

WITH句2つ目(tmp2)

各行に対して、分析関数を用いて、顧客番号ごとに前後の行の通し月数を付けます。
image.png

WITH句3つ目(tmp3)

各行に対して、monthly_seqとlag_seq、monthly_seqとlead_seqの差分を取得します。
lag_diff = lag_seq - monthly_seq ⇒ これが-1でない行は連続の開始行
lead_diff = lead_seq - monthly_seq ⇒ これが1でない行は連続の終了行
image.png

WITH句4つ目の一(tmp4a)

tmp3のデータを連続開始行(lag_diff<>-1の行)だけに絞り込んだテーブルを作ります。
ついでにあとで結合キーに使うための連番もにふっておきます。
image.png

WITH句4つ目の二(tmp4b)

tmp3のデータを連続終了行(lead_diff<>1の行)だけに絞り込んだテーブルを作ります。
ついでにあとで結合キーに使うための連番もにふっておきます。
image.png

WITH句5つ目(tmp5)

tmp4aとtmp4bを結合します。これにより連続の開始年月と終了年月が1行にまとめられるため、連続期間が求められます。
image.png

WITH句6つ目(tmp6)

ここは流石に自明なので省略。

おわりに

自分で思い付いた方法ですが、記録しておかないと翌日には忘れそうなので記事にまとめました。
Geminiに聞いたところ「Gap-and-Islands」と呼ばれる、より優れた解法があるそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?