[DataBase] 쿼리 최적화를 하는 10가지 방법

nana·2025년 5월 14일

DataBase

목록 보기
8/8
post-thumbnail

DB 최적화는 왜 중요할까?

시스템이 안정적으로 동작하려면 어떤 요소들이 가장 중요할까?

많은 백엔드 개발자들이 아래와 같은 시스템 품질 요소들을 떠올릴 것이다.

  • 신뢰성(Reliability): 시스템이 정상적으로 작동하고, 실패 시에도 빠르게 복구되는 능력

  • 가용성(Availability): 시스템이 언제든 접근 가능하고 멈추지 않는 것

  • 일관성(Consistency): 데이터가 항상 정확하고 신뢰할 수 있는 상태를 유지하는 것

  • 확장성(Scalability): 트래픽 증가에 유연하게 대응할 수 있는 구조

  • 관측 가능성(Observability): 문제 발생 시 원인을 빠르게 파악할 수 있도록 시스템 내부 상태가 모니터링되는 것

이 모든 요소들은 각기 중요하지만, 그 바탕에는 ‘잘 설계된 데이터베이스’‘최적화된 쿼리’ 가 있다고 생각한다.

아무리 시스템 구조가 견고하고 마이크로서비스로 분리되어 있더라도,

데이터베이스가 병목이 되면 전체 서비스의 신뢰성, 가용성, 심지어 확장성까지도 무너질 수 있다.

이 글에서는 단순히 쿼리 튜닝 기법을 나열하는 데 그치지 않고,
왜 DB 최적화가 중요한지,
시스템 전반에 어떤 영향을 미치는지,
그리고 실무에서 자주 마주하는 DB 병목 패턴과 이를 해결하는 전략들
에 대해 이야기해보려 한다.


DB가 시스템 전반에 미치는 영향

데이터베이스는 현대 애플리케이션의 핵심 구성요소이다.
성능 최적화는 시스템의 전반적인 효율성을 높이는 데 필수적이고, 데이터베이스의 성능이 저하된다면 애플리케이션의 응답시간이 길어지고 사용자 경험이 저하될 수 있다.

앞서 말한 요소들 대부분을 보장하는 데에 중요하므로 데이터베이스 최적화는 필수적으로 해야하는 작업이라고 볼 수 있다.


⭐ DB 최적화는 왜 중요할까?

1. 시스템 성능 병목은 대부분 DB에서 발생한다.

잘못된 인덱스 설계, 비효율적인 쿼리 하나가 수많은 사용자의 응답 속도를 떨어뜨릴 수 있다.

ex) 게시판 목록 조회 쿼리에 인덱스가 빠져있다면 1만건 중 10건을 찾는데도 전체 테이블을 다 스캔하게 된다.

2. DB는 리소스가 제한적이다.

서버는 수평 확장이 비교적 쉽지만, DB는 수평확장이 어렵고 비용이 많이 든다.
DB최적화를 통해 CPU, 메모리, I/O자원을 최소화 하는 쿼리와 구조가 중요하다.

3. 서비스의 신뢰성, 가용성, 확장성을 모두 좌우함.

  • 느린 쿼리 = 타임아웃 = 사용자 오류발생(신뢰성 저하)
  • DB CPU 100% = 전체 서비스 응답 불가 (가용성 저하)
  • 트래픽 증가 = DB에 과부하(확장성 한계)

DB가 병목이면 아무리 잘 만든 시스템도 무너진다.

4. 비용 절감에도 직접적

클라우드 환경에서는 RDS Aurora등의 사용량이 곧 비용과 직결된다.
한 쿼리의 최적화로 수천만원의 비용을 줄이는 사례도 실무에서 자주 있다.

5. 유지보수가 쉬워짐.

데이터 모델이 명확하고, 쿼리가 예측 가능하며, 인덱스가 체계적이면
장기적으로 기능 추가나 변경이 훨씬 수월해진다.

DB최적화는 '성능 향상'을 넘어, '시스템 전체의 안정성·확장성·비용·유지보수성’까지 책임지는 핵심 작업' 이다.

Redis나 Kafka, 등등의 기술들이 DB의 부하를 줄여준다지만 결국 그들또한 보조도구일 뿐이고 기본은 DB쿼리부터 시작되어야한다.


🧨자주 마주하는 병목 패턴

1. 인덱스 미사용 또는 잘못된 인덱스 설계

WHERE절, JOIN, ORDER BY등에 필요한 인덱스가 없으면 테이블을 전체 스캔(Full Table Scan)하게 된다.

2. N+1 문제

JPA나 ORM사용 시, 연관된 데이터를 지연로딩(lazy loading)하면서 반복문 안에서 추가 쿼리가 발생한다.

for (Order order : orders) {
    System.out.println(order.getCustomer().getName()); // N+1 발생 가능
}

3. 불필요한 DISTINCT, GROUP BY, JOIN 남용

무의식 적으로 사용한다면 성능에 큰 영향을 줄 수 있다.
: 쿼리 복잡도 증가, 정렬/집계비용 발생

4. 비효율적인 LIKE검색

%로 시작하는 검색은 인덱스를 사용할 수 없다
: 테이블 풀스캔하게됨.

SELECT * FROM product WHERE name LIKE '%샴푸%';

5. 과도한 트랜잭션 범위

트랜잭션을 오래 유지하면 락이 길게 걸려 다른 쿼리가 대기하게 된다.
: 대기 시간 증가, 데드락 발생 가능성 증가

@Transactional
public void process() {
    // 여기서 외부 API 호출하면 트랜잭션이 길어짐
}

6. 데이터 양이 너무 많아진 테이블(핫 테이블)

파티셔닝 없이 수천만 건 이상 쌓인 단일 테이블은 쿼리 성능이 급격히 떨어짐.
: 조회, 정렬 인덱스 유지 비용 증가
👉 테이블 파티셔닝, 기간별 분할 테이블 운영 으로 해결

7. DB Connection pool 부족

DB 커넥션 수가 부족하면 애플리케이션이 DB 커넥션을 받기 위해 대기하게 됨
: 전체 응답 지연, 타임아웃


⭐ 쿼리 최적화 방법

1. 불필요한 SELECT * 제거

SELECT *는 모든 컬럼을 불러오기 때문에 I/O비용이 커지고, 인덱스만으로 처리할 수 없는 경우가 많다.

SELECT * 보다는 필요한 컬럼, 전체 조회가 필요할 때는 모든 컬럼을 직접 적는 방법으로 최적화 할 수 있다.

2. 적절한 인덱스 활용

  • 단일 인덱스 : WHERE, JOIN, ORDER BY 등에 자주 사용되는 컬럼에 생성
  • 복합 인덱스 : 자주 같이 검색되는 컬럼은 순서를 고려해 복합 인덱스로.

3. 인덱스가 잘 타는 조건 사용

  • =, 범위 조건(<,>,BETWEEN)은 인덱스를 잘 탄다.
  • cf) not 조건인 <> 보다 '=가 인덱스를 잘 탄다.
    NOT IN 보다 IN이 인덱스를 잘 탄다.
  • %시작하는 LIKE는 인덱스 사용이 불가하다.
-- ✅ 인덱스 사용 가능
SELECT * FROM users WHERE name LIKE '홍%';

-- ❌ 인덱스 사용 불가
SELECT * FROM users WHERE name LIKE '%홍';

4. 서브 쿼리보다 JOIN을 고려하기.

서브쿼리는 상황에 따라 느릴 수 있다.
서브쿼리는 루프마다 실행되기 때문에 성능이 저하될 수 있음을 염두해두자.

5. JOIN 순서, 필터 순서 고려

  • 레코드 수가 적은 테이블을 먼저 JOIN하도록 쿼리를 작성하면 DB 옵티마이저가 효율적으로 실행 계획을 세운다.
  • WHERE 조건도 선 필터링 가능한 조건부터 작성한다.

✅ 가끔 레코드 적은것 부터 순서대로 작성하더라도 옵티마이저가 판단하여 조인 순서를 잘못 설정하는 경우도있다.
각 DBMS의 강제 순서 조건을 부여하여 조인 할 수 있다.
내가 사용하는 MS-SQL은 FORCE ORDER라는 조건을 붙여주면 된다.

✅ 요즘 옵티마이저는 순서대로 굳이 적지 않아도 알아서 레코드 개수를 판단하여 하는 경우도 있다고 하니 참고하자.

6. LIMIT, OFFSET 효율적으로 사용하기

  • OFFSET이 클 수록 성능이 나빠진다 (건너뛴 row도 모두 읽기 때문에)
  • 커서 기반 페이지네이션 또는 WHERE last_id <? LIMIT N방식을 추천한다.

7. GROUP BY, DISTINCT 최소화

  • 중복 제거나 집계가 필요하지 않다면 제거한다.
  • 반드시 필요한 경우엔 정렬이 포함된 DISTINCT, 다중 GROUP BY는 피하는게 좋다.

8. 함수 사용 최소화

  • WHERE 조건절에서 컬럼에 함수 사용 시 인덱스 사용이 불가하다.
-- ❌ 인덱스 못 탐
WHERE DATE(created_at) = '2025-01-01';

-- ✅ 인덱스 탐
WHERE created_at >= '2025-01-01' AND created_at < '2025-01-02';

✅ **좌변(컬럼) 은 건드리지 않는다.

9. 실행 계획(EXPLAIN)확인

  • 쿼리 튜닝의 시작과 끝은 실행 계획을 분석하는 일.
  • 주요 확인 포인트
    • type: ALL(풀스캔), index, ref, range, const
    • rows: 얼마나 읽는가?
    • Extra: Using filesort, temporary 등 주의

10. 정규화와 반정규화의 균형

과도한 정규화는 JOIN 과다로 이어지고
과도한 반정규화는 데이터 중복과 무결성 문제를 유발
할 수 있다.

쿼리 성능데이터 정합성 사이에서 적절한 트레이드오프가 필요함


📍 쿼리 최적화 적용 순서

  1. 쿼리 실행 계획 확인 (EXPLAIN, ANALYZE)
  2. 느린 쿼리 로그 분석 (MySQL: slow_query_log)
  3. 인덱스 추가/삭제 실험
  4. 쿼리 리팩토링 (JOIN, WHERE 조건 최적화)
  5. 데이터 모델 개선 (필요시 테이블 분할/파티셔닝 등)
  6. 장기적으로는 캐싱(Memcached, Redis) 도입 고려
profile
BackEnd Developer, 기록의 힘을 믿습니다.

0개의 댓글