Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
오늘의 문제 추출 쿼리 개선 #47
Problem
사용자가 테스트를 시작할 때, 문제를 추출하는 과정에는 몇 가지 중요한 조건이 필요했다.
대표적으로 ‘오늘의 문제’ 추출의 경우 5문제를 매일 추출해야 한다. 또한, 1번 문제의 경우 난이도 범위가 Bronze 5 ~ Bronze 1 이며, 2번 문제의 경우 난이도 범위가 Silver 5 ~ Silver 4 이며, 3번 문제의 경우 Silver 3 ~ Silver 1, 4번 문제의 경우 Gold 5 ~ Gold 4, 5번 문제의 경우 Gold 3 ~ Gold 1을 추출한다.
추후, 난이도 범위가 변경될 것을 고려하여 이를 추출하는 동적 쿼리를 다음과 같이 구성하였다.
현재 Problem (문제) 테이블과 DailyDefenseProblem (출제된 오늘의 문제) 테이블은 일대다 매핑이 되어있다.
또한, Where 문으로 Problem 테이블 내 활성화된 문제이면서, 문제 난이도 (Tier) 범위에 포함되면서, 정답자 수 범위에 포함되며, 오늘의 문제에 출제되지 않은 (NOT IN) 문제를 쿼리로 호출한 후 ORDER BY FUNCTION(’RAND’)를 통해 무작위로 한 문제를 추출하는 쿼리를 구성하였다.
해당 쿼리를 테스트 하기 위해 Problem 30000개를 구성하였고, 오늘의 문제로 출제된 문제는 15000개로 가정하였다. 각 문제의 난이도와 정답자 수는 무작위로 구성하였고 문제 1개를 추출하는데 쿼리 호출 시간을 알아본 결과 79ms가 발생하였다.
만약, 오늘의 문제나 코딩테스트 문제를 위해 5문제를 추출한다면 대략 395ms가 호출되는 셈이다.
이는 매우 비효율적이었다.
How To
1) NOT IN을 JOIN으로 변경
서브쿼리 대신 JOIN 쿼리를 이용하고 오늘의 문제가 NULL인 Problem 들을 가져오는 쿼리로 변경하였다.
NOT IN 접근법은 내부적으로 전체 결과를 계산한 후에 외부 쿼리와 비교하는 방식이다.
기본적으로 해당 서비스에서는 1만개 이상의 서브쿼리 데이터가 쌓일 수 있으므로 성능 저하의 주된 원인이 될수 있다.
반면에 JOIN은 NOT IN 쿼리에 비해 연관관계를 기반으로 빠르게 관련 데이터를 찾아낼 수 있다.
2) WHERE 절 최적화를 위한 인덱싱 적용
WHERE 절을 수행할 때 푼 사람 수, 난이도 범위, 활성화 여부를 모두 조건이 걸려있어 성능적으로 좋지 못하였다. 성능 문제를 해결하기 위해 데이터베이스 인덱싱 전략을 세밀하게 조정하였다. 주요 쿼리에서 '푼 사람 수', '난이도 범위', 그리고 '활성화 여부'를 기준으로 데이터를 필터링해야 했는데, 이 조건들의 카디널리티를 분석한 결과, 다음과 같은 인덱싱 전략을 수립하게 되었다.
따라서, '푼 사람 수', '난이도 범위', 그리고 '활성화 여부' 순서로 복합 인덱스를 구성하였다.
3) 애플리케이션 레벨에서 랜덤으로 문제 추출
쿼리 내에서 ORDER BY FUNCTION('RAND') 를 사용할 때, 결과 데이터의 크기가 클 경우 무작위로 데이터를 섞는 과정에서 심각한 성능 저하가 발생할 수 있다. 이를 해결하기 위해 페이징을 통해 50개의 데이터를 가져온 후 애플리케이션 레벨에서 랜덤 변수를 적용하여 임의의 데이터를 선택하였다. 이를 통해 데이터베이스의 성능 저하를 방지하였다.
Result
기존 쿼리의 실행 시간이 79ms에서 18ms로 줄어들어 성능이 크게 개선되었다.
만약, 5개의 문제를 추출한다면 총 실행 시간이 395ms에서 90ms로 감소한 것이라고 볼 수 있다.