본문 바로가기
SQL/문제풀이

실전 문제 풀이 (71) - 신규 유저 비중 변화 / 월별 이탈률 / 사용자별 최대 연속 구매 월 수

by 바른곰의 SQL천국 2026. 1. 26.

문제 1 — 월별 신규 유저 비중 및 전월 대비 변화

DROP TABLE IF EXISTS orders_m2;
CREATE TABLE orders_m2 (
  order_id INT PRIMARY KEY,
  user_id INT,
  order_date DATE,
  amount DECIMAL(10,2)
);

INSERT INTO orders_m2 VALUES
(1, 1, '2025-06-02', 120),
(2, 2, '2025-06-10', 200),
(3, 1, '2025-06-18',  80),
(4, 3, '2025-07-01', 150),
(5, 2, '2025-07-05', 100),
(6, 4, '2025-07-15',  90),
(7, 1, '2025-07-20',  60),
(8, 5, '2025-08-03', 300),
(9, 3, '2025-08-10', 100),
(10,6, '2025-08-18',  70);

목표

월별로

  • 전체 활성 사용자 수
  • 해당 월에 처음으로 주문한 신규 사용자 수
  • 신규 사용자 비중
  • 신규 사용자 비중의 전월 대비 증감률(%)

을 계산하라. 첫 달의 증감률은 NULL로 표시한다.

출력(요구사항) — 컬럼 설명

  • month : YYYY-MM 형식의 월
  • active_users : 해당 월에 주문한 서로 다른 사용자 수
  • new_users : 해당 월에 첫 주문을 한 사용자 수
  • new_user_ratio : new_users / active_users (소수 둘째 자리)
  • new_user_ratio_mom_pct : 전월 대비 신규 사용자 비중 증감률(%) (소수 둘째 자리)
WITH month_table AS (
  SELECT DISTINCT
    user_id,
    DATE_FORMAT(order_date, '%Y-%m') AS month
  FROM orders_m2
  ),
  
active_table AS (
  SELECT
    month,
    COUNT(*) AS active_users
  FROM month_table
  GROUP BY month
  ),

new_table AS (
  SELECT
    first_order_month,
    COUNT(*) AS new_users
  FROM (
    SELECT
      user_id,
      MIN(month) AS first_order_month
    FROM month_table
    GROUP BY user_id
    ) AS first_order_table
  GROUP BY first_order_month
  ),

join_table AS (
  SELECT
    a.month,
    a.active_users,
    n.new_users,
    ROUND(n.new_users / a.active_users, 2) AS new_user_ratio
  FROM active_table a
  JOIN new_table n
    ON a.month = n.first_order_month
  )
  
SELECT
  month,
  active_users,
  new_users,
  new_user_ratio,
  ROUND((new_user_ratio - LAG(new_user_ratio) OVER (ORDER BY month)) / LAG(new_user_ratio) OVER (ORDER BY month) * 100, 2) AS new_user_ratio_mom_pct
FROM join_table

 

 

 

문제 2 — 월별 이탈 유저 수 및 이탈률

DROP TABLE IF EXISTS orders_m5;
CREATE TABLE orders_m5 (
  order_id INT PRIMARY KEY,
  user_id INT,
  order_date DATE
);

INSERT INTO orders_m5 VALUES
(1, 1, '2025-06-01'),
(2, 2, '2025-06-05'),
(3, 3, '2025-06-20'),
(4, 1, '2025-07-03'),
(5, 2, '2025-07-10'),
(6, 4, '2025-07-15'),
(7, 1, '2025-08-01'),
(8, 5, '2025-08-05');

목표

월별로

  • 전월에 활동했지만 해당 월에는 주문하지 않은 사용자 수(이탈 유저)
  • 전월 활성 사용자 대비 이탈률

을 계산하라.
첫 달은 전월 정보가 없으므로 이탈 유저 수와 이탈률은 NULL로 표시한다.

출력(요구사항) — 컬럼 설명

  • month : YYYY-MM
  • prev_active_users : 전월에 주문한 서로 다른 사용자 수
  • churn_users : 전월에는 주문했지만 해당 월에는 주문하지 않은 사용자 수
  • churn_rate_pct : churn_users / prev_active_users × 100 (소수 둘째 자리)
WITH month_table AS (
  SELECT DISTINCT
    user_id,
    DATE_FORMAT(order_date, '%Y-%m-01') AS month
  FROM orders_m5
  ),
  
month_list_table AS (
  SELECT DISTINCT
    DATE_FORMAT(order_date, '%Y-%m-01') AS month
  FROM orders_m5
  )

SELECT
  DATE_FORMAT(mlt.month, '%Y-%m') AS month,
  COUNT(DISTINCT mt.user_id) AS prev_active_users,
  SUM(IF(mt2.user_id IS NULL, 1, 0)) AS churn_users,
  ROUND(SUM(IF(mt2.user_id IS NULL, 1, 0)) / COUNT(DISTINCT mt.user_id) * 100, 2) AS churn_rate_pct
FROM month_list_table mlt
LEFT JOIN month_table mt
  ON mt.month = DATE_SUB(mlt.month, INTERVAL 1 MONTH)
LEFT JOIN month_table mt2
  ON mt.user_id = mt2.user_id
  AND mlt.month = mt2.month
GROUP BY DATE_FORMAT(mlt.month, '%Y-%m')

 

 

 

문제 3 — 사용자별 최대 연속 구매 월 수

DROP TABLE IF EXISTS orders_m6;
CREATE TABLE orders_m6 (
  order_id INT PRIMARY KEY,
  user_id INT,
  order_date DATE
);

INSERT INTO orders_m6 VALUES
(1, 1, '2025-06-01'),
(2, 1, '2025-07-01'),
(3, 1, '2025-08-01'),
(4, 1, '2025-10-01'),
(5, 2, '2025-06-10'),
(6, 2, '2025-08-10'),
(7, 3, '2025-07-05'),
(8, 3, '2025-08-05'),
(9, 3, '2025-09-05'),
(10,3, '2025-10-05');

목표

각 사용자별로
연속해서 주문이 발생한 최대 개월 수를 계산하라.

  • 같은 달에 여러 번 주문해도 1개월로 계산한다.
  • 월이 끊기면 연속도 종료된다.

출력(요구사항) — 컬럼 설명

  • user_id : 사용자 ID
  • max_consecutive_months : 해당 사용자의 최대 연속 구매 개월 수
WITH month_table AS (
  SELECT DISTINCT
    user_id,
    DATE_FORMAT(order_date, '%Y-%m-01') AS month
  FROM orders_m6
  ),

flag_table AS (
  SELECT
    user_id,
    month,
    DATE_SUB(month, INTERVAL ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY month
      ) - 1 MONTH) AS flag
  FROM month_table
  ),

consecutive_table AS (
  SELECT
    user_id,
    flag,
    COUNT(*) AS consecutive_months
  FROM flag_table
  GROUP BY user_id, flag
  )
  
SELECT
  user_id,
  MAX(consecutive_months) AS max_consecutive_months
FROM consecutive_table
GROUP BY user_id

 

 


 

1번 문제 맨 처음에 monthlist 테이블과 month 테이블을 결합할 때 그냥 JOIN을 사용했는데 이렇게 하면 month테이블에 아무것도 없다면 해당 달이 아예 사라지기 때문에 LEFT JOIN을 사용해야 한다고 한다.