소개
이 랩에서는 쿼리 성능을 최적화하기 위해 MongoDB 인덱스를 사용하는 기본 사항을 배우게 됩니다. 인덱스는 컬렉션의 작고 검색하기 쉬운 데이터 부분을 보유하는 특수 데이터 구조로, MongoDB 가 전체 컬렉션을 스캔하는 것보다 훨씬 빠르게 문서를 찾을 수 있도록 합니다.
먼저 인덱스 없이 쿼리 성능을 관찰하는 것부터 시작합니다. 그런 다음 단일 필드 및 복합 인덱스를 생성하고 이러한 인덱스가 쿼리 및 정렬 속도를 극적으로 향상시키는 방법을 확인합니다. 마지막으로 인덱스를 나열하고 제거하여 인덱스를 관리하는 방법을 배우게 됩니다. 이 랩이 끝나면 MongoDB 애플리케이션을 더 효율적으로 만들기 위해 인덱스를 생성하고 사용하는 방법에 대한 실질적인 이해를 갖게 될 것입니다.
인덱스 없이 쿼리하기
인덱스를 생성하기 전에 인덱스 없이 MongoDB 가 어떻게 작동하는지 이해하는 것이 중요합니다. 이 단계에서는 샘플 컬렉션을 설정하고, 쿼리를 실행하고, 전체 컬렉션 스캔의 성능 영향을 확인하기 위해 실행 계획을 분석합니다.
먼저 MongoDB Shell (mongosh) 을 열어 데이터베이스와 상호 작용합니다. 이 명령줄 인터페이스를 통해 MongoDB 인스턴스에 직접 명령을 실행할 수 있습니다.
mongosh
쉘에 들어가면 > 프롬프트가 표시됩니다. indexlab이라는 새 데이터베이스로 전환하고 users 컬렉션에 샘플 문서를 삽입해 보겠습니다. 데이터베이스나 컬렉션이 존재하지 않으면 MongoDB 가 자동으로 생성합니다.
use indexlab
db.users.insertMany([
{ name: "Alice", age: 28, city: "New York" },
{ name: "Bob", age: 35, city: "San Francisco" },
{ name: "Charlie", age: 42, city: "Chicago" },
{ name: "David", age: 25, city: "New York" },
{ name: "Eve", age: 31, city: "San Francisco" }
]);
이제 30 세 이상인 모든 사용자를 찾아보겠습니다. MongoDB 가 이 쿼리를 어떻게 실행하는지 확인하기 위해 .explain("executionStats") 메서드를 사용합니다. 이 메서드는 쿼리의 실행 계획에 대한 자세한 통계를 제공합니다.
db.users.find({ age: { $gt: 30 } }).explain("executionStats");
출력은 쿼리 실행에 대한 자세한 통계를 제공합니다. winningPlan 및 executionStats 섹션을 확인하십시오.
예시 출력 (일부 발췌):
{
"queryPlanner": {
"winningPlan": {
"stage": "COLLSCAN",
"filter": { "age": { "$gt": 30 } },
"direction": "forward"
}
},
"executionStats": {
"executionSuccess": true,
"nReturned": 3,
"executionTimeMillis": 0,
"totalKeysExamined": 0,
"totalDocsExamined": 5
}
}
여기서 핵심 정보는 stage: "COLLSCAN" 및 totalDocsExamined: 5입니다.
COLLSCAN은 "Collection Scan"을 의미합니다. 이는 MongoDB 가 쿼리와 일치하는 문서를 찾기 위해 컬렉션의 모든 문서를 검사해야 했음을 의미합니다.totalDocsExamined: 5는 컬렉션의 모든 5 개 문서가 스캔되었음을 확인합니다.
작은 컬렉션의 경우 빠르지만, 수백만 개의 문서에 대한 컬렉션 스캔은 매우 느릴 것입니다. 다음 단계에서는 인덱스를 추가하여 이를 수정합니다.
단일 필드 인덱스 생성 및 사용
이제 컬렉션 스캔의 비효율성을 확인했으므로 인덱스를 생성하여 성능을 개선해 보겠습니다. age 필드에 대한 인덱스를 사용하면 MongoDB 가 전체 컬렉션을 스캔하지 않고 관련 문서를 빠르게 찾을 수 있습니다.
이전 단계에서 사용했던 mongosh 쉘에 계속 있어야 합니다.
age 필드에 오름차순으로 인덱스를 생성합니다. 1은 오름차순 인덱스를 지정하고, -1은 내림차순 인덱스를 지정합니다.
db.users.createIndex({ age: 1 });
MongoDB 는 인덱스가 성공적으로 생성되었음을 확인합니다. 이 인덱스의 기본 이름은 age_1입니다.
이제 이전 단계와 동일한 쿼리를 실행하고 실행 계획을 검토합니다.
db.users.find({ age: { $gt: 30 } }).explain("executionStats");
예시 출력 (일부 발췌):
{
"queryPlanner": {
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "age": 1 },
"indexName": "age_1"
}
}
},
"executionStats": {
"executionSuccess": true,
"nReturned": 3,
"executionTimeMillis": 0,
"totalKeysExamined": 3,
"totalDocsExamined": 3
}
}
실행 계획의 중요한 변경 사항을 확인하십시오.
stage가 이제IXSCAN으로 변경되었습니다. 이는 "Index Scan"을 의미하며, MongoDB 가age_1인덱스를 사용하여 일치하는 문서를 찾았음을 나타냅니다.totalKeysExamined와totalDocsExamined가 이제 5 가 아닌 3 입니다. MongoDB 는 인덱스를 통해 쿼리와 일치하는 3 개의 문서만 확인하면 되었고 나머지 2 개는 무시했습니다. 이것이 성능 향상의 원인입니다.
복합 인덱스를 사용한 정렬
인덱스는 쿼리 속도 향상뿐만 아니라 효율적인 정렬에도 매우 중요합니다. 인덱스가 없는 필드로 정렬하면 MongoDB 는 메모리 내에서 정렬을 수행해야 하며, 이는 느리고 상당한 RAM 을 소비할 수 있습니다. 여러 필드를 포함하는 **복합 인덱스 (compound index)**는 해당 필드로 필터링하고 정렬하는 쿼리를 최적화할 수 있습니다.
city(오름차순) 및 age(내림차순) 필드에 복합 인덱스를 생성해 보겠습니다. 인덱스 내 필드의 순서는 인덱스가 사용되는 방식에 중요합니다.
db.users.createIndex({ city: 1, age: -1 });
이제 사용자를 도시별로, 그리고 나이별로 정렬하는 쿼리를 실행해 보겠습니다. 인덱스가 정렬에 사용되는지 확인하기 위해 다시 .explain()을 사용합니다.
db.users.find().sort({ city: 1, age: -1 }).explain("executionStats");
예시 출력 (일부 발췌):
{
"queryPlanner": {
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "city": 1, "age": -1 },
"indexName": "city_1_age_-1"
}
}
}
}
IXSCAN 단계는 MongoDB 가 새로운 city_1_age_-1 인덱스를 사용했음을 보여줍니다. 데이터가 인덱스에 이미 정렬 기준에 따라 정렬되어 있으므로 MongoDB 는 별도의 비용이 많이 드는 메모리 내 정렬 단계를 수행할 필요가 없습니다.
실제 정렬된 결과를 보려면 .explain() 없이 쿼리를 실행합니다.
db.users.find().sort({ city: 1, age: -1 });
출력:
[
{ _id: ObjectId("..."), name: 'Charlie', age: 42, city: 'Chicago' },
{ _id: ObjectId("..."), name: 'Alice', age: 28, city: 'New York' },
{ _id: ObjectId("..."), name: 'David', age: 25, city: 'New York' },
{ _id: ObjectId("..."), name: 'Bob', age: 35, city: 'San Francisco' },
{ _id: ObjectId("..."), name: 'Eve', age: 31, city: 'San Francisco' }
]
문서는 복합 인덱스 정의와 일치하게 먼저 city별로 알파벳순으로, 그리고 각 도시 내에서 age별로 가장 많은 나이부터 적은 나이 순으로 올바르게 정렬됩니다.
인덱스 관리 및 제거
인덱스는 읽기 성능을 향상시키지만 무료는 아닙니다. 인덱스는 저장 공간을 소비하고 쓰기 작업 (삽입, 업데이트, 삭제) 에 약간의 오버헤드를 추가합니다. 따라서 주기적으로 사용되지 않는 인덱스를 검토하고 제거하는 것이 좋습니다.
먼저 getIndexes() 메서드를 사용하여 컬렉션의 모든 인덱스를 나열할 수 있습니다.
db.users.getIndexes();
출력:
[
{ "v": 2, "key": { "_id": 1 }, "name": "_id_" },
{ "v": 2, "key": { "age": 1 }, "name": "age_1" },
{ "v": 2, "key": { "city": 1, "age": -1 }, "name": "city_1_age_-1" }
]
이는 모든 컬렉션에 대해 자동으로 생성되는 _id 필드의 기본 인덱스와 우리가 생성한 두 개의 인덱스를 보여줍니다.
복합 인덱스 city_1_age_-1이 더 이상 필요하지 않다고 가정해 보겠습니다. 인덱스 이름을 인수로 전달하는 dropIndex() 메서드를 사용하여 제거할 수 있습니다.
db.users.dropIndex("city_1_age_-1");
MongoDB 는 삭제 작업 전에 존재했던 인덱스 수를 나타내는 객체를 반환합니다.
{ "nIndexesWas": 3, "ok": 1 }
이제 인덱스를 다시 나열하여 인덱스가 제거되었는지 확인합니다.
db.users.getIndexes();
출력:
[
{ "v": 2, "key": { "_id": 1 }, "name": "_id_" },
{ "v": 2, "key": { "age": 1 }, "name": "age_1" }
]
보시다시피 city_1_age_-1 인덱스가 사라졌습니다. 적절한 인덱스 관리는 건강하고 성능이 뛰어난 데이터베이스를 유지하는 핵심 부분입니다.
MongoDB 쉘을 종료하려면 exit를 입력하거나 Ctrl+D를 누르면 됩니다.
exit;
요약
이 실습에서는 MongoDB 인덱스를 사용하는 필수 기법을 배웠습니다. 인덱스가 없는 쿼리에서 COLLSCAN을 관찰하고 성능 제한 사항을 이해하는 것으로 시작했습니다. 그런 다음 단일 필드 인덱스를 생성하여 쿼리 계획을 훨씬 더 효율적인 IXSCAN으로 변경했습니다.
또한 복합 인덱스를 탐색하고 비용이 많이 드는 메모리 내 정렬을 피하면서 정렬 작업을 최적화하는 데 어떻게 사용할 수 있는지 확인했습니다. 마지막으로 getIndexes()로 인덱스를 나열하고 dropIndex()로 사용되지 않는 인덱스를 제거하여 인덱스를 관리하는 방법을 배웠습니다. 이러한 기술은 MongoDB 로 빠르고 확장 가능한 애플리케이션을 구축하는 데 기본이 됩니다.

