본문 바로가기
잡(job)기술

데이터베이스 성능의 핵심, 복합 인덱스를 이해하는 4가지 포인트

by 무니이구나 2026. 2. 4.

들어가며: 쿼리가 느려지는 이유

서비스가 커지면 데이터도 늘어난다. 1초 만에 끝나던 쿼리가 100만, 1,000만 건 앞에서는 느려진다. 인덱스 없는 데이터베이스는 수만 권 책이 꽂힌 도서관에서 1페이지부터 끝까지 읽는 것과 같다. 이를 전체 스캔(Full Scan, O(N))이라 한다.

인덱스는 단순한 성능 도구가 아니다. "이 서비스는 특정 데이터를 이 순서로 빠르게 조회하겠다"라는 개발자의 의도를 표현한다. 복합 인덱스(Composite Index)를 이해하면 시스템 병목을 해결할 통찰력을 얻을 수 있다.


1. 정렬 순서가 성능을 결정한다

복합 인덱스 idx_metrics_stream_time(['streamId', 'recordedAt'])는 단순 컬럼 묶음이 아니다. 계층적 정렬 구조를 가진다.

  • 첫 번째 컬럼 streamId로 먼저 정렬
  • 같은 streamId 내에서 recordedAt 순으로 정렬

비유: streamId가 챕터, recordedAt이 페이지인 책장 구조와 같다.

B-tree 계층 구조 예시

 

이 구조 덕분에 특정 스트림의 특정 시간대 데이터를 조회할 때, 데이터베이스는 챕터(streamId)를 먼저 선택하고 페이지만 연속적으로 읽으면 된다.

예제 쿼리

SELECT * 
FROM metrics 
WHERE streamId = 3 
  AND recordedAt BETWEEN '2026-01-01' AND '2026-01-02';

2. 왼쪽의 법칙(Left-most Prefix Rule)

복합 인덱스는 왼쪽 컬럼부터 조건에 사용해야 한다. 첫 번째 컬럼 없이 뒤 컬럼만 쓰면 인덱스는 무력해진다.

인덱스 활용 체크표

쿼리 조건 인덱스 활용 여부
WHERE streamId = ? ✅ 효과적
WHERE streamId = ? AND recordedAt BETWEEN ? AND ? ✅ 최적 상태
WHERE streamId = ? ORDER BY recordedAt DESC ✅ 정렬 활용 가능
WHERE recordedAt = ? ❌ 인덱스 효과 거의 없음

 

주의: 지표 데이터(FPS, Bitrate 등)는 동일한 streamId와 recordedAt을 가진 로우가 여러 개 존재할 수 있다. 따라서 @Unique 제약을 걸면 안 된다.


3. 인덱스는 '지도'일 뿐 '보물'이 아니다

인덱스에는 실제 데이터가 아닌 위치 정보가 들어 있다.

DB 종류 인덱스 구조
PostgreSQL ctid로 테이블(Heap) 위치 참조
MySQL (InnoDB) 보조 인덱스는 PK 값으로 참조 → 더블 룩업 발생

 

인덱스 과다 사용 문제

  • 저장 공간 증가
  • 삽입/수정 시 인덱스 재작성 → 쓰기 성능 저하

핵심: 모든 컬럼을 인덱스에 넣으면 안 된다. 인덱스 최적화는 항상 트레이드오프다.


4. 커버링 인덱스(Covering Index)

커버링 인덱스는 쿼리에 필요한 컬럼이 모두 인덱스에 포함된 상태다. 테이블로 이동하지 않고 인덱스만 읽어 결과를 반환할 수 있다.

예제: Index Only Scan

SELECT streamId, recordedAt 
FROM metrics
WHERE streamId = 3;

조회 과정 비교

종류 탐색 과정 장점
일반 인덱스 인덱스 → 테이블 이동 → 데이터 조회 인덱스 크기 작음, 일부 컬럼만 활용 가능
커버링 인덱스 인덱스만 읽고 결과 반환 테이블 이동 없음 → 속도 극대화

 

결과: 수천만 건도 빠르게 조회 가능. 필요한 컬럼만 선택하면 성능을 극대화할 수 있다.


마무리: 인덱스 설계는 서비스 설계와 같다

  • 인덱스 이름 하나에도 전략이 담겨야 한다 (idx_metrics_stream_time)
  • 선두 컬럼 결정과 인덱스 구성은 선택이 아니라 필수
  • 인덱스는 속도를 위한 마법이 아니라, 계산된 트레이드오프의 결과

질문: 지금 설계한 복합 인덱스의 첫 번째 컬럼은 제 역할을 하고 있는가?