MongoDB explain()으로 쿼리 실행 계획 해석하기
MongoDB에서 find() 메서드가 어떤 방식으로 쿼리를 실행하는지, 즉 쿼리 실행 계획(query plan)을 확인하려면 explain() 메서드를 사용합니다. 이 글에서는 실제 예제를 통해 컬렉션을 만들고, 정규식 조건에 따라 인덱스 스캔 범위(index bounds)가 어떻게 달라지는지 살펴보겠습니다.
1. 컬렉션 생성 및 문서 삽입
먼저 ClientName 필드에 오름차순 인덱스를 생성한 뒤, 세 개의 문서를 삽입합니다.
> db.demo637.ensureIndex({ClientName:1});
{
"createdCollectionAutomatically" : true,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.demo637.insert({ClientName:"John"});
WriteResult({ "nInserted" : 1 })
> db.demo637.insert({ClientName:"Bob"});
WriteResult({ "nInserted" : 1 })
> db.demo637.insert({ClientName:"Johnson"});
WriteResult({ "nInserted" : 1 })2. 저장된 문서 확인
find() 메서드를 사용해 컬렉션의 모든 문서를 조회합니다.
> db.demo637.find();
실행 결과는 다음과 같습니다.
{ "_id" : ObjectId("5e9c26916c954c74be91e6db"), "ClientName" : "John" }
{ "_id" : ObjectId("5e9c26936c954c74be91e6dc"), "ClientName" : "Bob" }
{ "_id" : ObjectId("5e9c269d6c954c74be91e6dd"), "ClientName" : "Johnson" }Case 1 — 정규식 /john/ 사용 시
문자열 중간에 포함된 패턴을 찾는 /john/ 정규식 쿼리에 대해 실행 계획을 확인해 보겠습니다.
> db.demo637.find({ClientName:/john/}).explain();출력 결과:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.demo637",
"indexFilterSet" : false,
"parsedQuery" : {
"ClientName" : {
"$regex" : "john"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"ClientName" : {
"$regex" : "john"
}
},
"keyPattern" : {
"ClientName" : 1
},
"indexName" : "ClientName_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"ClientName" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"ClientName" : [
"[\"\", {})",
"[/john/, /john/]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "DESKTOP-QN2RB3H",
"port" : 27017,
"version" : "4.0.5",
"gitVersion" : "3739429dd92b92d1b0ab120911a23d50bf03c412"
},
"ok" : 1
}핵심 포인트: /john/처럼 앵커(^) 없이 문자열 중간을 검색하는 정규식은 인덱스 전체 범위인 ["", {})를 스캔합니다. 즉, 인덱스를 사용하긴 하지만 모든 키를 훑어야 하므로 대용량 컬렉션에서는 비효율적일 수 있습니다.
Case 2 — 정규식 /^john/ 사용 시
이번에는 문자열 시작 부분을 고정하는 /^john/ 정규식 쿼리의 실행 계획을 확인합니다.
> db.demo637.find({ClientName:/^john/}).explain();출력 결과:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.demo637",
"indexFilterSet" : false,
"parsedQuery" : {
"ClientName" : {
"$regex" : "^john"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"ClientName" : 1
},
"indexName" : "ClientName_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"ClientName" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"ClientName" : [
"[\"john\", \"joho\")",
"[/^john/, /^john/]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "DESKTOP-QN2RB3H",
"port" : 27017,
"version" : "4.0.5",
"gitVersion" : "3739429dd92b92d1b0ab120911a23d50bf03c412"
},
"ok" : 1
}핵심 포인트: /^john/은 접두사(prefix) 검색이므로 인덱스 경계가 ["john", "joho")로 좁혀집니다. 이는 사전 순으로 "john"부터 "joho" 직전까지만 스캔한다는 의미로, 전체 인덱스를 훑는 Case 1보다 훨씬 적은 범위만 읽어 성능이 크게 향상됩니다.
정리
explain()의winningPlan항목에서 선택된 실행 계획과 각 단계(stage)를 확인할 수 있습니다.IXSCAN은 인덱스 스캔,FETCH는 해당 인덱스 키에 매칭되는 실제 문서를 가져오는 단계입니다.indexBounds를 보면 정규식 패턴에 따라 스캔 범위가 얼마나 달라지는지 알 수 있습니다.- 성능 최적화를 위해서는 가능한 한
^앵커를 활용해 접두사 기반 검색으로 인덱스 활용도를 높이는 것이 좋습니다.