데이터베이스 최적화 (Database Optimization)

목차


데이터베이스 최적화란

데이터베이스 최적화(Database Optimization) 는 쿼리, 인덱스, 스키마, 하드웨어, 캐시, 운영 설정을 조정해 더 적은 자원으로 더 빠르고 안정적으로 데이터를 처리하도록 만드는 작업입니다.

실무에서는 단순히 “쿼리를 빠르게 만든다”보다 다음 질문에 답하는 과정에 가깝습니다.

  • 어떤 요청이 느린가
  • 왜 느린가
  • 병목이 CPU, I/O, 락, 네트워크, 인덱스 부재 중 어디에 있는가
  • 구조를 바꿔야 하는가, 아니면 쿼리만 고치면 되는가

최적화는 병목 찾기부터 시작한다

최적화는 감으로 하면 안 되고, 실행 계획과 운영 지표를 함께 봐야 합니다.

  1. 느린 쿼리 수집
    • 슬로우 쿼리 로그, APM, 애플리케이션 타이머로 후보를 모읍니다.
  2. 실행 계획 분석
    • EXPLAIN, EXPLAIN ANALYZE로 어떤 스캔과 조인이 발생하는지 확인합니다.1
  3. 데이터 분포 확인
    • 전체 row 수, 조건 선택도, null 비율, 편향된 값 분포를 봅니다.
  4. 변경 전후 비교
    • 인덱스 추가, 쿼리 수정, 스키마 변경 후 latency와 resource usage가 실제로 개선됐는지 측정합니다.

최적화의 첫 단계는 “어떤 인덱스를 만들까”가 아니라 “무엇이 느린가”를 수치로 확인하는 것입니다.


인덱스 최적화

인덱스는 조회를 빠르게 만드는 가장 강력한 수단이지만, 모든 쿼리를 자동으로 빠르게 해주지는 않습니다.

인덱스가 효과적인 경우

  • 선택도가 높은 조건: 많은 row 중 일부만 읽는 조회
  • 정렬과 범위 검색: WHERE, ORDER BY, GROUP BY에 자주 사용되는 컬럼
  • 조인 키: FK, 사용자 ID, 주문 ID처럼 반복적으로 조인되는 컬럼
  • 조회 패턴이 안정적일 때: 자주 반복되는 핵심 쿼리

인덱스가 오히려 해가 되는 경우

  • 쓰기 비중이 높을 때: INSERT, UPDATE, DELETE마다 인덱스도 갱신해야 합니다.
  • 선택도가 낮을 때: 거의 모든 row를 읽는 쿼리는 풀 스캔이 더 나을 수 있습니다.
  • 과도한 중복 인덱스: 비슷한 인덱스를 여러 개 두면 저장 공간과 쓰기 비용만 늘어납니다.
  • 카디널리티를 고려하지 않은 설계: status 같은 저선택도 컬럼 단독 인덱스는 기대만큼 효과가 없을 수 있습니다.

복합 인덱스와 커버링 인덱스

복합 인덱스 는 여러 컬럼을 묶어 만든 인덱스입니다. 일반적으로 leftmost prefix를 고려해 컬럼 순서를 설계해야 합니다.2

예를 들어 다음 쿼리가 핵심이라면:

SELECT id, created_at
FROM orders
WHERE user_id = 100
  AND status = 'PAID'
ORDER BY created_at DESC;

다음과 같은 인덱스를 검토할 수 있습니다.

CREATE INDEX idx_orders_user_status_created_at
ON orders (user_id, status, created_at DESC);

커버링 인덱스 는 조회에 필요한 컬럼을 인덱스만으로 충족시키는 방식입니다.

  • 장점: 테이블 본문 접근을 줄여 I/O를 절감할 수 있습니다.
  • 단점: 인덱스가 커지고, 쓰기 비용이 증가합니다.
항목 단일 인덱스 복합 인덱스 커버링 인덱스
목적 단일 조건 최적화 조합 조건 최적화 테이블 접근 최소화
장점 단순하고 가볍다 핵심 쿼리에 매우 강하다 읽기 성능이 좋다
단점 조합 조건에는 약하다 컬럼 순서 설계가 중요하다 인덱스 크기와 쓰기 비용이 커진다

쿼리 최적화

인덱스가 있어도 쿼리 자체가 비효율적이면 성능이 잘 나오지 않습니다.

  • 필요한 컬럼만 조회: SELECT * 는 네트워크와 I/O를 불필요하게 늘립니다.
  • 불필요한 정렬 제거: 이미 인덱스로 해결 가능한 정렬인지 확인합니다.
  • 대량 범위 조회 분할: 한 번에 너무 많은 row를 읽지 않도록 페이지네이션이나 배치 처리를 씁니다.
  • 함수 사용 주의: 인덱스 컬럼에 함수가 적용되면 인덱스를 못 타는 경우가 많습니다.
  • JOIN 순서와 드라이빙 테이블 점검: 작은 집합을 먼저 줄이고 조인하는 편이 유리합니다.
  • N+1 문제 제거: 애플리케이션 레벨에서 반복 쿼리 대신 join, batch fetch, preloading을 사용합니다.

면접에서는 “인덱스를 걸었습니다”보다 “EXPLAIN으로 풀 스캔을 확인하고, 조건 순서에 맞는 복합 인덱스로 바꿨다”까지 말해야 답변이 강해집니다.

N+1 문제

N+1 문제 는 목록 1건을 읽은 뒤, 연관 데이터 조회가 항목 수만큼 추가로 발생하는 패턴입니다.

예를 들어 주문 100건을 조회한 뒤, 각 주문의 사용자 정보를 다시 개별 조회하면:

  • 1번 주문 목록 조회
  • 100번 사용자 조회

처럼 총 101번 쿼리가 나갈 수 있습니다.

이 문제는 특히 ORM을 사용할 때 자주 보입니다.

  • lazy loading을 무심코 둔 경우
  • 루프 안에서 연관 객체를 계속 접근하는 경우
  • API 응답 직렬화 과정에서 숨은 추가 조회가 발생하는 경우

대표 대응은 다음과 같습니다.

  • join fetch / eager loading: 필요한 연관 데이터를 한 번에 읽기
  • batch fetch / preloading: 여러 키를 모아 IN (...) 형태로 조회
  • projection 사용: 필요한 컬럼만 조회해서 응답 DTO로 바로 매핑

좋은 답변은 “join으로 해결합니다”에서 멈추지 않습니다.

  • join이 row 수를 과도하게 불릴 수 있는가
  • batch fetch가 더 나은가
  • 정말 필요한 연관 데이터만 읽고 있는가

즉, N+1은 단순 ORM 팁이 아니라
반복 조회 패턴을 실제 쿼리 수와 네트워크 비용으로 바꿔 보는 문제입니다.


스키마와 파티셔닝 최적화

쿼리와 인덱스만으로 해결되지 않으면 데이터 모델 자체를 조정해야 합니다.

  • 정규화: 데이터 중복을 줄이고 무결성을 높입니다.
  • 비정규화: 조회 성능을 위해 일부 중복을 허용합니다.
  • 파티셔닝: 큰 테이블을 논리적으로 나누어 관리하고, 파티션 프루닝으로 읽을 범위를 줄일 수 있습니다.3
  • 아카이빙: 오래된 데이터를 별도 테이블이나 스토리지로 분리해 hot data 크기를 줄입니다.

파티셔닝은 만능이 아닙니다.

  • 좋은 경우: 날짜 기반 로그, 주문 이력, 대용량 append-heavy 테이블
  • 조심할 경우: 샤드 키나 파티션 키 없이 자주 조회하는 패턴, 작은 테이블, 조인이 많은 스키마

운영 관점에서 확인할 항목

  • 슬로우 쿼리 로그: 어떤 쿼리가 실제로 느린지 봅니다.
  • 락 대기: 성능 문제처럼 보이지만 사실은 락 경합일 수 있습니다.
  • 통계 정보 최신성: 옵티마이저 통계가 오래되면 잘못된 실행 계획이 나올 수 있습니다.
  • 버퍼 캐시 히트율: 디스크 I/O 병목 여부를 판단하는 데 중요합니다.
  • 인덱스 사용률: 거의 안 쓰는 인덱스는 제거 후보입니다.
  • 증가 속도: 지금 빠른 구조도 데이터 10배 증가 시 유지되는지 봐야 합니다.

면접 포인트

  • 최적화는 인덱스 추가가 아니라 병목 진단부터 시작합니다.
  • 복합 인덱스는 컬럼 순서가 핵심입니다.
  • 읽기 성능을 높이면 쓰기 비용과 저장 공간 비용이 함께 증가합니다.
  • 파티셔닝은 운영성과 삭제 효율에도 큰 영향을 줍니다.
  • 실행 계획을 읽고 변경 전후를 수치로 비교할 수 있어야 실무형 답변이 됩니다.

참고 자료