[MongoDB] 인덱싱 #1
인덱싱 소개
MongoDB에서 인덱스는 데이터 검색 성능을 향상시키는 핵심 요소다. 인덱스가 없는 경우, MongoDB는 컬렉션의 모든 문서를 순차적으로 검색하는 컬렉션 스캔을 수행해야 한다. 이는 데이터 양이 많아질수록 성능 저하시킨다는 점에 유의해야한다.
인덱스를 생성하면 MongoDB는 특정 필드에 대한 값을 정렬된 형태로 저장하고, 해당 필드의 값을 기반으로 문서를 빠르게 찾을 수 있다. 이는 RDB의 인덱스와 유사하며, 효율적인 데이터 검색을 가능하게 한다.
인덱스 생성
MongoDB에서 인덱스를 생성하는 기본적인 방법은 createIndex() 메서드를 사용하는 것이다.
// 단일 필드 인덱스 생성
db.collection.createIndex({ fieldName: 1 }); // 오름차순 인덱스
db.collection.createIndex({ fieldName: -1 }); // 내림차순 인덱스
인덱스를 생성하면 explain()을 통해 인덱스가 적용되는지 확인할 수 있다.
db.collection.find({ fieldName: "value" }).explain("executionStats");
복합 인덱스 소개
복합 인덱스는 두 개 이상의 필드에 대해 단일 인덱스를 생성하는 방식이다. 이는 특정 필드의 조합을 기반으로 검색할 때 성능을 크게 향상시킨다.
db.collection.createIndex({ field1: 1, field2: -1 });
다만, 정렬을 포함할 때 결과가 32MB 이상이면 MongoDB는 데이터가 너무 많아서 정렬을 거부한다는 아래와 같은 오류가 발생할 수 있다.
> db.users.find({"age": {"$gte": 21, "$lte": 30}}).sort({"username": 1})
OperationFailure: Executor error during find command: OperationFailed: Sort operation used more than the maximum 33554432 bytes of RAM. Add an index, or specify a smaller limit.
이 오류는 정렬(Sort) 작업에 사용된 RAM이 32MB를 초과했기 때문이다. 해결 방법은 정렬 대상 필드에 인덱스를 추가하는 것이다.
db.collection.createIndex({ age: 1 });
복합 인덱스를 활용하여 여러 조건을 포함한 정렬도 최적화할 수 있다.
db.collection.createIndex({ username: 1, age: 1 });
이렇게 하면 필터링과 정렬이 동시에 인덱스를 활용하여 수행되므로 성능이 개선된다.
MongoDB가 인덱스를 선택하는 방법

MongoDB는 쿼리를 실행할 때 여러 실행 계획을 고려하고, 최적의 인덱스를 선택한다. MongoDB는 n개의 실행 계획을 병렬 스레드에서 실행하며, 가장 빠르게 실행이 완료된 계획을 선택한다. 이를 플랜 캐시방식으로 관리하며, 이후 동일한 쿼리 실행 시 가장 효율적인 계획을 재사용한다. 여기서 인덱스를 추가하거나 삭제, 변경되거나 컬렉션이 변경되면 캐시에서 제거된다. (서버 재시작할 대도 제거된다.)
db.collection.find({ field1: "value" }).explain("executionStats");
이를 통해 MongoDB가 어떤 인덱스를 선택했는지 확인할 수 있다.
복합 인덱스 사용
MongoDB는 최적의 실행 계획을 자동으로 선택하지만, 특정 인덱스를 강제하려면 hint()를 사용할 수 있다.
db.collection.find({ field1: "value" }).hint({ field1: 1 });
그러나 hint를 사용하지 않고도 효율적으로 실행하려면 적절한 인덱스를 설계하고, explain()을 활용해 실행 계획을 검토해야 한다. (hint()를 사용하지 않는 것을 권장하는 것 같다.)
키 방향 선택하기
복합 인덱스와 더불어 단일 인덱스일 경우에도 오름차순 혹은 내림차순으로 인덱스 키를 지정할 수 있었다. 여기서 주의해야할 점은 역방향 인덱스다. MongoDB에서 역방향 인덱스는 서로 동등하다. 예를 들어, {"age": -1}로 정령해야하는데 {"age": 1} 인덱스를 가져도 {"age": -1}로 인덱스를 가질 때 처럼 최적화할 수 있다.
커버드 쿼리
인덱스가 포함한 필드만으로 쿼리를 해결하는 경우 디스크 I/O를 줄여 성능이 향상된다.
db.collection.createIndex({ field1: 1, field2: 1 });
db.collection.find({ field1: "value" }, { field2: 1, _id: 0 }).explain("executionStats");
이렇게 하면 MongoDB는 문서를 직접 조회하지 않고 인덱스만으로 결과를 반환한다.
암시적 인덱스
MongoDB에서 인덱스가 N개의 키를 가진다면 키들의 앞 부분은 '공짜'인덱스가 된다. 아래 예시를 보자.
{"a": 1, "b": 1,..., "z": 1} // 이러한 인덱스가 있을 때
{"a": 1}, {"a": 1, "b": 1}, {"a": 1, "b": 1, "c": 1},...,{"a": 1, "b": 1,..., "z": 1} // 이러한 인덱스를 가진다.
$ 연산자의 인덱스 사용법
비효율적인 연산자: $ne
$ne(Not Equal) 연산자는 인덱스를 활용하지 못하므로 주의해야 한다.
db.collection.find({ field: { $ne: "value" } }); // 컬렉션 스캔 발생
범위
범위 검색에 사용되는 필드의 경우에는 인덱스를 생성할 대 마지막에 위치시키는 것이 좋다. 즉, 다중 필드로 인덱스를 설계할 때는 완전 일치가 사용되는 필드를 첫 번째에, 범위가 사용되는 필드를 마지막에 놓는다.
db.collection.find({ age: { $gt: 25 } }).explain("executionStats");
OR 쿼리
$or 연산자는 $or 절마다 하나씩 인덱스를 사용할 수 있다. 실제 explain을 보면 n 개의 인덱스 상에 있는 분리뒨 n 개의 쿼리 집합체다. 즉 n 번 쿼리해서 병합하므로 한 번 쿼리할 때보다 훨씬 비효율적이다. 따라서 $or 연산자 말고 $in 연산자를 사용하자.
객체 및 배열 인덱싱
MongoDB는 객체와 배열 필드에도 인덱스를 적용할 수 있다.
단, 배열 인덱싱의 경우, "comments.4"와 같이 특정 배열 요소를 찾는 쿼리에는 인덱스를 사용할 수 없다.
db.collection.createIndex({ "loc.city": 1 }); // 내장 도큐먼트의 서브 필드 인덱싱
db.collection.createIndex({ "loc": 1 }); // 내장 도큐먼트 인덱싱은 서브 도큐먼트 전체에 쿼리할 때만 도움이 된다.
db.collection.createIndex({ comments.date: 1 }); // 배열 인덱싱: comments의 개수만큼 인덱스가 생성되므로 지양한다.
db.collection.createIndex({ comments.10.votes: 1 }); // 배열 인덱싱: 11번째 배열 요소를 쿼리할 때만 유용하다.
인덱스 카디널리티
인덱스 카디널리티는 특정 필드의 고유한 값의 개수를 의미한다. 일반적으로 고유한 값이 많을수록 인덱스의 효율이 증가한다.
- 낮은 카디널리티: 성별과 같이 값의 종류가 적은 필드(인덱스 효율이 낮음)
- 높은 카디널리티: 이메일, 주민등록번호 등 유일한 값이 많은 필드(인덱스 효율이 높음)