모든 쿼리는 라이브 서버에서 실행해 검증했습니다(결과는 재현 가능한 시드 기준).
결과(일부)
함정:
COUNT(*)이 아니라COUNT(DISTINCT o.order_id).order_items와 조인하면 주문이 상품 수만큼 뻥튀기됩니다.
결과(일부)
결과(일부)
핵심: R 은 경과일이 작을수록 좋은 고객이므로
ORDER BY recency_days DESC로 NTILE 을 줘야 5점이 최근 고객에게 갑니다.
결과(일부)
WINDOW절로 같은 윈도우 정의를 재사용하면LAG(...) OVER (PARTITION BY ...)를 세 번 안 써도 됩니다.
결과
이 데이터는 고객 대부분이 다회 구매라 100%가 나옵니다.
SUM(불린식)은 참=1 합계로 카운트하는 관용구입니다.
결과
LEFT JOIN이 핵심: 주문이 아예 없는 고객(NULL)을 놓치지 않습니다.
결과(일부)
a.product_id < b.product_id가 두 가지를 동시에 해결: 자기 자신과의 쌍 제거, (A,B)와 (B,A) 중복 제거.
결과(일부)
m0 대비 m1/m2/m3 의 감소가 리텐션 곡선입니다. 2024-01 코호트는 22 → 17 로 4개월 뒤 77% 유지.
결과
결과
SUM(COUNT(*)) OVER ()— 집계 위에 윈도우를 얹어 전체 합을 각 행에 붙이는 관용구.
주의:
reviews,order_items를 동시에 조인하면 카티전 곱으로units_sold가 부풀 수 있습니다. 정밀 리포트라면 각각 서브쿼리로 집계 후 조인하세요(이 문제는 관찰용이라 허용).
결과(일부)
느린 버전 (EXPLAIN ANALYZE 실측)
DATE(logged_at) 로 컬럼을 감싸면 인덱스를 만들어도 못 씁니다(컬럼 값이 아니라 함수 결과로 비교).
튜닝: 인덱스 추가 + 컬럼을 건드리지 않는 반열린 범위로 재작성
128ms → 0.5ms. 결과 동일(둘 다 2,979행).
규칙: WHERE 절의 컬럼에는 함수를 씌우지 마라. 이걸 "sargable(Search ARGument able)" 이라고 합니다.
느린 버전
LIKE '%detail%' 는 앞에 %가 있어 B-트리 인덱스를 쓸 수 없습니다(인덱스는 접두사로 정렬돼 있음).
핵심 통찰: 여기서는 인덱스를 걸어도 크게 안 빨라집니다. detail 이 포함된 행이 전체의 25%(25만 행) 라 선택도가 낮기 때문입니다. 인덱스는 "소수만 고를 때" 이득입니다.
현실적 대안 3가지
/products/detail, /orders/detail 두 엔드포인트였을 것. 그러면:
Covering index range scan 으로 바뀝니다(단, 여전히 25%라 극적이진 않음 — 데이터 특성의 한계).LIKE 'detail%'(앞 % 없음)로 바꾸면 인덱스 사용 가능.교훈: "인덱스를 걸면 무조건 빨라진다"는 착각. 선택도가 낮으면(많이 매치되면) 옵티마이저는 인덱스를 무시하고 풀스캔을 고릅니다. 그게 실제로 더 빠르니까요.
느린 버전
SELECT (서브쿼리) 가 바깥 행마다 반복 실행됩니다(loops=30). 데이터가 커지면 치명적입니다.
튜닝: 한 번의 조인 + 그룹핑으로
서브쿼리 반복(loops=30)이 사라지고 한 번의 조인으로 끝납니다. LEFT JOIN 이라 주문 0건 고객도 order_count=0 으로 보존됩니다.
조건
status <> 'CANCELLED'를WHERE가 아니라ON절에 둔 이유:WHERE에 두면 주문 없는 고객(NULL)이 탈락해 LEFT JOIN 이 무의미해집니다.
라이브 서버에서 생성·제약 위반까지 검증한 스키마입니다.
| 결정 | 근거 |
|---|---|
정액/정률을 한 테이블 + discount_type ENUM | 두 타입의 공통 속성(유효기간/최소금액/한도)이 90% 동일. 테이블을 나누면 조회 시마다 UNION 필요. CHECK 로 타입별 규칙(정률 ≤ 100, 상한은 정률만)을 강제하면 한 테이블로 충분 |
금액은 DECIMAL(10,2) | 돈은 절대 FLOAT 금지(반올림 오차). DECIMAL 은 정확한 십진 연산 |
UNIQUE(coupon_id, customer_id) | 요구사항 4(중복 발급 금지)를 DB 레벨에서 보장. 앱 로직에만 맡기면 동시 요청에 뚫린다 |
UNIQUE(issue_id) on redemption | 요구사항 3(발급당 1회 사용)을 유니크 제약으로 보장. "이미 썼는지" 조회 후 INSERT 하는 앱 로직은 경합에 취약 |
| 발급/사용을 별도 테이블로 분리 | 발급받았으나 미사용 상태를 표현해야 함(status='ISSUED'). 사용 시점/할인액/주문 연결은 별도 이벤트라 정규화 |
issued_count + CHECK(issued_count <= total_quota) | 총 발급 한도. 단, CHECK 는 행 단위라 동시성까지는 못 막음 → 아래 참조 |
UNIQUE(coupon_id, customer_id) 가 최종 방어선. 두 요청이 동시에 와도 하나는 ERROR 1062 로 실패한다. 앱은 이 에러를 잡아 "이미 발급됨"으로 처리한다.issued_count 를 UPDATE ... SET issued_count = issued_count + 1 WHERE coupon_id=? AND (total_quota IS NULL OR issued_count < total_quota) 로 원자적 조건부 증가하고, ROW_COUNT()=0 이면 매진 처리. (또는 트랜잭션 + SELECT ... FOR UPDATE 로 행 잠금 → Step 19)UNIQUE(issue_id) on redemption. 조회-후-삽입이 아니라 삽입을 시도하고 유니크 위반이면 실패시키는 것이 경합에 안전하다.