DAU — COUNT(DISTINCT) × GROUP BY で日次アクティブユーザー数を計算する
DAU(Daily Active Users)はサービスの「日々の熱量」を測る最も基本的な KPI です。同日に何度行動しても 1 としてカウントする ため、COUNT(DISTINCT user_id) が必須です。
COUNT(DISTINCT user_id) -- ユニークユーザー数(重複除去) ← DAU に使う COUNT(user_id) -- イベント総数(重複あり) ← DAU には使えない COUNT(*) -- 行数(NULL含む重複あり) ← DAU には使えない
WHERE event_type IN ('purchase','search') のように絞ってから COUNT(DISTINCT) するのが実務の定番です。user_events テーブルから、各日付の DAU(日次アクティブユーザー数)を計算してください。出力列は event_date, dau、event_date 昇順で返してください。
| event_date | user_id | event_type |
|---|---|---|
| 2024-01-01 | U1 | view |
| 2024-01-01 | U2 | click |
| 2024-01-01 | U1 | purchase |
| 2024-01-02 | U2 | view |
| 2024-01-02 | U3 | view |
| 2024-01-02 | U4 | click |
| 2024-01-03 | U1 | view |
| 2024-01-03 | U3 | click |
| 2024-01-03 | U3 | view |
| event_date | dau |
|---|---|
| 2024-01-01 | 2 |
| 2024-01-02 | 3 |
| 2024-01-03 | 2 |
SELECT event_date, COUNT(DISTINCT user_id) AS dau -- 同日の重複ユーザーを除去してカウント FROM user_events GROUP BY event_date ORDER BY event_date; /* 実行順序(SQLの論理的な評価順): 1. FROM user_events → 行を読込 2. GROUP BY event_date → 日付でグループ化 3. COUNT(DISTINCT user_id) → 重複ユーザーを除きカウント 4. SELECT 2列 / ORDER BY event_date → 日付昇順で出力 */
LEGEND
① FROM user_events(9行)
FROM user_eventsuser_events テーブルを全件読み込みます。U1 は 01-01 に2回、U3 は 01-03 に2回登場しており、COUNT(*) では DAU を過大計上します。| event_date | user_id | event_type |
|---|---|---|
| 01-01 | U1 | view |
| 01-01 | U2 | click |
| 01-01 | U1 | purchase |
| 01-02 | U2 | view |
| 01-02 | U3 | view |
| 01-02 | U4 | click |
| 01-03 | U1 | view |
| 01-03 | U3 | click |
| 01-03 | U3 | view |
COUNT(*) は全行(NULL 含む)、COUNT(user_id) は NULL を除く行数(重複あり)、COUNT(DISTINCT user_id) は NULL を除くユニーク数です。DAU は必ず COUNT(DISTINCT user_id) を使います。dau が使えます。LAG(dau) OVER (ORDER BY event_date) を使うと、DAU 前日比・7日移動平均が得られます。Q2 でその LAG パターンを扱います。WHERE event_type = 'session_start' などでフィルタしてから COUNT(DISTINCT) を実行してください。MAU前月比成長率 — LAG() × CTE で月次KPIの成長率トレンドを計算する
MAU(Monthly Active Users)は「月に1回以上サービスを利用したアクティブユーザー数」を示す、プロダクトの規模と継続性を測る最重要KPIのひとつです。このMAUの月次推移を分析する際、LAG() ウィンドウ関数が活躍します。現在行の N 行前の値を取得できるため、サブクエリや自己結合なしで「前月比」などの成長率を簡単に計算できます。
LAG(mau) OVER (ORDER BY month) -- ORDER BY month の順で「1つ前の行」の mau を取得 -- 先頭行には前の行がないため NULL を返す (mau - prev_mau) * 100.0 / NULLIF(prev_mau, 0) -- NULLIF(prev_mau, 0): prev_mau=0 のとき NULL を返してゼロ除算を防ぐ -- * 100.0: 整数除算を避けて浮動小数点に変換
WITH lagged AS (...) でまず prev_mau 列を作り、外部クエリで prev_mau を参照するパターンが可読性・保守性ともに高いです。monthly_kpi テーブルから、各月の MAU・前月 MAU(prev_mau)・前月比成長率(mau_growth_pct)を計算してください。出力列は month, mau, prev_mau, mau_growth_pct、month 昇順で返してください。成長率は小数第2位まで丸めてください。
| month | mau |
|---|---|
| 2024-01 | 1200 |
| 2024-02 | 1380 |
| 2024-03 | 1450 |
| 2024-04 | 1390 |
| 2024-05 | 1560 |
| 2024-06 | 1820 |
| month | mau | prev_mau | mau_growth_pct |
|---|---|---|---|
| 2024-01 | 1200 | NULL | NULL |
| 2024-02 | 1380 | 1200 | 15.00 |
| 2024-03 | 1450 | 1380 | 5.07 |
| 2024-04 | 1390 | 1450 | -4.14 |
| 2024-05 | 1560 | 1390 | 12.23 |
| 2024-06 | 1820 | 1560 | 16.67 |
WITH lagged AS ( SELECT month, mau, LAG(mau) OVER (ORDER BY month) AS prev_mau -- 前月の MAU を取得 FROM monthly_kpi ) SELECT month, mau, prev_mau, ROUND( (mau - prev_mau) * 100.0 / NULLIF(prev_mau, 0), 2 -- 0 除算を回避(0なら NULL) ) AS mau_growth_pct -- 前月比成長率(%) FROM lagged ORDER BY month; /* 実行順序: 1. FROM monthly_kpi → 行を読込 2. LAG(mau) OVER (ORDER BY month) → 前月の mau を付与 3. WITH lagged → CTE 完成 4. (mau-prev_mau)*100.0/NULLIF(prev_mau,0) → 成長率を計算 5. ROUND(..., 2) / ORDER BY month → 丸めて月順に出力 */
LEGEND
① FROM monthly_kpi(6行)
FROM monthly_kpimonthly_kpi テーブルを全件読み込みます。2024-04 のみ前月より MAU が減少しており、この「マイナス成長」を LAG で正確に捕らえるのが目標です。| month | mau |
|---|---|
| 2024-01 | 1200 |
| 2024-02 | 1380 |
| 2024-03 | 1450 |
| 2024-04 | 1390 |
| 2024-05 | 1560 |
| 2024-06 | 1820 |
LAG(mau) は N=1・default=NULL のショートハンドです。LAG(mau, 12, 0) とすれば「12ヶ月前(YoY)の mau、なければ 0」が取得できます。前年同月比(YoY)は LEAD/LAG の offset を 12 に変えるだけで実装できます。NULLIF(prev_mau, 0) は prev_mau が 0 のとき NULL を返すため、後続の除算がエラーになりません。PostgreSQL では NULL / 数 = NULL (エラーなし)なので安全です。LAG(mau) OVER () は行順が未定義となり、DBエンジンが任意の順序で前行を選びます。LAG/LEAD は必ず OVER (ORDER BY 時系列列) と ORDER BY を明示してください。(1380-1200)/1200 はどちらも integer のため PostgreSQL では 0(整数除算)になります。* 100.0 または ::numeric キャストで必ず浮動小数点演算に変換してください。LAG(mau, 12) と offset を 12 に変えるだけです。コンバージョン率 — CASE WHEN × COUNT(DISTINCT) でファネル分析を実装する
コンバージョン率(CVR: Conversion Rate)は、サイト訪問や会員登録などのアクションを起こしたユーザーのうち、最終的な目標(購入や有料化など)を達成した割合を示す指標です。これを各ステップごとに分解して可視化するファネル分析は、ユーザーが「登録 → 体験 → 購入」のどの段階で離脱しているかを定量化する強力な手法です。CVR = ステップ通過者 / 直前ステップ通過者 を計算することで、改善すべきボトルネックを特定できます。
COUNT(DISTINCT CASE WHEN step = 'signup' THEN user_id END) -- step='signup' の行だけ user_id を返し、それ以外は NULL -- COUNT(DISTINCT) は NULL を無視するため、signup ユーザー数だけカウント ROUND(trial_users * 100.0 / NULLIF(signup_users, 0), 2)
CASE WHEN step='signup' THEN user_id END は ELSE 句がないため、条件不成立時は自動的に NULL を返します。funnel_events テーブルから、各ステップのユニークユーザー数と CVR を1行で出力してください。出力列は signup_users, trial_users, purchase_users, trial_cvr, purchase_cvr。
| user_id | step | event_date |
|---|---|---|
| U01 | signup | 2024-01-10 |
| U02 | signup | 2024-01-10 |
| U03 | signup | 2024-01-10 |
| U04 | signup | 2024-01-10 |
| U05 | signup | 2024-01-10 |
| U01 | trial_start | 2024-01-11 |
| U02 | trial_start | 2024-01-11 |
| U03 | trial_start | 2024-01-12 |
| U01 | purchase | 2024-01-15 |
| U02 | purchase | 2024-01-16 |
| signup_users | trial_users | purchase_users | trial_cvr | purchase_cvr |
|---|---|---|---|---|
| 5 | 3 | 2 | 60.00 | 66.67 |
SELECT COUNT(DISTINCT CASE WHEN step = 'signup' THEN user_id END) AS signup_users, -- 該当ステップのユニーク数(NULLは除外) COUNT(DISTINCT CASE WHEN step = 'trial_start' THEN user_id END) AS trial_users, COUNT(DISTINCT CASE WHEN step = 'purchase' THEN user_id END) AS purchase_users, ROUND( COUNT(DISTINCT CASE WHEN step = 'trial_start' THEN user_id END) * 100.0 / NULLIF(COUNT(DISTINCT CASE WHEN step = 'signup' THEN user_id END), 0), 2 ) AS trial_cvr, -- trial / signup の通過率(%) ROUND( COUNT(DISTINCT CASE WHEN step = 'purchase' THEN user_id END) * 100.0 / NULLIF(COUNT(DISTINCT CASE WHEN step = 'trial_start' THEN user_id END), 0), 2 ) AS purchase_cvr -- purchase / trial の通過率(%) FROM funnel_events; /* 実行順序: 1. FROM funnel_events → 行を読込 2. CASE WHEN step='x' THEN user_id END → 該当ステップのみ user_id を返す 3. COUNT(DISTINCT ...) → NULLをスキップしユニークカウント 4. CVR を計算 → 前段比で通過率を算出 */
LEGEND
① FROM funnel_events(10行)
FROM funnel_eventsfunnel_events テーブルを全件読み込みます。signup 5件、trial_start 3件、purchase 2件。U04・U05 はトライアルに進まず、U03 はトライアルで止まっています。| user_id | step | event_date |
|---|---|---|
| U01 | signup | 01-10 |
| U02 | signup | 01-10 |
| U03 | signup | 01-10 |
| U04 | signup | 01-10 |
| U05 | signup | 01-10 |
| U01 | trial_start | 01-11 |
| U02 | trial_start | 01-11 |
| U03 | trial_start | 01-12 |
| U01 | purchase | 01-15 |
| U02 | purchase | 01-16 |
CASE WHEN step='signup' THEN user_id END は ELSE 句がないため、条件不成立時に自動で NULL を返します。COUNT(DISTINCT) は NULL を無視するため、結果的に signup 行だけを数えるという動作になります。リテンション率 — CTE + LEFT JOIN でコホートの D1/D7 リテンションを計算する
リテンション率は「一度来たユーザーが再び戻ってくる割合」を示す KPI です。D1 リテンション = 初日の翌日も戻ったユーザーの割合、D7 = 7日後も戻った割合です。
WITH first_login AS ( SELECT user_id, MIN(login_date) AS cohort_date FROM user_logins GROUP BY user_id ) -- D1/D7判定 CASE WHEN l.login_date = f.cohort_date + 1 THEN l.user_id END CASE WHEN l.login_date = f.cohort_date + 7 THEN l.user_id END
user_logins テーブルから、2024-01-01 コホートの D1・D7 リテンション率を計算してください。出力列は cohort_date, cohort_size, d1_users, d1_retention, d7_users, d7_retention(retention は小数第2位まで丸め)。
| user_id | login_date |
|---|---|
| U1 | 2024-01-01 |
| U2 | 2024-01-01 |
| U3 | 2024-01-01 |
| U4 | 2024-01-01 |
| U5 | 2024-01-01 |
| U1 | 2024-01-02 |
| U3 | 2024-01-02 |
| U2 | 2024-01-08 |
| U3 | 2024-01-08 |
| U5 | 2024-01-08 |
| cohort_date | cohort_size | d1_users | d1_retention | d7_users | d7_retention |
|---|---|---|---|---|---|
| 2024-01-01 | 5 | 2 | 40.00 | 3 | 60.00 |
WITH first_login AS ( SELECT user_id, MIN(login_date) AS cohort_date -- 初回ログイン日=コホート FROM user_logins GROUP BY user_id ) SELECT f.cohort_date, COUNT(DISTINCT f.user_id) AS cohort_size, COUNT(DISTINCT CASE WHEN l.login_date = f.cohort_date + 1 THEN l.user_id END) AS d1_users, -- 翌日(+1)に再訪 ROUND( COUNT(DISTINCT CASE WHEN l.login_date = f.cohort_date + 1 THEN l.user_id END) * 100.0 / NULLIF(COUNT(DISTINCT f.user_id), 0), 2 ) AS d1_retention, -- D1 再訪率(%) COUNT(DISTINCT CASE WHEN l.login_date = f.cohort_date + 7 THEN l.user_id END) AS d7_users, ROUND( COUNT(DISTINCT CASE WHEN l.login_date = f.cohort_date + 7 THEN l.user_id END) * 100.0 / NULLIF(COUNT(DISTINCT f.user_id), 0), 2 ) AS d7_retention FROM first_login f LEFT JOIN user_logins l USING (user_id) -- コホートに全ログインを結合 GROUP BY f.cohort_date ORDER BY f.cohort_date; /* 実行順序: 1. CTE first_login → 各ユーザーの初回ログイン日を集計 2. LEFT JOIN user_logins → コホートに全ログインを結合 3. CASE WHEN login_date = cohort_date + N → D1/D7 の再訪を判定 4. リテンション率を計算 → 再訪数 / コホートサイズ */
LEGEND
① FROM user_logins(10行)
FROM user_loginsuser_logins テーブルを全件読み込みます。全ユーザーが 2024-01-01 に初回ログインしていることを確認します。| user_id | login_date |
|---|---|
| U1 | 01-01 |
| U2 | 01-01 |
| U3 | 01-01 |
| U4 | 01-01 |
| U5 | 01-01 |
| U1 | 01-02 |
| U3 | 01-02 |
| U2 | 01-08 |
| U3 | 01-08 |
| U5 | 01-08 |
date + integer:PostgreSQL では DATE型 + 1 で「翌日」が得られます。MySQL は DATE_ADD(login_date, INTERVAL 1 DAY)、SQL Server は DATEADD(day,1,login_date) と書き方が異なります。ARPU — LEFT JOIN + GROUP BY + COALESCE でプランセグメント別平均単価を計算する
ARPU(Average Revenue Per User)は「1ユーザーあたりの平均売上」を示す KPI で、プラン・チャネル・地域などのセグメント別に比較することでマネタイズ施策の優先度判断に使われます。
ARPU = SUM(revenue) / COUNT(DISTINCT user_id) -- 購入履歴がない users を除外しないため LEFT JOIN を使う FROM users u LEFT JOIN purchases p USING (user_id) -- 購入なしユーザーは SUM(amount)=NULL → COALESCE で0に変換 COALESCE(SUM(p.amount), 0) -- LEFT JOIN 後に user_id が重複するため DISTINCT 必須 COUNT(DISTINCT u.user_id)
users と purchases テーブルから、プラン別のユーザー数・合計売上・ARPU を計算してください。出力列は plan, user_count, total_revenue, arpu、arpu の降順で返してください。購入がないユーザーの売上は 0 として扱ってください。arpu は小数第2位まで丸めてください。
| user_id | plan |
|---|---|
| U1 | premium |
| U2 | premium |
| U3 | standard |
| U4 | standard |
| U5 | standard |
| U6 | free |
| U7 | free |
| U8 | free |
| user_id | amount |
|---|---|
| U1 | 5000 |
| U1 | 3000 |
| U2 | 4500 |
| U3 | 1500 |
| U4 | 2000 |
| U5 | 1800 |
| plan | user_count | total_revenue | arpu |
|---|---|---|---|
| premium | 2 | 12500 | 6250.00 |
| standard | 3 | 5300 | 1766.67 |
| free | 3 | 0 | 0.00 |
SELECT u.plan, COUNT(DISTINCT u.user_id) AS user_count, COALESCE(SUM(p.amount), 0) AS total_revenue, ROUND( COALESCE(SUM(p.amount), 0) * 1.0 / COUNT(DISTINCT u.user_id), -- DISTINCT 必須 2 ) AS arpu FROM users u LEFT JOIN purchases p USING (user_id) GROUP BY u.plan ORDER BY arpu DESC; /* 実行順序: 1. FROM users u → 行を読込 2. LEFT JOIN purchases p → 購入を結合(未購入はNULL) 3. GROUP BY u.plan → プランでグループ化 4. SUM(p.amount) → プラン別売上を集計 5. COALESCE(SUM(...), 0) → NULL を 0 に変換 6. COUNT(DISTINCT u.user_id) → プラン別ユーザー数 7. ARPU = revenue / count → 客単価を算出 8. ORDER BY arpu DESC → 降順で出力 */
LEGEND
① FROM users(8行)
FROM users uusers テーブルを全件読み込みます。プランは premium(2人)・standard(3人)・free(3人)の3種類です。| user_id | plan |
|---|---|
| U1 | premium |
| U2 | premium |
| U3 | standard |
| U4 | standard |
| U5 | standard |
| U6 | free |
| U7 | free |
| U8 | free |
COUNT(u.user_id)(DISTINCT なし)だと U1 が2回カウントされ分母が膨らみ ARPU が低く計算されます。複数行に展開される可能性がある JOIN の後は、常に COUNT(DISTINCT) を使ってください。SUM(p.amount)=NULL。COALESCE(SUM(amount), 0) は NULL を0 に変換し、ARPU = 0/3 = 0.00 という正しい計算を保証します。COALESCE(SUM(amount), 0) は integer 型のため、* 1.0 または ::numeric キャストで浮動小数点に変換しないと、1766.67 が 1766 に切り捨てられます。COUNT(*) は JOIN 後の全行数(9行)を返すため、分母が「ユーザー数(8)」ではなく「購入行数+NULLの合計(9)」になります。ARPU の定義「売上÷ユーザー数」を守るために COUNT(DISTINCT u.user_id) を使ってください。