쿼리 속도를 올리는 방법은 어떤 게 있을까? - 쿼리적으로

하나의 쿼리가 크게 비용을 발생시키는 경우가 있다.

이는 트랜잭션 전체의 성능 부하를 일으키고, 그에 따라 전반적인 응답의 속도 또한 저하시키게 된다.

이러한 쿼리를 아키텍처적으로 보완하는 방법에 대해 지난 글에서 분석해 보았다.

아키텍처를 개선하는 것이 도로를 잘 만드는 일이라면, 쿼리 최적화주행할 경로 선택이다. 즉 좋은 네비게이션인 것이다.

이번 글에서는 아키텍처가 동일한 상태에서 해당 쿼리를 어떻게 하면 경로를 잘 타도록 만들 수 있을지에 대해 알아 보려 한다.

짐을 줄이기

필요한 컬럼만 지정해 가지고 오기

벽돌이 담긴 수레를 상상해 보자.

벽돌을 100개 얹은 수레와, 벽돌을 1개 얹은 수레 중에 어떤 것이 옮기기 쉬울까?

데이터도 마찬가지일 것이다.

데이터를 가지고 올 때, 컬럼 하나를 가지고 오는 것컬럼 백 개를 가지고 오는 것은 성능 상 차이를 보일 것이다.

네트워크나 메모리 때문이라도 중요한 부분이고, 조인할 때도 역시 비교 대상이 적을수록 조인이 빨라진다.

적절한 길을 선택하기

랜덤 테이블 액세스 줄이기

내가 원하는 벽돌, 원하는 데이터를 찾기 위해서 뒤져야 하는 곳은? 데이터베이스에서는 테이블이다.

그런데 어디에 뭐가 있는지 정확하게 모른다면 빠르게 찾을 수 있을까?

서울에서 부산으로 가는 길이 만약 경부고속도로가 아니라 국도라면 어떻게 될까?

아무리 효율적으로 간다고 하더라도 훤하게 뚫린 경부고속도로보다는 느릴 것이다.

이처럼 인덱스를 아무리 효율화시킨다고 해도, 랜덤 테이블 액세스를 줄이는 것이 더욱 우선순위가 높다.

랜덤 테이블 액세스를 줄이는 것은 사실상 전체를 관통하는, 핵심이 되는 부분이라고 할 수 있다.

가는 길을 효율화하는 것보다, 덜 밟는 길로 가야 하는 것이다.

어떤 테이블을 타고 올지, 이상하게 돌아서 가지는 않는지, 테이블의 순서를 줄이는 것이 가장 중요하다.

의도한 인덱스를 제대로 타고 있는지 확인하기

덜 밟는 길을 잘 골랐다면, 그 다음은 뭘까?

덜 밟는 길 중 가장 덜 밟는 길을 누군가 깔아 뒀다면? 그게 바로 인덱스일 것이다.

여러 컬럼에 대응하도록 인덱스를 설정해 두었다면, 이번에는 쿼리에서 해당 인덱스를 타도록 만들어야 한다.

해당 인덱스를 타게 만드는 것은 두 가지 방법이 있다.

  1. 힌트로 직접 해당 인덱스를 지정하기
  2. 옵티마이저가 해당 인덱스를 선택할 수 있도록 쿼리 조건 대응하기

힌트로 인덱스를 지정하는 것은 당장은 간편하다. 하지만 추후 고려해 줄 사항이 많아진다. 데이터 양 등이 달라지면서 해당 힌트가 최적의 경로가 아니게 바뀔 수 있고, 더 빠른 방향이 생겨날 수도 있다.

데이터 양 등이 달라지면서 해당 힌트가 최적의 경로가 아니게 바뀔 수도 있고, 더 빠른 방향이 생겨날 수도 있다.

또한 JPA와 같은 ORM 환경에서는 쿼리에 직접적으로 SQL 힌트를 주기 위해 Native Query를 사용해야만 한다.

이러한 점을 고려해 보통 선택하는 것은 쿼리 조건 대응이다.

WHERE절에 배치하는 컬럼 순서를 미리 설정한 인덱스를 탈 수 있도록 맞춰 두는 것이다.

쿼리를 짤 때 최소한 첫 번째 컬럼이라도 인덱스를 타도록 만들어 주어야 해당 인덱스를 탈 확률이 올라간다.

LIKE 사용 시 와일드카드 문자열(%)을 앞에 사용한다든지, 컬럼과 다른 타입의 행을 WHERE절에 놓거나 하는 것들은 인덱스를 탈 확률을 낮추는 행동이다.

데이터베이스 종류와 버전에 따라서 인덱스 활용 조건이 다를 수 있기 때문에, 이 지점을 꼭 고려해야 한다.

가령 MySQL의 경우, 앞에 나오는 범위 연산은 뒤의 인덱스를 사용할 수 없기 때문에 가장 뒤에 두는 것이 맞다.

작업 규모 줄이기

적합한 JOIN 종류 정하기

조인에 있어 주로 사용하는 세 가지 방식이 있다.

  • Nested Loop Join: OLTP 환경, 그 외 일반적 상황
  • Sort Merge Join : 이미 정렬된 / 크기가 비슷한 두 개의 데이터
  • Hash Join: 배치 등 대량의 데이터

나는 이 세 가지 조인 방식을 방 정리에 비유해 보고 싶다.

Nested Loop Join은 무작정 청소를 시작하는 것과 비슷하다. 방이 다 치워졌다면 된 것이다.

OLTP 환경에서는 가용성, 주로 작은 데이터를 빠르게 반환하는 것이 관건이다.

따라서 LIMIT를 걸기도 하고, 추가 조건을 토대로 값을 빠르게 반환하는 데 초점을 맞춘다.

이는 OLTP 환경에서 Nested Loop Join이 주로 사용되는 이유가 된다.

그러나 NL 조인은 랜덤 테이블 액세스를 사용한다. 아까 이야기했듯, 랜덤 테이블 액세스는 성능이 떨어진다는 점을 유의해야 한다.

**Sort Merge Join**은 양말이나 신발과 같은 짝이 있는 물건을 정리하는 것과 비슷하다. 미리 정리가 되어 있다면 빠르게 갤 수 있겠지만, 그렇지 않으면 정렬해 찾는 시간이 든다.

미리 데이터가 정렬되어 있다면, 그 데이터 두 개를 연결만 시켜 주면 될 것이다.

이미 정렬이 된 상태이고, 두 테이블의 양이 비슷할 때는 Sort Merge Join을 선택할 수 있다.

Hash Join은 방 정리를 위해 서랍장을 만들어 설치하는 것과 비슷하다. 서랍장을 만드는 시간은 걸리지만, 배치할 곳이 명확하게 정해져 있기 때문에 이후의 시간이 절약된다.

주로 대용량의 데이터를 분석하거나 배치 쿼리 등 조인하는 두 테이블의 크기 차이가 클 때 사용된다.

랜덤 테이블 액세스에 드는 비용, 정렬에 드는 비용, 해시테이블 생성에 드는 비용을 잘 비교하여 조인 기법을 선택하자.

JOIN 순서는 작은 테이블에서 큰 테이블로

일반적인 경우, 따로 명시하지 않고 조인을 한다면 Nested Loop Join이 수행된다.

NL 조인에서도 해시 조인에서 사용하는 것처럼 작은 테이블을 기준으로 큰 테이블을 조인하는 것이 성능상 유리하다.

메모리에 올려놓고 사용해야 하는 부분은 작으면 작을수록 좋은데, 조인 시에는 드라이빙 테이블을 메모리에 올려 둔 상태에서 드리븐 테이블을 조회하기 때문이다.

이는 결과적으로 조인 작업이 빠르게 진행되는 결과를 낳는다.

분석 함수 / 정렬 / 연산은 최소화하기

  • GROUP BY / DISTINCT
  • SUM, AVG, COUNT
  • ORDER BY
  • 연산

분석 함수나 연산은 데이터 전체를 스캔해야 하는 작업들이라, 전부 비용을 많이 먹는 것들이다.

작은 연산이라고 하더라도 결국 해당 데이터에는 계속해서 선후 작업이 들어가게 되므로 비용이 든다.

마찬가지로 ORDER BY는 전체 데이터를 정렬 알고리즘을 토대로 재정렬하는 것이다.

가능하다면 최소화하고, 들어가게 되더라도 필터링을 전부 거친 후 범위를 좁혀 마지막에 처리하는 것이 좋다.


쿼리 속도를 올릴 수 있는 세 가지 섹션에 대해 알아보았다.

디테일보다는 큰 관점에서 피해야 할 것들을 묶어 두었기에 상세하게 들어가면 더 많은 이야기들을 할 수 있을 것 같다.

마일스톤으로 삼고, 하나하나 더 깊게 파고 들어가면 좋을 것 같다는 생각이 든다.

걸을 수 있는 효율적인 길이 있다면 그 길을 먼저 잘 깔아야겠지만, 잘 깔린 좋은 길을 두고서 먼 길을 돌아가고 있다는 걸 모르고 있다면 너무 아쉽지 않을까.

참고

친절한 SQL 튜닝 - 예스24

✅ 쿼리 최적화 첫걸음 — 보다 빠른 쿼리를 위한 7가지 체크 리스트

MySQL IN절을 통한 성능 개선 방법

ORACLE|JOIN 성능 - 일반적으로 크기가 작은 테이블이 먼저 드라이빙 되는 것이 빠르다.

Who is?

금융과 소비자의 교두보가 되고 싶은 개발자.