Last Updated on 1월 23, 2024 by Jade(정현호)
안녕하세요
이번 포스팅은 인덱스 생성과 비교/논리 연산자, 컬렉션 정보 확인에 관련하여 MongoDB in Action(몽고디비 인 액션) 2nd Edition 책의 내용 정리와 MongoDB 공식 문서를 참조한 글입니다
테스트 컬렉션 생성
조회 성능을 높이기 위해서는 인덱스를 생성하는 것이 가장 일반적인 방법입니다.
MongoDB도 인덱스를 사용할 수 있습니다.
인덱스에 대해 이미 알고 있다면 MongoDB 에서 인덱스를 생성하고 explain() 명령을 사용하여 인덱스에 대한 쿼리를 프로파일 하는 것이 얼마나 쉬운지 알게 될 것입니다.
인덱스에 대해서 확인해보기 위해서는 컬렉션에 많은 도큐먼트가 있어야만 인덱싱에 대한 예제로 의미가 있습니다.
그래서 먼저 numbers 라는 컬렉션에 20,000개의 간단한 도큐먼트를 추가하도록 하겠습니다.
MongoDB 쉘은 자바스크립트 인터프리터이므로 이 작업은 다음과 같이 진행하도록 하겠습니다.
> for(i = 0; i<20000; i++) { db.numbers.insertOne({num: i}); }
컬렉션 수를 확인해보겠습니다.
> db.numbers.count() 20000
들어가 있는 데이터 일부분만 확인해 보겠습니다.
> db.numbers.find() { "_id" : ObjectId("61c495078cfe17c8df46790e"), "num" : 0 } { "_id" : ObjectId("61c495088cfe17c8df46790f"), "num" : 1 } { "_id" : ObjectId("61c495088cfe17c8df467910"), "num" : 2 } { "_id" : ObjectId("61c495088cfe17c8df467911"), "num" : 3 } { "_id" : ObjectId("61c495088cfe17c8df467912"), "num" : 4 } { "_id" : ObjectId("61c495088cfe17c8df467913"), "num" : 5 } { "_id" : ObjectId("61c495088cfe17c8df467914"), "num" : 6 } { "_id" : ObjectId("61c495088cfe17c8df467915"), "num" : 7 } { "_id" : ObjectId("61c495088cfe17c8df467916"), "num" : 8 } { "_id" : ObjectId("61c495088cfe17c8df467917"), "num" : 9 } { "_id" : ObjectId("61c495088cfe17c8df467918"), "num" : 10 } { "_id" : ObjectId("61c495088cfe17c8df467919"), "num" : 11 } { "_id" : ObjectId("61c495088cfe17c8df46791a"), "num" : 12 } { "_id" : ObjectId("61c495088cfe17c8df46791b"), "num" : 13 } { "_id" : ObjectId("61c495088cfe17c8df46791c"), "num" : 14 } { "_id" : ObjectId("61c495088cfe17c8df46791d"), "num" : 15 } { "_id" : ObjectId("61c495088cfe17c8df46791e"), "num" : 16 } { "_id" : ObjectId("61c495088cfe17c8df46791f"), "num" : 17 } { "_id" : ObjectId("61c495088cfe17c8df467920"), "num" : 18 } { "_id" : ObjectId("61c495088cfe17c8df467921"), "num" : 19 } Type "it" for more
count() 쿼리를 통해서 컬렉션의 건수를 확인할 수 있으며 그 다음 find() 쿼리는 처음 20개의 결과를 보여 주며
it 명령어를 사용하면 추가 결과를 확인할 수 있습니다.
충분한 양의 도큐먼트를 만들었으니 이제 몇 가지 쿼리를 실행해보겠습니다
비교 연산자와 논리 연산자
쿼리를 실행 하기전에 조회와 관련된 비교 연산자와 논리 연산자에 대해서 확인해보겠습니다.
먼저 비교 연산자 입니다.
조회 시 $gt 와 $lt 라는 특수한 연산자를 통해서 범위에 대한 쿼리를 실행할 수 있습니다.
각각 greater than(보다 큰) 과 less than(보다 작은)을 뜻 합니다.
num의 값이 19,995 보다 큰 도큐먼트를 모두 가져오기 위한 쿼리는 다음과 같습니다.
> db.numbers.find( {num: { "$gt":19995 }}) { "_id" : ObjectId("61ba194d41d04a0d3764d565"), "num" : 19996 } { "_id" : ObjectId("61ba194d41d04a0d3764d566"), "num" : 19997 } { "_id" : ObjectId("61ba194d41d04a0d3764d567"), "num" : 19998 } { "_id" : ObjectId("61ba194d41d04a0d3764d568"), "num" : 19999 }
두 연산자를 같이 사용해서 상한값과 하한값을 설정할 수도 있습니다.
> db.numbers.find( {num: {"$gt":20, "$lt": 25}}) { "_id" : ObjectId("61ba194841d04a0d3764875e"), "num" : 21 } { "_id" : ObjectId("61ba194841d04a0d3764875f"), "num" : 22 } { "_id" : ObjectId("61ba194841d04a0d37648760"), "num" : 23 } { "_id" : ObjectId("61ba194841d04a0d37648761"), "num" : 24 }
위와 같이 간단한 JSON 도큐먼트를 사용하여 SQL에서와 같은 방식으로 정교한 범위 쿼리 언어를 만들 수 있습니다.
$gt 와 $lt는 MongoDB 쿼리에서 사용할 수 있는 키워드 중 하나이며 아래와 같은 비교 연산자가 있습니다.
$eq : (equals) 비교 값과 일치하는 값
$gt : (greater than) 비교 값 보다 큰 값
$gte : (greather than or equals) 비교 값 보다 크거나 같은 값
$lt : (less than) 비교 값 보다 작은 값
$lte : (less than or equals) 비교 값 보다 작거나 같은 값
$ne : (not equal) 비교 값과 일치하지 않는 값
$in : 비교 값 배열 안에 속하는 값
$nin : 비교 값 배열 안에 속하지 않는 값
그 다음은 논리 연산자 입니다.
$or : 여러 개의 조건 중에 적어도 하나를 만족하는 도큐먼트를 찾음
$and : 여러 개의 조건을 모두 만족하는 도큐먼트를 찾음
$not : 해당 조건이 맞지 않는 경우와 해당 필드가 없는 경우를 찾음
{filed : { $not: { <operator-expression> } } }
$nor : 여러 개의 조건을 모두 만족 하지 않는 도큐먼트를 찾음
{ $nor: [ { <expression1> }, { <expression2> }, ... { <expressionN> } ] }
실행 예시)
> db.numbers.find( { $or: [{ num:5},{num:7} ] } ) { "_id" : ObjectId("61c495088cfe17c8df467913"), "num" : 5 } { "_id" : ObjectId("61c495088cfe17c8df467915"), "num" : 7 }
인덱싱과 explain()
관계형 데이터베이스로 작업한 경험이 있다면 디버깅 또는 쿼리 최적화를 위한 여러가지 유용한 도구인 explain 이 익숙할 수도 있습니다.
데이터베이스가 쿼리를 받았을 때 이를 실행하는 방법에 대해서 계획을 세워야 하며, 이를 쿼리 플랜이라고 부릅니다.
EXPLAIN은 쿼리에서 인덱스를 사용한 경우 어떤 인덱스를 사용하였는지를 확인할 수 있으며 쿼리 경로에 대한 정보를 제공해줍니다.
EXPLAIN
쿼리는 여러 다양한 방식으로 수행되며, 이는 때때로 예상치 못한 결과를 만들어 내기도 하게 됩니다.
EXPLAIN 은 이러한 것들을 설명을 해주는 도구입니다.
EXPLAIN 은 아래와 같이 사용을 하면 됩니다.
> db.numbers.find({ num: {"$gt": 19995}}).explain("executionStats") { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.numbers", "indexFilterSet" : false, "parsedQuery" : { "num" : { "$gt" : 19995 } }, "winningPlan" : { "stage" : "COLLSCAN", <-- COLLSCAN: 전체 스캔 "filter" : { "num" : { "$gt" : 19995 } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true, "nReturned" : 4, "executionTimeMillis" : 6, "totalKeysExamined" : 0, "totalDocsExamined" : 20000, "executionStages" : { "stage" : "COLLSCAN", <-- COLLSCAN: 전체 스캔 "filter" : { "num" : { "$gt" : 19995 } }, "nReturned" : 4, "executionTimeMillisEstimate" : 0, "works" : 20002, "advanced" : 4, "needTime" : 19997, "needYield" : 0, "saveState" : 20, "restoreState" : 20, "isEOF" : 1, "direction" : "forward", "docsExamined" : 20000 } }, "serverInfo" : { "host" : "wmp", "port" : 27017, "version" : "4.4.10", "gitVersion" : "58971da1ef93435a9f62bf4708a81713def6e88c" }, "ok" : 1 }
위의 내용은 인덱스를 사용하지 못한 쿼리에 대한 explain("executionStats") 의 일반적인 결과입니다.
executionStats 는 MongoDB 3.0 에서 새롭게 추가된 명령어로 더 상세화 된 결과물을 확인할 수 있습니다
explain 결과를 살펴보면 4개의 결과(nReturned) 을 반환하는 쿼리입니다.
위의 예제에서는 단지 4개의 결과를 반환하기 위해서 컬렉션 전부인 20,000개의 도큐먼트(docsExamined) 를 스캔하였음을 알 수 있습니다.
totalKeysExamined 는 스캔한 인덱스 엔트리의 개수를 보여주게 되며, 위의 예제에서는 0 인 것을 확인할 수 있습니다.
스캔한 도큐먼트 수와 결과값의 큰 차이는 이 쿼리가 비효율로 실행되었음을 보여 주게 됩니다.
인덱스 생성
위의 예제 컬렉션(numbers)은 인덱스가 필요하며, createIndex() 명령을 사용하여 num 키에 대한 인덱스를 생성하도록 하겠습니다.
> db.numbers.createIndex({num: 1},{name:"ix_num"}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
MongoDB 3.0 부터 createIndex() 는 기존의 ensureIndex() 를 대체합니다.
ensureIndex()는 여전히 유효하지만 createIndex() 의 alias 로 존재합니다.
createIndex() 메소드에 인덱스를 생성할 키 값을 입력하여 생성을 합니다.
위의 예제에서는 num 키에 대해서 인덱스를 생성하는 것이며 :1 은 오름차순 정렬을 의미하며 :-1은 내림차순 정렬을 의미합니다
{name: "인덱스명} 을 통해서 인덱스명을 지정하면서 인덱스를 생성할 수 있으며 위의 예제에서는 인덱스 이름은 ix_num 으로 생성하였습니다.
numIndexesBefore 는 인덱스를 생성하기 전의 인덱스 수를 의미하고, numIndexesAfter 는 생성 후 인덱스 수를 나타냅니다.
컬렉션이 생성되면 _id 에 대해서 프라이머리 키가 생성되기 때문에 생성 전이 1이고, 인덱스 생성 후가 2 인 것을 확인할 수 있습니다.
인덱스가 정상적으로 생성되었는지는 getIndexes() 메서드를 통해서 확인할 수 있습니다.
> db.numbers.getIndexes() [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2, "key" : { "num" : 1 }, "name" : "ix_num" <-- 생성한 인덱스명 } ]
위의 결과는 이 컬렉션은 두개의 인덱스를 가지고 있습니다.
첫번째 인덱스는 모든 컬렉션에 자동으로 생성되는 표준 _id 인덱스이고, 두번째 인덱스는 num 에 대한 인덱스 ix_num 입니다.
인덱스가 생성되었기 때문에 다시 explain 을 수행해보겠습니다.
> db.numbers.find({ num: {"$gt": 19995}}).explain("executionStats") { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.numbers", "indexFilterSet" : false, "parsedQuery" : { "num" : { "$gt" : 19995 } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", <-- IXSCAN: 인덱스 스캔 "keyPattern" : { "num" : 1 }, "indexName" : "ix_num", <-- ix_num 인덱스를 사용 "isMultiKey" : false, "multiKeyPaths" : { "num" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "num" : [ "(19995.0, inf.0]" ] } } }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true, "nReturned" : 4, "executionTimeMillis" : 1, "totalKeysExamined" : 4, <--- 오직 네개의 도큐먼트만 스캔 "totalDocsExamined" : 4, <--- 오직 네개의 도큐먼트만 스캔 "executionStages" : { "stage" : "FETCH", "nReturned" : 4, "executionTimeMillisEstimate" : 0, "works" : 5, "advanced" : 4, "needTime" : 0, "needYield" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "docsExamined" : 4, "alreadyHasObj" : 0, "inputStage" : { "stage" : "IXSCAN", <-- IXSCAN: 인덱스 스캔 "nReturned" : 4, "executionTimeMillisEstimate" : 0, "works" : 5, "advanced" : 4, "needTime" : 0, "needYield" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "keyPattern" : { "num" : 1 }, "indexName" : "ix_num", "isMultiKey" : false, "multiKeyPaths" : { "num" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "num" : [ "(19995.0, inf.0]" ] }, "keysExamined" : 4, "seeks" : 1, "dupsTested" : 0, "dupsDropped" : 0 } } }, "serverInfo" : { "host" : "wmp", "port" : 27017, "version" : "4.4.10", "gitVersion" : "58971da1ef93435a9f62bf4708a81713def6e88c" }, "ok" : 1 }
num 키에 대해서 인덱스 ix_num 를 사용함에 따라서 네 개의 도큐먼트만 스캔하게 되었습니다.
(수행하는 시스템에 따라서 쿼리 실행 시간도 차이가 날수 있음)
inputStage.stage 가 IXSCAN 인 것을 확인할 수 있으며 각각의 의미는 아래와 같습니다.
• COLLSCAN: 전체 스캔
• IXSCAN: 인덱스 스캔
위에서 생성한 인덱스 외 다른 종류로 unique 인덱스를 생성할 수 있습니다.
유니크 인덱스는 생성시 unique: true 을 지정하고 생성을 진행합니다.
// 생성 // > db.numbers.createIndex( { num: 1 }, { unique: true } ) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } // 확인 // > db.numbers.getIndexes() [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2, "unique" : true, <-- unique 인덱스 "key" : { "num" : 1 }, "name" : "num_1" } ]
Note
WiredTiger의 LSM(Log-Structured Merge-trees) 에서는 B-Tree 인덱스가 지원되지 않습니다.
복합 인덱스
2개 이상의 키(필드)가 포함되어 같이 생성되는 인덱스를 의미하며 아래와 같이 생성시 포함하고자 하는 키를 입력하여 생성합니다.
> db.collection.createIndex( { 키1:1 , 키2:1 }, {name:"인덱스_이름"} )
인덱스 삭제
인덱스의 삭제는 두가지 방법으로 할 수 있으며 해당 키를 입력하여 삭제하거나 인덱스명을 명시적으로 입력하여 삭제할 수 있습니다.
1) 인덱스에 해당하는 키 를 입력
> db.numbers.dropIndex({num:1}) { "nIndexesWas" : 2, "ok" : 1 }
이때 키 이름과 인덱스 생성시 정렬 정보도 같이 기재하면 됩니다
1 또는 -1 이며, 해당정보는 getIndexes() 를 통해서 확인할 수 있습니다.
2) 인덱스 명을 통해 삭제
> db.numbers.dropIndex("ix_num") { "nIndexesWas" : 2, "ok" : 1 }
인덱스 재생성(reIndex())
인덱스에 대한 재생성은 reIndex() 를 통해 할 수 있으며, 해당 작업은 _id 를 비롯하여 모든 인덱스를 재생성 하게 됩니다.
이 작업은 많은 량의 데이터 또는 많은 수의 인덱스가 있을 경우 작업 비용(cost)가 많이 들 수 있는 작업입니다.
보통의 경우에는 해당 작업이 사실상 필요하지는 않습니다.
Replica Set의 경우 reIndex()는 primary에서 secondary 로 복제가 전파되지 않으며 단일 mongod 인스턴스에만 영향을 미치게 됩니다.
문서에도 replica set 환경 과 샤딩 클러스터의 컬렉션에서는 실행하지 말라는 내용이 기재되어 있습니다.
물론 인덱스는 어느정도 대가를 감수해야 합니다. 인덱스는 어느 정도의 공간이 필요하고 미세하게 insert의 성능을 떨어뜨리지만 쿼리 최적화를 위해 꼭 필요한 도구입니다.
인덱스에 대한 더 자세한 내용은 아래 포스팅을 참조하시면 됩니다.
데이터베이스, 컬렉션 정보 확인
정보 확인하는 몇 가지에 대해서 확인해보도록 하겠습니다.
show dbs
> show dbs admin 0.000GB config 0.000GB local 0.000GB test 0.001GB
show dbs 는 시스템상의 모든 데이터베이스 와 그 사용 용량을 보여 줍니다.
show collections
> show collections numbers
데이터 베이스내에 존재하는 컬렉션 목록을 확인할 수 있습니다.
또는 show tables 를 통해서도 컬렉션 리스트를 확인할 수 있습니다
현재 데이터베이스의 정보
현재 선택한 데이터베이스에 정보는 아래와 같이 조회할 수 있습니다.
> db.stats() { "db" : "test", "collections" : 1, "views" : 0, "objects" : 20000, "avgObjSize" : 35, "dataSize" : 700000, "storageSize" : 262144, "indexes" : 2, "indexSize" : 462848, "totalSize" : 724992, "scaleFactor" : 1, "fsUsedSize" : 52187213824, "fsTotalSize" : 93001322496, "ok" : 1 }
컬렉션 정보
컬렉션에 대해서도 stats()를 통해 확인할 수 있습니다.
> db.numbers.stats() { "ns" : "test.numbers", "size" : 700000, "count" : 20000, "avgObjSize" : 35, "storageSize" : 262144, "freeStorageSize" : 0, "capped" : false, "wiredTiger" : { "metadata" : { "formatVersion" : 1 }, "creationString" : "access_pattern_hint=none, allocation_size=4KB,app_metadata=(formatVersion=1), assert=(commit_timestamp=none,durable_timestamp=none, < 중략> "type" : "file", "uri" : "statistics:table:test/collection-0-609553672372511813", "LSM" : { "bloom filter false positives" : 0, "bloom filter hits" : 0, "bloom filter misses" : 0, "bloom filter pages evicted from cache" : 0, "bloom filter pages read into cache" : 0, "bloom filters in the LSM tree" : 0, "chunks in the LSM tree" : 0, "highest merge generation in the LSM tree" : 0, "queries that could have benefited from a Bloom filter that did not exist" : 0, "sleep for LSM checkpoint throttle" : 0, "sleep for LSM merge throttle" : 0, "total size of bloom filters" : 0 }, < ... 중략 ... >
scale 인자는 위에서 조회된 stats 결과에 대해서 사이즈(용량)의 단위를 변경하게 됩니다
위와 같이 조회하면 사이즈(용량)가 bytes 에서 kilobytes 로 보이게 됩니다.
> db.numbers.stats( {scale : 1024} ) { "ns" : "test.numbers", "size" : 683, <--- 원본 사이즈 : 700000 "count" : 20000, <--- count 는 사이즈가 아니라서 그대로 임 "avgObjSize" : 35, "storageSize" : 256, "freeStorageSize" : 0, < 중략 >
indexDetails 는 컬렉션 내의 각 인덱스와 관련된 정보가 포함된 더 상세한 내용을 확인할 수 있습니다
> db.numbers.stats( { indexDetails : true } )
Reference
Reference Book
• MongoDB in Action(몽고디비 인 액션) 2nd Edition
Reference link
• mongodb.com/db.collection.reIndex
• mongodb.com/query
• mongodb.com/db.collection.stats
관련된 다른 글
Principal DBA(MySQL, AWS Aurora, Oracle)
핀테크 서비스인 핀다에서 데이터베이스를 운영하고 있어요(at finda.co.kr)
Previous - 당근마켓, 위메프, Oracle Korea ACS / Fedora Kor UserGroup 운영중
Database 외에도 NoSQL , Linux , Python, Cloud, Http/PHP CGI 등에도 관심이 있습니다
purityboy83@gmail.com / admin@hoing.io