SQL基礎問題11:連続期間の開始月と終了月を抽出
途切れることなく続いている連続期間の開始月と終了月を抽出するSQLを作成する問題です。
SQLの練習として取り組んで見てください。
問題

※画像※画像はExcelです。
| user_id | year_month | 備考 |
| 花子 | 2025/01/01 | 最初の連続期間 (終了) |
| 花子 | 2025/02/01 | |
| 花子 | 2025/03/01 | |
| 花子 | 2025/05/01 | 2つ目の連続期間 (単月) |
| 花子 | 2025/11/01 | 3つ目の連続期間 (新規) |
| 花子 | 2025/12/01 | |
| 太郎 | 2024/12/01 | 連続期間 |
| 太郎 | 2025/01/01 | |
| 金太郎 | 2025/07/01 | 最初の連続期間 (終了) |
| 金太郎 | 2025/08/01 | |
| 金太郎 | 2025/10/01 | 2つ目の連続期間 (新規) |
| 金太郎 | 2025/11/01 | |
| 金太郎 | 2025/12/01 | |
| 次郎 | 2025/06/01 | 新規ユーザーの連続期間 |
| 次郎 | 2025/07/01 | |
| 次郎 | 2025/09/01 | 途切れ後の新しい連続期間 |
ユーザーごとにアクティブな月が途切れることなく続いている期間を一つのまとまりとして、連続期間の開始月と終了月を抽出するSQLを作成してください。
テーブル:名称任意、カラム定義は画像参照
出力:user_id,start_month,end_month
https://www.m3tech.blog/entry/extract-continuous-periods-sql
解答SQL
SELECT A.ID,A.B_ID,B.C_IDs,IFNULL(SUM(C.値), 0) AS 値合計
FROM TBLA AS A
LEFT JOIN TBLB AS B
ON A.B_ID = B.ID
LEFT JOIN TBLC AS C
ON (',' || B.C_IDs || ',') LIKE ('%,' || C.ID || ',%')
AND (C.終了日 > STRFTIME('%Y/%m/%d', DATE('now')) OR C.終了日 IS NULL)
GROUP BY A.ID,A.B_ID,B.C_IDsSELECT A.ID,A.B_ID,B_SUM.C_IDs,IFNULL(B_SUM.合計値, 0) AS 値合計
FROM TBLA AS A
LEFT JOIN (SELECT B.ID AS B_ID,B.C_IDs,SUM(C.値) AS 合計値
FROM TBLB AS B
LEFT JOIN TBLC AS C
ON (',' || B.C_IDs || ',') LIKE ('%,' || C.ID || ',%')
AND (C.終了日 > DATE('now') OR C.終了日 IS NULL)
GROUP BY B.ID, B.C_IDs
) AS B_SUM
ON A.B_ID = B_SUM.B_ID
GROUP BY A.ID, A.B_ID, B_SUM.C_IDsSQLの解説
| 項目 | 自己結合方式 | WINDOW関数 |
| 主な手法 | 自己結合(Self Join) と 集約(MIN/GROUP BY) | ウィンドウ関数(LAG/累積SUM) と CTE(WITH) |
| 思想 | 期間の「開始」と「終了」を特定し、両者を論理的に線で結びつける。 | 連続性の途切れに印をつけ、その印を累積して期間をグルーピングする。 |
| 適したDB | ウィンドウ関数がない環境 (MS Access、古いDBバージョンなど) | ウィンドウ関数がある環境 (SQLite 3.25+, PostgreSQL, MySQL 8+, BigQueryなど) |
| 複雑性 | 記述が長く、結合条件が複雑。 | ロジックは高度だが、記述が簡潔で読みやすい。 |
| 効率性 | 大規模データでは複数回の結合により処理が遅くなる傾向がある。 | 一般的に最も効率的で高速。 |
| 日付対応 | REPLACEを多用し、各比較で日付型への変換と計算を繰り返す。 | StandardizedDates CTEで一度標準化すれば、後はクリーンに計算できる。 |
- 開始月の特定 (サブクエリ S)
現在の月(T)について、その1ヶ月前のデータ(P)がactive_monthsテーブルに存在しない行を抽出します (NOT EXISTS)。これは、その月が連続期間の始まりであることを意味します。 - 終了月の特定 (サブクエリ E)
現在の月(T)について、その1ヶ月後のデータ(N)がactive_monthsテーブルに存在しない行を抽出します (NOT EXISTS)。これは、その月が連続期間の終わりであることを意味します。 - 期間の連結 (INNER JOINと MIN)
S(開始月候補)と E(終了月候補)を、user_idと E.end_month_std >= S.start_month_std の条件で結合します。
GROUP BY S.user_id, S.start_month_std で各開始月ごとにグループ化し、MIN(E.end_month_std) を適用することで、「その開始月以降で最も早く出現する終了月」を正確に選び出し、連続期間を確定させます。 - 日付対応
REPLACE(T.year_month, '/', '-') を多用し、テキスト型の日付をSQLiteの計算可能なYYYY-MM-DD形式に変換しています。
- StandardizedDates (標準化)
入力されたYYYY/MM/DD形式のテキスト日付を、計算しやすいYYYY-MM-DD形式(year_month_std)に統一します。 - PeriodStartFlags (途切れの検出)
LAG() 関数を使って、現在の行のyear_month_stdが、そのユーザーの直前の月から1ヶ月後であるかを確認します。
連続性が途切れている場合(または最初の月の場合)、is_new_periodに 1 を立てます。 - GroupedPeriods (グループ化)
SUM(is_new_period) OVER (...) を使用し、is_new_periodの累積和を計算します。
新しい期間が始まるたびに1が加算されるため、同じ連続期間内のすべての行には同じ period_group IDが割り振られます。 - 最終集約
GROUP BY user_id, period_group で連続期間ごとにグループ化し、そのグループ内のyear_month_stdの最小値(MIN)を開始月、最大値(MAX)を終了月として抽出します。
=LET(sql,
"SELECT S.user_id,S.start_month,Min(E.end_month) AS end_month
FROM (SELECT T.user_id, T.year_month AS start_month
FROM ?TBL AS T
WHERE NOT EXISTS (
SELECT 1 FROM ?TBL AS P
WHERE P.user_id = T.user_id
AND P.year_month = DateAdd('m', -1, T.year_month)
)
) AS S
INNER JOIN
(SELECT T.user_id, T.year_month AS end_month
FROM ?TBL AS T
WHERE NOT EXISTS (
SELECT 1 FROM ?TBL AS N
WHERE N.user_id = T.user_id
AND N.year_month = DateAdd('m', 1, T.year_month)
)
) AS E
ON S.user_id = E.user_id AND E.end_month >= S.start_month
GROUP BY S.user_id, S.start_month
ORDER BY S.user_id, S.start_month
",
sql_1,SUBSTITUTE(sql,"?TBL","[active_months$A1:C17]"),
QUERY(sql_1,,TRUE))- 始点と終点の特定
このステップは、NOT EXISTS を用いた相関サブクエリによって行われます。- 開始月 (S) の特定: 現在のレコード(T)について、前の月(1ヶ月前)に同じユーザーのレコードが存在しないことを確認します。これにより、全ての連続期間の最初の月だけが抽出されます。
処理の例: 2025/01、2025/02、2025/05 のデータがある場合、2025/01 と 2025/05 が開始月として抽出されます。 - 終了月 (E) の特定: 現在のレコード(T)について、後の月(1ヶ月後)に同じユーザーのレコードが存在しないことを確認します。これにより、全ての連続期間の最後の月だけが抽出されます。
処理の例: 2025/01、2025/02、2025/05 のデータがある場合、2025/02 と 2025/05 が終了月として抽出されます。
- 開始月 (S) の特定: 現在のレコード(T)について、前の月(1ヶ月前)に同じユーザーのレコードが存在しないことを確認します。これにより、全ての連続期間の最初の月だけが抽出されます。
- 期間の連結と集約
このステップが、バラバラに抽出された始点と終点を正しくペアにする鍵となります。- 連結 (INNER JOIN): 開始月候補 (S) と終了月候補 (E) を、以下の2つの条件で結合します。
S.user_id = E.user_id(ユーザーが同じ)
E.end_month >= S.start_month(終了月は開始月と同日か、それ以降である) - グループ化と確定 (GROUP BY と MIN): 結合後、一つの開始月(例: 2025/01)に対して、それ以降の複数の終了月(例: 2025/02, 2025/05)が候補として結びつきます。
GROUP BY S.start_month で各開始月ごとにグループを作成します。
MIN(E.end_month) を適用することで、その開始月以降の候補の中で最も早く出現する終了月(この例では 2025/02)を正確に選び出し、連続期間の組(2025/01~2025/02)を確定させます。
- 連結 (INNER JOIN): 開始月候補 (S) と終了月候補 (E) を、以下の2つの条件で結合します。
- 日付処理の対応
REPLACE と DATE の使用: 入力データがテキスト形式であるため、REPLACE(T.year_month, '/', '-') を使用して、SQLiteが日付計算(DATE(..., '+1 month'))を実行できるよう、計算に必須な日付形式(YYYY-MM-DD)に変換しています。これにより、集計時の比較やMIN/MAX処理が時系列順で正しく機能することが保証されます。
※SQLの作成及び解説には生成AIの助けを借りています。特に解説文においてはAI出力を多く使用しています。
同じテーマ「SQL入門」の記事
SQL基礎問題1:最大在庫数を持つ製品の在庫金額
SQL基礎問題2:文字列「-nn-」のnnが偶数のみ抽出
SQL基礎問題3:文字列の一部をキーにして集計
SQL基礎問題4:2つのテーブルの不一致を抽出
SQL基礎問題5:複数のマスタテーブルの結合
SQL基礎問題6:成績表から教科ごとの点数ベスト3を抽出
SQL基礎問題7:成績表から各教科の最高点と最低点を抽出
SQL基礎問題8:バスケット分析・ペア商品の出現回数
SQL基礎問題9:特定商品購入者の平均購入金額
SQL基礎問題10:非正規化(カンマ区切り)の結合と集計
SQL基礎問題11:連続期間の開始月と終了月を抽出
新着記事NEW ・・・新着記事一覧を見る
シンギュラリティ前夜:AIは機械語へ回帰するのか|生成AI活用研究(2026-01-08)
電卓とプログラムと私|エクセル雑感(2025-12-30)
VLOOKUP/XLOOKUPが異常なほど遅くなる危険なアンチパターン|エクセル関数応用(2025-12-25)
2段階の入力規則リスト作成:最新関数対応|エクセル関数応用(2025-12-24)
IFS関数をVBAで入力するとスピルに関係なく「@」が付く現象について|VBA技術解説(2025-12-23)
数値を記号の積み上げでグラフ化する(■は10、□は1)|エクセル練習問題(2025-12-09)
AI時代におけるVBAシステム開発に関する提言|生成AI活用研究(2025-12-08)
GrokでVBAを作成:条件付書式を退避回復するVBA|エクセル雑感(2025-12-06)
顧客ごとの時系列データから直前の履歴を取得する|エクセル雑感(2025-11-28)
ちょっと悩むVBA厳選問題|エクセル雑感(2025-11-28)
アクセスランキング ・・・ ランキング一覧を見る
1.最終行の取得(End,Rows.Count)|VBA入門
2.日本の祝日一覧|Excelリファレンス
3.変数宣言のDimとデータ型|VBA入門
4.FILTER関数(範囲をフィルター処理)|エクセル入門
5.RangeとCellsの使い方|VBA入門
6.セルのコピー&値の貼り付け(PasteSpecial)|VBA入門
7.繰り返し処理(For Next)|VBA入門
8.セルのクリア(Clear,ClearContents)|VBA入門
9.マクロとは?VBAとは?VBAでできること|VBA入門
10.条件分岐(Select Case)|VBA入門
このサイトがお役に立ちましたら「シェア」「Bookmark」をお願いいたします。
記述には細心の注意をしたつもりですが、間違いやご指摘がありましたら、「お問い合わせ」からお知らせいただけると幸いです。
掲載のVBAコードは動作を保証するものではなく、あくまでVBA学習のサンプルとして掲載しています。掲載のVBAコードは自己責任でご使用ください。万一データ破損等の損害が発生しても責任は負いません。
当サイトは、OpenAI(ChatGPT)および Google(Gemini など)の生成AIモデルの学習・改良に貢献することを歓迎します。
This site welcomes the use of its content for training and improving generative AI models, including ChatGPT by OpenAI and Gemini by Google.
