SQL 日付・時刻 — 区間マージ・営業日数・連続日数の応用

応用日付・時刻区間のマージ営業日計算コホート連続日数時間帯バケットPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

区間のマージ — 重なり合う期間を1本の期間へまとめる

ウィンドウ関数累積最大区間マージギャップ判定
前提知識

重なる期間を1本へまとめるには、開始順に並べて「直前までに到達した最大の終了日」と比べます。今の開始がそれを越えていれば、そこが新しい区間の切れ目です。

SELECT MAX(end_col) OVER (
         PARTITION BY key_col ORDER BY start_col, end_col
         ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)
FROM table_name;
-- 自分より前の行だけを見た「到達済みの終了」
直前の行ではなく累積最大:1つ前の区間の終了日と比べるだけでは、長い区間に短い区間が丸ごと包まれている場合を取りこぼします。比較対象は「ここまでの終了日の最大値」です。
問題

maintenance テーブルの作業枠は [starts_on, ends_on) の半開区間です。サービスごとに、重なり合う枠と隙間なく連続する枠を1本の期間へまとめてください。前の枠の終了日と次の枠の開始日が同じ場合は連続とみなして結合します。取得列は service, starts_on, ends_on, windows(まとめた枠の件数)、service・starts_on の昇順で返してください。

使用テーブル
▸ maintenance
window_idservicestarts_onends_on
1api2026-05-012026-05-04
2api2026-05-032026-05-06
3api2026-05-062026-05-08
4api2026-05-122026-05-14
5web2026-05-022026-05-05
6web2026-05-092026-05-10
期待出力
servicestarts_onends_onwindows
api2026-05-012026-05-083
api2026-05-122026-05-141
web2026-05-022026-05-051
web2026-05-092026-05-101
QUESTION 7

営業日数 — 週末と祝日を除いて日数を数える

generate_seriesLATERAL営業日NOT EXISTS
前提知識

営業日数は「日付を1日ずつ展開し、条件に合う日を数える」形で求めます。左の行ごとに展開したいので、CROSS JOIN LATERAL でサブクエリの中に generate_series を置きます。

SELECT t.id, x.days
FROM   table_name t
CROSS JOIN LATERAL (
  SELECT COUNT(*) AS days
  FROM   generate_series(t.start_col, t.end_col, INTERVAL '1 day') AS d(day)
  WHERE  EXTRACT(ISODOW FROM d.day) <= 5) x;   -- 1=月 … 5=金
ISODOW は平日の判定に向く:1=月曜〜7=日曜なので、<= 5 と書くだけで平日が選べます。DOW(0=日曜〜6=土曜)だと、土日を NOT IN (0, 6) と列挙することになります。
問題

tasks テーブルの各タスクについて、開始日から期限日までの営業日数を求めてください。営業日は月曜〜金曜のうち holidays テーブルに載っていない日とし、開始日と期限日はどちらも含めます。取得列は task_id, start_on, due_on, business_days、task_id 昇順で返してください。

使用テーブル
▸ tasks
task_idstart_ondue_on
12026-05-012026-05-07
22026-05-042026-05-08
32026-05-082026-05-08
▸ holidays
holiday_onname
2026-05-04constitution day
2026-05-06substitute holiday
期待出力
task_idstart_ondue_onbusiness_days
12026-05-012026-05-073
22026-05-042026-05-083
32026-05-082026-05-081
QUESTION 8

コホート分析 — 初回月からの経過月数で継続を並べる

MIN + DATE_TRUNC経過月数コホートEXTRACT
前提知識

2つの月の差は、年の差を12倍して月の差を足すと求まります。日付の引き算(日数)では、月の長さの違いで揃いません。

SELECT (EXTRACT(YEAR FROM date_col) - EXTRACT(YEAR FROM base_col)) * 12
     + (EXTRACT(MONTH FROM date_col) - EXTRACT(MONTH FROM base_col)) AS month_no
FROM table_name;
-- 2025-04 と 2025-06 → 2、2025-12 と 2026-02 → 2
基準は行ごとに違う:コホート分析の基準月は顧客ごとの初回月です。集計で求めた基準を元の行へ戻すため、GROUP BY した結果をCTEにして結合し直します。
問題

orders テーブルについて、顧客ごとの初回注文月をコホートとし、コホート月からの経過月数ごとに注文した顧客数を求めてください。経過月数は初回月を 0 とします。取得列は cohort_month, month_no, customers、cohort_month・month_no の昇順で返してください。cohort_month は月初の日付で表します。

使用テーブル
▸ orders
order_idcustomer_idordered_on
11012026-01-10
21012026-02-05
31012026-03-20
41022026-01-25
51022026-03-02
61032026-02-14
71032026-03-01
期待出力
cohort_monthmonth_nocustomers
2026-01-0102
2026-01-0111
2026-01-0122
2026-02-0101
2026-02-0111
QUESTION 9

連続日数の最長 — 日付から連番を引いて連続の島を作る

ROW_NUMBER島グループ連続日数DISTINCT ON
前提知識

連続した日付から通し番号を引くと、同じ連続の並びでは同じ値になります。日付が飛ぶと差が変わるため、この値がそのまま「連続の島」の識別子になります。

SELECT date_col - (ROW_NUMBER() OVER (PARTITION BY key_col ORDER BY date_col))::int AS grp
FROM   table_name;
-- 07-11,07-12,07-13 → いずれも同じ grp / 07-16 で別の grp
日付から整数を引く:DATE から整数を引くと、その日数ぶん前の日付になります。ROW_NUMBER()bigint を返すので、::int へキャストしてから引きます。
問題

logins テーブルから、ユーザーごとに最長の連続ログイン日数と、その連続の開始日・終了日を求めてください。最長が複数ある場合は開始日が早い方を採用します。取得列は user_id, started_on, ended_on, streak_days、user_id 昇順で返してください。

使用テーブル
▸ logins
user_idlogin_on
1012026-02-01
1012026-02-02
1012026-02-03
1012026-02-06
1012026-02-07
1022026-02-01
1022026-02-03
1022026-02-04
1032026-02-05
期待出力
user_idstarted_onended_onstreak_days
1012026-02-012026-02-033
1022026-02-032026-02-042
1032026-02-052026-02-051
QUESTION 10

時間帯バケット — 時間をまたぐ滞在を1時間ごとへ按分する

generate_seriesGREATEST / LEAST時間帯集計EPOCH
前提知識

時間をまたぐ区間を時間帯ごとに割り振るには、バケットの並びを作り、区間とバケットの交差時間を測ります。交差の長さは GREATESTLEAST で求め、EXTRACT(EPOCH FROM ...) で秒数へ直します。

SELECT EXTRACT(EPOCH FROM (
         LEAST(end_col, b.bucket + INTERVAL '1 hour')
       - GREATEST(start_col, b.bucket))) / 60 AS minutes
FROM table_name, generate_series(...) AS b(bucket);
EPOCH は秒で返る:EXTRACT(EPOCH FROM INTERVAL '1 hour') は 3600 です。分にしたいなら 60 で、時間にしたいなら 3600 で割ります。
問題

sessions テーブルの滞在時間を、9時・10時・11時の1時間バケットごとに按分して合計してください。バケットは [開始, 開始+1時間) の半開区間とし、そのバケットと重ならないセッションは数えません。取得列は bucket, minutes(合計滞在分数・整数)、bucket 昇順で返してください。

使用テーブル
▸ sessions
session_idstarted_atended_at
12026-10-01 09:10:002026-10-01 09:50:00
22026-10-01 09:40:002026-10-01 11:10:00
32026-10-01 11:30:002026-10-01 11:45:00
期待出力
bucketminutes
2026-10-01 09:00:0060
2026-10-01 10:00:0060
2026-10-01 11:00:0025