| 1 | SET search_path TO trekr;
|
|---|
| 2 |
|
|---|
| 3 | -- P6 Q2 (Annual): Training consistency, workload trend, and peak period analysis.
|
|---|
| 4 | -- Change report_year in params CTE when needed.
|
|---|
| 5 | WITH params AS (
|
|---|
| 6 | SELECT 2026::int AS report_year
|
|---|
| 7 | ),
|
|---|
| 8 | months AS (
|
|---|
| 9 | SELECT generate_series(1, 12) AS month_no
|
|---|
| 10 | ),
|
|---|
| 11 | training_base AS (
|
|---|
| 12 | SELECT
|
|---|
| 13 | tu.user_id,
|
|---|
| 14 | u.username,
|
|---|
| 15 | u.email,
|
|---|
| 16 | tu.gender,
|
|---|
| 17 | tu.age,
|
|---|
| 18 | tu.weight
|
|---|
| 19 | FROM training_users tu
|
|---|
| 20 | JOIN users u ON u.user_id = tu.user_id
|
|---|
| 21 | ),
|
|---|
| 22 | monthly_sessions AS (
|
|---|
| 23 | SELECT
|
|---|
| 24 | tb.user_id,
|
|---|
| 25 | m.month_no,
|
|---|
| 26 | COALESCE(COUNT(ts.training_id), 0) AS sessions_count,
|
|---|
| 27 | COALESCE(SUM(ts.duration), 0) AS total_duration_minutes,
|
|---|
| 28 | COALESCE(SUM(ts.calories), 0) AS total_calories,
|
|---|
| 29 | COALESCE(AVG(ts.duration), 0) AS avg_session_duration,
|
|---|
| 30 | COALESCE(AVG(ts.calories), 0) AS avg_session_calories
|
|---|
| 31 | FROM training_base tb
|
|---|
| 32 | CROSS JOIN months m
|
|---|
| 33 | LEFT JOIN training_sessions ts
|
|---|
| 34 | ON ts.training_user_id = tb.user_id
|
|---|
| 35 | AND EXTRACT(YEAR FROM ts.date)::int = (SELECT report_year FROM params)
|
|---|
| 36 | AND EXTRACT(MONTH FROM ts.date)::int = m.month_no
|
|---|
| 37 | GROUP BY tb.user_id, m.month_no
|
|---|
| 38 | ),
|
|---|
| 39 | monthly_ranked AS (
|
|---|
| 40 | SELECT
|
|---|
| 41 | ms.*,
|
|---|
| 42 | DENSE_RANK() OVER (PARTITION BY ms.user_id ORDER BY ms.total_calories DESC, ms.month_no ASC) AS peak_calorie_month_rank,
|
|---|
| 43 | DENSE_RANK() OVER (PARTITION BY ms.user_id ORDER BY ms.sessions_count DESC, ms.month_no ASC) AS peak_sessions_month_rank
|
|---|
| 44 | FROM monthly_sessions ms
|
|---|
| 45 | ),
|
|---|
| 46 | active_month_streaks AS (
|
|---|
| 47 | SELECT
|
|---|
| 48 | user_id,
|
|---|
| 49 | month_no,
|
|---|
| 50 | month_no - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY month_no) AS grp
|
|---|
| 51 | FROM monthly_sessions
|
|---|
| 52 | WHERE sessions_count > 0
|
|---|
| 53 | ),
|
|---|
| 54 | longest_streak AS (
|
|---|
| 55 | SELECT
|
|---|
| 56 | user_id,
|
|---|
| 57 | MAX(streak_len) AS longest_active_month_streak
|
|---|
| 58 | FROM (
|
|---|
| 59 | SELECT user_id, grp, COUNT(*) AS streak_len
|
|---|
| 60 | FROM active_month_streaks
|
|---|
| 61 | GROUP BY user_id, grp
|
|---|
| 62 | ) s
|
|---|
| 63 | GROUP BY user_id
|
|---|
| 64 | ),
|
|---|
| 65 | annual_training AS (
|
|---|
| 66 | SELECT
|
|---|
| 67 | user_id,
|
|---|
| 68 | SUM(sessions_count) AS annual_sessions,
|
|---|
| 69 | SUM(total_duration_minutes) AS annual_duration_minutes,
|
|---|
| 70 | SUM(total_calories) AS annual_calories,
|
|---|
| 71 | AVG(total_duration_minutes) AS avg_monthly_duration,
|
|---|
| 72 | AVG(total_calories) AS avg_monthly_calories,
|
|---|
| 73 | COUNT(*) FILTER (WHERE sessions_count > 0) AS active_months,
|
|---|
| 74 | REGR_SLOPE(total_calories::numeric, month_no::numeric) AS calories_trend_slope,
|
|---|
| 75 | REGR_SLOPE(total_duration_minutes::numeric, month_no::numeric) AS duration_trend_slope
|
|---|
| 76 | FROM monthly_sessions
|
|---|
| 77 | GROUP BY user_id
|
|---|
| 78 | ),
|
|---|
| 79 | peak_months AS (
|
|---|
| 80 | SELECT
|
|---|
| 81 | user_id,
|
|---|
| 82 | MAX(month_no) FILTER (WHERE peak_calorie_month_rank = 1) AS peak_calorie_month_no,
|
|---|
| 83 | MAX(month_no) FILTER (WHERE peak_sessions_month_rank = 1) AS peak_sessions_month_no
|
|---|
| 84 | FROM monthly_ranked
|
|---|
| 85 | GROUP BY user_id
|
|---|
| 86 | )
|
|---|
| 87 | SELECT
|
|---|
| 88 | tb.user_id,
|
|---|
| 89 | tb.username,
|
|---|
| 90 | tb.email,
|
|---|
| 91 | tb.gender,
|
|---|
| 92 | tb.age,
|
|---|
| 93 | tb.weight,
|
|---|
| 94 | at.annual_sessions,
|
|---|
| 95 | ROUND(at.annual_duration_minutes::numeric, 2) AS annual_duration_minutes,
|
|---|
| 96 | ROUND(at.annual_calories::numeric, 2) AS annual_calories,
|
|---|
| 97 | at.active_months,
|
|---|
| 98 | ROUND((at.active_months / 12.0)::numeric, 4) AS consistency_ratio,
|
|---|
| 99 | COALESCE(ls.longest_active_month_streak, 0) AS longest_active_month_streak,
|
|---|
| 100 | pm.peak_calorie_month_no,
|
|---|
| 101 | pm.peak_sessions_month_no,
|
|---|
| 102 | ROUND(COALESCE(at.calories_trend_slope, 0)::numeric, 4) AS calories_trend_slope,
|
|---|
| 103 | ROUND(COALESCE(at.duration_trend_slope, 0)::numeric, 4) AS duration_trend_slope,
|
|---|
| 104 | DENSE_RANK() OVER (
|
|---|
| 105 | ORDER BY
|
|---|
| 106 | at.annual_calories DESC,
|
|---|
| 107 | at.active_months DESC,
|
|---|
| 108 | COALESCE(ls.longest_active_month_streak, 0) DESC,
|
|---|
| 109 | tb.user_id ASC
|
|---|
| 110 | ) AS training_annual_rank
|
|---|
| 111 | FROM training_base tb
|
|---|
| 112 | JOIN annual_training at ON at.user_id = tb.user_id
|
|---|
| 113 | JOIN peak_months pm ON pm.user_id = tb.user_id
|
|---|
| 114 | LEFT JOIN longest_streak ls ON ls.user_id = tb.user_id
|
|---|
| 115 | ORDER BY training_annual_rank, tb.user_id;
|
|---|
| 116 |
|
|---|