기본 CRUD 쿼리
프로그램 언어로 MongoDB 쿼리를 작성하는 것은 쿼리 빌더와 같은 래퍼 클래스의 도움을 받으면 되기 때문에 그다지 어렵지 않으나, BSON 쿼리를 직접 손으로 입력해야 하는 경우에는 괄호 쌍을 맞추어야 한다.
BSON 쿼리는 기존 RDBMS 의 SQL 과 문법과 오퍼레이터만 다를 뿐 실제 대부분의 기능을 제공하고 있다.
RDBMS SQL | MongoDB BSON |
INSERT | db.collection.insert() |
Batch DML | db.collection.bulkWrite() |
UPDATE REPLCAE (INSERT ON DUPLICATE KEY UPDATE) |
db.collection.update() db.collection.update( { }, { $set : { } }, { upsert : true } ) |
DELETE | db.collection.remove() |
SELECT | db.collection.find() |
SELECT GROUP BY | db.collection.aggregate() MapReduce |
쿼리 작성
INSERT
InsertOne, InsertMany 의 함수도 지원하나 기본인 Insert 에 대해 알아본다.
일반적으로 두 개의 명령 인자를 사용하는데, 첫 번째 인자는 저장하고자 하는 도큐먼트이다.
두 번째 인자는 INSERT 를 처리할 때 사용할 여러 가지 옵션을 명시할 수 있는데 writeConcern 과 ordered 로 필수옵션이 아니기 때문에 생략이 가능하다.
옵션 | 설명 |
writeConcern | INSERT 명령이 어떤 조건에서 완료 응답을 반환할지를 결정 |
ordered | 첫 번째 인자가 도큐먼트의 배열일 때 INSERT 명령을 배치로 처리하는데, ordered 값이 true 인 경우 도큐먼트를 순서대로 저장하기 위해 단일 쓰레드로 하나씩 INSERT 를 실행 |
* ordered 가 false 라면 실제 컬렉션에 도큐먼트가 저장되는 순서가 사용자가 명시한 순서와 다르게 저장되므로 중간에 INSERT 에 실패했을 때 재처리를 어렵게 하거나 불필요한 작업을 해야할 수 있다.
* ordered 가 false 라면 에러가 발생한 경우 그냥 무시하고 나머지 도큐먼트를 모두 INSERT 한다. 만약 배열의 하나라도 INSERT 에 실패했을 때 즉시 작업을 취소해야 한다면 ordered 를 true 로 하는게 좋다.
* 마이너한 실패는 무시하고 최대한 빨리 저장하고 싶다면 ordered 를 false 로 하여 INSERT 시 여러 쓰레드를 사용하는 것이 좋다.
INSERT 도큐먼트의 ObjectId 조회
많은 RDBMS 서버가 AUTO INCREMENT 또는 시퀀스와 같이 자동으로 아이디 값이 증가되는 기능을 제공한다. 값의 포맷은 조금 다르지만 MongoDB 도 12바이트로 구성된 ObjectId 라는 AUTO INCREMENT 아이디 값을 제공하고 있다.
물론 샤딩 환경을 고려해야 하므로 유일한 값을 구성하는 자체가 기존 RDBMS 와 조금 다르지만 기본적인 용도는 동일하게 사용할 수 있다.
방법은 미리 _id 필드에 ObjectId 를 생성해서 할당하는 방식으로 INSERT 될 _id 필드의 값을 미리 확인할 수 있다.
이렇게 조회 시 실제 Mongo 서버가 할당한 값이 아니라 MongoDB 드라이버에서 할당한 값이지만, 이 값을 MongoDB 서버가 값 자체를 변경하는 것은 아니기 때문에 아무런 차이가 없다.
그리고 ObjectId 는 어느 서버에서 생성하든지 유일성이 보장되는 구조라서 굳이 단일 서버에서 발급해야 하는 제약 사항이 없다.
UPDATE
updateOne, updateMany, replaceOne 의 함수도 있으나 UPDATE 에 대해 알아본다.
INSERT 와 달리 3개의 인자를 사용하는데 세 번째 인자는 선택 옵션으로 '업데이트 대상 도큐먼트 검색 조건' 과 '업데이트 내용' 만 사용할 수도 있다.
첫 번째 인자에는 변경할 도큐먼트를 검색하는 조건을 사용한다.
두 번째 인자는 변경할 내용을 기술하는데 set 옵션이 없으면 기존 도큐먼트를 통째로덮어써 버리기 때문에 주의해야 한다. 이 외 다양한 오퍼레이터를 사용할 수 있다.
오퍼레이터 | 설명 |
$inc | 필드의 값을 주어진 값만큼 증가시켜서 저장 |
$mul | 필드의 값을 주어진 값만큼 배수로 저장 |
$rename | 필드의 이름을 변경 _id 를 입력하여 특정 도큐먼트의 필드명만 변경할수도 있으며 전체 도큐먼트 변경도 할수 있는데 이는 서비스에 상당한 영향을 미치므로 주의해야 함 |
$setOnInsert | set 과 동일한 방식으로 사용하지만 upsert 옵션이 true 인 UPDATE 명령이 업데이트 해야 할 도큐먼트를 찾지 못해서 INSERT 를 실행해야 할 때에만 적용 |
$set | 도큐먼트 필드 값을 변경 |
$unset | 도큐먼트의 필드를 삭제 |
$currentDate | 필드의 값을 현재 시각으로 변경 |
세 번째 인자는 UPDATE 를 처리할 때 적용할 여러가지 옵션을 설정한다.
옵션 | 설명 |
upsert | upsert 옵션을 false 로 설정하면 UPDATE 명령이 조건에 맞는 도큐먼트를 찾지 못하더라도 어떤 변경을 가하지 않으나 true 인 경우 새로운 도큐먼트를 INSERT 함 |
multi | 기본적으로 단일 도큐먼트의 업데이트만 수행하여 검색 조건에 일치하는 도큐먼트가 2개 이상이더라도 그 중 하나만 변경 true 로 설정하면 조건에 일치하는 모든 도큐먼트를 변경 |
writeConcern | UPDATE 명령이 어떤 조건에서 '완료' 응답을 반환할지를 결정 |
collation | UPDATE 명령이 변경할 대상 도큐먼트를 검색하 때 사용할 문자 셋과 콜레이션을 명시 |
배열 필드 업데이트
MongoDB 는 배열필드에 대한 많은 오퍼레이션을 제공한다.
따라서 배열 필드의 데이터를 변경하기 위해 도큐먼트의 클라이언트로 가져온 다음 가공해서 다시 변경하는 형태로 업데이트 하지 않고도 특정 위치의 배열 엘리먼트를 수정할 수 있다.
새로운 엘리먼트를 추가하고자 하는 경우 $push 명령을 이용해 추가할 수 있다.
뿐만 아니라 배열 엘리먼트 중 특정 필드값인 엘리먼트만 새로운 값으로 대체할 수 있다.
그러나 배열 타입의 값을 가지는 필드에서는 유일성이 보장되지 않는다.
이렇게 하나의 도큐먼트 내에서 배열 필드의 유일성을 보장해야 하는 경우에는 배열을 다시 새로운 서브 도큐먼트로 처리하면 데이터를 변경할 때 유일성을 보장받을 수 있다.
REMOVE
컬렉션의 도큐먼트를 삭제하는 방법으로는 deleteOne, deleteMany 도 있지만 기본명령어인 remove 에 대해 알아본다.
REMOVE 명령은 2개의 도큐먼트 인자를 필요로 한다.
첫 번째 인자는 삭제할 필드의 검색조건이며, 두 번째 인자는 선택 옵션을 사용할 수 있다.
옵션 | 설명 |
justOne | REMOVE 명령은 여러 도큐먼트가 삭제 만약 justOne 옵션을 명시하지 않으면 조건에 맞는 모든 도큐먼트를 삭제하나, true 로 설정하면 조건에 일치하는 첫 번째 도큐먼트만 삭제 |
writeConcern | REMOVE 명령이 어떤 조건에서 완료 응답을 반환할지 결정 |
collation | REMOVE 명령이 삭제할 대상 도큐먼트를 검색할 때 사용할 문자 셋과 콜레이션 |
격리된 UPDATE 와 REMOVE
쓰기 오퍼레이션은 도큐먼트 단위의 원자성만 제공한다.
하나의 쓰기 오퍼레이션으로 여러 도큐먼트를 변경하거나 삭제하더라도 내부적으로 하나의 트랜잭션으로 하나의 도큐먼트만 처리하는 방식으로 작동한다.
그래서 여러 도큐먼트를 변경하면 오퍼레이션이 완료되기도 전에 먼저 변경된 데이터들은 다른 커넥션에서 즉시 조회할수 있다.
만약 하나의 명령이 완료되기 전까지 다른 커넥션에서 변경 내용을 확인하지 못하게 하려면 격리된 UPDATE 또는 REMOVE 명령을 사용해야 한다. 이 명령 모두 업데이트 대상 도큐먼트를 검색하는 조건과 함께 $isolated : 1 옵셩르 사용하면 된다.
이 옵션을 사용하면 변경 작업이 완료될때까지 다른 커넥션이 변경중인 데이터를 볼 수는 없지만, 업데이트 명령의 트랜잭션을 보장하지는 않는다. 이 말은 작업 도중 에러가 발생하면 이미 처리된 도큐먼트에 대해서는 롤백을 보장하지 않는다는 뜻이다.
BulkWrite
데이터 변경 명령을 모아서 한 번에 실행할 수 있는 명령으로 반드시 하나의 컬렉션에 대해서만 데이터를 변경할 수 있다.
명령의 결과는 명령 단위로 정리해서 적용된 건수를 보여준다.
처리결과의 insertedIds 서브 도큐먼트에는 INSERT 된 도큐먼트들의 프라이머리 키 (_id ) 값을 반환한다.
upsertedIds 서브 도큐먼트에는 UPDATE 명령의 upsert 플래그가 true 일 때 INSERT 된 도큐먼트들의 프라이머리 키 값을 반환한다.
BulkWrite 명령도 다른 CRUD 명령과 같이 ordered 옵션을 사용할 수 있다. 이 때 ordered 옵션이 true 이면 BulkWrite 명령에 주어진 각 하위 명령이 순차적으로 실행되며, 중간에 실패 돜큐먼트가 발생하면 지금까지 변경된 도큐먼트는 그대로 유지하고 남은 작업은 모두 멈춘다.
ordered 옵션을 false 로 설정하면 여러 쓰레드로 나누어 병렬로 처리하며, 중간에 에러가 발생해도 나머지 작업을 멈추지 않고 처리한다.
만약 라우터를 사용해야 하는 클러스터 환경이면 ordered 옵션으로 인한 성능 차이는 더 크게 난다.
또한 WriteConcer 옵션도 같이 설정할 수 있다.
FIND
Find 는 데이터를 조회하는 명령으로, MongoDB 에서 사용하는 명령 중 가장 다양한 조건이나 옵션을 사용할 수 있는 명령이다.
FIND 명령은 2개의 인자를 사용하는데, 첫 번째 인자는 도큐먼트를 검색할 때 사용할 조건을 명시하며, 두 번째 인자에는 클라이언트로 반환할 필드를 명시한다. 이 두 인자는 모두 선택 옵션이므로 아무 인자 없이 명령을 사용할 수도 있다.
FIND 명령이 아무 조건이 주어지지 않으면 컬렉션을 풀 스캔한 결과를 반환한다.
만약 컬렉션의 모든 도큐먼트를 가져오지만 도큐먼트의 지정된 필드들만 가져오고자 한 다면, 검색 조건은 빈 도큐먼트로 설정하고, 두 번째 인자에 원하는 필드만 선택하면 된다.
FIND 명령에서 사용하는 두 번째 인자는 쿼리가 반환할 필드의 목록을 선택하는데, 이를 프로젝션(Projection) 이라고 한다.
MongoDB 의 프로젝션에서 가져오고자 하는 필드는 1로 표시하며 그렇지 않은 필드는 0 으로 표시한다.
그런데 프로젝션에서 0 과 1을 같이 사용할 수는 없다.
일부 필드에 대해 프로젝션 옵션을 명시하면 나머지 명시되지 않은 필드는 그 반대로 자동으로 설정돼서 프로젝션할 필드가 결정된다.
다만 _id 필드에 대해서는 이 제한이 예외로 적용된다.
FIND 연자
검색 조건에 사용할 수 있는 오퍼레이터를 MongoDB 매뉴얼에서는 크게 7가지 정도로 나눠서 구분한다.
하지만 굳이 검색조건을 사용하면서 사용하는 오퍼레이터가 어떤 그룹의 오퍼레이터인지 식별할 필요는 없다.
오퍼레이터는 비교 / 논리 결합 / 필드 메타 / 평가 / 공간 / 배열 / 비트 로 나뉜다.
MongoDB 의 검색 조건은 모두 JSON 포맷으로 사용해야 하므로 RDBMS 와 같은 비교 연산자 그대로를 사용할 수는 없고, 이를 모두 $ 로 시작하는 오퍼레이터로 새로 만들고 JSON 문법에서 사용하도록 하고 있다.
비교 오퍼레이터
연산자 | 설명 |
$eq | = |
$gt | > |
$gte | >= |
$lt | < |
$lte | <= |
$ne | <> 또는 != |
$in | IN |
$nin | NOT IN |
논리 결합 오퍼레이터
연산자 | 설명 |
$or | OR 예) db.collection.find( { $or : [ { name : "mozi" } , { blog : "tistory" } ] } ) |
$and | AND |
$not | 부정 연산 |
$nor | 배열로 주어진 모든 표현식에 일치하지 않는지 비교 |
필드 메타 오퍼레이터
연산자 | 설명 |
$exists | 필드를 가졌는지 |
$type | 필드의 데이터 타입을 비교 |
평가 오퍼레이터
연산자 | 설명 |
$mod | % |
$regex | 정규 표현식 |
$text | 전문 검색 비교 |
$where | 표현식에 일치하는 도큐먼트만 필터링해서 반환 where 절에 주어진 조건은 인덱스를 사용하지 못함 - 성능느림 |
배열 오퍼레이터
연산자 | 설명 |
$all | 배열 타입의 필드가 파라미터로 주어진 배열의 모든 엘리먼트를 가졌는지 비교 |
$elemMatch | 모든 조건에 일치하는 엘리먼트를 하나라도 가진 도큐먼트를 검색 배열에서 자주사용되는 기능 |
$size | 배열의 엘리먼트 개수를 비교 |
FIND 조건
다양한 형태의 도큐먼트를 검색할 수 있도록 배열이나 서브 도큐먼트 조건을 활용할 수 있게 지원한다.
MongoDB 에서 쿼리를 작성할 때 가장 중요한 점은 하나의 필드에 대한 조건은 반드시 하나의 서브 쿼리 도큐먼트로 작성해야 한다는 것이다.
db.collection.find ( { name : ${ gte : "m" } , name : { $lte : "u" } } )
이 명령은 name 필드의 값이 m 보다 크거나 같고 u 보다 작거나 같은 도큐먼트를 검색하는 쿼리이나,
실제로는 의도와 다르게 출력되는데 쿼리 두개 조건 중 하나는 버려지기 때문이다.
하나의 필드에 대한 조건은 항상 하나의 서브도큐먼트에 전부 포함되어야 하기에,
아래와 같이 변경되어야 한다.
db.collection.find ( { name : { ${ gte : "m" } , { $lte : "u" } } )
또 하나 기억해야 할 점은 별도의 논리 연산을 포함하지 않으면 AND 연산을 수행한다는 것이다.
도큐먼트 포맷별로 FIND 명령의 조건이다
- 스칼라 필드 : 단순히 정수나 문자열 등과 같이 하나의 값만 가지는 필드를 의미
- 서브 도큐먼트 필드 : 서브도큐먼트의 필드를 검색한다. 서브도큐먼트는 . 으로 들어간다.
- 배열 필드 : 배열 필드의 값을 검색한다.
커서 옵션 및 명령
FIND 쿼리는 항상 커서를 반환하는데, 커서를 통해 쿼리 결과 도큐먼트를 하나씩 읽을 수 있다.
커서는 단순히 검색 결과 도큐먼트를 읽는 수단으로만 사용되는 것은 아니며, FIND 쿼리의 검색 결과를 정렬하거나 지정된 건수의 도큐먼트를 건너뛰거나 제한하는 등의 기능도 제공한다.
데이터 정렬 ( cusor.sort() )
쿼리의 결과 데이터를 정렬하려면 sort 커서옵션을 사용한다.
콜레이션 변경 ( cursor.collation() )
별도의 콜레이션을 명시하지 않으면 FIND 쿼리검색 조건은 컬렉션의 콜레이션을 사용한다.
Read Concern ( cursor.readConcern() )
분산 처리 구조로 되어 있기 때문에 여러 레플리카 멤버 중에 어떤 멤버를 선택하는지에 따라 FIND 쿼리의 결과가 달라진다. 결과가 달라지는걸 막기위해 복제 동기화 상황에 따른 읽기를 할 수 있다.
Read Preferenced ( cursor.readPref() )
프라이머리가 아닌 세컨드리 멤버로 접속해서 쿼리를 실행하게 할 수도 있다.
쿼리 코멘트 ( cursor.comment() )
MongoDB 는 BSON 포맷을 사용하므로 주석이 허용되지 않는다. 그래서 FIND 쿼리의 커서 옵션으로 코멘트를 추가하면 쿼리 로그에 기록된 주석을 로그파일에서 확인할 수 있다.
쿼리 실행계획 ( cursor.explain() )
쿼리의 실행계획을 확인한다.
쿼리 힌트 ( cursor.hint() )
적절한 인덱스를 선택하지 못할경우 옵티마이저에 특정 인덱스를 사용하도록 강제한다.
쿼리 배치크기 ( cursor.batchSize() )
쿼리의 결과를 몇 건 단위로 잘라서 가져올지 결정한다. 쉘에서는 일반적으로 20건씩 가져온다.
FindAndModify
여러 명령을 하나의 트랜잭션으로 묶어서 사용할 수 없기 때문에 변경 직전이나 직후의 도큐먼트 데이터를 확인하기가 쉽지 않다.
변경 직전의 데이터를 확인하는 기능을 위해 FindAndModify 명령을 제공하는데, 검색 조건에 일치하는 도큐먼트를 검색하고 그 도큐먼트를 변경하거나 삭제하는 후속 오퍼레이션을 설정할 수 있다.
findAndModify 명령은 변경되거나 삭제된 도큐먼트를 결과로 반환하는데, 이때 new 옵션과 upsert 옵션에 따라 반환되는 도큐먼트가 달라질 수 있다.
확장 검색 쿼리
대용량 데이터 분석 및 통계 작업을 위한 맵 리듀스와 어그리게이션 기능을 제공하고 있다.
맵리듀스
MongoDB 의 맵 리듀스는 아키텍처 특성상 도큐먼트의 데이터를 자바스크립트 엔진의 변수로 할당하고,
자바스크립트의 엔진의 처리가 완료되면 결과를 다시 도큐먼트로 변환하는 작업이 매우 빈번하게 실행된다.
Map 함수와 Reduce 함수
Map 단계와 Reduce 단계로 나누어지는데,
사용자는 Map 단계를 위한 함수와 Reduce 단계를 위한 함수를 자바스크립트로 개발해서 맵리듀스 명령을 호출해야 한다.
또한 Map 함수는 맵리듀스 엔진에 의해 도큐먼트마다 한 번씩 호출되는데, Map 함수가 도큐먼트별로 한 번씩 emit 함수를 호출해야 하는 것은 아니다.
맵리듀스 엔진은 Map 함수의 결과를 Key 로 그룹핑해서 VAlue 의 배열을 Reduce 함수르 존달한다
Reduce 함수는 Key 단위로 전달된 Value 의 배열을 풀어서 적당한 형태로 가공하고 그 결과를 리턴한다.
맵리듀스 명령의 사용법과 옵션
옵션 | 설명 |
out | 결과를 저장하거나 출력할 위치를 명서 |
query | 실행할 대상 도큐먼트를 검색하는 조건을 명시 |
sort | 도큐먼트를 먼저 정렬해서 맵리듀스 엔진으로 전달 |
limit | 엔진으로 전달할 도큐먼트의 개수를 설정 |
finalize | Reduce 함수의 결과를 변경하거나 보완해서 최종 결과를 만들 때 |
scope | 글로벌 변수를 정의 |
jsMode | 자바스크립트 엔진 사이에서 변환되는 과정을 막음 |
verbose | 처리 결과에 단계별 처리 과정 및 소요 시간에 대한 정보포함 여부 결정 |
Finalizer 함수
맵리듀스 작업에서는 처리의 마지막에 실행되는 Finalizer 함수를 사용할 수 있다.
일반적으로 잘 사용하지 않지만 처리를 더하거나 최종 결과를 변경하고자 한다면 사용한다.
Finalize 함수는 두 개의 인자를 사용하는데 모두 Reduce 함수의 리턴 값이 주어지게 된다.
증분 맵 리듀스 작업
특정 컬렉션의 도큐먼트가 계속 증가할 때, 매일 전체 데이터에 대해 맵리듀스를 수행하는 것이 아니라 증가한 만큼의 도큐먼트만 맵리듀스를 수행해서 그 결과를 기존의 맵리듀스 결과와 병합하는 방식의 작업이 가능하다.
이를 증분 맵리듀스라고 한다.
증분 맵리듀스를 사용할 때 주의할 사항이다.
- 이전 맵리듀스 결과가 저장된 컬렉션의 데이터가 임의로 변경되면 증분 맵리듀스의 결과에도 영향을 미친다.
- 처음 전체 데이터에 대해 맵리듀스를 실행할 때는 out 컬렉션만 지정한다.
- 두번째 맵리듀스 작업부터는 증분으로 실행해야 한다.
맵리듀스 함수 개발 시 주의 사항 및 디버깅
- Map 함수에서 호출하는 emit() 함수의 두 번째 인자와 Reduce 함수의 리턴 값은 같은 포맷이어야 한다.
- Reduce 함수의 연산 작업은 멱등이어야 한다.
맵리듀스 성능튜닝
맵리듀스 작업을 컬렉션의 모든 데이터가 아니라 일부 조건에 일치하는 도큐먼트들에 대해서만 처리해야 하는 경우라면 맵리듀스 명령의 query 옵션을 사용하면 된다. 이 때 query 옵션의 검색 조건을 위한 인덱스를 준비하는 것이 좋다.
또한 맵리듀스 엔진은 조건에 일치하는 도큐먼트를 무조건 지정된 개수씩 모아서 청크 단위로 Map 함수와 Reduce 함수를 호출한다.
이 때 Map 함수는 도큐먼트의 건수만큼 호출되지만 Reduce 함수는 그 청크에서 유니크한 키 개수만큼 호출된다.
맵리듀스 엔진은 Reduce 함수를 호출하기 전에 임시 중간 저장소에서 이미 동일한 키의 중간 결과가 있는지 확인해야 하고, Reduce 함수의 결과를 받으면 다시 임시 중간 ㅓㅈ장소에서 동일한 키를 찾아서 덮어쓰기 하는 과정이 필요하다.
이러한 작업은 상당히 부담스럽기 때문에 Reduce 함수의 호출 횟수를 줄일 수 있도록 맵리듀스 명령에는 sort 옵션을 사용할 수 있다.
sort 옵션에 키를 이용해 먼저 정렬을 수행하면 Reduce 함수의 호출 횟수가 줄어들고 그만큼 임시 저장소를 찾는 작업이 줄어든다.
맵리듀스 처리는 하나의 샤드 서버 내에서는 병렬로 처리되지 못한다.
만약 맵리듀스를 병렬로 사용하고 싶다면 여러 쓰레드로 실행하되 각 쓰레드가 처리해야 할 데이터 영역을 조건으로 설정하는 방식을 사용하는 것이 좋다.
Aggregation
FIND 명령으로는 데이터를 그룹핑해서 특정 조건에 일치하는 도큐먼트의 개수를 확인하거나 하는 복잡한 처리는 수행할 수 없다.
Aggregaion 은 복잡한 데이터 분석 기능을 제공하는데, 일반적으로 SQL 에서 GROUP BY 절로 처리할 수 있는 기능들을 샤딩된 환경에서 실행할 수 있게 한다.
Aggregation 의 목적
이미 맵리듀스라는 분석 기능을 제공하는데, 새롭게 Aggregation 기능을 도입한 데에는 대표적으로 두 가지 이유를 생각할 수 있다.
- 간단한 분석 쿼리에도 자바스크립트를 이용해 맵리듀스 프로그램을 작성해야 함
- 멥리듀스 작업은 자바스크립트 엔진과 MongoDB 엔진 간의 빈번한 데이터 매핑으로 인한 성능제약이 심함
Aggregation 의 작동 방식
샤딩되지 않은 MongoDB 서버에서의 Aggregation 실행은 다른 오퍼레이션과 비교할 때 특별한 차이는 없다.
하지만 샤딩된 환경에서는 Aggregation 파이프라인의 각 스테이지가 어떤 샤드에서 실행되는지가 부하 분산 차원에서 상당히 중요한 문제가 된다.
MongoDB 서버는 이렇게 여러 샤드에서 데이터를 수집해야 하는 처리를 위해 프라이머리 샤드를 활용한다.
MongoDB 라우터는 사용자로부터 Aggregation 쿼리를 전달받으면 우선 요청된 Aggregation 쿼리의 파이프라인 선두에 match 스테이지가 있는지와 검색 조건니 샤드 키를 포함하는 비교한다.
만약 검색 조건이 필요로 하는 도큐먼트가 단일 샤드에만 있다면 해당 샤드로만 쿼리를 전송한다.
그렇지 않다면 쿼리를 대표 샤드로 전달한다. 대표 샤드는 쿼리를 전달받으면 나머지 샤드로 쿼리를 전송한다.
그리고 요청을 전달받은 샤드들이 쿼리 결과를 반환하면 대표 샤드는 그 결과를 병합하고 정렬하는 작업을 수행해서 MongoDB 라우터로 최종 결과를 전달한다.
3.2 이하버전에서는 Aggregation 이 프아리머리 샤드에서 진행되었으나
프라이머리에만 부하 불균형이 발생하여 3.2버전 이후 각 샤드가 개별적으로 처리하지 못하는 단계를 처리할 대표샤드를 랜덤하게 선정하도록 개선되어 특정 샤드가 과부하를 받게 되는 현상은 많이 줄었으나, 여전히 $out , $lookup 과 같은 단계를 사용하는 쿼리는 프라이머리 샤드에 의존할 수밖에 없다.
단일 목적의 Aggregation
MongoDB 서버에서는 Aggregate 와 같이 집계 처리를 수행하는 2개의 명령이 있는데, 일반적인 Aggregate 보다는 조금 단순하지만 더 사용 빈도가 높아 쉽게 Aggregate 기능을 사용할 수 있도록 별도의 명령을 지원한다.
count() 와 distinct() 2가지가 지원되고 있다.
count 명령은 특정 조건에 부합하는 도큐먼트의 건수를 확인하는 명령이다.
distinct 명령은 지정된 필드의 유니크한 값들만 배열로 반환하는데, 만약 유니크한 값의 개수를 확인하고자 한다면 결과에 length 를 출력하면 된다.
범용 Aggregation
단일 목적뿐 아니라 다양한 서비스 요건을 위해 사용자가 직접 작업 내용을 구현할 수 있도록 일반적인 Aggreegation 기능도 제공하는데 이를 범용 Aggregation 이라 한다.
범용 Aggregation 은 사용자가 필요한 데이터 가공 작업을 직접 작성해야 한다.
이 때 데이터를 가공하는 작업은 스테이지라는 단위 작업들로 구성되며,
데이터는 이렇게 작성된 스테이지들을 하나의 관처럼 흘러가면서 원하는 형태의 데이터로 변환된다.
그래서 스테이지를 파이프라인 이라고도 한다.
Aggregate 명령은 pipeline 과 options 두 개의 파라미터를 사용한다.
pipeline 인자는 2개의 스테이지를 가지는 배열로 구성되어 있다. 그리고 두 번째 options 인자는 allowDiskuUse 옵션이 true 로 설정되어 있다.
옵션 | 설명 |
explain | 실행 계획을 확인 |
allowDiskUse | Aggregate 시 100MB 의 메모리까지만 사용가능한데 디스크도 활용한다 |
cursor | 커서 배치 사이즈를 설정 |
maxTimeMS | 실행 될 최대 시간을 설정 |
SQL 문장에서 일반적으로 사용하는 GROUP BY 에 해당하는 Aggregate 를 살펴본다.
유형 | 쿼리 |
SQL | SELECT name, COUNT(*) FROM users GROUP BY name; |
Mongo | db.collection.aggregate( [ { $group : {_id: "$name" , counter : { $num : 1 } } } ] ) |
유형 | 쿼리 |
SQL | SELECT name, AVG(score) FROM users WHERE score > 50 GROUP BY name; |
Mongo | db.collection.aggregate( [ { $match : { score : { $gt : 50 } } } , { $group : {_id: "$name" , avg : { $avg : "$score" } } } ] ) |
유형 | 쿼리 |
SQL | SELECT name, count(*), AVG(score) FROM users GROUP BY name; |
Mongo | db.collection.aggregate( [ { $group : {_id: "$name" , counter : { $sum : 1 } , avg : { $avg : "$score" } } } ] ) |
유형 | 쿼리 |
SQL | SELECT COUNT(DISTINCT name) FROM users; |
Mongo | db.collection.aggregate( [ { $group : {_id: "$name" } } , { $group : {_id : 1 , cnt : { $sum : 1 } } } ] ) |
MongoDB 서버에서는 도큐먼트의 특정 필드 값이 NULL 또는 NOT NULL 일 수 있지만 필드 자체가 없을 수 있다.
$ifNull 연산자를 이용해서 필드의 값이 NULL 인지 아닌지만 판단하며 필드 자체가 존재하지 않으면 TRUE 를 반환한다.
그래서 ifNull 로만 체크하면 필드가 없는경우도 체크하고 싶은경우 결과가 달라질 수 있다.
NULL 인지 아닌지만 구분해서 그룹하는 방법과, 필드 존재 여부까지 체크하는 방법이다.
유형 | 쿼리 |
NULL 체크 | db.collection.aggregation ( [ { $project : { _id : 0, name:1, has_phone: { "$cond" : [ { "$ifNull" : [ "$phone", false ] } , true , false ] } } , { $group : { _id : "$has_phone" , count : { $sum : 1 } } } ] ) |
필드유무까지 체크 | db.collection.aggregation ( [ { $project : { _id : 0, name:1, has_phone: { $concat : [ $gt : [ "$phone", null ]}, "NOT-NULL" , "" ] } , $concat : [ $gt : [ "$phone", null ]}, "NULL" , "" ] } , $concat : [ $gt : [ "$phone", undefined ]}, "NOT-EXIST" , "" ] } ] } , { $group : { _id : "$has_phone" , count : { $sum : 1 } } } ] ) |
Aggregation 파이프라인 스테이지(Pipeline)와 표현식
스테이지 | 설명 |
$project | 입력으로 주어진 도큐먼트에서 필요한 필드만 선별해서 다음 스테이지로 넘겨주는 작업을 처리 |
$addFields | 입력으로 주어진 도큐먼트에 새로운 필드를 추가 |
$replaceRoot | 입력으로 주어진 도큐먼트에서 특정 필드를 최상위 도큐먼트로 만들고 나머지는 버림 |
$match | 컬렉션 또는 직전 스테이지에서 넘어온 도큐먼트에서 조건에 일치하는 도큐먼트만 다음 스테이지로 전달 |
$limit | 입력으로 주어진 도큐먼트에서 처음 몇 건의 도큐먼트만 다음 스테이지로 전달 |
$out | 처리의 결과를 컬렉션으로 저장하거나 클라이언트로 직접 전달 주의사항 - 저장할 컬렉션이 존재하지 않는다면 컬렉션을 생성해서 데이터를 저장 - 컬렉션이 있다면 기존 컬렉션의 데이터를 모두 삭제하고 대롭게 데이터를 저장 함 |
$wind | 입력 도큐먼트가 배열로 구성된 필드를 가지고 있으면 이를 여러 도큐먼트로 풀어서 다음 스테이지로 전달 |
$group | 입력으로 주어진 도큐먼트를 지정된 조건에 맞게 그룹핑해서 카운트나 합계 또는 평균 등의 계산을 처리 - $sum : 숫자 타입의 필드 합계를 계산 - $avg : 숫자 타입의 평균을 계산 - $first : 각 그룹의 첫번째 값을 반환 - $last : 각 그룹의 마지막 값을 반환 - $max : 각 그룹의 최댓값을 반환 - $min : 각 그룹의 최솟값을 반환 - $push : 각 그룹에 속한 모든 도큐먼트의 필드 값을 배열로 반환 |
$sample | 주어진 입력 도큐먼트 중 임의의 몇 개의 도큐먼트만 샘플링해서 다음 스테이지로 전달 다음 3가지 조건을 만족하는 경우에는 랜덤 커서를 이용해서 도큐먼트를 조회 - $sample 이 파이프라인의 첫 스테이지로 사용된 경우 - 샘플링하고자 하는 도큐먼트가 컬렉션의 전체 도큐먼트의 5% 미만인 경우 - 컬렉션이 100개 이상의 도큐먼트를 가진 경우 이 3가지 조건을 만족하지 못하면, $sample 스테이지는 입력된 도큐먼트에 랜덤값의 필드를 부여한 다음 그 필드를 기준으로 정렬을 수행하고 지정된 건수의 도큐먼트만 다음 스테이지로 전달 |
$sort | 주어진 입력 도큐먼트를 정렬해서 다음 스테이지로 전달 |
$count | 주어진 입력 도큐먼트의 개수를 세어서 다음 스테이지로 전달 |
$lookup | 주어진 입력 도큐먼트와 다른 컬렉션과 LEFT OUTER 조인을 실행하여 결과를 다음 스테이지로 전달 |
$facet | 하나의 스테이지로 다양한 차원의 그룹핑 작업을 수행 |
MongoDB Agggregation 에서는 일반적으로 사용되는 사칙연산 연산자를 그대로 사용할 수 없다.
실제 사칙연산자 대신 지정된 연산자를 대체해서 사용해야 한다.
주요한 사칙연산 4가지의 명령어는 다음과 같다.
사칙연산 | 명령어 |
+ | $add |
- | |
* | $multiply |
/ | $divide |
Aggregation 파이프라인 최적화
MongoDB 의 Aggregation 은 각 스테이지가 순차적으로 처리되며, 그 결과를 다음 스테이지로 전달하면서 사용자의 요청을 처리한다.
그래서 각 스테이지의 배열 순서는 처리 성능에 많은 영향을 미친다.
예를들어 필요한 도큐먼트만 필터링하는 스테이지는 데이터를 그룹핑하는 스테이지보다 앞쪽에 위치해야 그룹핑해야 할 도큐먼트의 건수를 줄일 수 있고 Aggregation 의 성능을 높일 수 있다.
MongoDB 서버도 내부적으로 이런 형태의 기본적인 최적화 기능을 내장하는데,
어떤 최적화를 자동으로 처리할 수 있는지와 어떤 최적화가 불가능한지 본다.
또 Aggregation 최적화를 해준다고 하도라도 스테이지 구성에 따라 자동 최적화가 불가능할 수도 있고 매뉴얼에 명시된 대로 작동하지 않을 수도 있다.
$project 스테이지 |
전체 도큐먼트에서 필요한 필드만 뽑아서 다음 스테이지로 전달하는 역할을 한다.
하지만 MongoDB 서버는 파이프라인을 실행하기 전에 먼저 각 스테이지를 스캔해서 사용되는 필드를 먼저 확인하고 원본 도큐먼트에서 필요한 필드만 뽑아서 처리한다. 기존 도큐먼트의 필드를 조합해서 새로운 서브 도큐먼트나 배열 필드 또는 가공된 값을 생성하는 경우가 아니라면 불필요하다. |
스테이지 순서 최적화 ($match 와 $sort , $project 와 $skip ) |
$sort 스테이지와 $match 스테이지가 순서대로 연결된 경우 먼저 $match 조건에 일치하는 도큐먼트만 필터링 한 다음 남은 도큐먼트만 정렬하도록 최적화한다. $project 스테이지 뒤에 바로 $skip 스테이지가 사용되면 서버는 $skip 을 $project 스테이지 앞쪽으로 옮겨서 실행한다. 그래서 다음 스테이지에서 버려질 도큐먼트를 불필요하게 가공하는 일을 줄여준다. |
스테이지 결합 | MongoDB 서버는 처리 성능을 향상시키기 위해 파이프라인에서 2개 이상의 스테이지를 하나의 스테이지로 결합해 처리하기도 한다. |
인덱스 사용 | 각 스테이지도 가능하다면 인덱스를 활용할 수 있는 형태로 최적화된다. 하지만 각 스테이지는 이전 스테이지의 가공된 결과를 전달받기 때문에 파이프라인의 앞쪽 스테이지 한두 개만 인덱스를 활용해서 최적화할 수 있는 경우가 대부분이다. 그리고 Aggregation 처리과정에서 한번 데이터를 가공하면 그 데이터는 컬렉션이 아니라 메모리나 디스크의 임시 버퍼 공간에 저장되므로 인덱스를 사용할 수 없게 된다. |
메모리 사용 | Aggregate 명령은 내부적으로 그룹핑 작업을 처리하기 위해 메모리를 사용하는데 100MB 로 제한되어 있다. 쿼리 결과의 정렬 작업에 사용되는 메모리 공간과 그룹핑 작업에 사용되는 메모리 공간의 크기 제한은 서로 다르게 적용된다. 만약 메모리 공간이 부족하다는 에러가 발생하면 allowDiskUse:true 를 사용하여 실행하는 것이 좋다. 이 옵션을 사용하면 데이터 디렉터리 하위에 _tmp 라는 디렉터리를 만들어 임시 가공용 데이터 파일을 저장한다. |
$lookup 과 $graphLookup
$lookup 과 $graphLookup 스테이지는 파이프라인에서 사용할 수 있는 조인 기능이다.
이 스테이지는 FIND 쿼리에서는 사용할 수 없고 Aggregation 에서만 사용이 가능하다.
컬렉션이 샤딩되어 있지 않다면 스테이지를 조인의 용도로 사용하는 데 있어 어떤 제약사항도 없다.
하지만 샤딩된 클러스터에서는 조인으로 연결되는 컬렉션이 샤딩되지 않은 경우에만 스테이지를 사용할 수 있다.
즉 로컬 컬렉션(드라이빙 테이블)은 샤딩 여부와 관계없지만, 외래 컬렉션(드리븐 테이블)은 샤딩되지 않은 컬렉션만 사용할 수 있다. 문제는 일반적으로 샤딩되지 않은 컬렉션은 샤드 중에서 특정 샤드(프라이머리)에만 저장된다.
그래서 이 때는 프라이머리 샤드의 고부하를 반드시 고려해야 한다.
Aggregation 쿼리의 $lookup 스테이지는 아우터 조인을 수행할 컬렉션과 그 컬렉션의 조인필드 그리고 Aggregation 컬렉션의 조인필드가 필요하다.
$graphLookup 스테이지는 주로 RDBMS 의 재귀 커리 형태의 셀프 조인을 처리할 수 있는 기능이다. Aggregation 쿼리에서만 사용할 수 있다.
재귀 조인이다 보니 무한 반복될 수 있어 maxDepth 옵션을 설정해 최대 실행 제한을 두면 좋다.
$facet
특정 그룹을 선택하면 다시 하위의 다른 기준으로 상품을 그룹핑해서 보여주는 기능을 Facet Query 라고 하는데, 일반적으로 RDBMS 에서는 하나의 쿼리로 다양한 기준의 그룹핑 쿼리를 수행할 수 없지만 MongoDB 에서는 가능하다.
Facet Aggregation 쿼리는 하나의 쿼리에 여러 개의 파이프라인을 나열해서 한 번에 여러 기준의 그룹핑 기능을 수행할 수 있다.
각 outputField 에는 기준별 그룹핑 결과를 담을 필드명을 명시하는데, 배열로 주어진 스테이지를 서브 파이프라인이라고 한다.
Facet Aggregation 쿼리의 각 서브 파이프라인은 다음 3개 중 하나의 Facet 서브 스테이지를 반드시 가져야 한다.
그리고 각 서브 스테이지는 서로의 결과를 다른 서브 스테이지와 공유하거나 참조할 수 없으며, 각 서브 스테이지는 Aggregation 쿼리에서 사용되던 다른 스테이지를 같이 사용할 수도 있다.
- $bucket : 사용자가 지정한 범위별로 특정 필드 값의 건수나 합계 산출
- $bucketAuto : 특정 필드값을 사용자가 지정한 개수만큼 자동으로 범위를 나누어 그룹핑하고 건수나 함계를 산출
- $sortByCount : 특정 필드나 표현식의 값으로 그룹을 만들어서 그룹별 건수를 산출
Facet 쿼리는 내부적으로 몇 개의 서브 스테이지를 가지고 있는지와 관계없이 컬렉션의 도큐먼트는 1번만 읽어서 처리된다. 그래서 서브 스테이지의 개수가 많아져도 컬렉션의 도큐먼트를 여러 번 참조하지는 않기 때문에 효율적으로 처리된다.
Fulltext Search
RDBMS 처럼 MongoDB 도 전문 검색을 위해 전문 검색 인덱스와 전무 검색을 위한 쿼리 문법을 제공한다.
전문 검색 인덱스를 생성할 때는 인덱스를 생성할 필드이름 뒤에 전문 검색 인덱스를 의미하는 키워드인 text 를 입력하고
검색 쿼리를 작성할 때는 $text 연산자를 이용해서 검색어를 입력하면 된다.
전문 검색 인덱스는 컬렉션당 하나만 생성할 수 있다.
불리언 검색
전문 검색 기능에서 많이 사용되는 기능은 검색어에 대한 불리언 연산과 필드별 중요도 설정일 것이다.
불리언 연산은 "-" 부호를 이용해서 검색 대상에서 제외할 수 있다.
특별히 불리언 연산자 없이 검색을 실행하면 나열된 모든 단어의 OR 연산을 수행한다.
그러나 특정 단어 앞에 - 부호를 넣어 검색하면 다른 단어는 포함하지만 해당 단어는 포함하지 않는 결과를 검색할 수 있다.
부정 검색을 위해서는 - 부호앞에 반드시 공백을 추가해야 한다.
그리고 OR 연산이 아니라 AND 연산자을 수행하고자 한다면 문장 검색을 실행한다. 문장 검색을 위해서는 " 로 감싸주면 된다.
중요도 설정
전문 검색 쿼리를 수행할 때 일치하는 검색어가 어떤 필드에 저장된 값인지에 따라서 중요도를 설정할 수도 있다.
중요도가 높을수록 검색 일치 영향도가 높다.
* 중요도에 따라 결과가 다르게 나오진 않을텐데,,, 정렬할때인가 ?
한글과 전문 검색
전문 검색을 수행하기 위한 쿼리의 기능도 중요하지만 전문 인덱스를 어떻게 구성하는지도 매우 중요하다.
도큐먼트가 저장될 때 각 필드의 값을 분석해서 전문 ㅇ니덱스를 구성하는 부분을 일반적으로 전문 파서라 한다.
전문 인덱스는 주여 언어에 대해 형태소 분석 작업을 거쳐 각 단어의 원형을 인덱스에 저장한다.
MongoDB 서버에서 한글을 위한 전문 검색 기능은 n-Gram 형태의 전문 파서가 도입되지 않는 이상 어려울 수 있다.
하지만 n-Gram 은 MongoDB 서버가 제공하고 있는 구분자 기준의 전문 파서보다 훨씬 많은 인덱스 키를 만들어내기 때문에 성능적인 이슈가 문제될 수 있다.
만약 MongoDB 에 한글을 위한 전문 검색 기능이 추가되기 전에 한글 전문 검색 기능이 필요하다면 전문 검색 필드의 값을 배열 타입의 값으로 변환해서 정규 표현식 검색을 적용하는 것도 방법일 수 있다.
정규 표현식은 반드시 프리픽스 일치형태를 사용해야 인덱스를 정상적으로 사용할 수 있다.
그래서 정규 표현식 조건은 ^ 로 시작하고 검색어 다음에는 아무런 정규 표현식이 없어야 한다.
MongoDB 의 ^aa 는 RDMBS 의 LIKE aa% 와 같음
한글과 n-Gram 인덱스
위의 이유로 MongoDB 서버가 가진 형태소 분석 기반의 전문 검색 기능은 한글에 적합하지 않다.
그래서 여기에서 MongoDB 서버의 코드에 한글을 위한 n-Gram 전문 인덱스를 위한 기능을 직접 추가해 사용한다.
전문 검색 인덱스를 관리하기 위해 구분자와 토크나이저가 적절히 입력된 문자열에서 단위문자열을 분리하는 과정을 거친다.
이렇게 분리된 단어 문자열을 전문 인덱스에 저장하고 사용자가 검색어를 입력하면 그 검색어 또한 같은 과정을 거쳐 단위 문자열로 분리되고 검색을 수행하게 된다.
n-Gram 은 2글자씩 잘라서 인덱싱하는 bi-Gram 토크나이저를 사용한다.
그래서 한 글자로 구성된 검색어는 어떤 경우에도 일치된 결과를 가져오지 못한다.
n-Gram 인덱스의 특성상 토크나이저가 사용하는 문자열의 길이보다 짧은 길이의 문자열은 검색할 수 없다.
만약 1글자 키워드로 검색할 수 있게 하고자 한다면 NGRAM_TOKEN_SIZE 를 1로 변경해서 다시 컴파일 하면되지만
토큰 사이즈가 줄어들수록 인덱스의 크기는 더 작아질 수 있지만 검색어에 일치하는 결과가 많아져서 내부적인 필터링 시간이 오래 걸리게 된다.
전문 인덱스 성능
전문 인덱스는 입력된 도큐먼트에서 검색 대상이 되는 필드의 값들을 파싱하고 형태소 분석을 거친 다음 유니크한 키워드만 모아서 인덱스 엔트리로 저장한다.
하지만 전문 인덱스는 주로 크기가 큰 도큐먼트를 파싱하여 인덱싱하므로 인덱스가 매우 커지고 처리 시간이 많이 소요될 수도 있다.
MongoDB 서버를 일반적인 서비스용 데이터베이스로 사용하면서 보조적인 기능으로 전문 검색 기능을 사용하는 경우라면 이런 대용량의 전문 검색 기능은 피하는 것이 좋다.
그리고 전문 검색 인덱스를 검색한 결과는 어떤 형태로든지 정렬이 보장되지 않는다.
그래서 전문 검색을 수행하면서 정렬이 필요한 경우 별도의 정렬 처리를 수행한 후에야 결과를 반환할 수 있다.
복합인덱스에서 인덱스 필드를 전문 인덱스 앞쪽에 위치시켜 인덱스를 생성하면
전문 검색 쿼리에서 반드시 선행 필드의 조건을 포함해야만 전문 인덱스를 사용할 수 있다.
이렇게 수행하지 않으면 성능이 느리게 결과가 나오는 것이 아니라 쿼리 자체가 실패하게 된다.
스키마 변경
RDBMS 보다 자유롭지만 MongoDB 에서도 스키마 변경 작업은 필요하다.
특히 컬렉션의 샤딩이나 보조 인덱스 생성 및 삭제 작업이 있기 때문에 DDL 작업은 많은 주의를 필요로 한다.
데이터베이스 관리
MongoDB 에서는 데이터베이스를 생성하는 명령은 별도로 제공되지 않는다.
MongoDB 에서는 데이터를 별도로 생성하지 않아도 되고 존재하지 않는 데이터베이스에 대해 권한을 부여할 수 있다.
하지만 이렇게 생성된 데이터베이스는 샤딩 관련 옵션이 모두 꺼진 상태이므로 별도로 샤딩을 활성화하거나 권한을 부여한느 작업이 필요하다.
데이터베이스 생성 및 삭제
데이터베이스 생성은 지원하지 않고 컬렉션을 생성하거나 도큐먼트를 INSERT 하면 자동으로 데이터베이스가 생성된다.
삭제는 명시적으로 dropDatabase 명령을 실행하면 된다.
데이터베이스 복사
MongoDB 는 데이터베이스를 통째로 복사할 수 있는 CopyDatabase 명령을 지웒나다.
같은 서버에서 뿐 아니라 다른 서버의 데이터베이스도 복사할 수 있다.
이 명령은 fromdb 와 todb 의 데이터베이스와 컬렉션에 별도의 잠금을 획득하지 않는다.
그래서 데이터베이스가 복사되는 도중 변경될 수 있는데, 복사 도중 변경되는 데이터는 모두 무시하게 된다.
그리고 컬렉션의 데이터를 모두 복사하고 마지막에 인덱스를 생성한다.
todb 에서 마지막 인덱스를 생성하는 과정은 백그라운드가 아닌 포그라운드로 진행되기 때문에 이 때는 todb 의 데이터를 변경하거나 조회할 수 없다.
샤딩 활성화
데이터베이스를 최초 생성하면 샤딩이 활성화되어있지 않다.
샤딩 활성화를 하고싶으면 데이터베이스에 enableSharding 으로 샤딩을 적용해주어야 한다.
데이터베이스 컴팩션
MongoDB 서버의 컬렉션에서 대량의 데이터를 삭제하는 경우에 WiredTiger 스토리지 엔진을 사용하는 컬렉션의 도큐먼트가 디스크 파일의 각 위치에 골고루 분포돼 있으면 용량이 자동으로 줄지 않는다. 즉 데이터 파일의 프레그멘테이션이 매우 많아진다.
이렇게 주기적인 데이터 삭제로 인해 데이터파일의 크기가 커진 경우에는 데이터베이스 또는 컬렉션 단위로 컴팩션을 수행할 수 있다.
MongoDB 에서는 repairDatabase 명령어로 진행하며 글로벌 쓰기 잠금을 획득하고, 데이터베이스의 컬렉션을 임시 공간에 복사하는 방식으로 실행된다.
그래서 이 명령을 실행하는 동안에는 다른 처리가 실행되지 못하고 모두 대기하게 되므로 주의해야 한다.
이 명령은 세컨드리에서도 실행할 수 있어서, 세컨더리 작업 후 마스터를 스위칭 한 후 새로운 세컨더리에서 작업하는 형태로 가능하다. 이 작업은 데이터베이스 크기가 크다면 오래걸리므로 세컨드리 멤버에서 repairDatabaes 를 실행하고 그 이후 컴팩션된 데이터 파일을 그대로 다른 세컨드리 멤버로 복사하여 사용하면 빠르게 컴팩션 효과를 얻을 수 있다.
세컨드리 멤버에서 진행할 때도 잠금을 걸고 진행되기 때문에 OpLog 를 처리하지 못하고 잠금 대기를 하게 된다.
그로인해 복제 지연이 심해지고 멤버의 상태가 SECONDARY 가 아니라 RECOVERY 상태로 전환될 수 있다.
컬렉션 관리
MongoDB 의 컬렉션은 RDBMS 의 테이블과는 달리 도큐먼트가 저장되는 시점에 자동으로 생성된다.
컬렉션의 목록은 show tables 명령으로 확인할 수 있다.
컬렉션 생성과 삭제 그리고 변경
MongoDB 에서 명시적으로 컬렉션을 생성하고자 할 때는 createCollection 명령을 사용한다.
컬렉션을 자동으로 생성할때는 옵션이 모두 디폴트 값이므로, 각 옵션이 디폴트 값이 아닌 컬렉션을 생성하고자 할 때는 명시적으로 createCollection 명령을 사용해야 한다.
컬렉션 삭제는 drop 명령을 사용하면 된다.
컬렉션 복사 및 이름 변경
cloneCollectionAsCapped 명령은 복사하는 대상 컬렉션을 일반 컬렉션이 아니라 Capped 컬렉션으로 생성한다.
결국 하나의 컬렉션의 데이터를 복사할 수 있는 직접적인 기능은 제공하지 않는다.
그래서 export / import 나 find / insert 로 우회하여 데이터를 복사해야 한다.
Aggregation 을 이용하면 원하는 도큐먼트만 선별하여 새로운 컬렉션으로 복사할 수 있다.
그렇지만 이미 컬렉션이 존재하면 기존 컬렉션의 모든 도큐먼트를 삭제하고 새롭게 도큐먼트를 저장한다.
이 때 기존 컬렉션이 가지고 있던 인덱스는 그대로 유지된다.
MongoDB 서버에서 컬렉션의 이름을 변경하는 작업은 renameCollection 으로 간단하게 처리할 수 있다.
같은 데이터베이스 내에서 컬렉션의 이름만 변경하는 작업은 스토리지 엔진의 메타 정보만 변경하면 되므로 매우 빠르게 처리되지만,
다른 데이터베이스인 경우 새로운 컬렉션을 생성하고 원본 컬렉션에서 도큐먼트를 한 건씩 복사한다.
복사 작업이 완료되면 원본 컬렉션을 삭제하고 컬렉션 이름 변경 작업을 완료하게 된다.
그런데 이 과정에서 글로벌 쓰기 잠금을 걸기 때문에 주의해야한다.
컬렉션 상태 및 메타 정보
컬렉션에 관련된 상세 정보는 stats 명령을 이용해 확인할 수 있다.
이 명령은 데이터 파일의 크기나 도큐먼트의 크기와 같은 기본적인 정보를 먼저 표시하고 그 밑에 스토리지 엔진이 가진 상세한 메타 정보를 보여준다.
컬렉션 stats 옵션으로는 아래가 있다.
옵션 | 설명 |
creationString | WiredTiger 스토리지 엔진이 이 컬렉션을 생성할 때 사용했던 옵션의 상세한 내용을 보여줌 |
type | file ? |
uri | 이 컬렉션이 사용 중인 데이터 파일의 이름 |
block-manager | 블록 매니저가 수행했던 처리 내용을 조회 |
btree | 도큐먼트를 저장히기 위한 B-Tree 의 정보 |
cache | 캐시가 운영체제의 데이터 파일로부터 읽어드린 페이지의 개수나 다시 데이터 파일로 기록한 횟수 메모리의 캐시에서 참조된 횟수를 보여줌 |
cache-walk | 각 컬렉션의 브랜치 노드나 리프 노드가 얼마나 상주하고 있는지 페이지가 얼마나 생성되고 스플릿됐고 데이터 파일로 기록됐는지를 보여줌 |
compression | 압축된 페이지가 얼마나 데이터 파일로부터 읽혔는지 페이지 크기로 인해 압축이 스킵됐는지를 보여줌 |
cursor | 커서 오퍼레이션이 얼마나 수행됐는지 정보를 보여줌 |
reconciliation | 변경 내용을 원본 이미지에 병합하는 과정동안 발생하는 작업 내용을 조회 |
인덱스에 대해 stats 도 가능하다.
컬렉션의 프레그멘테이션과 컴팩션
데이터 파일에서 사용되지 않는 빈 공간이 많아져 디스크 공간이 낭비되는 경우가 발생한다.
이러한 프레그멘테이션은 단순히 디스크 공간만 낭비하는 것이 아니라 ㅔㅁ모리 효율까지 떨어트린다.
이렇게 데이터 파일에서 사용되지 않는 빈 공간을 최소화하기 위해 최적화 기능을 제공하는 이를 일반적으로 컴팩션이라고 한다.
컴팩션은 다음 2가지 관점에서 사용하는 것이 좋다.
- 데이터 파일의 크기가 커져서 디스크의 여유 공간이 부족한 경우
- 장착된 메모리보다 데이터 파일이 커져서 쿼리 실행 시 디스크 읽기가 많이 발생하는 경우
WiredTiger 스토리지 엔진을 사용하는 경우 일반적으로 컴팩션 필요성이 많지 않으나
대량으로 컬렉션 도큐먼트가 삭제되는 경우에는 빈 공간이 자동으로 운영 체제로 반납되지 않는다.
또 샤드가 새로 추가되어 많은 청크가 다른 샤드로 이동할 때에도 발생한다.
compact 명령은 데이터베이스의 쓰기 잠금을 획득하므로 컴팩션이 실행되는 동안 같은 데이터베이스의 컬렉션에 대한 쿼리는 처리할 수 없다. 그래서 기본적으로 프라이머리 멤버에서는 실행할 수 없지만 실행하고자 한다면 force 옵션을 true 로 하면 된다.
컬렉션 샤딩
샤딩은 컬렉션 단위로 적용된다. 즉 하나의 데이터베이스에 여러 개의 컬렉션이 있다고 해서 모든 컬렉션을 동일한 샤드 키로 샤딩해야 하는 것은 아니다. 또한 하나의 데이터베이스에서 일부 컬렉션은 샤딩돼 있더라도 일부는 샤딩되지 않은 상태로 프라이머리 샤드에만 저장할 수도 있다.
MongoDB 에서 샤딩된 컬렉션은 다시 샤드 키를 변경해서 샤딩할 수 없기 때문에 샤드 키를 선정하는 작업은 신중해야 한다.
샤딩을 할 것인지 여부는 '쿼리 빈도나 패턴', '데이터 크기' 의 두 가지 측면을 고려한다.
MongoDB 의 쿼리를 샤딩하면 쿼리의 조건이 샤드 키를 가지고 있는지 아닌지에 따라 특정 샤드로 전송하거나 모든 샤드로 전송하게 된다.
이런 쿼리를 타겟쿼리, 브로드캐스트 쿼리라고한다.
또한 샤딩에서 해시샤딩과 레인지샤딩이 있고, 레인지 샤딩은 청크를 미리 스플릿해두는 것이 자동으로 처리되지 않는다.
해시 샤딩은 서버가 MD5 해시 함수를 이용해 샤드 키 값의 해시 키를 생성하기 때ㅔ문에 이미 해시 함수의 결과 값에 대한 범위가 결정되어 있다.
그래서 MongoDB 서버가 지정된 개수만큼 청크를 미리 스플릿하는 것이 가능하다.
shardCollection 명령어에서
numInitialChunks 옵션은 해시 샤딩인 경우메나 사용할 수 있고, 샤딩을 적용함과 동시에 미리 청크를 스플릿해둘 수 있는데 몇 개의 청크를 미리 생성할 것인지 설정한다.
권장 청크 개수의 산술식이다.
청크 개수 = 도쿠먼트 건수 * 도큐먼트 크기 / 64MB
해시 샤딩과 비교했을 때 레인지 샤딩은 샤드 간 데이터 불균형이 심해질 수 있으며 별도로 샤드를 추가하고 제거하는 작업이 아니어도 샤드 간 청크 이동이 심해질 수 있다. 그래서 가능하면 레인지 샤딩보다는 해시 샤딩을 사용하는 것이 좋다.
이미 데이터를 가진 컬렉션을 샤딩하는 방법도 빈 컬렉션과 동일하게 shardCollection 명령을 사용한다.
하지만 이미 도큐먼트를 가진 컬렉션에 대해 해시 샤딩을 적용하는 경우는 numInitialChunks 옵션을 이용할 수 없다.
컬렉션이 이미 도큐먼트를 가지고 있을 때는 레인지 해시 샤딩 모두 컬렉션을 풀 스캔해서 스플릿하는 방법만 가능하다.
데이터를 가진 컬렉션을 샤딩할 때는 아래와 같은 과정을 거친다.
- 청크 스플릿 실행
- 청크 밸런싱
shardCollection 명령은 이 두개의 과정 중 첫 번째 과정인 청크의 스플릿만 실행한다.
실제 하나의 샤드에만 집중된 청크를 전체 샤드로 분산해 밸런싱하는 작업은 샤딩이 적용된 이후 천천히 실행된다.
이미 데이터를 가진 컬렉션을 샤딩할 때, 컬렉션에 저장된 도큐먼트를 일정 크기 기준으로 청크를 스플릿할 지점을 찾아야 한다.
이 때 해당 컬렉션을 풀 스캔하고 이 결과에 대해 split 명령을 실행함으로써 컨피그 서버에 저장한다.
이렇게 컨피그 서버에 청크 정보가 저장되면 비로소 샤딩이 적용된다.
하지만 이 상태는 컬렉션이 샤딩된 것으로 표시되고 청크는 스플릿 됐지만 실제 컬렉션의 모든 도큐먼트는 하나의 샤드 서버에만 저장돼 있기 때문에 샤딩을 적용하기 전과 아무런 차이가 없는 상태이다.
실제 부하를 분산하려면 청크를 다른 샤드로 분산해야 하는데 이렇게 청크가 이동되는 과정은 상당한 시간이 필요하다.
인덱스 관리
MongoDB 서버는 RDBMS 와 동일한 기능의 인덱스를 제공한다.
이런 특성으로 인해 MongoDB 서버는 퀄 ㅣ튜닝 및 실행 계획 분석이 동일하게 수행된다.
인덱스 생성 및 삭제
MongoDB 서버는 백그라운드와 포그라운드 방식의 인덱스 생성 방식을 제공한다.
인덱스 생성 시 background 옵션을 true 로 설정하면 된다.
이 차이는 인덱스를 생성하는 도중 다른 커렉션의 쿼리를 실행할 수 있는지 여부이다.
포그라운드는 다른 커넥션의 쿼리 실행을 모두 막고 변경되지 않는 상태에서 빌드하기 때문에 빠르다.
백그라운드는 내부적으로 컬렉션이 소속된 데이터베이스에 잠금을 걸고 컬렉션의 부분 데이터를 읽어 인덱스를 빌드하는 작업을 진행한다. 그리고 잠금을 해제하여 다른 컨넥션들이 쿼리를 실행할 수 있도록 여유 시간을 만들어준다.
따라서 인덱스 생성 시간이 길어지게 된다.
인덱스를 삭제하는 작업은 메타 정보만 변경하고 인덱스와 연관된 데이터 파일만 삭제하면 되므로 매우 빠르게 진행된다.
인덱스 삭제는 dropIndex 명령으로 삭제할 수 있다.
인덱스 목록 조회
컬렉션이 가진 인덱스는 getIndexes 명령이나 getIndexKeys 명령으로 확인할 수 있다.
getIndexes 는 상세한 정보를 표시해 주지만, 어떤 필드로 구성되었는지 간략한 정보만 보고자 할때는 getIndexKeys 명령이 편한다.
'Database > MongoDB' 카테고리의 다른 글
[MongoDB] 실행 계획 및 쿼리 최적화(2) (0) | 2024.11.25 |
---|---|
[MongoDB] 실행 계획 및 쿼리 최적화(1) (0) | 2024.11.18 |
[MongoDB] 데이터 모델링 (1) | 2024.09.19 |
[MongoDB] 잠금과 트랜잭션(2) (0) | 2024.09.09 |
[MongoDB] 인덱스(3) (0) | 2024.08.12 |