쿼리 최적화
[느린 쿼리 탐지]
잘못된 애플리케이션 설계, 부적합한 데이터모델, 부족한 하드웨어 등으로 느려질 수 있으나 쿼리 최적화 방법으로 성능 개선가능.
대부분의 애플리케이션에서 쿼리는 100ms 이내에 실행되어야 안전한다.
//v2.6 log
Mon Sep 30 21:48:58.066 [conn20] query stocks.values query: { query: {
stock_symbol: "GOOG" }, orderby: { date: -1.0 } }
ntoreturn:1 ntoskip:0 nscanned:4308303 scanAndOrder:1 keyUpdates:0
numYields: 3 locks(micros) r:4399440
nreturned:1 reslen:194 4011ms
//v3.0 log
2015-09-11T21:17:15.414+0300 I COMMAND [conn99] command green.$cmd command:
insert { insert: "system.indexes", documents: [ { _id:
ObjectId('55f31aab9a50479be0a7dcd7'), ns: "green.users", key: {
addresses.zip: 1.0 }, name: "zip" } ], ordered: false } keyUpdates:0
writeConflicts:0 numYields:0 reslen:40 locks:{ Global: { acquireCount: { r:
1, w: 1 } }, MMAPV1Journal: { acquireCount: { w: 9 } }, Database: {
acquireCount: { W: 1 } }, Collection: { acquireCount: { W: 1 } }, Metadata: {
acquireCount: { W: 5 } } } 102ms
느린 쿼리 경고 메시지 : stocks.values에 대한 쿼리, 정렬이 수행, 실행하는데 4초 걸림
grep -E '[0-9]+ms' mongod.log
해당 경고 로그를 grep하는 명령어
- 프로파일러 사용
//디폴트로 사용 불가능인 상태이므로 상태 가능으로 변경 <-> 불가능은 레벨0
//특정 데이터베이스 선택 및 프로파일링 수준 지정 (읽기, 쓰기를 로그 기록)
use stocks
db.setProfilingLevel(2)
//150밀리초 이상이 소요된 쿼리 조회
db.system.profile.find({millis: {$gt: 150}})
db.system.profile.find().sort({$natural: -1}).limit(5).pretty()
// 출력 결과
{
"op" : "query",
"ns" : "stocks.values", //컬렉션의 이름
"query" : {
"query" : { },
"orderby" : {
"close" : -1
}
},
"ntoreturn" : 1,
"ntoskip" : 0,
"nscanned" : 4308303, //스캔된 도큐먼트 수
"scanAndOrder" : true,
"keyUpdates" : 0,
"numYield" : 3,
"lockStats" : {
"timeLockedMicros" : {
"r" : NumberLong(12868747),
"w" : NumberLong(0)
},
"timeAcquiringMicros" : {
"r" : NumberLong(1838271),
"w" : NumberLong(5)
}
},
"nreturned" : 1, //반환된 도큐먼트 수
"responseLength" : 194,
"millis" : 11030, //응답속도 밀리초 단위
"ts" : ISODate("2013-09-30T06:44:40.988Z"),
"client" : "127.0.0.1",
"allUsers" : [ ],
"user" : ""
}
- 느린 쿼리 분석 : 인덱스 추가, 인덱스 재구성 및 데이터 모델 수정
// explain 사용
db.values.find({}).sort({close: -1}).limit(1).explain()
{
"cursor" : "BasicCursor", //인덱스를 사용했다면 BtreeCursor
"isMultiKey" : false,
"n" : 1, //반환된 수
"nscannedObjects" : 4308303,
"nscanned" : 4308303, //스캔된 수
"nscannedObjectsAllPlans" : 4308303,
"nscannedAllPlans" : 4308303,
"scanAndOrder" : true,
"indexOnly" : false,
"nYields" : 4,
"nChunkSkips" : 0,
"millis" : 10927, // 11초 소요
"indexBounds" : { },
"server" : "localhost:27017"
}
//values 컬렉션 count
db.values.count()
4308303
//컬렉션 전체 스캔, n과 nscanned가 거의 비슷한 값을 가지는게 이상적
//v3.0 샘플
> db.inventory.find({}).sort({"quantity": -1}).limit(1).
explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "tutorial.inventory",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [ ]
},
"winningPlan" : {
"stage" : "SORT",
"sortPattern" : {
"quantity" : -1
},
"limitAmount" : 1,
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [ ]
},
"direction" : "forward"
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 11,
"executionStages" : {
"stage" : "SORT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 16,
"advanced" : 1,
"needTime" : 13,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"sortPattern" : {
"quantity" : -1
},
"memUsage" : 72,
"memLimit" : 33554432,
"limitAmount" : 1,
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [ ]
},
"nReturned" : 11,
"executionTimeMillisEstimate" : 0,
"works" : 13,
"advanced" : 11,
"needTime" : 1,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 11
}
}
},
"serverInfo" : {
"host" : "rMacBook.local",
"port" : 27017,
"version" : "3.0.6",
"gitVersion" : "nogitversion"
},
"ok" : 1
}
- 인덱스 추가 후 재시도
//인덱스 생성
db.values.createIndex({close: 1})
//explain 재실행
db.values.find({}).sort({close: -1}).limit(1).explain()
{
"cursor" : "BtreeCursor close_1 reverse",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0, //1밀리초도 안걸림
"indexBounds" : { //쿼리가 전체 인덱스에 대한 것임을 나타냄
"name" : [
[
{
"$maxElement" : 1
},
{
"$minElement" : 1
}
]
]
},
"server" : "localhost:27017"
}
- 인덱싱된 키 사용
//500보다 큰 종가에 대한 쿼리 explain
> db.values.find({close: {$gt: 500}}).explain()
{
"cursor" : "BtreeCursor close_1",
"isMultiKey" : false,
"n" : 309,
"nscannedObjects" : 309,
"nscanned" : 309,
"nscannedObjectsAllPlans" : 309,
"nscannedAllPlans" : 309,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 1,
"indexBounds" : { //하한선은 500 , 상한선은 무한대
"close" : [
[
500,
1.7976931348623157e+308
]
]
},
"server" : "localhost:27017"
}
- MongoDB 쿼리 옵티마이저 : 해당 쿼리를 가장 효율적으로 실행하기 위해 어떤 인덱스를 사용할지 결정하는 소프트웨어
- scanAndOrder 를 피한다. 쿼리가 정렬을 포함하고 있으면 인덱스를 사용한 정렬 시도
- 유용한 인덱스 제한 조건으로 모든 필드를 만족시킨다. 쿼리 셀렉터에 지정된 필드에 대한 인덱스를 최대한 사용
- 쿼리가 범위를 내포하거나 정렬을 포함하면 마지막 키에 대한 범위나 정렬에 도움이 되는 인덱스를 선택
//GOOG에 대해 200이 넘는 종가 조회
db.values.find({stock_symbol: "GOOG", close: {$gt: 200}})
//두 개의 인덱스 && close키가 마지막
db.values.createIndex({stock_symbol: 1, close: 1})
db.values.find({stock_symbol: "GOOG", close: {$gt: 200}}).explain()
{
"cursor" : "BtreeCursor stock_symbol_1_close_1",
"isMultiKey" : false,
"n" : 730,
"nscannedObjects" : 730,
"nscanned" : 730,
"nscannedObjectsAllPlans" : 730,
"nscannedAllPlans" : 730,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 2,
"indexBounds" : {
"stock_symbol" : [
[
"GOOG",
"GOOG"
]
],
"close" : [
[
200,
1.7976931348623157e+308
]
]
},
"server" : "localhost:27017"
}
// 인덱스를 나용하기 위해 축약된 getIndexKeys() 사용
db.values.getIndexKeys()
[
{
"_id" : 1
},
{
"close" : 1
},
{
"stock_symbol" : 1
}
]
> db.inventory.find({"quantity": 500,
"type":"toys"}).limit(1).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "tutorial.inventory",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"quantity" : {
"$eq" : 500
}
},
{
"type" : {
"$eq" : "toys"
}
}
]
},
"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 0,
"inputStage" : {
"stage" : "KEEP_MUTATIONS",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"type" : {
"$eq" : "toys"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"quantity" : 1
},
"indexName" : "quantity_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"quantity" : [
"[500.0, 500.0]"
]
}
}
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 1,
"totalKeysExamined" : 2,
"totalDocsExamined" : 2,
"executionStages" : {
"stage" : "LIMIT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 3,
"advanced" : 1,
"needTime" : 1,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"limitAmount" : 0,
"inputStage" : {
"stage" : "KEEP_MUTATIONS",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 1,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 0,
"invalidates" : 0,
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"type" : {
"$eq" : "toys"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 1,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 2,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 2,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 2,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"quantity" : 1
},
"indexName" : "quantity_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"quantity" : [
"[500.0, 500.0]"
]
},
"keysExamined" : 2,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0,
"matchTested" : 0
}
}
}
}
},
"serverInfo" : {
"host" : "rMacBook.local",
"port" : 27017,
"version" : "3.0.6",
"gitVersion" : "nogitversion"
},
"ok" : 1
}
//인덱스 추가
> db.inventory.createIndex( { quantity: 1, type: 1 } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
> db.inventory.find({"quantity": 500,
"type":"toys"}).limit(1).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "tutorial.inventory",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"quantity" : {
"$eq" : 500
}
},
{
"type" : {
"$eq" : "toys"
}
}
]
},
"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 0,
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"quantity" : 1,
"type" : 1
},
"indexName" : "quantity_1_type_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"quantity" : [
"[500.0, 500.0]"
],
"type" : [
"[\"toys\", \"toys\"]"
]
}
}
}
},
"rejectedPlans" : [
{
"stage" : "LIMIT",
"limitAmount" : 1,
"inputStage" : {
"stage" : "KEEP_MUTATIONS",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"type" : {
"$eq" : "toys"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"quantity" : 1
},
"indexName" : "quantity_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"quantity" : [
"[500.0, 500.0]"
]
}
}
}
}
}
]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 1,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "LIMIT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"limitAmount" : 0,
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 1,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 1,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 1,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"quantity" : 1,
"type" : 1
},
"indexName" : "quantity_1_type_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"quantity" : [
"[500.0, 500.0]"
],
"type" : [
"[\"toys\", \"toys\"]"
]
},
"keysExamined" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0,
"matchTested" : 0
}
}
}
},
"serverInfo" : {
"host" : "rMacBook.local",
"port" : 27017,
"version" : "3.0.6",
"gitVersion" : "nogitversion"
},
"ok" : 1
}
- 쿼리 플랜과 HINT() 보여주기
//기존 복합 인덱스 삭제 후 개별 인텍스 생성
db.values.dropIndex("stock_symbol_1_close_1")
db.values.createIndex({stock_symbol: 1})
db.values.createIndex ({close: 1})
// true를 전달하는 것은 쿼리 옵티마이저가 시도하는 플랜의 리스트 포함
db.values.find({stock_symbol: "GOOG", close: {$gt: 200}}).explain(true)
{
"cursor" : "BtreeCursor stock_symbol_1",
"isMultiKey" : false,
"n" : 730,
"nscannedObjects" : 894,
"nscanned" : 894, //스캔한 도큐먼트 수
"nscannedObjectsAllPlans" : 1097,
"nscannedAllPlans" : 1097,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 4, //쿼리 수행 시간
"indexBounds" : {
"stock_symbol" : [[ // 일치함(equality)에 대한 질의
"GOOG", //동일한 인덱스 범위
"GOOG"
]
]
},
"allPlans" : [ //시도한 쿼리 플랜의 배열
{
"cursor" : "BtreeCursor close_1",
"n" : 0,
"nscannedObjects" : 102,
"nscanned" : 102,
"indexBounds" : {
"close" : [
[
200,
1.7976931348623157e+308
]
]
}
},
{
"cursor" : "BtreeCursor stock_symbol_1",
"n" : 730,
"nscannedObjects" : 894,
"nscanned" : 894,
"indexBounds" : {
"stock_symbol" : [
[
"GOOG",
"GOOG"
]
]
}
},
{
"cursor" : "BasicCursor", //BasicCursor를 사용한 컬렉션 스캔
"n" : 0,
"nscannedObjects" : 101,
"nscanned" : 101,
"indexBounds" : { }
}
],
"server" : "localhost:27017"
}
//close 인덱스를 사용하지 않는 이유를 알기위해 hint 사용, hint()는 쿼리 옵티마이저로 하여금 강제로 특정 인덱스를 사용하도록 만듦
query = {stock_symbol: "GOOG", close: {$gt: 200}}
db.values.find(query).hint({close: 1}).explain()
{
"cursor" : "BtreeCursor close_1",
"isMultiKey" : false,
"n" : 730,
"nscannedObjects" : 5299,
"nscanned" : 5299, //스캔되는 값이 확연히 많음 왜?????
"nscannedObjectsAllPlans" : 5299,
"nscannedAllPlans" : 5299,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 22,
"indexBounds" : {
"close" : [
[
200,
1.7976931348623157e+308
]
]
},
"server" : "localhost:27017"
}
- 쿼리 플랜 캐시 : 옵티마이저가 어떻게 캐시를 하고 선택한 쿼리 플랜의 사효를 만료하는가의 내용.
{
pattern: {
stock_symbol: 'equality',
close: 'bound',
index: {
stock_symbol: 1
},
nscanned: 894
}
}