데이터베이스 최적화 (Database Optimization)
목차
데이터베이스 최적화란
데이터베이스 최적화(Database Optimization) 는 쿼리, 인덱스, 스키마, 하드웨어, 캐시, 운영 설정을 조정해 더 적은 자원으로 더 빠르고 안정적으로 데이터를 처리하도록 만드는 작업입니다.
실무에서는 단순히 “쿼리를 빠르게 만든다”보다 다음 질문에 답하는 과정에 가깝습니다.
- 어떤 요청이 느린가
- 왜 느린가
- 병목이 CPU, I/O, 락, 네트워크, 인덱스 부재 중 어디에 있는가
- 구조를 바꿔야 하는가, 아니면 쿼리만 고치면 되는가
최적화는 병목 찾기부터 시작한다
최적화는 감으로 하면 안 되고, 실행 계획과 운영 지표를 함께 봐야 합니다.
- 느린 쿼리 수집
- 슬로우 쿼리 로그, APM, 애플리케이션 타이머로 후보를 모읍니다.
- 실행 계획 분석
EXPLAIN,EXPLAIN ANALYZE로 어떤 스캔과 조인이 발생하는지 확인합니다.1
- 데이터 분포 확인
- 전체 row 수, 조건 선택도, null 비율, 편향된 값 분포를 봅니다.
- 변경 전후 비교
- 인덱스 추가, 쿼리 수정, 스키마 변경 후 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배 증가 시 유지되는지 봐야 합니다.
면접 포인트
- 최적화는 인덱스 추가가 아니라 병목 진단부터 시작합니다.
- 복합 인덱스는 컬럼 순서가 핵심입니다.
- 읽기 성능을 높이면 쓰기 비용과 저장 공간 비용이 함께 증가합니다.
- 파티셔닝은 운영성과 삭제 효율에도 큰 영향을 줍니다.
- 실행 계획을 읽고 변경 전후를 수치로 비교할 수 있어야 실무형 답변이 됩니다.