Jak modelować i partycjonować dane w usłudze Azure Cosmos DB przy użyciu przykładu wziętego z życia

DOTYCZY: NoSQL

W tym artykule przedstawiono kilka pojęć związanych z usługą Azure Cosmos DB, takich jak modelowanie danych, partycjonowanie i aprowizowana przepływność , aby zademonstrować sposób rozwiązywania rzeczywistych problemów z projektowaniem danych.

Jeśli zwykle pracujesz z relacyjnymi bazami danych, prawdopodobnie masz wbudowane nawyki i intuicje dotyczące projektowania modelu danych. Ze względu na określone ograniczenia, ale także unikatowe mocne strony usługi Azure Cosmos DB, większość z tych najlepszych rozwiązań nie tłumaczy się dobrze i może przeciągnąć Cię do nieoptymalnych rozwiązań. Celem tego artykułu jest przeprowadzenie pełnego procesu modelowania rzeczywistego przypadku użycia w usłudze Azure Cosmos DB— od modelowania elementów po kolokację jednostek i partycjonowanie kontenerów.

Pobierz lub wyświetl kod źródłowy wygenerowany przez społeczność , który ilustruje koncepcje z tego artykułu.

Ważne

Współautor społeczności przyczynił się do tego przykładu kodu, a zespół usługi Azure Cosmos DB nie obsługuje jego konserwacji.

Scenariusz

W tym ćwiczeniu rozważymy domenę platformy blogowania, w której użytkownicy mogą tworzyć wpisy. Użytkownicy mogą również lubić i dodawać komentarze do tych wpisów.

Porada

Wyróżniliśmy niektóre wyrazy kursywą; te słowa identyfikują rodzaj "rzeczy", które nasz model będzie musiał manipulować.

Dodanie kolejnych wymagań do naszej specyfikacji:

  • Na stronie głównej jest wyświetlane źródło ostatnio utworzonych wpisów.
  • Możemy pobrać wszystkie wpisy dla użytkownika, wszystkie komentarze do wpisu i wszystkie polubień dla wpisu,
  • Wpisy są zwracane z nazwą użytkownika swoich autorów oraz liczbą komentarzy i polubień, które mają,
  • Komentarze i polubienia są również zwracane przy użyciu nazwy użytkownika użytkowników, którzy je utworzyli,
  • Gdy są wyświetlane jako listy, wpisy muszą przedstawiać tylko obcięte podsumowanie ich zawartości.

Identyfikowanie głównych wzorców dostępu

Na początek udostępniamy strukturę naszej początkowej specyfikacji, identyfikując wzorce dostępu rozwiązania. Podczas projektowania modelu danych dla usługi Azure Cosmos DB ważne jest, aby zrozumieć, które żądania naszego modelu muszą obsłużyć, aby upewnić się, że model efektywnie obsługuje te żądania.

Aby ułatwić wykonywanie ogólnego procesu, kategoryzujemy te różne żądania jako polecenia lub zapytania, pożyczając trochę słownictwa z CQRS. W usługach CQRS polecenia są żądaniami zapisu (czyli intencjami aktualizacji systemu), a zapytania są żądaniami tylko do odczytu.

Oto lista żądań uwidacznianych przez naszą platformę:

  • [C1] Tworzenie/edytowanie użytkownika
  • [Q1] Pobieranie użytkownika
  • [C2] Tworzenie/edytowanie wpisu
  • [Q2] Pobieranie wpisu
  • [Q3] Wyświetlanie listy wpisów użytkownika w krótkim formularzu
  • [C3] Tworzenie komentarza
  • [Q4] Wyświetlanie listy komentarzy w wpisie
  • [C4] Podobnie jak w poście
  • [Q5] Wyświetlanie listy polubień wpisu
  • [Q6] Wyświetlanie listy x najnowszych wpisów utworzonych w formie krótkiej (kanał informacyjny)

Na tym etapie nie zastanawialiśmy się nad informacjami o tym, co zawiera każda jednostka (użytkownik, wpis itp.). Ten krok jest zwykle jednym z pierwszych, które należy rozwiązać podczas projektowania względem magazynu relacyjnego. Najpierw zaczynamy od tego kroku, ponieważ musimy ustalić, jak te jednostki tłumaczą się pod względem tabel, kolumn, kluczy obcych itp. Jest to znacznie mniej istotne w przypadku bazy danych dokumentów, która nie wymusza żadnego schematu podczas zapisu.

Głównym powodem, dla którego ważne jest zidentyfikowanie naszych wzorców dostępu od początku, jest to, że ta lista żądań będzie naszym zestawem testów. Za każdym razem, gdy iterujemy po modelu danych, przechodzimy przez każde z żądań i sprawdzamy jego wydajność i skalowalność. Obliczamy jednostki żądań używane w każdym modelu i optymalizujemy je. Wszystkie te modele korzystają z domyślnych zasad indeksowania i można je zastąpić przez indeksowanie określonych właściwości, co może dodatkowo poprawić użycie jednostek RU i opóźnienie.

Wersja 1: pierwsza wersja

Zaczynamy od dwóch kontenerów: users i posts.

Kontener Użytkownicy

Ten kontener przechowuje tylko elementy użytkownika:

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

Dzielimy ten kontener na partycje za pomocą idelementu , co oznacza, że każda partycja logiczna w tym kontenerze zawiera tylko jeden element.

Publikowanie kontenera

Ten kontener hostuje jednostki, takie jak wpisy, komentarze i polubień:

{
    "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>"
}

Partycjonujemy ten kontener za pomocą postIdelementu , co oznacza, że każda partycja logiczna w tym kontenerze zawiera jeden wpis, wszystkie komentarze dla tego wpisu i wszystkie polubień dla tego wpisu.

Wprowadziliśmy type właściwość w elementach przechowywanych w tym kontenerze, aby odróżnić trzy typy jednostek hostujących ten kontener.

Ponadto wybraliśmy odwoływanie się do powiązanych danych zamiast ich osadzania (zapoznaj się z tą sekcją , aby uzyskać szczegółowe informacje na temat tych pojęć), ponieważ:

  • nie ma górnego limitu liczby wpisów, które użytkownik może utworzyć,
  • posty mogą być dowolnie długie,
  • nie ma górnego limitu liczby komentarzy i polubień, które mogą zawierać posty,
  • Chcemy mieć możliwość dodania komentarza lub polubionego wpisu bez konieczności aktualizowania samego wpisu.

Jak dobrze działa nasz model?

Nadszedł czas, aby ocenić wydajność i skalowalność naszej pierwszej wersji. Dla każdego z zidentyfikowanych wcześniej żądań mierzymy jego opóźnienie i liczbę jednostek żądań, z których korzysta. Ten pomiar jest wykonywany względem fikcyjnego zestawu danych zawierającego 100 000 użytkowników z 5 do 50 postów na użytkownika oraz maksymalnie 25 komentarzy i 100 polubień na wpis.

[C1] Tworzenie/edytowanie użytkownika

To żądanie jest proste do zaimplementowania, ponieważ po prostu tworzymy lub aktualizujemy element w kontenerze users . Żądania są ładnie rozłożone na wszystkie partycje dzięki kluczowi id partycji.

Diagram przedstawiający zapisywanie pojedynczego elementu w kontenerze użytkowników.

Opóźnienie Opłata za jednostki RU Wydajność
7 Pani 5.71 RU

[Q1] Pobieranie użytkownika

Pobieranie użytkownika odbywa się przez odczytanie odpowiedniego elementu z kontenera users .

Diagram pobierania pojedynczego elementu z kontenera użytkowników.

Opóźnienie Opłata za jednostki RU Wydajność
2 Pani 1 RU

[C2] Tworzenie/edytowanie wpisu

Podobnie jak w przypadku [C1], musimy tylko zapisywać dane w kontenerze posts .

Diagram przedstawiający zapisywanie pojedynczego elementu wpisu w kontenerze postów.

Opóźnienie Opłata za jednostki RU Wydajność
9 Pani 8.76 RU

[Q2] Pobieranie wpisu

Zaczynamy od pobrania odpowiedniego dokumentu z kontenera posts . Ale to nie wystarczy, zgodnie z naszą specyfikacją musimy również zagregować nazwę użytkownika autora wpisu, liczbę komentarzy i liczbę polubień dla wpisu. Wymienione agregacje wymagają wystawienia 3 dodatkowych zapytań SQL.

Diagram pobierania wpisu i agregowania dodatkowych danych.

Każda z większej liczby zapytań filtruje klucz partycji odpowiedniego kontenera, co jest dokładnie tym, co chcemy zmaksymalizować wydajność i skalowalność. Jednak w końcu musimy wykonać cztery operacje, aby zwrócić pojedynczy wpis, więc poprawimy to w następnej iteracji.

Opóźnienie Opłata za jednostki RU Wydajność
9 Pani 19.54 RU

[Q3] Wyświetlanie listy wpisów użytkownika w krótkim formularzu

Najpierw musimy pobrać żądane wpisy z zapytaniem SQL, które pobiera wpisy odpowiadające temu konkretnemu użytkownikowi. Musimy jednak również wydać więcej zapytań, aby zagregować nazwę użytkownika autora oraz liczbę komentarzy i polubień.

Diagram pobierania wszystkich wpisów dla użytkownika i agregowania ich dodatkowych danych.

Ta implementacja przedstawia wiele wad:

  • zapytania agregujące liczby komentarzy i polubień muszą zostać wydane dla każdego wpisu zwróconego przez pierwsze zapytanie.
  • zapytanie główne nie filtruje klucza posts partycji kontenera, co prowadzi do wymyślenia i skanowania partycji w kontenerze.
Opóźnienie Opłata za jednostkę RU Wydajność
130 Pani 619.41 RU

[C3] Tworzenie komentarza

Komentarz jest tworzony przez zapisanie odpowiedniego elementu w kontenerze posts .

Diagram przedstawiający pisanie pojedynczego elementu komentarza do kontenera postów.

Opóźnienie Opłata za jednostkę RU Wydajność
7 Pani 8.57 RU

[Q4] Wyświetlanie listy komentarzy w poście

Zaczynamy od zapytania, które pobiera wszystkie komentarze do tego wpisu i po raz kolejny, musimy również agregować nazwy użytkowników oddzielnie dla każdego komentarza.

Diagram pobierania wszystkich komentarzy dla wpisu i agregowania dodatkowych danych.

Chociaż główne zapytanie filtruje klucz partycji kontenera, agregowanie nazw użytkowników oddzielnie karze ogólną wydajność. Ulepszymy to później.

Opóźnienie Opłata za jednostkę RU Wydajność
23 Pani 27.72 RU

[C4] Lubię wpis

Podobnie jak [C3], tworzymy odpowiedni element w kontenerze posts .

Diagram przedstawiający pisanie pojedynczego elementu (na przykład) do kontenera postów.

Opóźnienie Opłata za jednostkę RU Wydajność
6 Pani 7.05 RU

[Q5] Wyświetlanie listy polubień wpisu

Podobnie jak [Q4], wysyłamy zapytania dotyczące polubień tego wpisu, a następnie agregujemy ich nazwy użytkownika.

Diagram pobierania wszystkich polubień wpisu i agregowania dodatkowych danych.

Opóźnienie Opłata za jednostkę RU Wydajność
59 Pani 58.92 RU

[Q6] Wyświetlanie listy x najnowszych wpisów utworzonych w formie krótkiej (kanał informacyjny)

Pobieramy najnowsze wpisy, wysyłając posts zapytanie do kontenera posortowanego według daty utworzenia malejącego, a następnie agregujemy nazwy użytkowników i liczby komentarzy i polubień dla każdego z wpisów.

Diagram pobierania najnowszych wpisów i agregowania dodatkowych danych.

Po raz kolejny nasze początkowe zapytanie nie filtruje klucza posts partycji kontenera, co wyzwala kosztowny wentylator. Jest to jeszcze gorsze, ponieważ celujemy większy zestaw wyników i sortujemy wyniki za pomocą ORDER BY klauzuli , co sprawia, że jest droższa pod względem jednostek żądania.

Opóźnienie Opłata za jednostkę RU Wydajność
306 Pani 2063.54 RU

Zastanawianie się nad wydajnością wersji 1

Patrząc na problemy z wydajnością, które napotkaliśmy w poprzedniej sekcji, możemy zidentyfikować dwie główne klasy problemów:

  • niektóre żądania wymagają wystawienia wielu zapytań w celu zebrania wszystkich danych, które należy zwrócić,
  • niektóre zapytania nie filtrują klucza partycji kontenerów, których celem są, co prowadzi do wentylatora, co utrudnia skalowalność.

Rozwiążmy każdy z tych problemów, zaczynając od pierwszego.

Wersja 2: Wprowadzenie do denormalizacji w celu zoptymalizowania zapytań odczytu

Powodem, dla którego musimy wydać więcej żądań w niektórych przypadkach, jest to, że wyniki początkowego żądania nie zawierają wszystkich danych, które musimy zwrócić. Denormalizacja danych rozwiązuje ten rodzaj problemu w naszym zestawie danych podczas pracy z magazynem danych nierelacyjnych, takim jak Usługa Azure Cosmos DB.

W naszym przykładzie zmodyfikujemy elementy publikowania, aby dodać nazwę użytkownika autora wpisu, liczbę komentarzy i liczbę polubień:

{
    "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>"
}

Modyfikujemy również komentarz i lubimy elementy, aby dodać nazwę użytkownika, który je utworzył:

{
    "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>"
}

Denormalizacja komentarza i polubień

Chcemy osiągnąć to, że za każdym razem, gdy dodamy komentarz lub podobne, zwiększamy również commentCount wartość lub likeCount w odpowiednim poście. W miarę postId partycjonowania kontenera posts nowy element (komentarz lub polubienie) i odpowiadający mu wpis znajdują się w tej samej partycji logicznej. W związku z tym możemy użyć procedury składowanej do wykonania tej operacji.

Podczas tworzenia komentarza ([C3]) zamiast dodawać nowy element w kontenerze wywołujemy następującą procedurę składowaną w posts tym kontenerze:

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
          );
        }
      );
    })
}

Ta procedura składowana przyjmuje identyfikator wpisu i treść nowego komentarza jako parametry, a następnie:

  • pobiera wpis
  • zwiększa liczbę commentCount
  • zastępuje wpis
  • dodaje nowy komentarz

Ponieważ procedury składowane są wykonywane jako transakcje niepodzielne, wartość commentCount i rzeczywista liczba komentarzy zawsze pozostaje zsynchronizowana.

Oczywiście wywołujemy podobną procedurę składowaną podczas dodawania nowych polubień, aby zwiększać likeCountwartość .

Denormalizacja nazw użytkowników

Nazwy użytkowników wymagają innego podejścia, ponieważ użytkownicy nie tylko znajdują się w różnych partycjach, ale w innym kontenerze. Gdy musimy zdenormalizować dane między partycjami i kontenerami, możemy użyć źródła danych zmian kontenera źródłowego.

W naszym przykładzie używamy zestawienia zmian kontenera users , aby reagować za każdym razem, gdy użytkownicy aktualizują swoje nazwy użytkownika. W takim przypadku propagujemy zmianę, wywołując inną procedurę składowaną w kontenerze posts :

Diagram przedstawiający denormalizację nazw użytkowników w kontenerze postów.

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);
      }
    });
}

Ta procedura składowana przyjmuje identyfikator użytkownika i nową nazwę użytkownika jako parametry, a następnie:

  • pobiera wszystkie elementy pasujące userId do elementu (które mogą być wpisami, komentarzami lub polubień)
  • dla każdego z tych elementów
    • zastępuje element userUsername
    • zastępuje element

Ważne

Ta operacja jest kosztowna, ponieważ wymaga wykonania tej procedury składowanej na każdej partycji kontenera posts . Zakładamy, że większość użytkowników wybiera odpowiednią nazwę użytkownika podczas rejestracji i nigdy jej nie zmieni, więc ta aktualizacja będzie działać bardzo rzadko.

Jakie są zyski z wydajności w wersji 2?

Porozmawiajmy o niektórych wzrostach wydajności w wersji 2.

[Q2] Pobieranie wpisu

Teraz, gdy nasza denormalizacja jest w miejscu, musimy pobrać tylko jeden element, aby obsłużyć to żądanie.

Diagram pobierania pojedynczego elementu z zdenormalizowanego kontenera postów.

Opóźnienie Opłata za jednostkę RU Wydajność
2 Pani 1 RU

[Q4] Wyświetlanie listy komentarzy w poście

Tutaj ponownie możemy oszczędzić dodatkowe żądania, które pobrały nazwy użytkowników i skończyć na jednym zapytaniu, które filtruje klucz partycji.

Diagram pobierania wszystkich komentarzy dla zdenormalizowanego wpisu.

Opóźnienie Opłata za jednostkę RU Wydajność
4 Pani 7.72 RU

[Q5] Wyświetlanie listy polubień wpisu

Dokładnie taka sama sytuacja podczas wyświetlania listy polubień.

Diagram pobierania wszystkich polubień dla zdenormalizowanego wpisu.

Opóźnienie Opłata za jednostkę RU Wydajność
4 Pani 8.92 RU

Wersja 3: Upewniając się, że wszystkie żądania są skalowalne

Nadal istnieją dwa żądania, które nie zostały w pełni zoptymalizowane podczas przeglądania naszych ogólnych ulepszeń wydajności. Te żądania to [Q3] i [Q6]. Są to żądania obejmujące zapytania, które nie filtrują klucza partycji kontenerów, których dotyczą.

[Q3] Wyświetlanie listy wpisów użytkownika w krótkim formularzu

To żądanie korzysta już z ulepszeń wprowadzonych w wersji 2, co pozwala zaoszczędzić więcej zapytań.

Diagram przedstawiający zapytanie umożliwiające wyświetlenie listy zdenormalizowanych wpisów użytkownika w krótkiej formie.

Jednak pozostałe zapytanie nadal nie filtruje klucza partycji kontenera posts .

Sposób myślenia o tej sytuacji jest prosty:

  1. To żądanie musi filtrować, userId ponieważ chcemy pobrać wszystkie wpisy dla określonego użytkownika.
  2. Nie działa dobrze, ponieważ jest wykonywany względem posts kontenera, który nie ma userId partycjonowania.
  3. Stwierdzając oczywiste, rozwiązalibyśmy nasz problem z wydajnością, wykonując to żądanie względem kontenera podzielonego na partycje za pomocą userIdpolecenia .
  4. Okazuje się, że mamy już taki kontener: users kontener!

Dlatego wprowadzamy drugi poziom denormalizacji przez duplikowanie całych wpisów do kontenera users . Dzięki temu skutecznie uzyskujemy kopię naszych postów, tylko partycjonowanych wzdłuż innego wymiaru, dzięki czemu są one bardziej wydajne w celu pobrania przez nich userId.

Kontener users zawiera teraz dwa rodzaje elementów:

{
    "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>"
}

W tym przykładzie:

  • Wprowadziliśmy type pole w elemencie użytkownika, aby odróżnić użytkowników od wpisów,
  • Dodaliśmy userId również pole w elemencie użytkownika, które jest nadmiarowe z id polem, ale jest wymagane, ponieważ users kontener jest teraz podzielony na userId partycje (a nie id tak jak wcześniej)

Aby osiągnąć to denormalizacja, po raz kolejny użyjemy kanału informacyjnego zmian. Tym razem reagujemy na zestawienie zmian kontenera w celu wysłania nowego lub zaktualizowanego posts wpisu do kontenera users . A ponieważ wyświetlanie wpisów nie wymaga zwrócenia pełnej zawartości, możemy je obcinać w procesie.

Diagram przedstawiający denormalizację wpisów w kontenerze użytkowników.

Teraz możemy kierować zapytanie do kontenera users , filtrując klucz partycji kontenera.

Diagram pobierania wszystkich wpisów dla zdenormalizowanego użytkownika.

Opóźnienie Opłata za jednostkę RU Wydajność
4 Pani 6.46 RU

[Q6] Wyświetlanie listy x najnowszych wpisów utworzonych w formie krótkiej (kanał informacyjny)

Musimy radzić sobie z podobną sytuacją: nawet po zachowaniu większej ilości zapytań pozostawionych przez denormalizację wprowadzoną w wersji 2 pozostałe zapytanie nie filtruje klucza partycji kontenera:

Diagram przedstawiający zapytanie umożliwiające wyświetlenie listy x najnowszych wpisów utworzonych w krótkim formularzu.

Zgodnie z tym samym podejściem maksymalizowanie wydajności i skalowalności tego żądania wymaga osiągnięcia tylko jednej partycji. Tylko trafienie jednej partycji jest możliwe, ponieważ musimy zwrócić tylko ograniczoną liczbę elementów. Aby wypełnić stronę główną platformy rejestrowania, wystarczy uzyskać 100 najnowszych postów bez konieczności stronicowania całego zestawu danych.

Dlatego aby zoptymalizować to ostatnie żądanie, wprowadzamy trzeci kontener do naszego projektu, całkowicie dedykowany do obsługi tego żądania. Zdenormalizujemy nasze wpisy w tym nowym feed kontenerze:

{
    "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>"
}

Pole type partycjonuje ten kontener, który jest zawsze post w naszych elementach. Dzięki temu wszystkie elementy w tym kontenerze będą zasiadać w tej samej partycji.

Aby osiągnąć denormalizację, musimy tylko podłączyć potok zestawienia zmian, który wcześniej wprowadziliśmy, aby wysłać wpisy do tego nowego kontenera. Należy pamiętać o tym, że musimy upewnić się, że przechowujemy tylko 100 najnowszych postów; w przeciwnym razie zawartość kontenera może wzrosnąć poza maksymalny rozmiar partycji. To ograniczenie można zaimplementować, wywołując wyzwalacz po każdym dodaniu dokumentu do kontenera:

Diagram przedstawiający denormalizację wpisów w kontenerze kanału informacyjnego.

Oto treść wyzwalacza końcowego, który obcina kolekcję:

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);
        });
    }
  }
}

Ostatnim krokiem jest przekierowanie zapytania do nowego feed kontenera:

Diagram pobierania najnowszych wpisów.

Opóźnienie Opłata za jednostkę RU Wydajność
9 Pani 16.97 RU

Podsumowanie

Przyjrzyjmy się ogólnym ulepszeniom wydajności i skalowalności, które wprowadziliśmy w różnych wersjach naszego projektu.

Wersja 1 Wersja 2 Wersja 3
[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

Zoptymalizowano scenariusz z dużą liczbą operacji odczytu

Być może zauważyliśmy, że skupiliśmy nasze wysiłki na rzecz poprawy wydajności żądań odczytu (zapytań) kosztem żądań zapisu (poleceń). W wielu przypadkach operacje zapisu wyzwalają kolejną denormalizację za pośrednictwem źródeł zmian, co sprawia, że są bardziej kosztowne obliczeniowo i dłuższe w celu zmaterializowania.

Uzasadniamy to skupienie się na wydajności odczytu przez fakt, że platforma rejestrowania (podobnie jak większość aplikacji społecznościowych) jest ciężka do odczytu. Obciążenie z dużym obciążeniem odczytu wskazuje, że ilość żądań odczytu, które musi obsłużyć, jest zwykle większa niż liczba żądań zapisu o wielkości większej niż liczba żądań zapisu. Warto więc sprawić, że żądania zapisu będą droższe do wykonania w celu umożliwienia, aby żądania odczytu mogły być tańsze i wydajniejsze.

Jeśli przyjrzymy się najbardziej ekstremalnej optymalizacji, [Q6] przeszedł od 2000+ jednostek RU do zaledwie 17 jednostek RU; Osiągnęliśmy to poprzez denormalizację postów kosztem około 10 jednostek RU na element. Ponieważ służylibyśmy o wiele więcej żądań kanału informacyjnego niż tworzenie lub aktualizacje postów, koszt tej denormalizacji jest nieznaczny, biorąc pod uwagę ogólne oszczędności.

Denormalizacja można stosować przyrostowo

Ulepszenia skalowalności, które omówiliśmy w tym artykule, obejmują denormalizację i duplikowanie danych w zestawie danych. Należy zauważyć, że te optymalizacje nie muszą być wprowadzane w dniu 1. Zapytania filtrujące klucze partycji działają lepiej na dużą skalę, ale zapytania obejmujące wiele partycji mogą być dopuszczalne, jeśli są one wywoływane rzadko lub w przypadku ograniczonego zestawu danych. Jeśli po prostu tworzysz prototyp lub uruchamiasz produkt z małą i kontrolowaną bazą użytkowników, prawdopodobnie możesz oszczędzić te ulepszenia później. Ważne jest , aby monitorować wydajność modelu, aby zdecydować, czy i kiedy nadszedł czas, aby je wprowadzić.

Kanał informacyjny zmian używany do dystrybuowania aktualizacji do innych kontenerów przechowuje wszystkie te aktualizacje w sposób trwały. Ta trwałość umożliwia zażądanie wszystkich aktualizacji od czasu utworzenia kontenera i zdenormalizowanych widoków bootstrap jako jednorazowej operacji nadrabiania zaległości, nawet jeśli system ma już wiele danych.

Następne kroki

Po wprowadzeniu do praktycznego modelowania danych i partycjonowania warto zapoznać się z następującymi artykułami, aby zapoznać się z omówioną koncepcją: