MongoDB에서 $elemMatch 연산자를 사용할 때 인덱스가 제대로 작동하는지 확인하려면 explain() 메서드를 활용하는 것이 가장 확실한 방법입니다. 이번 글에서는 인덱스 생성부터 쿼리 실행 계획 분석까지 전 과정을 실제 예제와 함께 단계별로 살펴보겠습니다.
인덱스 생성 및 테스트 데이터 준비
먼저 컬렉션에 인덱스를 생성합니다. 아래 예제에서는 Information.StudentDetails.StudentName 필드에 대해 sparse 옵션과 background 옵션을 적용한 인덱스를 생성합니다. sparse 인덱스는 해당 필드가 존재하는 문서만 색인하므로 저장 공간을 절약할 수 있고, background 옵션은 인덱스 생성 중에도 읽기·쓰기 작업을 차단하지 않습니다.
> db.workingOfIndexesDemo.createIndex({"Information.StudentDetails.StudentName":1},{ sparse : true, background : true } );
{
"createdCollectionAutomatically" : true,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.workingOfIndexesDemo.insertOne({"Information":{"StudentDetails":{"StudentName":"Chris"}}});
{
"acknowledged" : true,
"insertedId" : ObjectId("5e06f94825ddae1f53b621f7")
}
> db.workingOfIndexesDemo.insertOne({"Information":{"StudentDetails":{"StudentName":"David"}}});
{
"acknowledged" : true,
"insertedId" : ObjectId("5e06f94f25ddae1f53b621f8")
}
> db.workingOfIndexesDemo.insertOne({"Information":{"StudentDetails":{"StudentName":"Mike"}}});
{
"acknowledged" : true,
"insertedId" : ObjectId("5e06f95325ddae1f53b621f9")
}실행 결과를 보면 컬렉션이 자동으로 생성되었고(createdCollectionAutomatically: true), 기본 _id 인덱스 1개에 새 인덱스 1개가 추가되어 총 2개의 인덱스가 된 것을 확인할 수 있습니다.
저장된 문서 확인하기
find() 메서드를 사용하면 컬렉션에 저장된 모든 문서를 조회할 수 있습니다.
> db.workingOfIndexesDemo.find();
실행 결과는 다음과 같습니다.
{ "_id" : ObjectId("5e06f94825ddae1f53b621f7"), "Information" : { "StudentDetails" : { "StudentName" : "Chris" } } }
{ "_id" : ObjectId("5e06f94f25ddae1f53b621f8"), "Information" : { "StudentDetails" : { "StudentName" : "David" } } }
{ "_id" : ObjectId("5e06f95325ddae1f53b621f9"), "Information" : { "StudentDetails" : { "StudentName" : "Mike" } } }$elemMatch 쿼리에 explain() 적용하기
이제 $elemMatch 조건이 포함된 쿼리에 explain()을 붙여 실행 계획을 확인해 보겠습니다.
> db.workingOfIndexesDemo.find({"Information.StudentDetails": { $elemMatch: { "StudentName" : "David"} } } ).explain();실행 결과는 다음과 같습니다.
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.workingOfIndexesDemo",
"indexFilterSet" : false,
"parsedQuery" : {
"Information.StudentDetails" : {
"$elemMatch" : {
"StudentName" : {
"$eq" : "David"
}
}
}
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"Information.StudentDetails" : {
"$elemMatch" : {
"StudentName" : {
"$eq" : "David"
}
}
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"Information.StudentDetails.StudentName" : 1
},
"indexName" : "Information.StudentDetails.StudentName_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"Information.StudentDetails.StudentName" : [ ]
},
"isUnique" : false,
"isSparse" : true,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"Information.StudentDetails.StudentName" : [
"[\"David\", \"David\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "DESKTOP-QN2RB3H",
"port" : 27017,
"version" : "4.0.5",
"gitVersion" : "3739429dd92b92d1b0ab120911a23d50bf03c412"
},
"ok" : 1
}실행 결과 해석하기
출력 결과에서 주목해야 할 핵심 포인트는 다음과 같습니다.
- IXSCAN 스테이지: winningPlan의 inputStage가
IXSCAN으로 표시되어 있습니다. 이는 컬렉션 전체를 훑는 COLLSCAN(컬렉션 스캔)이 아니라 인덱스 스캔으로 쿼리가 실행되었음을 의미하며, $elemMatch 조건에서도 인덱스가 정상적으로 활용되었음을 보여줍니다. - indexBounds:
["David", "David"]범위로 표시된 것은 인덱스가 StudentName 값이 "David"인 구간만 한정하여 스캔했다는 뜻입니다. 필요한 문서만 정확히 찾아내므로 매우 효율적입니다. - FETCH 스테이지: 인덱스로 대상 문서를 먼저 좁힌 뒤, FETCH 단계에서 해당 문서를 가져오고 남은 $elemMatch 필터를 최종적으로 적용합니다.
- isSparse: true: 앞서 sparse 옵션으로 생성한 인덱스가 그대로 사용되었음을 나타냅니다.
정리
$elemMatch를 사용한다고 해서 반드시 인덱스가 무시되는 것은 아닙니다. 인덱스가 설정된 하위 필드 경로가 $elemMatch 내부 조건과 일치하면 MongoDB는 해당 인덱스를 활용해 쿼리 성능을 크게 높일 수 있습니다. 인덱스 동작 여부가 궁금하다면 항상 explain()을 실행하여 winningPlan에 IXSCAN이 포함되어 있는지 확인하는 습관을 들이는 것이 좋습니다.