-- users: 사용자 기본 정보
CREATE TABLE users (
user_id INT PRIMARY KEY,
signup_date DATE,
user_type VARCHAR(10), -- free / paid / guest
country VARCHAR(2)
);
INSERT INTO users VALUES
(1,'2025-11-25','free','KR'),
(2,'2025-11-26','paid','KR'),
(3,'2025-11-27','free','US'),
(4,'2025-11-27','guest','KR'),
(5,'2025-11-28','free','KR'),
(6,'2025-12-01','paid','US'),
(7,'2025-12-02','free','KR'),
(8,'2025-12-05','guest','KR'),
(9,'2025-12-07','paid','KR'),
(10,'2025-12-10','free','US');
-- events: 사용자 행동 이벤트 (view, signup, purchase 등)
CREATE TABLE events (
event_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
event_type VARCHAR(20),
event_date DATE
);
-- (샘플)
INSERT INTO events (user_id, event_type, event_date) VALUES
(1,'view','2025-12-01'),
(1,'signup','2025-12-02'),
(1,'purchase','2025-12-05'),
(2,'view','2025-12-01'),
(2,'purchase','2025-12-03'),
(3,'view','2025-12-02'),
(3,'signup','2025-12-03'),
(3,'view','2025-12-10'),
(4,'view','2025-12-02'),
(5,'view','2025-12-03'),
(5,'signup','2025-12-04'),
(6,'view','2025-12-05'),
(6,'signup','2025-12-06'),
(6,'purchase','2025-12-10'),
(7,'view','2025-12-02'),
(7,'purchase','2025-12-15'),
(8,'view','2025-12-06'),
(9,'view','2025-12-07'),
(9,'signup','2025-12-08'),
(10,'view','2025-12-10'),
(10,'signup','2025-12-11');
-- experiments: A/B 실험 할당 및 전환 플래그(문제7 전용)
CREATE TABLE experiments (
user_id INT PRIMARY KEY,
variant CHAR(1), -- 'A' or 'B'
assigned_date DATE,
converted INT, -- 0/1
conversion_date DATE
);
INSERT INTO experiments VALUES
(1,'A','2025-12-01',1,'2025-12-05'),
(2,'A','2025-12-01',1,'2025-12-03'),
(3,'A','2025-12-01',0,NULL),
(4,'B','2025-12-01',0,NULL),
(5,'B','2025-12-01',1,'2025-12-04'),
(6,'B','2025-12-01',1,'2025-12-10'),
(7,'A','2025-12-01',0,NULL),
(8,'B','2025-12-01',0,NULL),
(9,'A','2025-12-01',1,'2025-12-08'),
(10,'B','2025-12-01',0,NULL);
질문: users 테이블을 이용해 국가별 사용자 수를 구하라.
기대 출력 (정답 예시):
| country | user_count |
|---|---|
| KR | 7 |
| US | 3 |
힌트: GROUP BY country 사용. NULL 없음 가정.
채점 포인트: 정확한 집계, 정렬 요구가 없으면 값만 맞으면 정답.
select country , count(distinct user_id) as user_count
from users
group by country;
질문: users와 events를 사용해 구매 이벤트(purchase) 를 한 유저들만 대상으로 first_purchase_date - signup_date의 평균(일 단위)을 구하라. (구매가 없는 유저는 제외)
기대 출력(스칼라): avg_days_to_first_purchase = 9.75 (소수 둘째 자리 반올림 허용)
힌트: 먼저 각 유저의 첫 구매일을 구한 뒤 signup과 차이를 계산. DATEDIFF 또는 DB별 날짜 함수 주의.
채점 포인트: null 제외 처리, 날짜 차이 계산 단위(일) 확인.
문제 2 에서
select *
from users u join events e
on u.user_id = e.user_id
where exists (
select 1
from events
where event_type = "purchase"
)
구매 이벤트(purchase) 를 한 유저들만 대상으로 만 조회하려고 했었는데, 이거 exists 사용을 잘못해서 모든 결과가 잘 나오고 있었다.
서브쿼리가 바깥 쿼리와 아무 관계가 없어서 일어난 사단인데,
내가 적은 exists 조건을 봐 보면
그 결과 모든 row가 필터 없이 통과하게 되는 것.
그래서 구매한 적 없는 유저(3,4,5,8,10번)까지 전부 출력된 것이다..
내가 원하는 조건대로 exists를 쓰려면 이렇게 썼어야 했다.
select *
from users u join events e
on u.user_id = e.user_id
where exists (
select 1
from events p
where p.user_id = u.user_id
and p.event_type = 'purchase'
)
이벤트 타입이 'purchase' 인 이벤트만 존재하는 테이블 events p 에서
존재하는 p.user_id 들과 같은 id를 가진 유저들을 추리고싶은 거니까
where p.user_id = u.user_id 조건을 써야한다
하지만 해당 문제는
조금 어렵게 생각하고 있었나 보다. 사실 구매를 했는지 여부만 보면 가입일은 아이디별로 정해져 있기 때문에 event_type 만 where절로 필터링 해서 나온 조건들로만 집계했다
with first_purchase as (
select u.user_id, u.signup_date, min(e.event_date) first_purchase
from users u join events e
on u.user_id = e.user_id
where e.event_type = 'purchase'
group by 1
)
select avg(DATEDIFF(first_purchase ,signup_date)) avg_days_to_first_purchase
from first_purchase
질문: events에서 이벤트 수가 많은 상위 3명의 user_id와 event_count를 내림차순으로 출력하라. 동률 처리: 동률이면 user_id 오름차순.
기대 출력:
| user_id | event_count |
|---|---|
| 1 | 3 |
| 3 | 3 |
| 6 | 3 |
힌트: GROUP BY user_id, ORDER BY event_count DESC, user_id ASC, LIMIT 3.
채점 포인트: 동률 정렬 규칙 준수, 정확한 집계.
select u.user_id, count(e.event_id) as event_count
from events e join users u
on e.user_id = u.user_id
group by u.user_id
order by 2 desc, 1
limit 3
질문: 각 signup_date 별 코호트(같은 signup_date로 묶음)에 대해 cohort_size(그 날 가입한 고유 유저 수)와 가입일 기준 +7일에 이벤트(어떤 이벤트든) 가 발생한 고유 유저 수(retained_count) 및 7일 리텐션 비율(retention_7d)을 구하라. (출력 컬럼: signup_date, cohort_size, retained_count, retention_7d)
기대 출력 (부분):
| signup_date | cohort_size | retained_count | retention_7d |
|---|---|---|---|
| 2025-11-25 | 1 | 1 | 1.0 |
| 2025-11-26 | 1 | 1 | 1.0 |
| 2025-11-27 | 2 | 0 | 0.0 |
| 2025-11-28 | 1 | 0 | 0.0 |
| 2025-12-01 | 1 | 0 | 0.0 |
| ... | ... | ... | ... |
(샘플 데이터 전체 코호트에 대한 값 포함)
힌트 / 접근:
signup_date + INTERVAL 7 DAY를 계산.event_date = signup_date + 7인 경우를 카운트(고유 유저 수).COUNT(DISTINCT user_id)로 집계.DISTINCT 사용으로 중복 이벤트 처리, 날짜 더하기 연산 정확성, 코호트가 없는 경우 0 처리.# retention d+7
# 가입일 +7일에 다시행동한 코호트 유저 수 / 가입일 코호트 유저 수
SELECT
u.signup_date,
count(distinct u.user_id) as cohort_size,
count(distinct case
when e.event_date = DATE_ADD(u.signup_Date, INTERVAL 7 DAY)
then u.user_id
end) as retained_count,
count(distinct case
when e.event_date = DATE_ADD(u.signup_date, INTERVAL 7 day)
THEN u.user_id
end
) * 1.0 / count(distinct u.user_id) as retention_7d
from users u
left join events e
on u.user_id = e.user_id
group by u.signup_date
order by u.signup_date
LEFT JOIN을 써야 하는 경우
INNER JOIN을 써도 되는 경우
질문: 2025-12월(2025-12-01 ~ 2025-12-31) 동안 적어도 한 번이라도 이벤트를 발생시킨 유저의 고유 수를 user_type별로 구하라. (출력: user_type, mau)
기대 출력:
| user_type | mau |
|---|---|
| free | 5 |
| guest | 2 |
| paid | 3 |
힌트: events.event_date를 월 단위로 필터한 다음 DISTINCT user_id와 JOIN users로 user_type을 가져온다.
채점 포인트: 월 필터(YYYY-MM), user_type 매칭, 중복 user_id 제거.
SELECT u.user_type, count(distinct u.user_id)
from users u join events e
on u.user_id = e.user_id
and e.event_date >= '2025-12-01'
and e.event_date <= '2025-12-31'
group by u.user_type
질문: 2025-12-10을 기준으로 직전 7일(12-04 ~ 12-10, inclusive) 동안 이벤트를 발생시킨 고유 유저 수를 user_type별로 구하라. (출력: user_type, active_7d)
기대 출력:
| user_type | active_7d |
|---|---|
| free | 4 |
| guest | 1 |
| paid | 2 |
힌트: 날짜 범위 필터 후 DISTINCT user_id로 집계. 실무에선 윈도우 함수나 날짜 시계열을 잘 다룸.
채점 포인트: 날짜 범위 정확성(+inclusive), user_type 매칭, 중복 제거.
select u.user_type, count(distinct u.user_id) as active_7d
from users u join events e
on u.user_id = e.user_id
and e.event_date <= '2025-12-10'
and e.event_date >= '2025-12-04'
group by user_type
좋습니다 — 요청하신 대로 원래 문제를 수정한 최종 버전(시험지에 바로 붙여넣을 수 있는 형태)을 깔끔하게 정리해 드립니다.
문제 본문, 예시 데이터(스키마 + 샘플), 기대 출력(샘플), 풀기 힌트, 모범답안(두 가지: z-score만 SQL / z + p-value 근사식(SQL)), 채점 기준을 모두 포함합니다.
시험지/문제은행에 바로 넣으시면 됩니다.
experiments 테이블을 이용해 각 버전(variant A/B)에 대해 users, conversions, conversion_rate를 구하고, A vs B의 차이에 대한 z-score와 two-sided p-value(근사 허용)를 SQL로 계산하라.
주의: 일부 DB는 표준정규분포 CDF(Φ)나 ERF 함수를 제공하지 않으므로 p-value는 SQL 내 근사(Abramowitz–Stegun 등)로 계산하거나, SQL로 z-score까지만 제출하고 p-value는 Python/R로 계산해 함께 제출해도 채점 대상이 된다.
출력 요구
variant, users, conversions, conv_rate (variant별 summary table)z_score (필수)p_value (two-sided, 근사 허용) — SQL 근사 제출 시 근사 방법 명시샘플 기대값 (우리 샘플데이터 기준)
힌트(중요)
CREATE TABLE experiments (
user_id INT PRIMARY KEY,
variant CHAR(1), -- 'A' or 'B'
assigned_date DATE,
converted INT, -- 0 or 1
conversion_date DATE
);
INSERT INTO experiments VALUES
(1,'A','2025-12-01',1,'2025-12-05'),
(2,'A','2025-12-01',1,'2025-12-03'),
(3,'A','2025-12-01',0,NULL),
(4,'B','2025-12-01',0,NULL),
(5,'B','2025-12-01',1,'2025-12-04'),
(6,'B','2025-12-01',1,'2025-12-10'),
(7,'A','2025-12-01',0,NULL),
(8,'B','2025-12-01',0,NULL),
(9,'A','2025-12-01',1,'2025-12-08'),
(10,'B','2025-12-01',0,NULL);
(위 샘플에서 A: users=5, conversions=3, B: users=5, conversions=2)
users, conversions, conv_rate 정확성 (중복·NULL 처리) — 40%with ab_result as (
select variant,
count(distinct u.user_id) users ,
count(if(e.converted = 1, 1, null)) conversions,
count(if(e.converted = 1, 1, null) ) /count(distinct u.user_id) conv_rate
from experiments e join users u
on e.user_id = u.user_id
group by variant
),
pooled_calc as (
select sum(users) sum_n, sum(conversions) sum_x, sum(conversions)/sum(users) pooled_p
from ab_result
),
se_calc as (
select
SQRT(
( select pooled_p from pooled_calc ) *
( 1-(select pooled_p from pooled_calc)) *
( 1/(select max(users) from ab_result where variant ='A' ) +
( 1/(select max(users) from ab_result where variant ='B' ))
)
) as se_val
),
z_calc as (
select
((select conv_rate from ab_result where variant = 'B')
- (select conv_rate from ab_result where variant = 'A'))
/ se_val as z_score
from se_calc
)
select z_score
from z_calc
WITH conversions AS (
SELECT
variant,
COUNT(DISTINCT user_id) AS users,
COUNT(IF(converted = 1, 1, NULL)) AS conversions,
COUNT(IF(converted = 1, 1, NULL)) * 1.0 / COUNT(DISTINCT user_id) AS conv_rate
FROM experiments
GROUP BY variant
),
ab AS (
SELECT
a.users AS n_a,
a.conversions AS x_a,
a.conv_rate AS p_a,
b.users AS n_b,
b.conversions AS x_b,
b.conv_rate AS p_b
FROM conversions a
JOIN conversions b
ON a.variant = 'A'
AND b.variant = 'B'
),
stats AS (
SELECT *,
(x_a + x_b) * 1.0 / NULLIF(n_a + n_b, 0) AS pooled_p
FROM ab
)
SELECT
n_a, x_a, p_a,
n_b, x_b, p_b,
pooled_p,
-- 표준오차 (SE)
SQRT(
pooled_p * (1 - pooled_p)
* (1.0 / NULLIF(n_a, 0) + 1.0 / NULLIF(n_b, 0))
) AS se,
-- z-score
(p_b - p_a) /
NULLIF(
SQRT(
pooled_p * (1 - pooled_p)
* (1.0 / NULLIF(n_a, 0) + 1.0 / NULLIF(n_b, 0))
), 0
) AS z_score
FROM stats;
출력에 variant별 summary(table)와 별도 z_score row(또는 같은 쿼리의 마지막 컬럼) 제출 권장.
NULLIF로 0나누기 방지.
이 쿼리는 z와 p_value_approx(two-sided)를 반환합니다.
시험 제출 시 “Abramowitz–Stegun 근사 사용”이라고 주석/설명 추가하면 좋음.
pooled_p, SE, z — 왜 쓰는가 (요약)