9.1 스키마 설계 고려 사항
- 애플리케이션에서 원하는 방식으로 데이터를 표현하는게 가장 좋은 설계 방식이다.
- RDB와 다르게 스키마를 모델링하기 전에 쿼리 및 데이터 접근을 먼저 이해해야 한다.
제약사항
- DB와 하드웨어 제약 사항을 이해해야 한다.
- 도큐먼트 최대 크기는 16MB
- 갱신은 전체 도큐먼트를 다시 쓰며, 원자성 갱신은 도큐먼트 단위로 갱신
쿼리 및 쓰기의 접근 패턴
- 애플리케이션 및 더 넓은 시스템의 워크로드를 식별하고 정량화해야한다.
- 쿼리가 실행되는 시기와 빈도를 알면 가장 일반적인 쿼리를 식별할 수 있다.
- 자주 사용하지 않는 데이터도 다른 컬렉션으로 이동하자.
- 동적 데이터와 정적 데이터를 분리할 수 있는지도 생각하자
- 스키마 설계의 우선 순위를 가장 일반적인 쿼리에 지정할 때 결과가 최상의 성능을 가진다.
관계 유형
- 애플리케이션 요구 사항 측면과 도큐먼트 간 관계 측에서 어떤 데이터가 관련되어 있는지 생각해야 한다.
- 쿼리하지 않고 도큐먼트를 참조하는 방법을 파악해야 한다.
- 관계가 변경될 때 갱신되는 도큐먼트 수를 알아야 함
- 쿼리하기 쉬운 구조인지 고려하자
카디널리티
SQL에서 카디널리티라는 용어는 데이터베이스 테이블의 특정 열에 포함된 데이터 값의 고유성을 나타냄. 카디널리티가 낮을수록 열에 더 많은 중복 요소가 있다. 가능한 가장 낮은 카디널리티를 가진 열은 모든 행에 대해 동일한 값을 갖는다.
- 도큐먼트와 데이터가 어떻게 관련돼 있는지 확인한 후에는 카디널리티 고려
- 일대일, 일대다, 다대다 … 관계 고려
- 모델링에 최선의 형식을 사용하도록 관계의 카디널리티를 설정하는것이 매우 중요
- 데이터 필드에 대한 읽기 갱신 비율도 고려해야 함
9.1.1 스키마 설계 패턴
애플리케이션 성능에 직접적인 영향이 가기 때문에 스키마 설계는 중요
다형성 패턴
- 컬렉션 내 모든 도큐먼트가 유사하지만 동일하지 않은 구조를 가질 때 적합
- 공통 쿼리를 지원하는 도큐먼트에서 공통 필드를 식별하는 것이 포함 됨
- 동일하지 않은 도큐먼트로 구성된 단일 컬렉션에서 간단한 쿼리를 사용해 쿼리 성능을 향상 시킬 수 있다.
속성 패턴
- 정렬하거나 쿼리하려는 도큐먼트에 필드의 서브셋이 있는 경우, 정렬하려는 필드가 도큐먼트의 서브셋에만 존재하는 경우 또는 두 조건이 모두 해당하는 경우에 적합
- 도큐먼트당 많은 유사한 필드를 대상으로 지정하기에 필요한 인덱스가 적어지고 쿼리 작성이 간단해짐
버킷 패턴
- 버킷 패턴은 데이터가 일정 기간 동안 스트림으로 유입되는 시계열 데이터에 적합
- 특정 시간 범위의 데이터를 각각 보유하는 도큐먼트 셋으로 ‘버킷화’하면 시간/데이터 포인트의 포인트당 도큐먼트를 만들 때보다 효율적이다.
- 1시간 버킷을 사용해 해당 시간 동안의 모든 판독 값을 단일 도큐먼트 내 배열에 배치할 수 있다.
이상치 패턴
- 도큐먼트의 쿼리가 애플리케이션의 정삭적인 패턴을 벗어 날 때 사용
- 인기도가 중요한 상황을 위해 설계된 고급 스키마 패턴으로, 주요영향 요인, 도서 판매, 영화 리뷰 등이 있는 소셜 네트워크에서 볼 수 있다.
- 플래그를 사용해 도큐먼트가 이상점임을 나타내며 추가 오버플로를 도큐먼트에 저장한다.
- 플래그는 애플리케이션 코드에서 오버플로 도큐먼트를 검색하기 위한 추가 쿼리를 만드는데 사용된다.
계산된 패턴
- 계산된 패턴은 데이터를 자주 계산해야 할 때나 데이터 접근 패턴이 읽기 집약적일 때 사용
- 주요 도큐먼트가 주기적으로 갱신되는 백그라운드에서 계산을 수행하도록 권장
- 읽기가 게산을 트리거하고 읽기-쓰기 비율이 높은 경우에 특히 동일한 계산의 반복을 방지함으로써 CPU에 가해지는 부담을 크게 줄일 수 있다.
서브셋 패턴
- 장비의 램 용량을 초과하는 작업 셋이 있을 때 사용
- 애플리케이션에서 사용하지 않는 정보를 많이 포함한는 대용량 도큐먼트 때문에 발생할 수 있다.
- 서브셋 패턴은 자주 사용하는 데이터와 자주 사용하지 않는 데이터를 두 개의 개별 컬렉션으로 분할하도록 한다.
확장된 참조 패턴
- 각각 고유한컬렉션이 있는 여러 논리 엔티티 또는 ‘사물’이 있고, 특정 기능을 위해 엔티티들을 모을 때 사용한다.
- 일반적인 전자상거래 스키마에서 주문, 고객, 재고에 대한 별도의 컬렉션이 있을 때 개별 컬렉션에서 단일 주문에 대한 정보를 모두 수집하면 성능에 영향이 갈 수 있다.
- 자주 접근하는 필드를 식별하고 주문 도큐먼트로 복제하면 문제 해결 가능
- 데이터를 중복시키는 대신 정보를 조합하는 데 필요한 쿼리 수를 줄인다.
근사 패턴
- 리소스가 많이 드는 계산이 필요하지만 높은 정확도가 필요하지 않은 상황에서 좋다.
- 근사 패턴을 적용해 추천이나 조회 수가 1회가 아니라 100회가 될 때마다 카운터를 갱신하면 쓰기 횟수를 크게 줄일 수 있다.
트리 패턴
- 쿼리가 많고 구조적으로 주로 계층적인 데이터가 있을 때 적용한다.
- 전체 계층고조 필드는 배열에 보관돼 해당 값에 다중키 인덱스를 사용하는 기능을 제공한다.
- 즉각적인 범주 필드를 사용하면 해당 범주와 직접 관련된 모든 항목을 찾을 수 있다.
사전 할당 패턴
- 사전 할당 패턴은 주로 MMAP 스토리지 엔진과 함께 사용된다.
- 빈 구조를 사전 할당한다.
- 예약정보 관리 시스템에서, 예약 가능 여부와 현재 예약 상태를 추적하는데 적요ㅇ
- 리소스와 날짜의 2차원 구조를 사용해 쉽게 가용성을 확인하고 계산할 수 있다.
도큐먼트 버전 관리 패턴
- 도큐먼트의 이전 버전을 유지하는 메터니즘을 제공
- 메인 컬렉션의 도큐먼트 버전을 추적하려면 각 도큐먼트에 부가 필드를 추가해야 함. 도큐먼트의 모든 수정 사항을 포함하는 추가 컬렉션이 필요하다
- 각 도큐먼트의 버전은 횟수가 제한되고, 버전 관리가 필요한 도큐먼트가 많지 않으며, 쿼리는 각 도큐먼트의 현재 버전에서 먼저 수행된다.
9.2 정규화 vs 비정규화
일반적으로 정규화는 쓰기를 빠르게 하고, 비정규화는 읽기를 빠르게 한다.
정규화
- 컬렉션 간의 참조를 이용해 데이터를 여러 컬렉션으로 나누는 작업
- 데이터를 변경하려면 한 도큐먼트만 갱신하면 된다.
- foreign-key로 left outer join을 수행하는 $lookup 을 제공
비정규화
- 모든 데이터를 하나의 도큐먼트에 내장하는 것
- 정보가 변경되면 여러 도큐먼트가 갱신돼야 하지만, 하나의 쿼리로 모든 데이터를 가져올 수 있다.
9.2.1 데이터 표현 예제
> db.studentClasses.findOne({"studentId": id})
>> {
"_id": ObjectID(1),
"studentId": ObjectID(2),
"classes": [
ObjectID(3),
ObjectID(4),
ObjectID(5),
ObjectID(6),
]
}
>>
{
"_id": objectID(1),
"name": "name",
"classes": [
{
"class": "Math",
"credits": 3,
"room": "201"
},
{
"class": "Korean",
"redits": 3,
"room": "301"
},
{
"class": "Physics",
"redits": 2,
"room": "302",
},
"class": "History",
"redits": 2,
"room": "101"
}
]
}
{
"_id": ObjectID(),
"name": "my name",
"classes": [
{
"_id": ObjectID(1),
"class": "Math",
},
{
"_id": ObjectID(2),
"class": "Korean",
},
{
"_id": ObjectID(3),
"class": "Physics",
},
{
"_id": ObjectID(4),
"class": "History",
}
]
}
- 정보가 읽히는 빈도에 비해 얼마나 자주 변경되는지 고려해야 함
- 정기적으로 갱신된다면 정규화
- 내장 도큐먼트를 사용하고 도큐먼트를 갱신해야 한다면, 모든 도큐먼트가 성공적으로 갱신되도록 보장하기 위해 cron 작업을 해야한다.
- 다중갱신을 시도했지만 모든 도큐먼트가 갱신되기 전에 멈추면 갱신을 재시도할 방법이 필요
- $set은 아무리 갱신을 시도해도 변하지 않는다. $inc는 여러번 시도하면 결과가 변할 수 있다. $set이 안전
- 포함된 필드는 도큐먼트의 데이터에 포함돼야 한다.
- 도큐먼트에 쿼리할 때 결과에서 거의 항상 제외되는 필드는 다른 컬렉션에 속해도 된다.
내장방식 vs 참조방식
내장 방식
- 작은 서브 도큐먼트
- 주기적으로 변하지 않는 데이터
- 결과적인 일관성이 허용될 때
- 증가량이 적은 도큐먼트
- 두 번째 쿼리를 수행하는 데 자주 필요한 데이터
- 빠른 읽기
참조 방식
- 큰 서브도큐먼트
- 자주 변하는 데이터
- 즉각적인 일관성이 필요할 때
- 증가량이 많은 도큐먼트
- 결과에서 자주 제외되는 데이터
- 빠른 쓰기
계정 관련 예시
계정 설정
- 해당 사용자 도큐먼트에만 관련 있으며, 도큐먼트 내 다른 정보와 함께 노출
- 계정 설정은 일반적으로 내장
최근 활동
- 최근 활동의 증가량과 변화량에 따라 다름
- 크기가 고정되면 내장하는것이 좋다
친구
유저가 생성한 모든 내용
9.2.2 카디널리티
블로그 애플리케이션을 가정
9.2.3 친구, 팔로워 벤?
구독
게시자를 구독자의 도큐먼트에 넣는 방법
{
"username": "I am producer",
"email": "producer@emai.com"
"following": [
ObjectID(1),
ObjectID(2),
ObjectID(3),
]
}
db.activities.find({"user": {"$in": user["following"]}})
{
"username": "I am producer",
"email": "producer@emai.com"
"followers": [
ObjectID(1),
ObjectID(2),
ObjectID(3),
]
}
-
팔로워 모두에게 기능을 제공해야 할 때 유용
-
팔로우하는 모든 사람을 찾는건 users 컬렉션 전체를 조회해야하는 단점
-
자주 반환되지 않고, 자주 변환되는 필드에 유용한 방법
-
도큐먼트를 간단한 형태로 유지 팔로워 정보 얻기 위해선 추가 쿼리가 필요함
{
"_id: ObjectID(1),
"followers": [
ObjectID(2),
ObjectID(3),
ObjectID(4),
]
}
유명인 사용자로 인한 영향에 대처하기
- 연속 도큐먼트로 처리
- to be continued(tbc) 로 처리한것을 볼 수 있다.
{
"username": "I am producer",
"email": "producer@emai.com",
"tbc": [
ObjectID(1),
ObjectID(2),
],
"followers": [
ObjectID(4),
ObjectID(5),
ObjectID(6),
]
}
{
"_id: ObjectID(),
"followers": [
ObjectID(),
ObjectID(),
ObjectID(),
]
}
{
"_id: ObjectID(),
"followers": [
ObjectID(),
ObjectID(),
ObjectID(),
]
}
9.3 데이터 조작을 위한 최적화
- 읽기 최적화는 올바른 인덱스를 사용해 하나의 도큐먼트에서 가능한 많은 정보를 반환하는 것과 관련있다
- 쓰기 최적화는 보통 갖고 있는 인덱스 개수를 최소화하고 갱신을 효율적으로 하는것과 관련있다
- 쓰기 읽기 뿐아니라 비율또한 최적화 요소
- 쓰기가 중요하지만 쓰기에 대해 읽기를 수천번하면 읽기를 먼저 최적화하자
9.3.1 오래된 데이터 제거
- 데이터마다 중요한 기간이 있을 수 있다
오래된 데이터 제거방법
- 제한컬렉션
- 가장 쉬운방법
- 컬렉션 크기 설정 후 오래된 데이터 밀어내게 만든다
- 사용자 작업에 제약이 생김
- 급격히 늘어나는 트래픽에 취약
- TTL 컬렉션
- 도큐먼트가 제거될 때 조절가능
- 쓰기를 많이 하는 컬렉션에서는 빠르지X
- 일정 주기로 컬렉션 삭제
- 주기마다 컬렉션을 따로 관리한다
- 트래픽에 버티기 좋다
- 애플리케이션 구축이 복잡
9.4 데이터베이스와 컬렉션 구상
- 일반적으로 스키마가 유사한 도큐먼트는 가은 컬렉션에 보관
- 몽고db는 보통 다른 컬렉션에 있는 데이터 결합을 허용하지 않는다.
- 함께 쿼리하거나 집계해야 하는 도큐먼트는 하나의 컬렉션에 넣는게 좋다.
- 컬렉션에서 락과 저장을 고려해야 함
- 쓰기가 높다면 여러 물리적 볼륨을 통해 병목 현상을 줄일 수 있다.
- 컬렉션 내 데이터의 중요도에 따라 데이터베이스를 나눌 수 있다.
- 중요도에 따라 성능 또한 차별을 둘 수 있다.
9.5 일관성 관리
-
셸을 두 개 열때 데이터베이스에 대한 연결이 두개가 된다. 하나의 셸에서 insert를 하면 이후에 다른 셸에서 발생하는 쿼리는 삽입된 도큐먼트를 반환하지 못한다. 그러나 단일 셸에서 삽입하고 쿼리하면 삽입된 도큐먼트가 반환 됨
-
각 각 스레드에서 작동하기에 일어나는 현상, ruby, python, java에서 connection pooling을 사용해서 일어나는 동작
-
읽기 요청을 복제 셋의 세컨더리로 보낼 때 문제가 될 수 있다.
-
세컨더리는 초, 분, 시간 단위 전부터 데이터를 읽으므로 primary에 뒤처질 수 있다.
-
모든 읽기 요청을 primary에 보내면 데이터가 오래되어도 사용 가능
-
readConcern, writeConcern 를 결합하면 애플리케이션에 대한 일관성과 가용성 보장을 제어할 수 있다.
-
읽기 부실을 방지하려면 majority를 사용
-
읽기 작업을 시작하기 전 완료된 쓰기를 모두 반영하는 데이터를 반환하는 linearizable을 사용할 수 있다.
-
결과를 반환하기 전에 동시 실행되는 쓰기가 완료될 때까지 기다리게 linearizable readConcern를 사용할 수 있다.
9.6 스키마 마이그레이션
- 애플리케이션 규모가 커지고 요구 사항이 변할수록 스키마도 커지고 변화한다.
- 도큐먼트 버전 관리 패턴을 적용하는게 이상적이다.
스키마를 애플리케이션의 요구에 맞춰 변화시키는 방법
-
애플리케이션이 구 번전 스키마를 모두 지원하는지 확인
-
스키마 버전이 서로 충돌하면 지저분해진다.
-
특정 필드를 요구하고, 하지않고, 선택적으로 요구하는 경우 코드를 더 복잡하게 만든다.
-
도큐먼트에 version 필드를 추가해서 판별할 수 있다.
스키마가 변경될 때 모든 데이터를 마이그레션하는 방법
-
일반적으로 좋지X
-
모든 도큐먼트가 성공적으로 갱신했는지 확인하는 몽고db 특성상(트랜잭션) 중간에 충돌하면 이전 스키마가 유지된다.
9.7 스키마 관리
- 몽고db 버전 3.2에서 스키마 유효성 검사를 통해 갱신 및 삽입중에 유효성 검사를 허용한다.
- 버전 3.6에서 $jsonSchema를 사용해서 JSON 스키마 유효성 검사 추가
- 유효성 검사는 기존 도큐먼트가 수정되면 확인하고 컬렉션별로 구성
- 기존 컬렉션에 유효성 검사를 추가하려면 validator 옵션과 collMod 명령을 사용
- 새 컬렉션에 유효성검사를 추가하려면 createCollection에서 validator옵션을 지정
- validationLevel - 기존 도큐먼트 갱신 중 유효성 검사 규칙 엄격도 결정
- validationAction - 불법 도큐먼트? 오류와 함께 거절 or 경고 pass 결정
9.8 몽고DB를 사용하지 않는 경우
- 조인이 많은 작업의 경우 관계형 데이터베이스에 적합
- 몽고db를 지원하지 않는 도구를 사용할 수 있기 때문에 - tableplus, datagrip 사용 가능 함