스키마 모델링
일반 RDBMS는 중복을 최소화하기 위해 정규화를 한다. 하지만 Mongodb는 물리적인 특성상 객체를 내장할 경우 디스크에서 같은 곳에 위치하기 때문에 더 효율적이다.
예를 들어 같은 컬렉션내에 있는 도큐먼트에 접근하기 위해서는 내장되어 있을 때 같은 위치에 접근하기 때문에 효율적이지만, 내장되어있는 도큐먼트가 다른 컬렉션을 참조하는 경우에는 추가적인 비용이 필요하다.
스키마 디자인 할 때 일반적인 규칙이 있는데 고민하게 만든 사항을 살펴보겠다.
한 객체에 포함관계로 모델링된 객체들은 내장
다대다 관계는 보통 참조
성능 이슈가 생기면 무조건 내장한다.
여기서 내장한다는게 정확히 무슨 말은 한다는 걸까?
아래와 같이 서브 도큐먼트를 내장한다는 걸까?
collection = {
doc : new Schema {
name: String
}
}
아래와 같이 다른 컬렉션에 대한 도큐먼트의 id를 내장한다는 걸까?
collection2 = {
doc2 : [
{type: Schema.ObjectId}
]
}
그리고 일반 임베디드 도큐먼트라는 타입이 있는데 이걸 말하는걸가?
collection3 = {
doc3 : {
name: String
}
}
각 타입에 대한 설명은 다음 링크를 참조한다.
mongoose와 유용한 schema 기능
모델링 타입은 2가지가 있다.
Normalized Data Models
Embedded Data Models
각 타입에 대한 자세한 설명은 다음 링크를 참조한다.
데이터 모델링 타입
스키마 디자인 팁
우선 객체간에 관계를 생각해보자.
객체간에 관계에서 생각해보는 입장을 취해보면 다음과 같이 나뉘어진다.
One-to-Few
One-to-Many
One-to-Squillions
양방향 참조
그리고 읽기 대 쓰기 효율을 고려하여 비정규화를 시킬 수 있다.
Many-to-One 관계 비정규화
One-to-Many 관계 비정규화
One-to-Squillions 관계 비정규화
하나씩 살표보자
One-to-Few
{
name:"John",
address: [
{ p1: 'village', p2: 'village', p3: 'village'}
{ p1: 'pocket', p2: 'pocket2', p3: 'pocket3'}
]
}
하나 당 적은 관계의 수는 내포하여 한 번의 쿼리에 모든 정보를 보낼 수 있다. 하지만 내포된 데이터만 독자적으로 불러올 수 없다.
One-to-Many
//comments
{
_id: ObjectId('AAA'),
name: "John"
}
//boards
{
_id: ObjectId('BOA');
comments: [
ObjectId('AAA'),
ObjectId('BBB')
]
}
각각의 문서를 독자적으로 다룰 수 있지만 여러번 호출해야 하는 단점이 있다. 이 경우 DB 레벨이 아닌 어플리케이션 레벨의 join으로 두 문서를 연결해야 한다.
boards = db.boards.findOne({_id: ObjectId('BOA')})
comments_array = db.comments.find({_id: {$id: boards.comments}}).toArray();
One-to-Squillions
//host
{
_id: ObjectId('AAA'),
ipAddr: '111.111.111.111'
}
//logs
{
time: ISODate()
host: ObjectId('AAA')
}
엄청나게 큰 많은 데이터가 필요한 경우 단일 문서의 크기가 한정되어 있기 때문에 부모 참조 방식을 활용해야 한다. 조인 방식은 다음과 같다.
host = db.hosts.findOne({_id: ObjectId('AAA')})
last_logs = db.logs.find({host: host._id}).sort({time:-1}).limit(5000);
양방향 참조
//comments
{
parent: ObjectId('BOA')
_id: ObjectId('AAA'),
name: "John"
}
//boards
{
_id: ObjectId('BOA');
comments: [
ObjectId('AAA'),
ObjectId('BBB')
]
}
양방향 참조는 One-to-Many에서 서로 참조하는 경우이다. 각 컬렉션에서 다른 컬렉션에 쉽게 접근할 수 있지만 문서를 삭제하는데 있어 쿼리를 두 번 보내야 한다.
그 외에 비정규화의 관계를 사용하게 되면(중복을 허용) 쓰기 성능보다 읽기 성능이 높아진다.
다음 링크에서 관련된 자세한 내용을 확인할 수 있다.
스키마 디자인 6가지 전략
성능을 높이기 위해 Index를 사용할 때 주의할 점.
Mongodb의 물리적 데이터 저장 구조를 살펴 보면 아래와 같다.
먼저 살펴보자면, 데이터 저장 시 논리적 메모리 공간에 write하고 일정 주기에 따라서 disk로 flsuh를 하는 Write Back 방식을 사용한다.(이 때문에 메모리에만 적재하면 되는 특성으로 인해 Write 속도가 빠르다는 말이 나오는 것 같다.)
데이터의 Read시에도 파일의 index를 메모리에 로딩해놓고 그 후 검색을 한다. 이 점에서 메모리에 저장되는 내용을 실제 데이터 블럭과 index 자체가 저장된다. 이 때문에 index를 남용하면 안된다. index를 생성하거나 업데이트할 때는 자원이 들어가며 index가 메모리에 상주하고 있어야 제대로 된 성능을 낸다.
메모리에 공간이 부족하면 page fault가 일어나게 되고 메모리에서 disk로부터 해당 데이터 블록을 요청하여 page와 disk 사이에 스위칭이 일어나게 되고 disk i/o 가 발생하여 성능을 떨어뜨리게 된다.
Index 선택에도 다음과 같은 규칙이 있다.
찾고자 하는 키에 대한 필드는 인덱스 한다.
보통 정렬하는 필드에 인덱스 한다.
인덱스를 사용하게 되면 쓰기의 성능은 떨어지기에 쓰기보다 읽기 비율이 높은 컬렉션에 인덱스를 사용하면 좋다.(위에서 말한것처럼 인덱스를 과용 사용하게 되면 메모리에 과잉문제가 생길 수 있다.)
결론
답은 없는 것 같다. 우선 구현 해보고 쿼리를 이용해 성능 테스트 해보면서 서비스 특성에 맞게 조정해봐야 할 것 같다. 여기에 많은 시간을 사용하였지만 아직 잘 모르겠다. 다음에 알아봐야 할 점으로는 다음과 같다.
Mongodb는 많은 수의 collection을 제공한다. collection의 수가 성능에 미치는 영향이 있는가?(하나의 컬렉션에다가 관리하는 것보다 각 컬렉션에서 쿼리를 사용하는게 더 효율적인 것 같지만.. 컬렉션이 많다고 좋은 점만 있는 것 같지는 않다.)
네트워크 호출 비용 vs 몽고디비 쿼리 조인 비용(이건 네트워크 호출보다 쿼리로 조인하는게 좋다고 생각하지만 알아볼 가치는 있는 것 같다.)
참조링크 :
Mongodb의 물리 데이터 저장 구조
스키마 디자인 6가지 전략
스키마 디자인
mongoose 스키마와 유용한 기능
데이터 모델링 타입