100만 유저 가정 리그 랭킹 조회 병목 해결
1. 상황 및 문제
유저 100만 명 규모를 가정한 리그 랭킹 조회에서 응답 속도가 크게 저하되었습니다. 랭킹은 서비스의 첫 화면에 노출되는 핵심 조회 경로였습니다.
2. 원인 분석
실행 계획을 확인한 결과 전체 테이블 스캔과 대규모 조인이 발생하고 있었고, 정렬이 메모리를 넘어 디스크 정렬(external sort)로 떨어지고 있었습니다.
3. 해결 단계
Step 1. 복합·커버링 인덱스로 Index Scan 유도
랭킹 조회 조건과 정렬 키에 맞춘 복합 인덱스를 설계하고, 조회 컬럼을 인덱스에 포함시켜(커버링) 테이블 접근 자체를 줄였습니다. 실행 계획이 Seq Scan에서 Index Scan으로 바뀐 것을 확인했습니다.
Step 2. CTE MATERIALIZED로 상위 10건만 추출 후 조인
랭킹 페이지에 실제로 필요한 것은 상위 10건뿐이라는 점에 착안해, CTE에 MATERIALIZED 힌트를 주어 10건을 먼저 확정한 뒤 나머지 정보를 조인했습니다. 조인 대상이 10행으로 줄면서 옵티마이저가 Nested Loop Join을 선택하게 되었습니다.
WITH top_ranks AS MATERIALIZED ( SELECT user_id, score FROM league_ranking WHERE league_id = :leagueId ORDER BY score DESC LIMIT 10 ) SELECT r.user_id, u.nickname, r.score FROM top_ranks r JOIN users u ON u.id = r.user_id;상위 10건을 먼저 확정한 뒤 조인 — Nested Loop Join 유도
4. 검증 및 결과
전체 테이블 스캔과 디스크 정렬이 사라지고, 소수 행에 대한 Nested Loop Join으로 실행 계획이 안정화되어 랭킹 조회 응답이 크게 개선되었습니다.