blog post cover

쿼리 튜닝에서 가장 흔한 착각은 “쿼리가 느리다”는 말을 Cardinality / 실세계의 데이터 분포를 배제하고 쓰는 것이다.


생성 스크립트 구성

  • geo_data_import.py: 시도/시군구/읍면동 적재
  • gym_data_import.py: 헬스장 생성 + 제공 종목 매핑
  • membership_import.py: 헬스장별 회원권 생성
  • user_import.py: 사용자 대량 생성
  • trainer_import.py: 트레이너 생성 + 종목 매핑
  • gym_class_import.py: 수업 생성 + 담당 트레이너 매핑
  • user_membership_import.py: 사용자-회원권 매핑

실제 데이터 규모

실제 데이터 규모

user_memberships가 90만 행 이상이기 때문에, 인덱스가 없으면 풀스캔이 거의 확정이다.


분포 특성

날짜 분포

user_memberships는 최근 1년 안에서 시작일이 랜덤이고, 종료일은 시작일 + 기간이다. 중요한 사실 하나는 started_at이 항상 과거라는 점이다. 즉, 이 데이터셋에서는 started_at <= NOW()가 항상 참이다.

그래서 활성 조건은 사실상 아래 한 줄로 정리된다.

ended_at >= NOW()

이 특성이 인덱스 설계의 방향을 결정했다.

활성/비활성 비율

종료일 분포

헬스장/종목 분포

헬스장마다 제공 종목 수가 1~3개로 랜덤이다. 특정 종목에 편향이 생기며, 그 편향이 조인 결과를 흔든다.

type_count  gyms
1           98984
2           98314
3           98808

헬스장별 회원권 수는 현재 데이터에서 모두 동일하다.

membership_count  gyms
5                 296106

트레이너 분포

헬스장별로 0~20명 사이에서 트레이너가 배정된다. 일부 헬스장에는 트레이너가 없어 수업이 생성되지 않는다. 이 결손이 실제 서비스의 편차를 흉내 낸다.

트레이너 분포


권장 적재 순서

  1. geo_data_import.py
  2. exercise_types 시드 데이터 입력
  3. gym_data_import.py
  4. membership_import.py
  5. user_import.py
  6. trainer_import.py
  7. gym_class_import.py
  8. user_membership_import.py

검증 쿼리

SELECT
  (SELECT COUNT(*) FROM users) AS users,
  (SELECT COUNT(*) FROM gyms) AS gyms,
  (SELECT COUNT(*) FROM memberships) AS memberships,
  (SELECT COUNT(*) FROM user_memberships) AS user_memberships;

이제 이 분포를 바탕으로 EXPLAIN을 읽어보자.