Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

오늘의 문제 추출 쿼리 개선 #50

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open

오늘의 문제 추출 쿼리 개선 #50

wants to merge 7 commits into from

Conversation

aj4941
Copy link
Member

@aj4941 aj4941 commented Aug 21, 2024

Problem

사용자가 테스트를 시작할 때, 문제를 추출하는 과정에는 몇 가지 중요한 조건이 필요했다.

  1. 백준 사이트에서 설정된 특정 정답자 수 범위에 속하는 문제를 추출해야 한다.
  2. 백준 사이트 내 난이도 범위에 맞는 문제를 추출해야 한다.
  3. '코딩테스트’ 형식의 경우 사용자가 풀지 않은 문제, ‘오늘의 문제’ 형식의 경우 과거에 출제되지 않는 문제를 추출해야 한다.
  4. 정렬된 형태가 아닌 무작위로 문제를 추출해야 한다.

​대표적으로 ‘오늘의 문제’ 추출의 경우 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을 추출한다.

​추후, 난이도 범위가 변경될 것을 고려하여 이를 추출하는 동적 쿼리를 다음과 같이 구성하였다.

image

현재 Problem (문제) 테이블과 DailyDefenseProblem (출제된 오늘의 문제) 테이블은 일대다 매핑이 되어있다.

image

또한, Where 문으로 Problem 테이블 내 활성화된 문제이면서, 문제 난이도 (Tier) 범위에 포함되면서, 정답자 수 범위에 포함되며, 오늘의 문제에 출제되지 않은 (NOT IN) 문제를 쿼리로 호출한 후 ORDER BY FUNCTION(’RAND’)를 통해 무작위로 한 문제를 추출하는 쿼리를 구성하였다.

image

해당 쿼리를 테스트 하기 위해 Problem 30000개를 구성하였고, 오늘의 문제로 출제된 문제는 15000개로 가정하였다. 각 문제의 난이도와 정답자 수는 무작위로 구성하였고 문제 1개를 추출하는데 쿼리 호출 시간을 알아본 결과 79ms가 발생하였다.
만약, 오늘의 문제나 코딩테스트 문제를 위해 5문제를 추출한다면 대략 395ms가 호출되는 셈이다.
이는 매우 비효율적이었다.

How To

1) NOT IN을 JOIN으로 변경
서브쿼리 대신 JOIN 쿼리를 이용하고 오늘의 문제가 NULL인 Problem 들을 가져오는 쿼리로 변경하였다.
NOT IN 접근법은 내부적으로 전체 결과를 계산한 후에 외부 쿼리와 비교하는 방식이다.
기본적으로 해당 서비스에서는 1만개 이상의 서브쿼리 데이터가 쌓일 수 있으므로 성능 저하의 주된 원인이 될수 있다.
반면에 JOIN은 NOT IN 쿼리에 비해 연관관계를 기반으로 빠르게 관련 데이터를 찾아낼 수 있다.

image

2) WHERE 절 최적화를 위한 인덱싱 적용
WHERE 절을 수행할 때 푼 사람 수, 난이도 범위, 활성화 여부를 모두 조건이 걸려있어 성능적으로 좋지 못하였다. 성능 문제를 해결하기 위해 데이터베이스 인덱싱 전략을 세밀하게 조정하였다. 주요 쿼리에서 '푼 사람 수', '난이도 범위', 그리고 '활성화 여부'를 기준으로 데이터를 필터링해야 했는데, 이 조건들의 카디널리티를 분석한 결과, 다음과 같은 인덱싱 전략을 수립하게 되었다.

  1. 푼 사람 수: 0명에서 10만 명까지의 매우 높은 카디널리티를 갖는다. 이는 인덱스를 통한 검색에 매우 유리하여, 효율적인 데이터 접근과 빠른 쿼리 응답 시간을 가능하게 한다.
  2. 난이도 범위: 브론즈 5부터 루비 1까지 총 30개의 단계를 포함한다.
  3. 활성화 여부: True와 False 두 가지 값만을 가지는 낮은 카디널리티를 보입니다. 이 필드는 혼자서는 인덱싱의 효과가 제한적이다.

따라서, '푼 사람 수', '난이도 범위', 그리고 '활성화 여부' 순서로 복합 인덱스를 구성하였다.

image

3) 애플리케이션 레벨에서 랜덤으로 문제 추출
쿼리 내에서 ORDER BY FUNCTION('RAND') 를 사용할 때, 결과 데이터의 크기가 클 경우 무작위로 데이터를 섞는 과정에서 심각한 성능 저하가 발생할 수 있다. 이를 해결하기 위해 페이징을 통해 50개의 데이터를 가져온 후 애플리케이션 레벨에서 랜덤 변수를 적용하여 임의의 데이터를 선택하였다. 이를 통해 데이터베이스의 성능 저하를 방지하였다.

image

Result

기존 쿼리의 실행 시간이 79ms에서 18ms로 줄어들어 성능이 크게 개선되었다.
만약, 5개의 문제를 추출한다면 총 실행 시간이 395ms에서 90ms로 감소한 것이라고 볼 수 있다.

image

Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant