![[쿼리튜닝 - 1] 신입 개발자의 간단한 사내 조회 쿼리의 쿼리튜닝 여정.](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqrPvp%2FbtsBXNwdEVr%2F293INSI85brPpPk9xeL7T1%2Fimg.jpg)
발단
계속해서 짧은 주기로 프로젝트를 쏟아내고 있던 와중에 DB연산이 많은 작업을 수행하는 경우가 생겼다.
개발 단계에서 API 자체를 돌리는 과정에서도 1~2000ms가 되어 걱정하고 있던 과정에
QA를 진행했더니 난리가났다.
이상하게 이에 대해 아무도 피드백을 해 주지 않았다.

들쭉날쭉한 건 요청이 한번에 서버로 몰릴 때로 인식을 하겠다지만,
30건 정도밖에 안되는 데이터를 연산하는데 말이 안된다고 생각했고, 작업에 들어갔다.
원인 파악하기
원인을 파악하기 위해, 어쩔 수 없이 로직의 각 구간별로 로깅을 시도했다.

연산 처리속도는 빨랐는데, 애초에 데이터를 그렇게 많이 들고나오지 않기 때문이다.
연산은 간략히 설명하자면 다음과 같다
데이터를 row형태로 가져나오는데, PK가 일치할 경우, 같은 문의에 상품을 여러개 담았다고 가정해야한다. (반드시 해당 경우밖에 없다)
일치할 경우 상품의 이름을 이전 row에 추가하여 한 row에 여러개의 상품 이름이 담기도록 해야했다.
이를 위해 map을 사용했고, map을 리턴할 때 다시 리턴 객체로 변환하여 사용했다.
대충 아래와 같은 로직이다.
//no 중복일 같은 로우에 붙이기
if (setDto.length > 1) {
//map to dto
const map: Map<Number | string, RsvListDto> = await dtoToMapForCheckDuplicateNo(setDto);
// map key 빼고 value로 DTO 리턴
const result: RsvListDto[] = await mapToDto(map.values());
return result;
}
export const dtoToMapForCheckDuplicateNo = async (dto: RsvListDto[]): Promise<Map<number | string, RsvListDto>> => {
const map = new Map();
dto.forEach((data) => {
const { no, ...rest } = data;
if (map.has(no)) {
const mapData = map.get(no);
mapData[0].name += ', ' + data.name;
} else {
map.set(no, [{ no, ...rest }]);
}
})
return map;
}
export const mapToDto = async (mapValues: Iterable<any>): Promise<RsvListDto[]> => {
const resultArray: RsvListDto[] = Array.from(mapValues).map((entry) => {
const data = entry[0];
const getRsvListDto = new RsvListDto();
getRsvListDto.no = parseInt(data.no);
getRsvListDto.name = data.name.length > 1 ? data.name.split(', ') : data.name;
( ... 생략 ... )
return getRsvListDto;
});
return resultArray;
}
또한, 해당 로직은 장바구니 같은 공간으로, 상품에 대한 정렬, 처리 상태에 대한 정렬도 필요에따라 수행 해야한다.
테스트 시에는 소팅이 생략되어있는데, 자바스크립트의 filter문법으로 소팅했다고 생각하면 되겠다.
쿼리 연산의 문제
좌측 쿼리는 2개의 테이블에 조인을 시도하고, SELECT 시 IF연산이 3건 있는데, 단순 해당 컬럼을 조회하여 데이터를 셋팅하는 연산이다.
우측 쿼리는 4개의 테이블에 조인을 시도하고, IF연산은 세 건 존재한다.
IF를 CASE로 변경하지는 않았다.
`IF(couponNo != 0, 'A', 'B') AS coupon`,
IF 연산에 대한 코드는 위와 같이 단순하기 때문이다. 조인의 복잡성과는 크게 관련없다고 판단했다.
마무리로 두 쿼리를 유니온한 결과를 최종으로 들고나온 뒤, 위의 소팅이나 추가 작업을 수행하고 리턴한다.
const [rsvQuery, rsvParams] = (await this.customHallReqMainRepo.findRsvListQueryByWebId(web_id)).getQueryAndParameters();
const [ebQuery, ebParams] = (await this.customEasyBookRepo.findEasyBookListQueryByWebId(web_id)).getQueryAndParameters();
const unionResult: RsvListDto[] = await getManager().query(`
(${rsvQuery}) UNION (${ebQuery})
ORDER BY time DESC, no ASC
`, [rsvParams, ebParams]);
위에서 타임테이블로 로깅을 했을 때, 짧아도 1초 이상, 평균 2초 이상의 시간 소요가 있다.
필수적으로 데이터를 받아와야 되는 구간들이 있기 때문에, 조인을 더 줄일 수는 없었고, LEFT JOIN을 INNER JOIN으로 바꾼다던가 할 수도 없었다.
개선하기
개선사항들을 체크해보았고 아래와 같이 결론지을 수 있었다.
1. 인덱싱이 되어있지 않았음
2. UNION을 사용하라고 했는데, UNION을 하는 이유를 못찾겠음. 중복 데이터가 없기 때문
여기서 2번을 짚고 넘어가자면, 현재 다니는 회사는 DBA까지는 아닌데, DB를 책임지고 있는 개발자분이 계셔서, 이전까지는 관습인지 모르겠지만 쿼리도 직접 작성해주시고, DB에 대한 관리도 직접 하셨다고 했다. 이번에 체크해보니 안되어있는 부분들이 있고, 쿼리 튜닝이 덜 되어서 넘어온 것이었다.
처음 개선하는 것이었기 때문에 조심스레 의견을 팀즈로 공유해드렸고, 진행해도 된다고 하셔서 작업을 수행했다. 아래는 워크벤치에서의 개선에 대한 타임테이블이다.

현재 할 수 있는 모든 작업을 완료했지만. 최장 1.3초정도 걸리는 모습이다. 더 개선할 수 있는 방법이 없을까 고민해보았지만 현재로써는 마땅히 답이 보이지 않는다.
최종 API 결과
길어도 1초 안팎의 모습을 보여주고 있다. 뿌듯하다.

무언가, 누군가의 요청에 의해서가 아닌. 내가 만들어 놓은 내 새끼를 내놓기 전에 스스로 내가 관찰하고, 개선사항을 어떻게 개선할까 고민하면서 개선을 시도하고, 성공해서 뿌듯하기도하고, 성패 여부를 떠나 긍정적인 경험이었던 것 같다.
추가
알고보니 UNION ALL을 적용하지 않아서, 최종 로그이다.. ㅋㅋ

2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!