기능을 만들다보면 처음 정해진 사항에서 요구사항이 더 추가되거나 변경되는 경우가 많다.

현재 진행하던 프로젝트에서도 추가적으로 웹 페이지에 보여지면 더 좋을 듯한 정보를 추가하게 되었다.

하지만 쿼리를 변경함으로써 1초도 안걸리던 데이터 조회가 3초 이상이 걸리는 걸 확인하였다. 데이터 확인이 중요했던 터라 우선은 쿼리를 적용하였지만 초당 데이터를 조회해 확인해야 하는 페이지였기 때문에 쿼리 수정이 불가피하였다.

 

해당 포스팅은 쿼리의 성능을 향상시킬 때 어떻게 튜닝을 하면 될지에 대한 가이드라인으로 작성하였다.(미래의 나를 위해서 말이다.)

아래의 내용들은 어디까지나 참고용이고, 성능의 저하는 다른 방식으로 발생할 수 있다.

 

SELECT시에 필요한 컬럼만 불러오기

-- Inefficient
SELECT * FROM table_name;

-- Improved
SELECT column_name FROM table_name;

많은 필드 값을 불러올수록 데이터베이스는 더 많은 로드를 부담하게 된다.

때문에 컬럼을 불러올 때 꼭 필요한 열만 불러오는 것이 좋다.

 

SELECT DISTINCT, UNION DISTINCT와 같이 중복 값을 제거하는 연산 줄이기

-- Inefficient
SELECT DISTINCT a.column, a.name
FROM table_a a
INNER JOIN table_b b
ON a.id = b.id;

-- Improved
SELECT a.column, a.name
FROM table_a a
WHERE EXISTS (SELECT 1 FROM table_b b WHERE a.id = b.id);

중복값을 제거하는 연산은 쿼리 실행 시간을 증가시킨다.

때문에 DISTINCT 연산을 사용해야 할 경우, 해당 연산을 대체하거나 연산의 대상이 되는 테이블의 크기를 최소화하는 방법을 고민해야 한다. 대표적인 DISTINCT의 대체 방법으로는 EXISTS가 있다.

 

인덱스 사용하기

인덱스는 대용량 DB에서 데이터 검색 속도를 향상시키는 데 유용하다. 인덱스를 사용하면 특정 열을 쉽게 찾을 수 있어 쿼리 실행 속도를 빠르게 해준다.

인덱스를 사용할 때는 WHERE 절에 사용되는 컬럼들에 대해서, 또는 자주 조회되는 컬럼에 대해서 인덱스를 생성하는 것이 좋다.

ORDER BY를 사용할 때 속도가 저하되는 경우도 대부분 index를 사용하여 정렬해주면 처릭 속도가 향상된다.(또한, ASC가 DESC보다 더 처리가 빠르다.)

SELECT *
FROM table_name
WHERE date > '2023-11-16'
ORDER BY id;

해당 쿼리는 date 열을 조건으로 사용하고, id를 정렬 기준으로 사용하고 있다. 이러한 경우 date와 id를 인덱스로 설정하여 사용하면 쿼리 실행 속도를 높일 수 있다.

 

하지만 필자의 경우, 생성한 테이블에 인덱스가 있음에도 인덱스 스캔을 타지 않아 쿼리 실행 속도가 저하되었다.

이와 같이, 인덱스 스캔을 타지 않는다면 아래의 포스팅을 참고하는 것을 추천한다.

 

[DataBase] Index Scan이 실행되지 않고 Full Scan이 실행되는 경우

프로젝트를 진행하면서 여러 테이블에서 정보를 한 번에 select 해와야 하는 기능을 추가하게 되었다. 초마다 데이터를 select 해와야하기 때문에 빠른 쿼리 조회가 중요한데 조회 쿼리를 실행해보

justdo1tme.tistory.com

 

WHERE절에 조건 걸지 않기

DB 컬럼에 별도의 연산이나 CASE문을 적용하지 않아야 쿼리가 인덱스를 타게 된다.

인덱스를 타야 쿼리 성능이 빨라질 수 있다는 설명은 위에서 했기 때문에 생략하겠다.

-- Inefficient
SELECT column_name
FROM table_name
WHERE TO_CHAR(column_name, 'YYYYMMDD') = '20231115';

-- Improved
SELECT column_name
FROM table_name
WHERE column_name = TO_DATE('20231115', 'YYYYMMDD');

 

GROUP BY 연산 사용시, HVING절 보다는 WHERE절 사용하기

-- Inefficient
SELECT a.column1, COUNT(b.column2) AS b_cnt,
	AVG(b.column3) AS avg_b
FROM table_a a
INNER JOIN table_b b
ON a.id = b.id
GROUP BY id
HAVING a.id > 1000;

-- Improved
SELECT a.column1, COUNT(b.column2) AS b_cnt,
	AVG(b.column3) AS avg_b
FROM table_a a
INNER JOIN table_b b
ON a.id = b.id
WHERE a.id > 1000
GROUP BY id;

GROUP BY 연산을 사용해야 할 때 조건절로 HAVING 절을 사용하기 보단 WHERE 절을 사용하는 것을 권고한다.

쿼리 실행 순서에서, WHERE 절이 HAVING 절보다 먼저 실행되기 때문에 WHERE 절로 먼저 데이터의 크기를 작게 만들면 GROUP BY 연산에서 다루는 데이터의 크기도 작아지기 때문에 빠른 연산이 가능하기 때문이다.

 

3개 이상의 테이블을 INNER JOIN 할 때의 테이블 배치

3개 이상의 테이블을 INNER JOIN할 때, 크기가 가장 큰 테이블을 FROM 절에 배치하고 INNER JON 절에는 남은 테이블을 작은 순서대로 배치하는 것이 좋다.

이렇게 배치하는 이유는 최소한의 Combination을 탐색하도록 하기 위해서이지만, 항상 통용되는 것은 아니다. 간단한 INNER JOIN의 경우, 대부분의 Query Planner에서 가장 효과적인 순서를 탐색해 INNER JOIN의 순서를 바꾸기 때문이다.

 

서브쿼리 최소화하기

서브쿼리는 SELECT 문 안에 포함된 또 다른 SELECT문이다.

서브쿼리는 복잡한 쿼리를 작성할 때 유용하지만, 성능 저하의 원인이 될 수 있어 최소화하는 것이 좋다.

서브쿼리 대신 JOIN, UNION, EXISTS 등 다른 방법을 사용하여 쿼리를 작성하는 것을 권고한다.

또한, 서브쿼리를 사용할 때는 서브쿼리의 결과를 캐시하여 재사용하는 것이 좋다.

필자 또한, 다른 성능 저하의 원인으로 서브쿼리의 실행이 실행 속도를 저하시켜 JOIN문으로 바꿔 문제를 해결하였다.

-- Inefficient
SELECT *
FROM table_a
WHERE column_id IN (SELECT coulmn_id
                    FROM table_b
                    WHERE column_gender = 'F');

-- Improved
SELECT a.*
FROM table_a a
JOIN table_b b
ON a.column_id = b.column_id
WHERE b.column_gender = 'F';

 

자주 사용하는 데이터가 있는 테이블을 따로 보관/관리하기

이 방법은 RDBMS의 원칙에 어긋나는 측면이 있고, DB의 실시간성을 반영하지 못할 가능성이 높기 때문에, 대부분 운영계보다는 분석계에서 더 많이 사용된다.

예를 들어, 사용자에 의해 발생한 log 데이터 중 필요한 Event만 모아서 따로 적재해두는 것, 또는 핵심 서비스 지표를 주기적으로 계산해서 따로 모아두는 것 등을 사례로 들 수 있다.

 


[ 참고 ]

https://jaejade.tistory.com/128

https://medium.com/watcha/쿼리-최적화-첫걸음-보다-빠른-쿼리를-위한-7가지-체크-리스트-bafec9d2c073

https://chung-develop.tistory.com/145

https://blog.naver.com/platinasnow/220198302742