관리자 화면에 활성 사용자 4,102명이 나오는데 마케팅 쪽에서 받은 자료에는 5,880명이었다. 같은 것을 세는 줄 알았다.
Table of contents
Open Table of contents
세는 조건이 달랐다
화면 쪽 쿼리를 봤다.
SELECT COUNT(*) FROM user WHERE del_yn = 'N' AND last_login >= DATE_SUB(NOW(), INTERVAL 30 DAY);
30일 안에 접속한 사람이다. 마케팅 쪽 쿼리를 받아 봤다.
SELECT COUNT(*) FROM user WHERE del_yn = 'N';
탈퇴 안 한 사람 전부다. 둘 다 활성 사용자라고 부르고 있었으니 조건이 다르면 숫자가 다른 것이 당연했다.
어느 것이 맞나가 아니었다
처음에는 어느 쪽이 맞는지 정하려 했는데 따져 보니 둘 다 필요한 숫자였다.
30일 접속자는 지금 얼마나 쓰는지를 말하고 탈퇴 안 한 사람은 계정이 몇 개인지를 말한다. 어느 하나를 틀렸다고 하면 그쪽 용도가 사라진다.
문제는 둘을 같은 이름으로 부른 것이었고 그래서 이름을 셋으로 나눴다.
가입 회원 del_yn = 'N'
활성 회원 del_yn = 'N' AND last_login >= 30일
휴면 회원 del_yn = 'N' AND last_login < 90일
정의를 문서로 적었다
숫자를 쓰는 화면과 보고서가 여럿이라 정의를 한 곳에 적었다.
## 회원 수 정의
가입 회원 del_yn='N'
탈퇴하지 않은 계정 전체
활성 회원 del_yn='N' AND last_login >= (오늘 - 30일)
최근 30일 내 접속. 30일은 (근거: 월 단위 보고 주기)
휴면 회원 del_yn='N' AND last_login < (오늘 - 90일)
90일 이상 미접속. 관련 규정에 따름
주의
- last_login 이 NULL 인 계정 = 가입 후 한 번도 접속 안 함. 활성에 안 들어감
- 탈퇴 회원은 어느 숫자에도 안 들어감
주의 항목이 중요했다. last_login 이 NULL 인 계정이 312개 있었는데 이것이 활성에 들어가는지가 사람마다 다르게 알고 있었다.
뷰와 시점 고정
정의를 적어도 각자 쿼리를 쓰면 또 어긋나서 뷰로 만들었다.
CREATE VIEW v_user_active AS
SELECT * FROM user
WHERE del_yn = 'N' AND last_login >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);
SELECT COUNT(*) FROM v_user_active;
정의가 바뀌면 CREATE OR REPLACE VIEW 한 번으로 끝난다. 뷰가 느릴 수 있어서 실행 계획을 확인했는데 last_login 에 인덱스가 있어서 괜찮았다.
같은 쿼리를 다른 시각에 돌리면 숫자가 다른 것도 문제였다. NOW() 가 계속 바뀌니 경계가 움직인다. 보고서용은 CURDATE() 로 그날 자정 기준을 쓰게 해서 같은 날 안에서는 결과가 같게 했다.
일별 수치는 매일 새벽에 계산해서 표에 저장했다.
CREATE TABLE user_stat_daily (
stat_date DATE NOT NULL,
total_cnt INT NOT NULL,
active_cnt INT NOT NULL,
dormant_cnt INT NOT NULL,
PRIMARY KEY (stat_date)
);
지난 수치를 다시 계산하면 값이 달라진다. 그때 활성이던 사람이 지금은 휴면일 수 있으니 user_stat_daily 에 저장해 둬야 그 시점 값이 남는다.
검증 — 다른 지표와 검산
회원 수를 정리하고 나서 다른 것도 봤다. 주문 건수는 취소 주문 포함인지가 갈렸고 매출은 배송비와 부가세 포함 여부가 갈렸으며 상품 수는 판매중지 포함인지가 갈렸다.
셋 다 화면마다 다르게 세고 있어서 같은 문서에 정의를 추가하니 지표가 열 개쯤 됐다.
정의를 정하고 나서 각 숫자가 맞물리는지도 봤다.
가입 회원 = 활성 + 비활성
비활성 = 휴면 + (30~90일 미접속)
세어 보니 12명이 안 맞았는데 last_login 이 미래 날짜인 계정이 있었고 시간대 처리 실수로 들어간 값이었다.
정의를 정하고 검산하니 그때까지 안 보이던 자료 오류가 드러난 것이다.
정리
- 같은 이름으로 다른 조건을 세면 숫자가 다르다
- 둘 다 각자의 용도에서는 맞는 숫자다
- 어느 것이 맞는지가 아니라 이름을 나누는 것이 답일 때가 많다
- 정의에
NULL인 행을 어떻게 다루는지까지 적는다 - 뷰로 만들면 정의가 한 곳에 모인다
NOW()는 계속 바뀌니 보고용은CURDATE()로 고정한다- 지난 수치는 저장해 둬야 하고 다시 계산하면 값이 달라진다
- 정의를 정한 뒤 숫자가 맞물리는지 검산한다
- 검산에서 자료 오류가 드러난다