실제 예제를 사용하여 Azure Cosmos DB에서 데이터를 모델링하고 분할하는 방법

적용 대상: NoSQL

이 문서는 실제 데이터 설계 연습을 처리하는 방법을 보여 주기 위해 데이터 모델링, 분할프로비저닝된 처리량과 같은 여러 Azure Cosmos DB 개념을 기반으로 하여 작성되었습니다.

일반적으로 관계형 데이터베이스를 사용하는 경우 데이터 모델을 설계하는 방법에 대한 습관과 직관을 구축했을 수 있습니다. 특정 제약 조건뿐만 아니라 Azure Cosmos DB의 고유한 장점으로 인해 이러한 모범 사례의 대부분은 제대로 변환되지 않고 최적이 아닌 솔루션으로 끌어들일 수 있습니다. 이 문서의 목표는 항목 모델링에서 엔터티 공동 배치 및 컨테이너 분할에 이르기까지 Azure Cosmos DB의 실제 사용 사례를 모델링하는 전체 프로세스를 안내하는 것입니다.

이 문서의 개념을 설명하는 커뮤니티 생성 소스 코드를 다운로드하거나 봅니다.

Important

커뮤니티 기여자가 이 코드 샘플을 제공했으며 Azure Cosmos DB 팀은 유지 관리를 지원하지 않습니다.

시나리오

이 연습에서는 사용자가 게시물을 만들 수 있는 블로깅 플랫폼의 도메인을 살펴보겠습니다. 사용자는 이러한 게시물에 대해 좋아요를 표시하고 댓글을 추가할 수도 있습니다.

기울임꼴의 몇 가지 단어를 강조했습니다. 이러한 단어는 모델에서 조작해야 하는 "상황(things)"의 종류를 식별합니다.

사양에 추가해야 하는 요구 사항은 다음과 같습니다.

  • 첫 페이지에는 최근에 만든 게시물의 피드가 표시됩니다.
  • 사용자에 대한 모든 게시물, 게시물에 대한 모든 댓글, 게시물에 대한 모든 좋아요를 가져올 수 있습니다.
  • 게시물은 작성자의 사용자 이름과 획득한 댓글 및 좋아요의 수와 함께 반환됩니다.
  • 댓글 및 좋아요도 작성한 사용자의 사용자 이름과 함께 반환됩니다.
  • 목록으로 표시되는 경우 게시물은 해당 콘텐츠의 잘린 요약만 제공해야 합니다.

기본 액세스 패턴 식별

먼저, 솔루션의 액세스 패턴을 식별하여 초기 사양에 몇 가지 구조를 제공합니다. Azure Cosmos DB용 데이터 모델을 디자인할 때 모델이 해당 요청을 효율적으로 제공하는지 확인하기 위해 모델이 제공해야 하는 요청을 이해하는 것이 중요합니다.

전체 프로세스를 더 쉽게 따를 수 있도록 다양한 요청을 명령 또는 쿼리로 분류하여 CQRS에서 일부 어휘를 대여합니다. CQRS에서 명령은 쓰기 요청(즉, 시스템을 업데이트하려는 의도)이며 쿼리는 읽기 전용 요청입니다.

플랫폼이 노출하는 요청 목록은 다음과 같습니다.

  • [C1] 사용자 만들기/편집
  • [Q1] 사용자 검색
  • [C2] 게시물 만들기/편집
  • [Q2] 게시물 검색
  • [Q3] 약식으로 사용자의 게시물 나열
  • [C3] 댓글 만들기
  • [Q4] 게시물의 댓글 나열
  • [C4] 게시물에 대한 좋아요 표시
  • [Q5] 게시물에 대한 좋아요 나열
  • [Q6] 최근에 만든 x개 게시물을 약식으로 나열(피드)

이 단계에서는 각 엔터티(사용자, 게시물 등)에 포함된 세부 정보에 대해 생각하지 않았습니다. 이 단계는 일반적으로 관계형 저장소에 대해 디자인할 때 해결해야 할 첫 번째 단계 중 하나입니다. 이러한 엔터티가 테이블, 열, 외세 키 등의 측면에서 어떻게 변환되는지 파악해야 하기 때문에 먼저 이 단계로 시작합니다. 쓰기 시 스키마를 적용하지 않는 문서 데이터베이스에 대한 우려는 훨씬 적습니다.

처음부터 액세스 패턴을 식별하는 것이 중요한 주된 이유는 이 요청 목록이 테스트 도구 모음이 되기 때문입니다. 데이터 모델을 반복할 때마다 각 요청을 살펴보고 성능 및 확장성을 확인합니다. 각 모델에서 사용되는 요청 단위를 계산하고 최적화합니다. 이러한 모든 모델은 기본 인덱싱 정책을 사용하며 특정 속성을 인덱싱하여 이를 재정의할 수 있으므로 RU 사용 및 대기 시간을 더욱 개선할 수 있습니다.

V1: 첫 번째 버전

두 개의 컨테이너(usersposts)로 시작합니다.

사용자 컨테이너

다음 컨테이너는 사용자 항목만 저장합니다.

{
    "id": "<user-id>",
    "username": "<username>"
}

이 컨테이너를 분할합니다 id. 즉, 해당 컨테이너 내의 각 논리 파티션에는 하나의 항목만 포함됩니다.

posts 컨테이너

이 컨테이너는 게시물, 메모 및 좋아요와 같은 엔터티를 호스트합니다.

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "title": "<post-title>",
    "content": "<post-content>",
    "creationDate": "<post-creation-date>"
}

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "creationDate": "<like-creation-date>"
}

이 컨테이너를 분할합니다 postId. 즉, 해당 컨테이너 내의 각 논리 파티션에는 하나의 게시물, 해당 게시물에 대한 모든 메모 및 해당 게시물에 대한 모든 좋아요가 포함됩니다.

이 컨테이너에서 호스트하는 type 세 가지 유형의 엔터티를 구분하기 위해 이 컨테이너에 저장된 항목에 속성을 도입했습니다.

또한 다음과 같은 이유로 관련 데이터를 포함하는 대신 참조하도록 선택했습니다(이러한 개념에 대한 자세한 내용은 이 섹션 참조).

  • 사용자가 만들 수 있는 게시물의 수에 대한 상한이 없습니다.
  • 게시물은 임의로 길 수 있습니다.
  • 게시물에서 획득한 댓글과 좋아요에 대한 상한이 없습니다.
  • 게시물 자체를 업데이트하지 않고도 게시물에 댓글 또는 좋아요를 추가할 수 있기를 원합니다.

모델의 성능은 어떻나요?

이제 첫 번째 버전의 성능과 확장성을 평가하는 시간입니다. 이전에 식별된 각 요청에 대해 지연 시간과 소비하는 요청 단위의 크기를 측정합니다. 이 측정은 사용자당 5~50개의 게시물 및 게시물당 최대 25개의 댓글 및 100개의 좋아요가 있는 100,000명의 사용자가 포함된 더미 데이터 세트에 대해 수행되었습니다.

[C1] 사용자 만들기/편집

이 요청은 users 컨테이너에서 항목을 만들거나 업데이트할 때 쉽게 구현할 수 있습니다. 요청은 파티션 키 덕분에 id 모든 파티션에 잘 분산됩니다.

사용자의 컨테이너에 단일 항목을 쓰는 다이어그램입니다.

대기 시간 RU 요금 성능
7 ms(ms) 5.71 RU(RU)

[Q1] 사용자 검색

users 컨테이너에서 해당 항목을 읽어 사용자를 검색합니다.

사용자의 컨테이너에서 단일 항목을 검색하는 다이어그램

대기 시간 RU 요금 성능
2 ms(ms) 1 RU(RU)

[C2] 게시물 만들기/편집

[C1]과 마찬가지로 posts 컨테이너에 쓰기만 하면 됩니다.

게시물 컨테이너에 단일 게시물 항목을 쓰는 다이어그램입니다.

대기 시간 RU 요금 성능
9 ms(ms) 8.76 RU(RU)

[Q2] 게시물 검색

posts 컨테이너에서 해당 문서를 검색하는 것으로 시작합니다. 그러나 사양에 따라 게시물 작성자의 사용자 이름, 댓글 수 및 게시물에 대한 좋아요 수를 집계해야 합니다. 나열된 집계를 실행하려면 3개의 SQL 쿼리가 추가로 필요합니다.

게시물을 검색하고 추가 데이터를 집계하는 다이어그램

각 쿼리는 각 컨테이너의 파티션 키를 필터링하며, 이는 성능과 확장성을 최대화하려는 것입니다. 하지만 결국에는 4개의 작업을 수행하여 하나의 게시물을 반환해야 하므로 이 부분은 다음 반복에서 개선됩니다.

대기 시간 RU 요금 성능
9 ms(ms) 19.54 RU(RU)

[Q3] 약식으로 사용자의 게시물 나열

먼저 특정 사용자에 해당하는 게시물을 가져오는 SQL 쿼리를 사용하여 원하는 게시물을 검색해야 합니다. 그러나 작성자의 사용자 이름과 댓글 및 좋아요 수를 집계하기 위해 더 많은 쿼리를 실행해야 합니다.

사용자의 모든 게시물을 검색하고 추가 데이터를 집계하는 다이어그램.

이 구현에는 다음과 같이 많은 단점이 있습니다.

  • 댓글 및 좋아요의 수를 집계하는 쿼리는 첫 번째 쿼리에서 반환된 각 게시물에 대해 실행되어야 합니다.
  • 기본 쿼리는 컨테이너의 posts 파티션 키를 필터링하지 않으므로 컨테이너 전체에서 팬아웃 및 파티션 검색이 수행됩니다.
대기 시간 RU 요금 성능
130 ms(ms) 619.41 RU(RU)

[C3] 댓글 만들기

댓글은 해당 항목을 posts 컨테이너에 기록함으로써 만들어집니다.

게시물 컨테이너에 단일 메모 항목을 쓰는 다이어그램입니다.

대기 시간 RU 요금 성능
7 ms(ms) 8.57 RU(RU)

[Q4] 게시물의 댓글 나열

먼저 해당 게시물에 대한 모든 댓글을 가져오는 쿼리로 시작하고, 각 댓글에 대한 사용자 이름도 개별적으로 집계해야 합니다.

게시물에 대한 모든 댓글을 검색하고 추가 데이터를 집계하는 다이어그램.

주 쿼리는 컨테이너의 파티션 키를 필터링하지만, 사용자 이름을 개별적으로 집계하면 전체 성능이 저하됩니다. 나중에 개선합니다.

대기 시간 RU 요금 성능
23 ms(ms) 27.72 RU(RU)

[C4] 게시물에 대한 좋아요 표시

[C3]과 마찬가지로 해당 항목을 posts 컨테이너에 만듭니다.

게시물 컨테이너에 단일 항목(예: )을 작성하는 다이어그램

대기 시간 RU 요금 성능
6 ms(ms) 7.05 RU(RU)

[Q5] 게시물에 대한 좋아요 나열

[Q4]와 마찬가지로 해당 게시물에 대한 좋아요를 쿼리한 다음, 해당 사용자 이름을 집계합니다.

게시물에 대한 모든 좋아요를 검색하고 추가 데이터를 집계하는 다이어그램.

대기 시간 RU 요금 성능
59 ms(ms) 58.92 RU(RU)

[Q6] 최근에 만든 x개 게시물을 약식으로 나열(피드)

posts 컨테이너를 쿼리하여 만든 날짜를 내림차순으로 정렬한 최근 게시물을 가져온 다음, 각 게시물에 대한 사용자 이름과 댓글 및 좋아요의 수를 집계합니다.

가장 최근 게시물을 검색하고 추가 데이터를 집계하는 다이어그램.

다시 한번, 초기 쿼리는 컨테이너의 posts 파티션 키를 필터링하지 않으므로 비용이 많이 드는 팬아웃을 트리거합니다. 이는 더 큰 결과 집합을 대상으로 지정하고 절을 사용하여 ORDER BY 결과를 정렬하므로 요청 단위 측면에서 비용이 더 많이 들기 때문에 더욱 악화됩니다.

대기 시간 RU 요금 성능
306 ms(ms) 2063.54 RU(RU)

V1의 성능 반영

이전 섹션에서 직면한 성능 문제를 살펴보면 다음과 같은 두 가지 유형의 주요 문제를 식별할 수 있습니다.

  • 일부 요청에서는 반환해야 하는 모든 데이터를 수집하기 위해 여러 개의 쿼리를 실행해야 합니다.
  • 일부 쿼리는 대상 컨테이너의 파티션 키를 필터링하지 않으므로 확장성을 방해하는 팬아웃이 수행됩니다.

첫 번째 문제부터 시작하여 각 문제를 해결해 보겠습니다.

V2: 읽기 쿼리 최적화를 위한 비정규화 도입

경우에 따라 더 많은 요청을 발급해야 하는 이유는 초기 요청의 결과에 반환해야 하는 모든 데이터가 포함되지 않기 때문입니다. 데이터를 비정규화하면 Azure Cosmos DB와 같은 비관계형 데이터 저장소로 작업할 때 데이터 집합에서 이러한 종류의 문제가 해결됩니다.

다음 예제에서는 게시물 작성자의 사용자 이름, 댓글 수 및 좋아요 수를 추가하도록 게시물 항목을 수정합니다.

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

또한 댓글 및 좋아요 항목을 만든 사용자의 사용자 이름을 추가하도록 해당 항목을 수정합니다.

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "userUsername": "<comment-author-username>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "userUsername": "<liker-username>",
    "creationDate": "<like-creation-date>"
}

댓글 및 좋아요 수 비정규화

달성하고자 하는 것은 댓글이나 좋아요를 추가할 때마다 해당 게시물에서 commentCount 또는 likeCount도 증분시키는 것입니다. 컨테이너를 분할할 postspostId 새 항목(주석 등)과 해당 게시물이 동일한 논리 파티션에 배치됩니다. 따라서 저장 프로시저를 사용하여 해당 작업을 수행할 수 있습니다.

주석([C3])을 만들 때 컨테이너에 새 항목을 추가하는 대신 해당 컨테이너에서 posts 다음 저장 프로시저를 호출합니다.

function createComment(postId, comment) {
  var collection = getContext().getCollection();

  collection.readDocument(
    `${collection.getAltLink()}/docs/${postId}`,
    function (err, post) {
      if (err) throw err;

      post.commentCount++;
      collection.replaceDocument(
        post._self,
        post,
        function (err) {
          if (err) throw err;

          comment.postId = postId;
          collection.createDocument(
            collection.getSelfLink(),
            comment
          );
        }
      );
    })
}

이 저장 프로시저는 게시물의 ID와 새 댓글의 본문을 매개 변수로 사용하여 다음을 수행합니다.

  • 게시물 검색
  • commentCount 증분
  • 게시물 대체
  • 새 댓글 추가

저장 프로시저가 원자성 트랜잭션으로 실행될 때 값 commentCount 과 실제 주석 수는 항상 동기화 상태로 유지됩니다.

새 좋아요를 추가하여 likeCount를 증분시킬 때도 분명히 이와 비슷한 저장 프로시저를 호출합니다.

사용자 이름 비정규화

사용자는 다른 파티션뿐만 아니라 다른 컨테이너에도 배치되므로 사용자 이름에는 다른 접근 방식이 필요합니다. 파티션과 컨테이너 간에 데이터를 비정규화해야 하는 경우 원본 컨테이너의 변경 피드를 사용할 수 있습니다.

다음 예제에서는 사용자가 자신의 사용자 이름을 업데이트할 때마다 users 컨테이너의 변경 피드를 사용하여 반응합니다. 이 경우 posts 컨테이너에 대해 다른 저장 프로시저를 호출하여 변경 내용을 전파합니다.

게시물 컨테이너에 사용자 이름을 비정규화하는 다이어그램입니다.

function updateUsernames(userId, username) {
  var collection = getContext().getCollection();
  
  collection.queryDocuments(
    collection.getSelfLink(),
    `SELECT * FROM p WHERE p.userId = '${userId}'`,
    function (err, results) {
      if (err) throw err;

      for (var i in results) {
        var doc = results[i];
        doc.userUsername = username;

        collection.upsertDocument(
          collection.getSelfLink(),
          doc);
      }
    });
}

이 저장 프로시저는 사용자의 ID와 사용자의 새 사용자 이름을 매개 변수로 사용하여 다음을 수행합니다.

  • userId와 일치하는 모든 항목(게시물, 댓글 또는 좋아요 항목일 수 있음) 가져오기
  • 각 항목의 경우
    • userUsername 대체
    • 항목 대체

Important

이 작업은 posts 컨테이너의 모든 파티션에서 이 저장 프로시저를 실행해야 하므로 비용이 많이 듭니다. 대부분의 사용자는 등록 중에 적절한 사용자 이름을 선택하고 변경하지 않으므로 이 업데이트는 거의 실행되지 않습니다.

V2의 성능 향상 효과는?

V2의 성능 향상에 대해 알아보겠습니다.

[Q2] 게시물 검색

이제 비정규화가 완료되었으므로 해당 요청을 처리하기 위해 하나의 항목만 가져오면 됩니다.

비정규화된 게시물 컨테이너에서 단일 항목을 검색하는 다이어그램

대기 시간 RU 요금 성능
2 ms(ms) 1 RU(RU)

[Q4] 게시물의 댓글 나열

여기서 다시, 사용자 이름을 가져온 추가 요청을 줄이고 파티션 키를 필터링하는 단일 쿼리로 끝낼 수 있습니다.

비정규화된 게시물에 대한 모든 메모를 검색하는 다이어그램.

대기 시간 RU 요금 성능
4 ms(ms) 7.72 RU(RU)

[Q5] 게시물에 대한 좋아요 나열

좋아요를 나열하는 경우와 똑같은 상황입니다.

비정규화된 게시물에 대한 모든 좋아요를 검색하는 다이어그램.

대기 시간 RU 요금 성능
4 ms(ms) 8.92 RU(RU)

V3: 모든 요청이 확장 가능한지 확인

전반적인 성능 향상을 살펴볼 때 완전히 최적화되지 않은 두 가지 요청이 여전히 있습니다. 이러한 요청은 [Q3][Q6]입니다. 대상 컨테이너의 파티션 키를 필터링하지 않는 쿼리와 관련된 요청입니다.

[Q3] 약식으로 사용자의 게시물 나열

이 요청은 이미 V2에 도입된 향상된 기능으로 인해 더 많은 쿼리를 절약할 수 있습니다.

사용자의 비정규화된 게시물을 짧은 형식으로 나열하는 쿼리를 보여 주는 다이어그램

그러나 나머지 쿼리에서는 아직 posts 컨테이너의 파티션 키를 필터링하지 않습니다.

이 상황에 대해 생각하는 방법은 간단합니다.

  1. 이 요청 특정 사용자에 대한 userId 모든 게시물을 가져오려고 하기 때문에 필터링해야 합니다.
  2. 분할이 없는 컨테이너에 대해 posts 실행되므로 성능이 userId 좋지 않습니다.
  3. 분명히 말하면, 로 분할된 userId컨테이너에 대해 이 요청을 실행하여 성능 문제를 해결할 것입니다.
  4. 이미 users 컨테이너가 있습니다!

따라서 전체 게시물을 users 컨테이너에 복제하여 두 번째 수준의 비정규화를 도입합니다. 이렇게 하면 게시물의 복사본을 효과적으로 얻을 수 있으며, 다른 차원을 따라 분할되어 해당 게시물에 의해 userId검색하는 것이 더 효율적입니다.

이제 컨테이너에는 users 다음 두 종류의 항목이 포함됩니다.

{
    "id": "<user-id>",
    "type": "user",
    "userId": "<user-id>",
    "username": "<username>"
}

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

이 예에서는 다음이 적용됩니다.

  • 사용자 항목에 게시물과 사용자를 구분하는 필드가 도입되었습니다 type .
  • 또한 사용자 항목에 필드를 추가 userId 했습니다. 이 필드는 필드와 id 중복되지만 컨테이너가 이제 이전과 같이 분할되기 userId 때문에 users 필요합니다.id

이 비정규화를 달성하기 위해 변경 피드를 다시 한번 사용합니다. 이번에는 posts 컨테이너의 변경 피드에 반응하여 새 게시물이나 업데이트된 게시물을 users 컨테이너로 발송합니다. 게시물을 나열해도 전체 콘텐츠를 반환할 필요가 없으므로 프로세스에서 삭제할 수 있습니다.

사용자의 컨테이너에 대한 게시물을 비정규화하는 다이어그램.

이제 쿼리를 users 컨테이너로 라우팅하여 해당 컨테이너의 파티션 키를 필터링할 수 있습니다.

비정규화된 사용자의 모든 게시물을 검색하는 다이어그램.

대기 시간 RU 요금 성능
4 ms(ms) 6.46 RU(RU)

[Q6] 최근에 만든 x개 게시물을 약식으로 나열(피드)

여기서도 비슷한 상황을 처리해야 합니다. V2에 도입된 비정규화로 인해 더 많은 쿼리가 불필요하게 남아 있는 경우에도 나머지 쿼리는 컨테이너의 파티션 키를 필터링하지 않습니다.

짧은 형식으로 작성된 가장 최근 게시물 x개를 나열하는 쿼리를 보여 주는 다이어그램

동일한 접근 방식에 따라 이 요청의 성능과 확장성을 최대화하려면 하나의 파티션만 적중해야 합니다. 제한된 수의 항목만 반환해야 하므로 단일 파티션을 적중하는 것만 생각할 수 있습니다. 블로깅 플랫폼의 홈페이지를 채우려면 전체 데이터 집합을 페이지를 매길 필요 없이 가장 최근 게시물 100개만 가져와야 합니다.

따라서 이 마지막 요청을 최적화하기 위해 전적으로 이 요청을 처리하는 데 전념하는 세 번째 컨테이너를 설계했습니다. 게시물을 새 feed 컨테이너에 비정규화합니다.

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

type 필드는 항상 post 항목에 있는 이 컨테이너를 분할합니다. 이렇게 하면 이 컨테이너의 모든 항목이 동일한 파티션에 배치됩니다.

비정규화를 달성하기 위해 앞에서 도입한 변경 공급 파이프라인에 후크하여 게시물을 새 컨테이너로 발송하기만 하면 됩니다. 명심해야 할 한 가지 중요한 점은 100개의 최근 게시물만 저장해야 한다는 것입니다. 그렇지 않으면 컨테이너의 콘텐츠가 파티션의 최대 크기를 초과할 수 있습니다. 이 제한은 컨테이너에 문서가 추가될 때마다 사후 트리거를 호출하여 구현할 수 있습니다.

피드 컨테이너에 대한 게시물을 비정규화하는 다이어그램.

컬렉션을 자르는 사후 트리거의 본문은 다음과 같습니다.

function truncateFeed() {
  const maxDocs = 100;
  var context = getContext();
  var collection = context.getCollection();

  collection.queryDocuments(
    collection.getSelfLink(),
    "SELECT VALUE COUNT(1) FROM f",
    function (err, results) {
      if (err) throw err;

      processCountResults(results);
    });

  function processCountResults(results) {
    // + 1 because the query didn't count the newly inserted doc
    if ((results[0] + 1) > maxDocs) {
      var docsToRemove = results[0] + 1 - maxDocs;
      collection.queryDocuments(
        collection.getSelfLink(),
        `SELECT TOP ${docsToRemove} * FROM f ORDER BY f.creationDate`,
        function (err, results) {
          if (err) throw err;

          processDocsToRemove(results, 0);
        });
    }
  }

  function processDocsToRemove(results, index) {
    var doc = results[index];
    if (doc) {
      collection.deleteDocument(
        doc._self,
        function (err) {
          if (err) throw err;

          processDocsToRemove(results, index + 1);
        });
    }
  }
}

마지막 단계는 쿼리를 새 feed 컨테이너로 다시 라우팅하는 것입니다.

가장 최근 게시물을 검색하는 다이어그램

대기 시간 RU 요금 성능
9 ms(ms) 16.97 RU(RU)

결론

다양한 버전의 디자인에 대해 도입한 전반적인 성능 및 확장성 향상을 살펴보겠습니다.

V1 V2 V3
[C1] 7 ms / 5.71 RU 7 ms / 5.71 RU 7 ms / 5.71 RU
[Q1] 2 ms / 1 RU 2 ms / 1 RU 2 ms / 1 RU
[C2] 9 ms / 8.76 RU 9 ms / 8.76 RU 9 ms / 8.76 RU
[Q2] 9 ms / 19.54 RU 2 ms / 1 RU 2 ms / 1 RU
[Q3] 130 ms / 619.41 RU 28 ms / 201.54 RU 4 ms / 6.46 RU
[C3] 7 ms / 8.57 RU 7 ms / 15.27 RU 7 ms / 15.27 RU
[Q4] 23 ms / 27.72 RU 4 ms / 7.72 RU 4 ms / 7.72 RU
[C4] 6 ms / 7.05 RU 7 ms / 14.67 RU 7 ms / 14.67 RU
[Q5] 59 ms / 58.92 RU 4 ms / 8.92 RU 4 ms / 8.92 RU
[Q6] 306 ms / 2063.54 RU 83 ms / 532.33 RU 9 ms / 16.97 RU

읽기가 많은 시나리오를 최적화했습니다.

쓰기 요청(명령)을 희생하면서 읽기 요청(쿼리)의 성능을 개선하기 위한 노력을 집중했을 수 있습니다. 대부분의 경우 쓰기 작업은 변경 피드를 통해 후속 비정규화를 트리거하므로 이를 구현하는 데 있어 컴퓨팅 비용이 더 많이 들고 시간이 더 오래 걸립니다.

대부분의 소셜 앱과 같은 블로깅 플랫폼이 읽기가 무거우므로 읽기 성능에 중점을 두는 것이 좋습니다. 읽기 작업이 많은 워크로드는 처리해야 하는 읽기 요청의 양이 일반적으로 쓰기 요청 수보다 큰 순서임을 나타냅니다. 따라서 읽기 요청이 더 저렴하고 더 효율적으로 수행될 수 있도록 쓰기 요청을 실행하는 데 더 많은 비용을 들이는 것이 좋습니다.

가장 극단적인 최적화 를 살펴보면 [Q6] 은 2000개 이상의 RU에서 단 17RU로 진행되었으며, 항목당 약 10RU의 비용으로 게시물을 비정규화하여 이를 달성했습니다. 게시물을 만들거나 업데이트하는 것보다 훨씬 더 많은 피드 요청을 처리하므로 전체 비용의 절감을 고려할 때 이 비정규화 비용은 무시할 수 있습니다.

비정규화를 증분 방식으로 적용할 수 있음

이 문서에서 살펴본 향상된 확장성 기능은 데이터 세트 전체의 데이터 비정규화 및 복제와 관련이 있습니다. 이러한 최적화는 1일차에 시행할 필요가 없습니다. 파티션 키를 필터링하는 쿼리는 대규모로 더 잘 수행되지만 파티션 간 쿼리는 드물게 호출되거나 제한된 데이터 집합에 대해 호출되는 경우 허용될 수 있습니다. 프로토타입을 빌드하거나 작고 제어된 사용자 기반을 사용하여 제품을 출시하는 경우 나중에 이러한 개선 사항을 절약할 수 있습니다. 중요한 것은 모델의 성능을 모니터링하여 모델을 가져올 시기와 시기를 결정할 수 있도록 하는 것입니다.

업데이트를 다른 컨테이너에 배포하는 데 사용하는 변경 피드는 모든 업데이트를 영구적으로 저장합니다. 이러한 지속성을 통해 시스템에 이미 많은 데이터가 있는 경우에도 컨테이너 및 부트스트랩 비정규화된 뷰를 일회성 캐치업 작업으로 만들기 때문에 모든 업데이트를 요청할 수 있습니다.

다음 단계

실제 데이터 모델링 및 분할에 대한 소개 후 다음 문서를 확인하여 설명한 개념을 검토할 수 있습니다.