Paweł Łukasiewicz
2024-04-20
Paweł Łukasiewicz
2024-04-20
Udostępnij Udostępnij Kontakt
Wprowadzenie

Niektóre z aplikacji, które będziemy przygotowywać będą potrzebowały zapytań do danych jedynie przy użyciu klucza głównego tabeli podstawowej. Warto jednak mieć na uwadze, że mogą się pojawić sytuacje (wraz z rozwojem naszej aplikacji), kiedy alternatywny klucz sortowania okaże się bardzo przydatny. Aby dać naszej aplikacji możliwość wyboru klucza sortowania możemy utworzyć jeden lub więcej lokalnych indeksów wtórnych na tabeli DynamoDB i wydawać polecenia żądania dla zapytań lub operacji skanowania uwzględniając te indeksy.

Podobnie jak w poprzednim przypadku przygotujemy przykłady w oparciu o język C#, jednakże najpierw skupimy się na teorii, żeby w pełni świadomie korzystać z możliwości jaki daje ten indeks. W tym wpisie przejdziemy przez kilka tematów, do których zaliczamy przykładowy scenariusz użycia, rzutowanie atrybutów, tworzenie indeksów lokalnych, odczyt danych z tych indeksów. Przejdziemy również przez zagadnienia związane z przepustowością indeksów.

Użycie lokalnego indeksu pomocniczego/wtórnego

W tym wpisie, jako przykładem, będziemy posługiwać się tabelą Thread, której definicję (oraz sposób tworzenia) możecie prześledzić w oficjalnej dokumentacji AWS dostępnej pod tym adresem: Creating tables and loading data for code examples in DynamoDB

Tak przygotowana tabela może być wykorzystywana jako zaplecze do różnego rodzaju forów dyskusyjnych. Na poniższym schemacie możecie zobaczyć kilka z kluczowych atrybutów (nie wszystkie są pokazane). Tabela tematów na forum zawiera atrybuty takie jak nazwy forów, tematów, czas ostatniego posta czy liczbę odpowiedzi:

+-----------+-----------+------------------------+---------+
| ForumName | Subject   | LastPostDateTime       | Replies |
+-----------+-----------+------------------------+---------+
|  "S3"     | "aaa"     | "2022-09-09:12:45:00"  |  12     | ...
+-----------+-----------+------------------------+---------+
|  "S3"     | "bbb"     | "2022-09-10:12:45:00"  |  34     | ...
+-----------+-----------+------------------------+---------+
|  "S3"     | "ccc"     | "2022-09-11:12:45:00"  |  43     | ...
+-----------+-----------+------------------------+---------+
|  "S3"     | "ddd"     | "2022-09-12:12:45:00"  |  21     | ...
+-----------+-----------+------------------------+---------+

|  "EC2"    | "yyy"     | "2022-09-13:12:45:00"  |  45     | ...
+-----------+-----------+------------------------+---------+
|  "EC2"    | "zzz"     | "2022-09-14:12:45:00"  |  21     | ...
+-----------+-----------+------------------------+---------+

|  "RDS"    | "rrr"     | "2022-09-15:12:45:00"  |  18     | ...
+-----------+-----------+------------------------+---------+
|  "RDS"    | "sss"     | "2022-09-16:12:45:00"  |  15     | ...
+-----------+-----------+------------------------+---------+
|  "RDS"    | "ttt"     | "2022-09-17:12:45:00"  |  0      | ...
+-----------+-----------+------------------------+---------+

DynamoDB przechowuje wszystkie elementy z tą samą wartością klucza partycji w sposób ciągły. W powyższym przykładzie, biorąc pod uwagę konkretną nazwę atrybutu, tj. ForumName, operacja Query może natychmiast zlokalizować wszystkie wątki dla tego forum. W obrębie grupy, elementy z tą samą wartością klucza partycji, są sortowane według wartości klucza sortowania. Jeżeli klucz sortowania (Subject) jest również podany w zapytaniu, DynamoDB może zawęzić wyniki, które są zwracane, np. zwracają wszystkie wątki w forum S3, które mają temat zaczynający się od litery "a".

Niektóre zapytania mogą wymagać bardziej złożonych wzorców dostępu do danych. Spójrzmy na poniższe przykłady:

  • Które wątki na forum otrzymują najwięcej odsłon i odpowiedzi?
  • Który wątek na danym forum na największą liczbę wiadomości?
  • Ile wątków zostało zapisanych na danym forum w określonym czasie?

Aby odpowiedź na powyższe pytania, samo zapytanie Query nie byłoby wystarczające. Zamiast tego musielibyśmy przeskanować całą tabelę. W przypadku tabeli z milionami elementów, operacja taka mogłaby pochłonąć dużą ilość przepustowości oraz zająć dużo czasu.

Możemy jednak wykorzystać lokalny indeks wtórny na atrybutach nie będącymi kluczami, jakimi jak Replies czy LastPostDateTime.

Lokalny indeks wtórny przechowuje alternatywny klucz sortowania dla danej wartości klucza partycji. Lokalny indeks pomocniczy zawiera również kopię niektórych lub wszystkich atrybutów z tabeli bazowej. Określamy, które atrybuty są rzutowane na lokalny indeks wtórny podczas tworzenia tabeli. Dane w lokalnym indeksie wtórnym są zorganizowane według tego samego klucza partycji co tabela podstawowa, ale z innym kluczem sortowania. Pozwala to na efektywny dostęp do elementów danych w różnych wymiarach. Dla większej elastyczności zapytań lub skanowania można utworzyć do pięciu lokalnych indeksów wtórnych na tabelę.

Załóżmy, że aplikacja musi znaleźć wszystkie wątki, które zostały napisane w ciągu ostatnich trzech miesięcy na określonym forum. Bez lokalnego indeksu wtórnego aplikacja musiałby przeskanować całą tabelę Thread i odrzucić wszystkie posty, które nie mieszczą się w określonym przedziale czasowym. Z lokalnym indeksem wtórnym, operacja zapytania mogłaby użyć atrybutu LastPostDateTime jako klucza sortowania i szybko znaleźć te dane.

Poniższy diagram przedstawia lokalny indeks wtórny o nazwie LastPostIndex. Zwróćcie uwagę, że klucz partycji jest taki jak sam jak w tabeli Thread ale kluczem sortowania jest LastPostDateTime:

+-----------+------------------------+---------+
| ForumName | LastPostDateTime       | Subject |
+-----------+------------------------+---------+
|  "S3"     | "2022-09-12:12:45:00"  |  "ddd"  | ...
+-----------+------------------------+---------+
|  "S3"     | "2022-09-10:12:45:00"  |  "bbb"  | ...
+-----------+------------------------+---------+
|  "S3"     | "2022-09-11:12:45:00"  |  "ccc"  | ...
+-----------+------------------------+---------+
|  "S3"     | "2022-09-09:12:45:00"  |  "aaa"  | ...
+-----------+------------------------+---------+

|  "EC2"    | "2022-09-14:12:45:00"  |  "zzz"  | ...
+-----------+------------------------+---------+
|  "EC2"    | "2022-09-13:12:45:00"  |  "yyy"  | ...
+-----------+------------------------+---------+

|  "RDS"    | "2022-09-15:12:45:00"  |  "rrr"  | ...
+-----------+------------------------+---------+
|  "RDS"    | "2022-09-17:12:45:00"  |  "ttt"  | ...
+-----------+------------------------+---------+
|  "RDS"    | "2022-09-16:12:45:00"  |  "sss"  | ...
+-----------+------------------------+---------+

Każdy lokalny indeks wtórny musi spełniać następujące warunki:

  • klucz partycji jest taki, jak klucz jego tabeli bazowej;
  • klucz sortowania składa się z dokładnie jednego atrybutu skalarnego (typ skalarny to typ reprezentujący dokładnie jedną wartość);
  • klucz sortowania tabeli bazowej jest rzutowany na indeks, gdzie pełni rolę atrybutu nie będącego kluczem (non-key attribute).

W powyższym przykładzie kluczem partycji jest ForumName a kluczem sortowania lokalnego indeksu wtórnego jest LastPostDateTime. Dodatkowo wartość klucza sortowania z tabeli podstawowej (w tym przykładzie Subject) jest rzutowana do indeksu ale nie jest częścią klucza indeksu. Jeżeli aplikacja potrzebuje listy opartej na ForumName i LastPostDateTime może wydać zapytanie do LastPostIndex. Wyniki zapytania są posortowane według LastPostDateTime i mogą być zwrócone w kolejności rosnącej lub malejącej. Zapytanie może również stosować warunki kluczowe takie jak zwracanie tylko elementów, które mają LastPostDateTime w określonym przedziale czasowym.

Każdy lokalny indeks wtórny automatycznie zawiera klucze partycji i sortowania z tabeli bazowej – można opcjonalnie rzutować atrybuty nie będące kluczami do indeksu. Kiedy zadajemy zapytanie do indeksu, DynamoDB może efektywnie pobierać te zaprojektowane atrybuty. Kiedy zapytamy lokalny indeks wtórny, zapytanie może również pobrać atrybuty, które nie są rzutowane do indeksu. DynamoDB automatycznie pobiera te atrybuty z tabeli podstawowej ale z większym opóźnieniem i wyższymi kosztami przepustowości.

Dla każdego lokalnego indeksu wtórnego można przechowywać do 10 GB danych na każdą odrębną wartość klucza partycji. Liczba ta obejmuje wszystkie elementy w tabeli podstawowej oraz wszystkie elementy w indeksach, które mają tę samą wartość klucza partycji. Więcej informacji dostępne w dokumentaji AWS pod adresem: Item collections in Local Secondary Indexes

Rzutowanie atrybutów

Wykorzystując indeks LastPostIndex nasza aplikacja mogłaby wykorzystać atrybuty ForumName oraz LastPostDateTime jako kryteria zapytania. Jednakże, aby pobrać dodatkowe atrybuty, DynamoDB musi wykonać dodatkowe operacje odczytu z tabeli Thread. Te dodatkowe odczyty znane są jako fetches (operacje pobierania) i mogą zwiększyć całkowitą ilość dostarczonej przepustowości wymaganej dla zapytania.

Załóżmy, w ramach przykładu, że chcemy wypełnić stronę internetową listą wszystkich wątków w temacie S3 (więcej o S3 w tym wpisie: AWS - S3) i liczbą odpowiedzi dla każdego wątku, posortowanych według daty/czasu ostatniej odpowiedzi, zaczynając od najnowszych odpowiedzi. Do wypełnienia tej listy potrzebowalibyśmy następujących atrybutów: Subject, Replies oraz LastPostDateTime.

Najbardziej wydajnym sposobem zapytania o te dane i uniknięcia operacji pobierania byłoby rzutowanie atrybutu Replies z tabeli do lokalnego indeksu wtórnego jak możecie zobaczyć na poniższym przykładzie:

+-----------+------------------------+---------+---------+
| ForumName | LastPostDateTime       | Subject | Replies |
+-----------+------------------------+---------+---------+
|  "S3"     | "2022-09-12:12:45:00"  |  "ddd"  |  9      | ...
+-----------+------------------------+---------+---------+
|  "S3"     | "2022-09-10:12:45:00"  |  "bbb"  |  3      | ...
+-----------+------------------------+---------+---------+
|  "S3"     | "2022-09-11:12:45:00"  |  "ccc"  |  4      | ...
+-----------+------------------------+---------+---------+
|  "S3"     | "2022-09-09:12:45:00"  |  "aaa"  |  12     | ...
+-----------+------------------------+---------+---------+

|  "EC2"    | "2022-09-14:12:45:00"  |  "zzz"  |  0      | ...
+-----------+------------------------+---------+---------+
|  "EC2"    | "2022-09-13:12:45:00"  |  "yyy"  |  18     | ...
+-----------+------------------------+---------+---------+

|  "RDS"    | "2022-09-15:12:45:00"  |  "rrr"  |  3      | ...
+-----------+------------------------+---------+---------+
|  "RDS"    | "2022-09-17:12:45:00"  |  "ttt"  |  5      | ...
+-----------+------------------------+---------+---------+
|  "RDS"    | "2022-09-16:12:45:00"  |  "sss"  |  11     | ...
+-----------+------------------------+---------+---------+

Projekcja jest zestawem atrybutów, które są kopiowane z tabeli do indeksu wtórnego. Klucz partycji i klucz sortowania tabeli są zawsze rzutowane na indeks – jak doskonale wiecie możemy również rzutować inne atrybuty aby wspierać różne wymagania zapytań z naszych aplikacji. Kiedy zapytam o indeks, DynamoDB może uzyskać dostęp do każdego atrybutu w projekcji tak jakby te atrybuty były w swojej własnej tabeli.

Kiedy tworzymy indeks wtórny musimy określić atrybuty, które będą rzutowane na indeks. DynamoDB udostępnia trzy różne opcje w tym zakresie:

  • KEYS_ONLY - każdy element w indeksie składa się tylko z wartości klucza partycji tabeli i klucza sortowania oraz wartości klucza indeksu. Opcja KEYS_ONLY powoduje powstanie najmniejszego możliwego indeksu wtórnego;
  • INCLUDE - oprócz atrybutów opisanych w opcji KEYS_ONLY indeks wtórny będzie zawierał inne atrybuty nie będące kluczami, które określamy;
  • ALL - indeks wtórny zawiera wszystkie atrybuty z tabeli źródłowej. Ponieważ wszystkie dane tabeli są powielane w indeksie, rzutowanie/projekcja ALL skutkuje największym możliwym indeksem wtórnym.

W przypadku poprzedniej tabeli (schemat) atrybut nie-kluczowy (non-key) Replies jest rzutowany na LastPostIndex. Nasza aplikacja może zapytać LastPostIndex zamiast pełnej tabeli Thread w celu wyświetlenia atrybutów Subject, Replies oraz LastPostDateTime, tj. wszystkich interesujących nas informacji. Jeżeli wymagane są inne atrybuty nie będące kluczami, DynamoDB będzie musiało pobrać te atrybuty z tabeli Thread.

Z punktu widzenia aplikacji, pobieranie dodatkowych atrybutów z tabeli podstawowej jest automatyczne i przejrzyste więc nie ma potrzeby przepisywania żadnej logiki w naszej aplikacji. Musimy jednak pamiętać, że takie pobieranie może zmniejszyć przewagę wydajnościową używania lokalnego indeksu wtórnego.

Wybierając atrybuty do projekcji w lokalnym indeksie wtórnym musimy rozważyć kompromis pomiędzy kosztami przepustowości i kosztami przechowywania:

  • jeżeli potrzebujemy dostępu do zaledwie kilku atrybutów z najniższym możliwym opóźnieniem, powinniśmy rozważyć projekcję tylko tych atrybutów do lokalnego indeksu wtórnego. Im mniejszy indeks, tym mniej kosztuje jego przechowywanie i tym mniejsze są koszty zapisu. Jeżeli istnieją atrybuty, które od czasu do czasu trzeba pobrać, koszt dostarczonej przepustowości może znacznie przewyższyć długoterminowy koszt przechowywania tych atrybutów;
  • jeżeli aplikacja często korzysta z atrybutów nie będących kluczami, powinniśmy rozważyć projekcję tych atrybutów do lokalnego indeksu wtórnego. Dodatkowe koszty przechowywania lokalnego indeksu wtórnego kompensują koszty częstego skanowania tabeli;
  • jeżeli potrzebujemy częstego dostępu do większości atrybutów nie będących kluczami, możemy rzutować te atrybuty – lub nawet całą tabelę podstawową – na lokalny indeks wtórny. Daje to maksymalną elastyczność i najniższe zużycie przepustowości, ponieważ nie będzie wymagane pobieranie danych. Jednakże, koszt przechowywania wzrasta lub nawet podwaja się, jeżeli rzutujemy wszystkie atrybuty;
  • jeżeli aplikacja potrzebuje rzadkich zapytań do tabeli, ale musi wykonywać wiele zapisów lub aktualizacji danych w tabeli, powinniśmy rozważyć rzutowanie typu KEYS_ONLY. Lokalny indeks wtórny będzie miał minimalny rozmiar ale nadal będzie dostępny, gdy będzie potrzeby do wykonywania zapytań.

Tworzenie lokalnego indeksu wtórnego

W celu utworzenia jednego lub więcej lokalnych indeksów wtórnych na tabeli musimy użyć parametru LocalSecondaryIndexes w operacji CreateTable. Lokalne indeksy wtórne tworzone są podczas tworzenia tabeli. Gdy usuwamy tabelę, wszystkie lokalne indeksy wtórne na tej tabeli są również usuwane.

Musimy określić jeden atrybut nie będący kluczem, który będzie zachowywał się jak klucz sortowania lokalnego indeksu wtórnego. Wybrany atrybut musi być typem skalarnym, tj. Number, String lub Binary. Inne typy skalarne, tj. typ dokumentów i typ zestawów nie są dozwolone.

Musimy pamiętać, że dla tabel z lokalnymi indeksami wtórnymi istnieje ograniczenie rozmiaru 10GB na wartość klucza partycji. Tabela z lokalnymi indeksami dodatkowymi może przechowywać dowolną liczbę elementów o ile całkowity rozmiar dla jednej wartości klucza partycji nie przekracza 10GB.

Możemy rzutować atrybuty dowolnego typu danych na lokalny indeks wtórny. Obejmuje to typy skalarne, dokumenty oraz wspomniane wcześniej zestawy.

Odczyt danych z lokalnego indeksu wtórnego

Możemy pobierać elementy z lokalnego indeksu wtórnego za pomocą operacji Query oraz Scan. Operacje GetItem oraz BatchItem nie mogą być wykonywane na lokalnym indeksie wtórnym.

Zapytania na lokalnym indeksie wtórnym

W tabeli DynamoDB połączona wartość klucza partycji i wartość klucza sortowania dla każdego elementu musi być unikalna. Jednakże, w lokalnym indeksie wtórnym, wartość klucza sortowania nie musi być unikalna dla danej wartości klucza partycji. Jeżeli istnieje wiele elementów w lokalnym indeksie wtórnym, które mają tę samą wartość klucza sortowania, operacja zapytania zwraca wszystkie elementy, które mają tę samą wartość klucza partycji. W odpowiedzi, pasujące elementy nie są zwracane w żadnej konkretnej kolejności.

Lokalny indeks wtórny możemy zapytać używając odczytu ostatecznie spójnego lub silnie spójnego. Aby określić typ spójności musimy użyć parametru ConsistendRead operacji Query. Odczyt silnie spójny z lokalnego indeksu wtórnego zawsze zwraca najnowsze zaktualizowane wartości. Jeżeli zapytanie musi pobrać dodatkowe atrybuty z tabeli podstawowej, atrybuty te będą spójne względem indeksu.

Zanim przejdziemy dalej spójrzmy na poniższy przykład:

{
    "TableName": "Thread",
    "IndexName": "LastPostIndex",
    "ConsistentRead": false,
    "ProjectionExpression": "Subject, LastPostDateTime, Replies, Tags",
    "KeyConditionExpression": 
    "ForumName = :v_forum and LastPostDateTime between :v_start and :v_end",
    "ExpressionAttributeValues": 
    {
        ":v_start": {"S": "2015-08-31T00:00:00.000Z"},
        ":v_end": {"S": "2015-11-31T00:00:00.000Z"},
        ":v_forum": {"S": "EC2"}
    }
}

W tym zapytaniu:

  • DynamoDB uzyskuje dostęp do LastPostIndex używając klucza partycji ForumName, aby odnaleźć element indeksu dla EC2. Wszystkie elementy indeksu z tym kluczem są przechowywane obok siebie w celu szybkiego wyszukiwania;
  • w ramach tego forum DynamoDB używa indeksu do wyszukiwania kluczy, które nie pasują do określonego warunku LastPostDateTime;
  • Z uwagi na fakt, że atrybut Replies jest rzutowany na indeks, DynamoDB może pobrać ten atrybut bez zużywania dodatkowej przepustowości;
  • Atrybut Tags nie jest rzutowany na indeks, więc DynamoDB musi uzyskać dostęp do tabeli Thread i pobrać ten atrybut;
  • wyniki są zwracane posortowane według LastPostDateTime. Wpisy indeksu są sortowane według wartości klucza partycji a następnie według wartości klucza sortowania a Query zwraca je w kolejności w jakiej są przechowywane. Możemy również wykorzystać parametr ScanIndexForward w celu zwrócenia wyników w porządku malejącym.

Z uwagi na fakt, że atrybut Tags nie jest rzutowany na lokalny indeks wtórny, DynamoDB musi zużywać dodatkowe jednostki pojemności odczytu, aby pobrać ten atrybut z tabeli podstawowej. Jeżeli musimy często uruchamiać to zapytanie powinniśmy dokonać rzutowania atrybutu Tags do LastPostIndex w celu uniknięcia pobierania z tabeli bazowej. Jednakże, jeżeli potrzebujemy dostępu do atrybutu Tags tylko od czasu do czasu, dodatkowy koszt przechowywania dla rzutowania tego atrybutu do indeksu może nie być opłacalny.

Skanowanie lokalnego indeksu wtórnego

Operacja Scan pozwala na pobranie wszystkich danych z lokalnego indeksu wtórnego. Musimy podać nazwę tabeli bazowej oraz nazwę indeksu w przygotowanym żądaniu. Podczas skanowania DynamoDB odczytuje wszystkie dane z indeksu i zwraca je do aplikacji. Możemy również przygotować żądanie, w którym chcemy, aby tylko niektóre dane zostały zwrócone a pozostałe odrzucone. Aby to zrobić musimy posłużyć się parametrem FilterExpression operacji Scan.

Zapisywanie elementów i lokalne indeksy wtórne

DynamoDB automatycznie utrzymuje wszystkie lokalne indeksy wtórne zsynchronizowane z ich odpowiednimi tabelami. Aplikacje nigdy nie zapisują bezpośrednio do indeksów. Musimy jednak zdawać sobie sprawę z tego jak DynamoDB utrzymuje te indeksy.

W momencie tworzenia lokalnego indeksu wtórnego określamy atrybut służący jako klucz sortowania dla indeksu. Określamy również typ danych dla tego atrybutu. Oznacza to, że zapisując element do tabeli podstawowej, jeśli element określa atrybut klucza indeksu, jego typ musi odpowiadać typowi danych schematu klucza indeksu. W przypadku LastPostIndex, klucz sortowania LastPostDateTime w indeksie jest określony jak typ danych String. Jeżeli spróbujemy dodać element do tabeli Thread i określić inny typ dla LastPostDateTime, np. Number, DynamoDB zwróci wyjątek ValidationException z powodu niedopasowania typu danych.

Jeżeli chodzi o relacje: nie ma wymogu relacji jeden do jednego między elementami w tabeli podstawowej a elementami w lokalnym indeksie wtórnym. W rzeczywistości takie zachowanie może być korzystne dla wielu aplikacji.

Tabela z wieloma lokalnymi indeksami wtórnymi ponosi wyższe koszty zapisu niż tabela z mniejszą liczbą indeksów.

My musimy pamiętać, że dla tabel z lokalnymi indeksami wtórnymi istnieje limit rozmiaru 10GB na wartość klucza partycji. Tabela z lokalnymi indeksami wtórnymi może przechowywać dowolną liczbę elementów o ile całkowity rozmiar dla jednej wartości klucza partycji nie przekracza 10GB.

Uwagi dotyczące przepustowości dla lokalnych indeksów wtórnych

Tworząc tabelę w DynamoDB ustalamy jednostki przepustowości odczytu i zapisu dla oczekiwanego obciążenia tabeli. Obciążenie to obejmuje odczyt i zapis na lokalnych indeksach wtórnych. W dalszych wpisach przyjrzymy się temu zagadnieniu głębiej skupiając się na analizie ruchu wykorzystując CloudWatch: DynamoDB: analiza WCU DynamoDB: analiza RCU

Jednostki pojemności odczytu

Podczas wykonywania zapytania do lokalnego indeksu wtórnego liczba jednostek pojemności odczytu zależy od sposobu dostępu do danych.

Podobnie jak w przypadku zapytań do tabeli, zapytanie do indeksu może wykorzystać odczyty ostatecznie spójne lub silnie spójne – w zależności od ustawień parametru ConsistentRead. Jeden silnie spójny odczyt zużywa jedną jednostkę pojemności odczytu. Ostatecznie spójny odczyt z kolei zużywa tylko połowę tej wartości. W związku z powyższym, wybierając ostatecznie spójne odczyty, możemy zmniejszyć opłaty za jednostki pojemności odczytu – jak już doskonale wiecie, z pewnymi konsekwencjami.

Dla zapytań o indeks, które żądają nie tylko kluczy indeksu i rzutowanych atrybutów, DynamoDB oblicza aktywność odczytu rezerwowego w taki sam sposób jak dla zapytań przeciwko tabelom. Jedyną różnicą jest to, że obliczenia są oparte na rozmiarach wpisów indeksu a nie na rozmiarze elementu w tabeli podstawowej. Liczba jednostek pojemności odczytu jest sumą wszystkich przewidywanych rozmiarów atrybutów we wszystkich zwracanych elementach – wynik jest następnie zaokrąglany w górę do następnej granicy 4 KB.

Dla zapytań o indeks, które odczytują atrybuty, które nie są rzutowane na lokalny indeks wtórny, DynamoDB musi pobierać te atrybuty z tabeli podstawowej, oprócz odczytu rzutowanych atrybutów z indeksu. Pobieranie to ma miejsce, gdy dołączamy jakiekolwiek nieprojektowane atrybuty do parametrów Select lub ProjectionExpression operacji Query. Pobieranie powoduje dodatkowe opóźnienia w odpowiedziach zapytań a także podnosi koszty wykorzystania przepustowości – oprócz opisanych powyżej odczytów z lokalnego indeksu pobierana jest opłata za jednostki pojemności odczytu dla każdego pobieranego elementu tabeli podstawowej. Opłata ta dotyczy każdego całego elementu z tabeli, nie tylko żądanych atrybutów.

Maksymalny rozmiar wyników zwróconych przez operację Query wynosi 1MB. Limit ten obejmuje rozmiary wszystkich nazw atrybutów i wartości we wszystkich zwróconych elementach. Jednakże, jeżeli zapytanie skierowane do lokalnego indeksu wtórnego powoduje, że DynamoDB pobiera atrybuty elementów z tabeli podstawowej, maksymalny rozmiar danych w wynikach może być niższy. W tym przypadku rozmiar wyniku jest sumą:

  • rozmiar pasujących elementów w indeksie zaokrąglony w górę do następnych 4 KB;
  • rozmiar każdego pasującego elementu w tabeli bazowej, przy czym każdy element z osobna jest zaokrąglany w górę do następnych 4 KB.

Korzystając z tej formuły, maksymalny rozmiar wyników zwracanych przez operację Query to nadal 1 MB.

W ramach przykładu spójrzmy na tabelę, w której rozmiar każdego elementu wynosi 300 bajtów. Istnieje lokalny indeks wtórny na tej tabeli, ale tylko 200 bajtów każdego elementu jest rzutowane na indeks. Załóżmy również, że wykonujemy zapytanie do tego indeksu oraz że zapytanie wymaga pobrania tabeli dla każdego elementu i że zapytanie zwraca 4 elementy. DynamoDB podsumowuje powyższe wartości/założenia w poniższy sposób:

  • rozmiar pasujących elementów w indeksie: 200 bajtów x 4 pozycje = 800 bajtów – jest to zaokrąglane w górę do 4 KB;
  • rozmiar każdego pasującego elementu w tabeli podstawowej: (300 bajtów zaokrąglone do 4KB) x 4 elementy = 16 KB.

W takim wypadku całkowity rozmiar wynosi zatem 20 KB.

Jednostki pojemności zapisu

Gdy element jest dodawany, aktualizowany lub usuwany, aktualizacja lokalnych indeksów wtórnych zużywa jednostki pojemności zapisu dla tabeli. Całkowity koszt przepustowości zapisu jest sumą jednostek pojemności zapisu zużytych przez zapis do tabeli i tych zużytych przez aktualizację lokalnych indeksów wtórnych.

Koszt zapisu elementu do lokalnego indeksu wtórnego zależy od kilku czynników:

  • jeżeli dodajemy nowy element do tabeli, który definiuje atrybut indeksowany, lub aktualizujemy istniejący element, aby zdefiniować wcześniej niezdefiniowany atrybut indeksowany to do umieszczenia elementu w indeksie wymagana jest jedna operacja zapisu;
  • jeżeli aktualizacja tabeli zmienia wartość indeksowanego atrybutu klucza (z A na B), wymagane są dwa zapisy: jeden do usunięcia poprzedniego elementu z indeksu i drugi zapis do umieszczenia nowego elementu w indeksie;
  • jeżeli element nie jest obecny w indeksie przed lub po aktualizacji elementu, nie ma dodatkowego kosztu zapisu dla indeksu;
  • jeżeli element nie jest obecny w indeksie przed lub po aktualizacji elementu, nie ma dodatkowego kosztu zapisu dla indeksu.

Wszystkie te czynniki zakładają, że rozmiar każdego elementu w indeksie jest mniejszy lub równy rozmiarowi elementu 1 KB do liczenia jednostek pojemności zapisu. Większe zapisy indeksu wymagają dodatkowych jednostek pojemności zapisu. Możemy zminimalizować koszty zapisu poprzez rozważenie, które atrybuty musza być zwrócone przez zapytania i rzutowanie tylko tych atrybutów do indeksu.

Uwagi dotyczące przechowywania lokalnych indeksów wtórnych

Kiedy nasza aplikacja zapisuje elementy w tabeli, DynamoDB automatycznie kopiuje właściwy podzbiór atrybutów do wszelkich lokalnych indeksów wtórnych w których te atrybuty powinny się pojawić. Nasze skonfigurowane konto AWS jest obciążane za przechowywanie elementu w tabeli bazowej, a także za przechowywanie atrybutów we wszelkich lokalnych indeksach wtórnych na tej tabeli.

Ilość miejsca używanego przez element indeksu jest sumą następujących elementów:

  • rozmiar w bajtach klucza głównego tabeli bazowej (klucza partycji i klucza sortowania);
  • rozmiar w bajtach atrybutu klucza indeksu;
  • rozmiar w bajtach przewidywanych atrybutów (jeżeli występują);
  • 100 bajtów kosztów ogólnych na element indeksu.

W celu oszacowania wymagania dotyczącego pojemności przechowywania dla lokalnego indeksu wtórnego możemy oszacować średni rozmiar elementu w indeksie a następnie dokonać mnożenia przez liczbę elementów w tabeli podstawowej.

Jeżeli tabela zawiera element w którym określony atrybut nie jest zdefiniowany, ale ten atrybut jest zdefiniowany jako klucz sortowania indeksu, wtedy DynamoDB nie zapisuje żadnych danych dla tego elementu do indeksu.

Zbiory elementów w lokalnych indeksach pomocnicznych

Wszystko co przeczytacie w tym akapicie odnosi się tylko do tabel posiadających zdefiniowane lokalne indeksy wtórne.

W DynamoDB kolekcja elementów jest grupą elementów, które mają tę samą wartość klucza partycji w tabeli i wszystkich jej lokalnych indeksach wtórnych. W poniższym przykładzie (w ramach tego akapitu) kluczem partycji dla tabeli Thread jest ForumName a kluczem partycji dla LastPostIndex jest również ForumName. Wszystkie elementy tabeli i indeksu o tej samej nazwie ForumName są częścią tej samej kolekcji elementów – np. w tabeli Thread i lokalnym indeksie wtórnym LastPostIndex istnieje kolekcja elementów dla forum EC2 i inna kolekcja dla forum RDS.

Spójrzcie na poniższych schemat tabeli pokazujący kolekcję elementów dla ‘forum’: DynamoDB: przykład kolekcji elementów

Na powyższych schemacie kolekcja elementów składa się ze wszystkich elementów w tabeli Thread i indeksie LastPostIndex, gdzie wartość klucza partycji ForumName to "S3". Jeżeli istniałyby inne lokalne indeksy wtórne na tabeli, wszystkie elementy w tych indeksach z ForumName = S3, byłyby one również częścią kolekcji elementów.

Możemy użyć dowolnej z następujących operacji w DynamoDB, aby zwrócić informacje o kolekcjach elementów:

  • BatchWriteItem
  • DeleteItem
  • PutItem
  • UpdateItem
  • TransactWriteItems

Każda z tych operacji obsługuje parametr ReturnCollectionMetrics. Ustawienie jego wartości na SIZE pozwoli na wyświetlenie informacji o rozmiarze każdej kolekcji elementów w indeksie.

Spójrzmy na poniższy przykład odpowiedzi z operacji UpdateTable na tabeli Thread z wartością parametru ReturnItemCollectionMetrics ustawioną na wartość SIZE. Element, który został zaktualizowany miał wartość ForumName = "EC2" - odpowiedź zawiera więc informacje o tej kolekcji elementów:

{
    ItemCollectionMetrics: 
    {
        ItemCollectionKey: 
        {
            ForumName: "EC2"
        },
    SizeEstimateRangeGB: [0.0, 1.0]
    }
}

Obiekt SizeEsitmateRangeGB pokazuje, że rozmiar tej kolekcji elementów jest pomiędzy 0 a 1GB. DynamoDB okresowo aktualizuje oszacowanie tego rozmiaru więc przy następnej operacji modyfikacji elementu wartości te mogą się różnić.

Limit wielkości zbioru elementów

Maksymalny rozmiar kolekcji elementów dla tabeli, która posiada jeden lub więcej lokalnych indeksów wtórnych wynosi 10 GB. Nie dotyczy to kolekcji elementów w tabelach bez lokalnych indeksów wtórnych a także nie dotyczy kolekcji elementów w globalnych indeksach wtórnych. Dotyczy to tylko tabel, które mają jeden lub więcej lokalnych indeksów wtórnych.

Jeżeli kolekcja elementów przekroczy limit 10 GB, DynamoDB zwraca wyjątek ItemCollectionSizeLimitExceededException i nie będzie można dodać więcej elementów do kolekcji elementów ani zwiększyć rozmiarów elementów, które są w kolekcji elementów. (Operacje odczytu i zapisu, które zmniejszają rozmiar kolekcji elementów są nadal dozwolone). Nad możemy dodawać elementy do innych kolekcji elementów.

Aby zmniejszyć rozmiar kolekcji elementów możemy wykonać jedną z następujących czynności:

  • usunąć wszystkie niepotrzebne elementy z daną wartością klucza partycji. Kiedy usuwamy elementy z tabeli podstawowej, DynamoDB usuwa również wszelkie wpisy indeksu, które mają tę samą wartość klucza partycji;
  • aktualizacja elementów poprzez usunięcie atrybutów lub zmniejszenie ich rozmiaru. Jeżeli te atrybuty są rzutowane na jakiekolwiek lokalne indeksy wtórne, DynamoDB zmniejsza również rozmiar odpowiednich wpisów indeksu;
  • utworzenie nowej tabeli z tym samym kluczem partycji i kluczem sortowania a następnie przeniesienie elementów ze starej tabeli do nowej. Takie podejście może być dobre/poprawne, jeżeli tabela zawiera dane historyczne, które są rzadko dostępne. Innym rozwiązaniem jest archiwizacja danych historycznych przy pomocy Amazon S3.

W momencie, kiedy rozmiar kolekcji elementów spadnie poniżej 10 GB możemy ponownie dodać elementy z tą samą wartością klucza partycji.

AWS, jako najlepszą praktykę, zaleca przygotowanie aplikacji w taki sposób, aby monitorowała rozmiar kolekcji elementów. Jednym ze sposobów jest ustawienie parametru ReturnItemCollectionMetrics na SIZE przy każdym użyciu metod: BatchWriteItem, DeleteItem, PutItem lub UpdateItem. Takie wykorzystanie parametru (oraz odpowiednie przygotowanie aplikacji) pozwoli na sprawdzanie obiektu ReturnItemCollectionMetrics za każdym razem po otrzymaniu odpowiedzi z powyższych metod i przygotowanie komunikatu o błędzie/albo ostrzeżenia za każdym razem, gdy kolekcja elementów przekroczy zdefiniowany przez użytkownika limit, np. 9 GB. Ustawienie takiego ostrzeżenia będzie swojego rodzaju system wczesnego ostrzegania, który da na informacje (i czas) na zareagowanie w odpowiednim czasie tak, abyśmy nie przekroczyli wspominanego na wstępie limitu 10 GB.

Kolekcje elementów i partycje

W tabeli z jednym lub wieloma lokalnymi indeksami wtórnymi, każda kolekcja elementów jest przechowywana w jednej partycji. Całkowity rozmiar takiej kolekcji elementów jest ograniczony do możliwości przechowywania tej partycji, tj. 10 GB. W przypadku aplikacji, w której model danych obejmuje kolekcje elementów o nieograniczonym rozmiarze lub w przypadku, gdy można oczekiwać, że niektóre kolekcje elementów w przyszłości przekroczą 10 GB, należy rozważyć użycie globalnego indeksu wtórnego.

Nasze aplikacje powinniśmy projektować w taki sposób, aby dane z tabeli były równomiernie rozłożone na różne wartości klucza partycji. W przypadku tabel z lokalnymi indeksami wtórnymi, aplikacje nie powinny tworzyć tzw. gorących punktów (hot spots) odczytu i zapisu w obrębie pojedynczej kolekcji elementów na pojedynczej partycji.

To tyle w ramach teorii, w kolejnym wpisie wykorzystamy API AWS SDK dla .NET i przejdziemy przez praktyczne przykłady.