Database/MongoDB
[MongoDB] MongoDB 인덱스 2편 - 복합 인덱스의 원리와 효과적인 사용법
comgu
2025. 6. 1. 12:03
반응형
복합 인덱스(Compound Index)는 MongoDB에서 가장 강력하면서도 까다로운 인덱스 유형이다. 여러 필드를 조합하여 복잡한 쿼리의 성능을 극적으로 향상시킬 수 있지만, 잘못 설계하면 오히려 성능을 해칠 수 있다.
목차
복합 인덱스의 내부 동작 원리
B-tree에서의 저장 구조
복합 인덱스 [("username", 1), ("age", -1), ("created_at", -1)]는 다음과 같이 정렬되어 저장된다:
alice, 30, 2024-01-15
alice, 25, 2024-01-14
alice, 20, 2024-01-12
bob, 35, 2024-01-11
bob, 28, 2024-01-13
charlie, 40, 2024-01-16
charlie, 22, 2024-01-10
탐색 과정의 단계별 분석
쿼리 {"username": "bob", "age": 28}를 실행할 때:
1단계: 첫 번째 필드로 범위 좁히기
- B-tree에서 username = "bob"인 구간을 빠르게 찾음
- 전체 데이터에서 bob 관련 데이터만 추출
2단계: 두 번째 필드로 정확한 위치 찾기
- username = "bob"인 영역 내에서 age = 28인 레코드 탐색
- 이미 축소된 범위에서만 검색하므로 매우 효율적
인덱스 프리픽스 규칙의 의미
왜 [B]나 [B, C] 쿼리는 인덱스를 활용할 수 없을까?
핵심 이유: B-tree 탐색은 항상 첫 번째 필드부터 시작해야 한다.
# 복합 인덱스: [("category", 1), ("price", -1), ("created_at", -1)]
# ✅ 효율적인 쿼리들 (인덱스 활용 가능)
await collection.find({"category": "electronics"})
await collection.find({"category": "electronics", "price": {"$gte": 500}})
await collection.find({
"category": "electronics",
"price": {"$gte": 500},
"created_at": {"$gte": datetime(2024, 1, 14)}
})
# ⚠️ 부분적으로 효율적 (category만 인덱스 사용)
await collection.find({
"category": "electronics",
"created_at": {"$gte": datetime(2024, 1, 14)}
})
# ❌ 비효율적인 쿼리들 (Collection Scan 발생)
await collection.find({"price": {"$gte": 500}})
await collection.find({"created_at": {"$gte": datetime(2024, 1, 14)}})
await collection.find({
"price": {"$gte": 500},
"created_at": {"$gte": datetime(2024, 1, 14)}
})
성능 차이의 실제 측정
# 100만 개 문서가 있는 컬렉션에서
# 인덱스 활용 쿼리 - 0.001초
result1 = await collection.find({"category": "electronics"})
# 부분 인덱스 활용 - 0.005초
result2 = await collection.find({
"category": "electronics",
"created_at": {"$gte": datetime(2024, 1, 12)}
})
# Collection Scan - 2.5초
result3 = await collection.find({"price": {"$gte": 500}})
복합 인덱스 설계 전략
1. ESR 규칙 (Equality, Sort, Range)
필드 순서를 정할 때 다음 우선순위를 따르는 것이 좋다:
- Equality (동등 조건): = 조건으로 검색하는 필드
- Sort (정렬): 정렬 기준이 되는 필드
- Range (범위 조건): $gte, $lt 등 범위 조건 필드
# 쿼리 패턴 분석
queries = [
{"status": "published", "category": "tech", "views": {"$gte": 1000}},
{"status": "published", "category": "python", "created_at": {"$gte": "2024-01-01"}},
{"status": "draft", "category": "tutorial"}
]
# ESR 규칙에 따른 최적 인덱스
collection.create_index([
("status", 1), # Equality - 항상 등등 조건
("category", 1), # Equality - 항상 등등 조건
("views", -1), # Range - 범위 조건
("created_at", -1) # Range - 범위 조건
])
2. 카디널리티 고려
카디널리티: 필드가 가질 수 있는 고유값의 개수
# 높은 카디널리티 → 낮은 카디널리티 순서로 배치
collection.create_index([
("user_id", 1), # 카디널리티: 100만 (높음)
("status", 1), # 카디널리티: 3 (낮음)
("created_at", -1)
])
이유: 높은 카디널리티 필드를 앞에 배치하면 더 효과적으로 데이터 범위를 축소할 수 있다.
3. 쿼리 빈도 분석
# 80%의 쿼리가 이 패턴이라면
frequent_queries = [
{"category": "tech", "status": "published"},
{"category": "python", "status": "published"},
{"category": "tutorial", "status": "draft"}
]
# category를 첫 번째 필드로 배치
collection.create_index([("category", 1), ("status", 1), ("created_at", -1)])
# 20%의 쿼리가 이 패턴이라면
rare_queries = [
{"status": "published", "views": {"$gte": 1000}},
{"status": "draft", "author_id": "12345"}
]
# 별도 인덱스 생성 고려
collection.create_index([("status", 1), ("views", -1)])
복합 인덱스의 고급 활용
1. 정렬 최적화
# 정렬이 포함된 쿼리
result = await collection.find({
"category": "technology"
}).sort([("created_at", -1)]).limit(10)
# 최적 인덱스: 필터 + 정렬 조합
collection.create_index([("category", 1), ("created_at", -1)])
효과: 정렬 작업이 메모리에서 수행되지 않고 인덱스 순서를 그대로 활용
2. 커버링 인덱스 (Covering Index)
# 자주 사용되는 쿼리
result = await collection.find(
{"category": "tech", "status": "published"},
{"title": 1, "created_at": 1, "_id": 0}
)
# 모든 필드를 포함하는 커버링 인덱스
collection.create_index([
("category", 1),
("status", 1),
("title", 1),
("created_at", -1)
])
효과: 실제 문서를 읽지 않고 인덱스만으로 결과 반환 (성능 극대화)
3. 부분 인덱스와의 조합
# 활성 사용자의 최근 활동만 빠르게 검색
collection.create_index(
[("user_id", 1), ("action_type", 1), ("timestamp", -1)],
partialFilterExpression={
"is_active": True,
"timestamp": {"$gte": datetime(2024, 1, 1)}
}
)
실행 계획으로 성능 검증
1. 기본 실행 계획 확인
explain = await collection.find({
"category": "electronics",
"price": {"$gte": 500}
}).explain()
# 인덱스 사용 여부 확인
winning_plan = explain["queryPlanner"]["winningPlan"]
if winning_plan["stage"] == "IXSCAN":
print(f"✅ 인덱스 사용: {winning_plan['indexName']}")
print(f"검사한 키: {explain['executionStats']['totalKeysExamined']}")
print(f"검사한 문서: {explain['executionStats']['totalDocsExamined']}")
else:
print("❌ Collection Scan 발생")
2. 상세 성능 분석
# 실행 통계 상세 분석
stats = explain["executionStats"]
# 효율성 지표
key_to_doc_ratio = stats["totalKeysExamined"] / stats["totalDocsExamined"]
selectivity = stats["totalDocsReturned"] / stats["totalDocsExamined"]
print(f"키/문서 비율: {key_to_doc_ratio:.2f}") # 1에 가까울수록 좋음
print(f"선택도: {selectivity:.2f}") # 1에 가까울수록 좋음
print(f"실행 시간: {stats['executionTimeMillis']}ms")
복합 인덱스 설계 체크리스트
✅ 해야 할 것들
- 쿼리 패턴 분석: 실제 애플리케이션의 쿼리 로그 분석
- ESR 규칙 적용: Equality → Sort → Range 순서
- 카디널리티 고려: 높은 카디널리티 필드를 앞쪽에 배치
- 실행 계획 검증: explain()으로 성능 확인
- 정기적인 모니터링: 인덱스 사용률과 성능 추적
❌ 피해야 할 것들
- 과도한 복합 인덱스: 필요 이상으로 많은 필드 포함
- 중복 인덱스: 이미 존재하는 인덱스와 겹치는 조합
- 잘못된 필드 순서: ESR 규칙을 무시한 순서
- 미사용 인덱스: 실제로 사용되지 않는 인덱스 방치
- 성능 검증 없이 운영: explain() 없이 인덱스 배포
다음 편 예고
3편에서는 텍스트 인덱스의 심화 원리와 검색 성능 최적화 방법에 대해 알아본다. 특히 일반 인덱스 vs 텍스트 인덱스의 차이점과 복합 텍스트 인덱스의 효과적인 활용법을 다룬다.
반응형