区間のマージ — 重なり合う期間を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; -- 自分より前の行だけを見た「到達済みの終了」
maintenance テーブルの作業枠は [starts_on, ends_on) の半開区間です。サービスごとに、重なり合う枠と隙間なく連続する枠を1本の期間へまとめてください。前の枠の終了日と次の枠の開始日が同じ場合は連続とみなして結合します。取得列は service, starts_on, ends_on, windows(まとめた枠の件数)、service・starts_on の昇順で返してください。
| window_id | service | starts_on | ends_on |
|---|---|---|---|
| 1 | api | 2026-05-01 | 2026-05-04 |
| 2 | api | 2026-05-03 | 2026-05-06 |
| 3 | api | 2026-05-06 | 2026-05-08 |
| 4 | api | 2026-05-12 | 2026-05-14 |
| 5 | web | 2026-05-02 | 2026-05-05 |
| 6 | web | 2026-05-09 | 2026-05-10 |
| service | starts_on | ends_on | windows |
|---|---|---|---|
| api | 2026-05-01 | 2026-05-08 | 3 |
| api | 2026-05-12 | 2026-05-14 | 1 |
| web | 2026-05-02 | 2026-05-05 | 1 |
| web | 2026-05-09 | 2026-05-10 | 1 |
営業日数 — 週末と祝日を除いて日数を数える
営業日数は「日付を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=金
<= 5 と書くだけで平日が選べます。DOW(0=日曜〜6=土曜)だと、土日を NOT IN (0, 6) と列挙することになります。tasks テーブルの各タスクについて、開始日から期限日までの営業日数を求めてください。営業日は月曜〜金曜のうち holidays テーブルに載っていない日とし、開始日と期限日はどちらも含めます。取得列は task_id, start_on, due_on, business_days、task_id 昇順で返してください。
| task_id | start_on | due_on |
|---|---|---|
| 1 | 2026-05-01 | 2026-05-07 |
| 2 | 2026-05-04 | 2026-05-08 |
| 3 | 2026-05-08 | 2026-05-08 |
| holiday_on | name |
|---|---|
| 2026-05-04 | constitution day |
| 2026-05-06 | substitute holiday |
| task_id | start_on | due_on | business_days |
|---|---|---|---|
| 1 | 2026-05-01 | 2026-05-07 | 3 |
| 2 | 2026-05-04 | 2026-05-08 | 3 |
| 3 | 2026-05-08 | 2026-05-08 | 1 |
コホート分析 — 初回月からの経過月数で継続を並べる
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 は月初の日付で表します。
| order_id | customer_id | ordered_on |
|---|---|---|
| 1 | 101 | 2026-01-10 |
| 2 | 101 | 2026-02-05 |
| 3 | 101 | 2026-03-20 |
| 4 | 102 | 2026-01-25 |
| 5 | 102 | 2026-03-02 |
| 6 | 103 | 2026-02-14 |
| 7 | 103 | 2026-03-01 |
| cohort_month | month_no | customers |
|---|---|---|
| 2026-01-01 | 0 | 2 |
| 2026-01-01 | 1 | 1 |
| 2026-01-01 | 2 | 2 |
| 2026-02-01 | 0 | 1 |
| 2026-02-01 | 1 | 1 |
連続日数の最長 — 日付から連番を引いて連続の島を作る
連続した日付から通し番号を引くと、同じ連続の並びでは同じ値になります。日付が飛ぶと差が変わるため、この値がそのまま「連続の島」の識別子になります。
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 昇順で返してください。
| user_id | login_on |
|---|---|
| 101 | 2026-02-01 |
| 101 | 2026-02-02 |
| 101 | 2026-02-03 |
| 101 | 2026-02-06 |
| 101 | 2026-02-07 |
| 102 | 2026-02-01 |
| 102 | 2026-02-03 |
| 102 | 2026-02-04 |
| 103 | 2026-02-05 |
| user_id | started_on | ended_on | streak_days |
|---|---|---|---|
| 101 | 2026-02-01 | 2026-02-03 | 3 |
| 102 | 2026-02-03 | 2026-02-04 | 2 |
| 103 | 2026-02-05 | 2026-02-05 | 1 |
時間帯バケット — 時間をまたぐ滞在を1時間ごとへ按分する
時間をまたぐ区間を時間帯ごとに割り振るには、バケットの並びを作り、区間とバケットの交差時間を測ります。交差の長さは GREATEST と LEAST で求め、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);
EXTRACT(EPOCH FROM INTERVAL '1 hour') は 3600 です。分にしたいなら 60 で、時間にしたいなら 3600 で割ります。sessions テーブルの滞在時間を、9時・10時・11時の1時間バケットごとに按分して合計してください。バケットは [開始, 開始+1時間) の半開区間とし、そのバケットと重ならないセッションは数えません。取得列は bucket, minutes(合計滞在分数・整数)、bucket 昇順で返してください。
| session_id | started_at | ended_at |
|---|---|---|
| 1 | 2026-10-01 09:10:00 | 2026-10-01 09:50:00 |
| 2 | 2026-10-01 09:40:00 | 2026-10-01 11:10:00 |
| 3 | 2026-10-01 11:30:00 | 2026-10-01 11:45:00 |
| bucket | minutes |
|---|---|
| 2026-10-01 09:00:00 | 60 |
| 2026-10-01 10:00:00 | 60 |
| 2026-10-01 11:00:00 | 25 |