https://gani-dev.tistory.com/140
이전 글에서는 현재 쿼리의 분석과 문제점을 파악했다.
현재 상황 분석
- 모든 사용자의 포인트를 조회했을 때, MTT 는 18초 , TPS는 5.3을 기록했다.
- 쿼리에서 oauth_id를 기준으로 인덱스 타고 있었음에도 불구하고,, 속도가 느린 문제가 발생했다.
- 실행 계획을 통해 원인을 분석한 결과 , 쿼리가 조인 연산을 대략 24만번 수행하고 있었다.
select u.oauth_id , COALESCE(SUM(p.change_point), 0) as totalPoint from users u left join point_history p on u.oauth_id=p.oauth_id group by u.oauth_id;
문제점
현재 조인 쿼리는 아래와 같이 동작한다.
아래와 같이 N과 M을 정의해보자.
N : 외부 테이블 (User) 의 데이터 크기
M : 내부 테이블 (Point_history) 의 조인 조건에 일치하는 데이터 크기
동작 순서
1. N * M 번의 조인 연산 진행
2. Group by 로 그룹을 묶음
3. COALESCE(SUM(p.change_point), 0) 집계함수 실행
위 과정의 문제점은 조인하는데 많은 연산이 필요하다는 것이다.
예를 들어 100만명의 유저가, 각자 100개씩의 포인트 내역이 있다면 1억번의 연산이 필요하다. N과 M이 늘어나면 더 늘어날 것...
그렇기 때문에, 조인 연산 비용을 줄이는 방법을 고민했다.
개선하기
조인되는 내부 테이블의 데이터 수를 줄이기
조인 연산을 줄이려면 어떻게 해야할까 ?
N 또는 M의 크기가 작아야한다.
N은 모든 유저의 수이기 때문에, 크기를 줄일 순 없다. 그렇다면 M을 줄여보자.
어차피, point_history 테이블은 SUM 집계함수가 필수인데, 미리 처리하고 join하면 안될까 ?
서브쿼리를 이용해서, 조인을 시도해보자
select u.oauth_id, COALESCE(p.total_point, 0) as total_point
from Users u left join (
select oauth_id, SUM(change_point) as total_point
from POINT_HISTORY
group by oauth_id
) p
on u.oauth_id = p.oauth_id;
서브쿼리로 유저별 포인트 금액을 그룹화하고, sum 집계함수를 실행했다. 실행 계획을 확인해 보자
실행계획을 확인해 보니, User 테이블에서 인덱스가 oauth_id가 아닌 Nickname으로 타서, Hint를 사용했다.
select u.oauth_id, COALESCE(p.total_point, 0) as total_point
from Users u USE INDEX(PRIMARY) left join (
select oauth_id, SUM(change_point) as total_point
from POINT_HISTORY
group by oauth_id
) p
on u.oauth_id = p.oauth_id;
결과
성능 테스트 결과
이전 테스트와 동일한 설정에서 MTT 가 18초에서 9초로 줄었다 ! 그만큼 TPS도 2배 늘었다.
비록 , 18초에서 9초로 46.73% 속도 개선 됐지만, 너무 느리다…
코드 내부에서 실행 시간을 측정해보자
실행 시간 측정
//Controller
@ApiVersion("1")
@GetMapping("/allUserPoint")
public ResponseEntity<SuccessSingleResponse<List<UserPointResponse>>> getAllUserPoint() {
long startTime = System.currentTimeMillis();
List<UserPointResponse> responses = pointService.getAllUserPoint();
long endTime = System.currentTimeMillis();
long duration = (endTime - startTime);
System.out.println("test - duration: " + duration); // 시간 출력
return ResponseEntity.ok()
.body(new SuccessSingleResponse<>(HttpStatus.OK.getReasonPhrase(), responses));
}
//PointService
public List<UserPointResponse> getAllUserPoint() {
long startTime = System.currentTimeMillis();
List<Map<String,Object>> rawResults = pointHistoryRepository.findAllUserPoint();
long endTime = System.currentTimeMillis();
long duration = (endTime - startTime);
System.out.println("findAllUserPoint db join - duration: " + duration); // 시간 출력
return rawResults.stream().map(r -> new UserPointResponse((String)r.get("oauth_id"), (Integer)r.get("totalPoint"))).toList();
}
DB조회하는데 걸린 시간은 로직을 처리하는데 걸린 시간에 비해 적었다.
- 비즈니스 로직을 처리하는데 총 걸린 시간 : 7869ms
- DB 조회하는데 걸린 시간 : 1894ms
더이상 속도 저하의 주 원인은 DB가 아닌 것 같다.
SQL 쿼리 개선은 여기서 마무리하고, 다음에는 JVM을 분석하여 로직을 처리하는데 어디서 시간이 많이 소요되었는지 확인해봐야 할 것 같다.
결론
- 서브쿼리를 이용하여 조인 비용을 줄임
- 성능테스트 - MTT 는 18초 -> 9초 46.73% 속도 개선됨
- 그러나 아직도 9초나 걸린다는 문제가 있음
- 실행 시간 측정 결과 , DB보다 Java 코드에서 처리하는데 시간이 더 오래걸림
아래 ' 서브쿼리를 이용한 조인 쿼리 개선하기(3) - JAVA 병목 원인 찾기 '에서 계속됩니다 !
'개발일지 > 문제 해결하기 ~' 카테고리의 다른 글
서브쿼리를 이용한 조인 쿼리 개선하기(3) - JAVA 병목 원인 찾기 (0) | 2024.08.06 |
---|---|
서브쿼리를 이용한 조인 쿼리 개선하기(1) - 현재 상황 분석 (0) | 2024.07.30 |
연속 요청으로 인한 데드락 문제 해결하기 (1) | 2024.07.24 |
꾸준히 기록할 수 있는 사람이 되자 !