인덱스는 DB의 검색을 빠르게 하기 위하여 데이터의 순서를 미리 정리해 두는 과정이다.
index는 어떤 데이터가 도큐먼트에 추가되거나 수정될 때( write 작업) 그 컬렉션에 생성되어 있는 index도 새로운 도큐먼트를 포함시켜 수정된다. 이로 인하여 index 추가 시 wirte 작업은 느려질 수 있다. 따라서 index는 read 작업 위주의 애플리케션에서 유용하고 읽기보다 쓰기 작업이 많으면 index를 추가하는 것은 고려해야 한다.
가장 기본적인 인덱스. 하나의 필드에 대해 B-Tree 구조로 인덱스를 생성한다. 1은 오름차순, -1은 내림차순이지만, 단일 필드 인덱스는 양방향 탐색이 가능해서 방향이 크게 의미 없다.
// age 필드에 오름차순 인덱스 생성
db.users.createIndex({ age: 1 })
// 특정 필드가 존재하는 도큐먼트만 인덱싱 (sparse 옵션)
db.users.createIndex(
{ phone: 1 },
{ sparse: true }
)
💡
_id필드는 MongoDB가 컬렉션 생성 시 자동으로 단일 필드 인덱스를 만들어준다. 삭제 불가.
두 개 이상의 필드를 묶어 하나의 인덱스로 생성. 필드 순서가 핵심이다 — 앞 필드 기준으로 먼저 정렬되고, 값이 같을 때 다음 필드로 넘어간다.
MongoDB 공식 문서에서 권장하는 ESR 규칙을 따르면 대부분의 경우 최적 성능을 낼 수 있다.
// ESR 규칙: Equality → Sort → Range 순서로 필드 배치
db.orders.createIndex({
status: 1, // Equality (등호 조건)
createdAt: -1, // Sort (정렬 기준)
amount: 1 // Range (범위 조건)
})
💡 인덱스 prefix 활용:
{ status, createdAt, amount }인덱스가 있으면{ status },{ status, createdAt }쿼리도 이 인덱스가 커버한다.
필드 값이 배열일 때 MongoDB가 자동으로 적용하는 인덱스. 배열 내 각 요소마다 별도의 인덱스 엔트리가 생성된다. 별도 선언이 필요 없고, 배열 필드에 createIndex를 걸면 자동으로 다중 키 인덱스가 된다.
// tags 필드가 배열인 경우 → 자동으로 Multikey 인덱스
// { title: "MongoDB", tags: ["database", "nosql", "index"] }
db.posts.createIndex({ tags: 1 })
// 이제 아래 쿼리들이 인덱스를 탄다
db.posts.find({ tags: "nosql" })
db.posts.find({ tags: { $in: ["database", "index"] } })
⚠️ 복합 인덱스에서 배열 타입 필드는 1개만 허용. 두 배열 필드를 묶은 복합 인덱스는 생성 자체가 불가능하다.
좌표 데이터에 특화된 인덱스. 2dsphere(지구 구면 기준, 경위도)와 2d(평면 좌표) 두 종류가 있다. 실무에서는 대부분 GeoJSON과 함께 2dsphere를 쓴다.
// GeoJSON 형식으로 위치 저장
db.restaurants.insertOne({
name: "맛있는 식당",
location: {
type: "Point",
coordinates: [126.9784, 37.5665] // [경도, 위도]
}
})
db.restaurants.createIndex({ location: "2dsphere" })
// 현재 위치 기준 1km 내 식당 검색
db.restaurants.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [126.97, 37.56] },
$maxDistance: 1000
}
}
})
💡 배달 앱, 지도 기반 서비스의 핵심 인덱스.
$near,$geoWithin,$geoIntersects연산자와 함께 사용.
문자열 필드에 대해 형태소 분석 기반의 풀텍스트 검색을 지원. stop word 처리와 stemming(어근 추출)이 내장돼 있다. 컬렉션 당 텍스트 인덱스는 1개만 생성 가능하지만, 여러 필드를 묶을 수 있다.
// 여러 필드를 하나의 텍스트 인덱스로
db.articles.createIndex({
title: "text",
body: "text",
summary: "text"
})
// 검색
db.articles.find({
$text: { $search: "MongoDB index performance" }
})
// 관련도 점수 기준 정렬
db.articles.find(
{ $text: { $search: "MongoDB" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })
⚠️ 한국어 형태소 분석은 기본 텍스트 인덱스로 커버되지 않는다. 한국어 검색이 필요하다면 Atlas Search(Lucene 기반) 사용을 고려할 것.
필드 값을 해시 함수에 통과시켜 저장하는 인덱스. 동등 비교(=)만 지원하고 범위 쿼리($gt, $lt 등)는 불가. 주 용도는 샤딩 환경에서 데이터를 균등 분산하는 것이다.
// 해시 인덱스 생성
db.users.createIndex({ userId: "hashed" })
// 샤딩 설정 시 활용
sh.shardCollection(
"mydb.users",
{ userId: "hashed" }
)
⚠️ 범위 검색이 필요한 필드에는 단일 or 복합 인덱스를 써야 한다. 해시 인덱스는 샤딩 키 용도로만 사용하는 게 일반적.
하나의 도큐먼트에 N개의 인덱스가 있고, 도큐먼트에 쿼리를 요청하면 몽고DB는 쿼리 모양을 확인한다. 쿼리 모양에서는 검색할 필드와 정렬 여부 등 추가 정보가 있다. 해당 쿼리 모양을 통하여 쿼리에 충족하는 인덱스 후보 집합을 식별한다고 한다. 즉, 쿼리 레이스라고 이야기한다.
쿼리 레이스의 순서
몽고DB가 N개의 인덱스 중 4개의 인덱스 후보를 잡는다.
각 인덱스에 대한 쿼리 플랜을 만든다.
병렬 스레드를 사용하여 쿼리를 실행한다.
가장 빨리 결과를 반환하는 쿼리에 대해서 캐시화하여 쿼리플랜으로 만들어 관리한다.
이때 성공한 플랜은 몽고DB explain에서 winning plan, 그 외 플랜에 대해서는 reject plan으로 나온다.
쿼리플랜의 유지 시간
컬렉션과 인덱스(추가, 수정, 삭제)에 대해서 변경 및 몽고DB 프로세스가 재시작하면 삭제된다. 또한 명시적으로 제거할 수 있다.
| 인덱스 | 주요 용도 | 범위 쿼리 | 특이사항 |
|---|---|---|---|
| 단일 필드 | 단일 조건 쿼리 | ✅ | 기본, _id 자동 생성 |
| 복합 | 다중 조건/정렬 | ✅ | ESR 규칙, prefix 활용 |
| 다중 키 | 배열 필드 검색 | ✅ | 배열 시 자동 적용 |
| 지리 공간 | 위치 기반 검색 | ✅ | 2dsphere / 2d 구분 |
| 텍스트 | 풀텍스트 검색 | ❌ | 컬렉션당 1개 제한 |
| 해시 | 샤딩 균등 분산 | ❌ | 동등 비교만 지원 |