Local Secondary Index (LSI) DynamoDB-ben
|

Ez a cikk a DynamoDB-ben történő adatmodellezésről szóló cikksorozat harmadik része (az első a kulcsválasztásról, a második a Global Secondary Index-ek használatáról szólt) - és még ez után is lesz folytatás.

Meddig jutottunk el az előző két alkalommal?

Az első részben abban maradtunk, hogy érdemes odafigyelni az elsődleges kulcs megválasztására. A kulcs nagyban meghatározza, hogy mennyire könnyen férünk hozzá az adatainkhoz, ugyanis kevés költséggel csak az elsődleges kulcsra tudunk szűrni (itt nincs olyan flexibilis eszközünk, mint az SQL-megvalósítások WHERE záradéka). Ha még emlékszel: az elsődleges kulcs állhat csak egy Partition Key-ből, de lehet összetett is, ilyenkor a Partition Key nem szükségszerűen egyedi, de van egy Sort Key-ed is, és a kettő együtt már egyedi kell, hogy legyen. Az elsődleges kulcs tehát maximum két jellemző (SQL-ben gondolkodva: mező) lehet.

A második részben láttuk, hogy amikor a lekérdezésünkben szeretnénk az elsődleges kulcsban egyébként nem szereplő jellemzőt szerepeltetni, akkor igazán hatékonyan akkor járunk el, ha létrehozunk egy Global Secondary Index-et, azaz GSI-t. A GSI lényegében egy másik tábla, amelyikben az eredeti tábla valamelyik másik jellemzője az elsődleges kulcs, és így már erre is tudunk szűrni a lekérdezésünkben (lekérdezést írni DynamoDB-ben csak az elsődleges kulcsra lehet). Az új tábla közvetlenül nem frissíthető, de a tartalmának változása pontosan követi a fő tábláét. Hátrány, hogy ezért az indextábláért külön fizetünk.

Idézzük csak fel a táblát, amivel dolgozunk:

{
  "client": "jozsi",
  "ordertime": 20200706080245,
  "stat": "open"
}
{
  "client": "jozsi",
  "ordertime": 20200706080244,
  "stat": "closed"
}
{
  "client": "johanna",
  "ordertime": 20200706080245,
  "stat": "closed"
}

Összetett elsődleges kulcsunk van, azaz két "mező" együtt azonosít egy bejegyzést. A client a Partition Key, az ordertime a Sort Key, és remekül tudunk olyat kérdezni a DynamoDB-től, hogy

  • add meg jozsi rendeléseit, vagy
  • add meg jozsi 2020. júliusi rendeléseit,

de nem tudunk olyan lekérdezést írni, hogy "add meg a lezárt rendeléseket." A múltkor úgy oldottuk meg a problémát, hogy GSI-t hoztunk létre, aminek az elsődleges kulcsa a stat jellemző, és így már tudtunk lekérdezést írni.

Színre lép az LSI

LSI létregozása DynamoDB-ben

Az LSI, azaz Local Secondary Index kicsit olyan, mint a GSI, de azért mégis más. Abban az értelemben olyan, mint a GSI, hogy ez is a lekérdezési lehetőségeink tágítására való: újabb jellemzőkre (mezőkre) kérdezhetünk vele. Minden másban más. A különbségeket a cikk végén majd összefoglaljuk még, de kezdjük az első igazán jelentős különbséggel: az LSI-ket a tábla létrehozásakor meg kell adnunk. Nincs kivétel. Ha csak utólag alakul ki a "de jó lenne ha lenne" érzésünk, akkor az egyetlen opció egy új tábla létrehozása, és az adatok átmásolása. Ha ehhez nem fűlik a fogad, nem lesz LSI-d. Egy LSI létrehozásakor tudnod kell, hogy

  • csak olyan táblához készíthető, aminek összetett elsődleges kulcsa van,
  • az LSI elsődleges kulcsában a Partition Key mindenképp megegyezik a főtábláéval,
  • a Sort Key viszont valami más.

Alakítsuk ki a múltkori táblánkat, a stat mezőt megadva az LSI Sort Key mezőjeként - mégpedig a változatosság kedvéért parancssorból. Szükségünk lesz egy szép JSON-fájlra, ami definiálja a táblát (és az LSI-t):

{
    "TableName": "Order",
    "AttributeDefinitions": [
      { "AttributeName": "client", "AttributeType": "S" },
      { "AttributeName": "ordertime", "AttributeType": "N" },
      { "AttributeName": "stat", "AttributeType": "S" }
    ],
    "KeySchema": [
      { "AttributeName": "client", "KeyType": "HASH" },
      { "AttributeName": "ordertime", "KeyType": "RANGE" }
    ],
    "ProvisionedThroughput": {
      "ReadCapacityUnits": 1,
      "WriteCapacityUnits": 1
    },
    "LocalSecondaryIndexes": [
        {
            "IndexName": "client-stat-index",
            "Projection": { "ProjectionType": "ALL" },
            "KeySchema": [
              { "AttributeName": "client", "KeyType": "HASH" },
              { "AttributeName": "stat", "KeyType": "RANGE" }
            ]
        }
    ]
}

Figyeljük meg, hogy

  • azt a jellemzőt (stat) is felvesszük a listába - és innentől kötelező jellemzővé válik -, ami az LSI Sort Key-e,
  • a LocalSecondaryIndexes főnév a többes számból következtethetünk rá, hogy több is megadható, és valóban: akár ötöt is használhatunk,
  • a kulcsok neve nem egyezik meg a webes konzolon látottakkal, a Partition Key neve itt HASH key, a Sort Key neve pedig RANGE key.

Megfigyeléseinket követően adjuk ki a fájlt felhasználó, a tábla létrehozására szolgáló parancsot:

aws dynamodb create-table --cli-input-json file://dynamodb-tabladefinicio.json

Megnézhetjük a webes konzolon, ott lesz a táblánk. Betöltjük a három adatunkat is, amihez kelleni fog a következő JSON:

{
    "Order": [
        {
            "PutRequest": {
                "Item": {
                    "client": {"S":"jozsi"},
                    "ordertime": {"N":"20200706080245"},
                    "stat": {"S":"open"}
                }
            }
        },
        {
            "PutRequest": {
                "Item": {
                    "client": {"S":"jozsi"},
                    "ordertime": {"N":"20200706080244"},
                    "stat": {"S":"closed"}
                }
            }
        },
        {
            "PutRequest": {
                "Item": {
                    "client": {"S":"johanna"},
                    "ordertime": {"N":"20200706080245"},
                    "stat": {"S":"closed"}
                }
            }
        }
    ]
}

A betöltést pedig a következő egysoros végzi:

aws dynamodb batch-write-item --request-items file://dynamodb-adatok.json

Lekérdezés LSI-vel

Ugyebár összetett elsődleges kulcsot eredetileg azért használunk, mert szeretnénk egy adott Partition Key érték mellett olyan lekérdezést írni, ahol a Sort Key értékei alapján kutakodunk. Épp azért készítettük az eredeti táblánkat olyanra, amilyen lett, hogy feltehessük az "add meg jozsi 2020. júliusi rendeléseit" típusú kérdéseinket. Ha megnézzük, hogy egy DynamoDB táblán vagy indexen futtatott lekérdezés "Key Condition Expression"-je milyen műveleteket tud, akkor ott megtaláljuk a BETWEEN-t ami tökéletesen használható lesz ahhoz, amit csinálni akarunk. (A Key Condition épp azt jelenti, hogy a tábla elsődleges kulcsának állapotát adjuk meg ilyenkor.) A jozsi által 2020. júliusában, a hónap 6-án, 8 óra 2 perc 45 másodperccel kezdődően leadott rendeléseket így kapjuk meg:

aws dynamodb query --table-name Order \
  --key-condition-expression 'client = :ugyfel AND ordertime BETWEEN :ido1 AND :ido2' \
  --expression-attribute-values '{":ugyfel": {"S": "jozsi"}, ":ido1": {"N": "20200706080245"}, ":ido2": {"N": "20200731235959"}}'

A lehetséges műveleteket tartalmazó linket azonban nem ezért helyeztük el pár sorral föntebb. Ha megnyitottátok a linket, akkor feltűnhetett, hogy "You must specify the partition key name and value as an equality condition." kitétel.

A múltkor ugye GSI-t használtunk azért, hogy megkapjuk a lezárt rendeléseket (ahol a stat jellemző értéke closed). Ehhez az kellett, hogy a stat jellemző legyen az elsődleges kulcs. Nos, az említett jellemző az LSI elsődleges kulcsának része, próbálkozzunk tehát a múltkori, a GSI használatával foglalkozó cikkben megismert lekérdezéssel:
aws dynamodb query --table-name Order --index-name client-stat-index --key-condition-expression  "stat = :allapot" --expression-attribute-values '{":allapot":{"S":"closed"}}'

amire válaszul egy otromba An error occurred (ValidationException) when calling the Query operation: Query condition missed key schema element: client (azaz a client értékét hiányoló) üzenetet kapunk. Hát ez sajnálatos módon összecseng a fent idézett angol nyelvű kitétellel, ami magyar fordításban annyit tesz, hogy "A partíciós kulcs nevét és értékét egyenlőségi feltétel formájában kell megadnia." Vagyis nincs olyan, hogy a Partition Key kimarad a lekérdezésből, és nem adhatok meg olyan értékeket, mint "NOT NULL" vagy "*" - csak azt mondhatom meg, hogy konkrétan mi legyen. Ez a mi esetünkben bizony azt jelenti, hogy nem tudom egyszerű lekérdezéssel megkapni a lezárt rendelések listáját egy LSI-n futtatott lekérdezéssel. A jozsi nevű felhasználó lezárt rendeléseit viszont simán:

aws dynamodb query --table-name Order --index-name stat-index --key-condition-expression  "client = :ugyfel and stat = :allapot" --expression-attribute-values '{":ugyfel":{"S":"jozsi"},":allapot":{"S":"closed"}}'

Felmerülhet a kérdés, hogy mi a különbség a tábla eredeti összetett elsődleges kulcsa és az LSI között, hiszen LSI kicsit olyan, mint ha több Sort Key-t is definiálhatnánk egy táblában. Alapvetően nem tévedünk ezzel a gondolattal nagyot, de van egy fontos különbség is: az eredeti táblában a Partition Key + Sort Key páros együtt az elsődleges kulcs, márpedig annak illik egyedinek lenni, és ez a követelmény itt, az indexben nincs.

GSI vs LSI

Következik egy kis összevetés, hogy könnyebben eldönthesd, melyiket volna jó használnod.

  • Az elsődleges kulcs GSI esetében lehet egyszerű vagy összetett, az LSI esetében csak összetett lehet, és a Partition Key kötelezően megegyezik a tábláéval.
  • Az elsődleges kulcs egyedisége egyik esetben sem feltétel.
  • Méretkorlát GSI esetében nincs, az LSI esetében 10 GB (ez komoly gond lehet).
  • Az indexek maximális száma GSI esetében táblánként 20, LSI esetében táblánként 5.
  • Létrehozásuk GSI esetében nincs időhöz kötve, LSI-t viszont csak a táblával együtt készíthetsz.
  • Lekérdezéskor GSI esetében beutazhatod az összes partíciót, LSI-vel viszont csak egy partíción belül tudsz lekérdezni (hiszen meg kell adnod a Partition Key értékét).
  • A lekérdezésben szereplő jellemzők GSI esetében csak azok lehetnek, amik az indexben is szerepelnek, LSI esetében a tábla valamennyi jellemzőjét kérdezgethetjük.
  • Konzisztencia GSI esetében eventual (azaz a főtáblán végzett változtatás idővel itt is látható lesz), LSI használatakor kérhetsz strongly consistent indexet (azaz abban a szent szúrásban érvényre jut a változtatás, ahogy a főtáblában megejted). A késleltetés a GSI-nél is pár pillanat, az egy másodperc már dühöngésre adhat okot, de ha azonnal vissza is olvasnád a frissen beírt adatot, akkor ez még lehet túl sok.
  • Az olvasás sebessége GSI esetében független az adattábláétól, az LSI-n végzett műveletek azonban a főtábla Read Capacity Unit-jait fogyasztják.
  • Az index ára GSI esetében tőlünk függ, pontosabban attól, hogy mennyi Read Capacity Unit-ot adunk neki, azaz milyen gyors válaszra van szükségünk. Az LSI viszont a főtbla egységeit fogyasztja, ilyen értelemben ingyen van.

Említettem már, hogy az SQL-es adatmodellezés során vérünkké vált módszerek DynamoDB esetében nem igazán működnek jól? Legközelebb tovább rontjuk a helyzetet:)

[Vissza a bejegyzésekhez]