Ga naar hoofdinhoud

Performance en schaalbaarheid onbegrensd: aan de slag met single-table-databaseontwerp in DynamoDB

Eén tabel om ze allemaal te temmen: vereenvoudig, schaal en geef je NoSQL-database een turbo!
· 12 min lezen

Verken met AI

Welkom in de wereld van DynamoDB, waar efficiëntie en onbeperkte schaalbaarheid samenkomen in één tabel. In deze uitgebreide tutorial gaan we op een verhelderende reis en onthullen we het ware potentieel van NoSQL single-table-databaseontwerp.

Ontdek de kunst van het vereenvoudigen van complexe datamodellen terwijl je de volle kracht benut van een uniforme structuur die moeiteloos met je groeiende behoeften meegroeit.

Of je nu een doorgewinterde ontwikkelaar bent die bekend is met NoSQL-databases of net begint aan je databaseroute: maak je klaar om met een krachtige data-architectuur een nieuw niveau van schaalbaarheid voor je applicaties te ontsluiten!

Wat is NoSQL?

Voordat we beginnen, is het belangrijk om het verschil tussen NoSQL en de relationele database te begrijpen, in welke scenario's we welke gebruiken en waarom deze technologieën zijn uitgevonden. Daarvoor moeten we kort naar de geschiedenis van gegevensverwerking kijken.

Je kunt meer lezen over het belang van NoSQL-databases in data science in een aparte blogpost.

Relationele databases

De relationele database is een bekende, veelgebruikte technologie die al sinds de jaren 70 bestaat. Twee van de belangrijkste problemen die de relationele database oplost, zijn opslag en referentiële integriteit.

De data wordt “genormaliseerd”, meestal tot de derde normaalvorm, wat de datavoetafdruk verkleint en er tegelijk voor zorgt dat updates aan de data consistent blijven met de rest van de data.

De grootte van de persistente datavoetafdruk was cruciaal omdat vroeger de harddrive het duurste onderdeel in het datacenter was. Dat is tegenwoordig niet meer zo: nu is de CPU het duurst en zijn de opslagkosten drastisch gedaald. Dat betekent niet dat de relationele database achterhaald is; die heeft nog steeds zinvolle toepassingen voor:

  • OLAP
  • Datawarehouses
  • Ad-hocqueries
  • Applicaties waarbij de toegangspatronen niet goed gedefinieerd zijn
  • CMS / headless CMS

NoSQL-databases

Datadruk is het vermogen van een systeem om een bepaalde hoeveelheid data binnen redelijke kosten en/of tijd te verwerken.

NoSQL-databases zijn eind jaren 90 uitgevonden om problemen met datadruk aan te pakken die de prestaties van relationele databases beïnvloedden door steeds grotere hoeveelheden applicatiedata.

De CPU in een relationele database kun je zien als een van de belangrijkste bottlenecks, omdat de server moet zwoegen om genormaliseerde data op te halen en samen te voegen tot de gedenormaliseerde views die applicaties doorgaans consumeren.

Je kunt dit verlichten door read-replicaservers toe te voegen, maar NoSQL kiest een andere aanpak en slaat applicatiedata gedenormaliseerd op, klaar voor gebruik door applicaties. Dat betekent dat de datavoetafdruk groter is dan bij een relationele database, maar het ontlast de CPU, waardoor de databaseserver feitelijk fungeert als een eenvoudige router die met een hash-algoritme naar een locatie op een schijf in het datacenter wijst. Typische toepassingen voor NoSQL-databases zijn:

  • OLTP
  • Applicaties met bekende toegangspatronen
  • Applicaties die horizontaal moeten schalen om grote regionale of wereldwijde vraag aan te kunnen

Tal van studies hebben genoeg objectieve data opgeleverd om een grafiek te presenteren die de prestaties van SQL versus NoSQL op schaal weergeeft.

vergelijkingsgrafiek massa-operaties

Bron afbeelding

Het laatste punt dat we willen verduidelijken: de data die in een NoSQL-database is opgeslagen, is nog steeds relationele data. Als de data niet relationeel was, zouden we die in een eenvoudige filestore bewaren.

NoSQL-antipatronen

We modelleren een NoSQL-database niet op dezelfde manier als een relationele database, dus het is een antipatroon om een genormaliseerd ontwerp direct te vertalen naar een NoSQL-database met meerdere tabellen die je aan elkaar zou joinen, want NoSQL heeft geen join-operator en ook geen referentiële integriteit.

Een tabel in NoSQL is het equivalent van een catalogus in een relationele database. Modelleer de data in plaats daarvan gedenormaliseerd, klaar voor consumptie door applicaties.

Een ander antipatroon zijn Hot Keys. Dat betekent dat de meeste of zelfs alle toegangsverzoeken op één opslagnode terechtkomen, wat erop wijst dat de keyspace niet correct is ontworpen.

De heatmap van de toegang ziet er ongeveer zo uit:

image14.png

DynamoDB

AWS DynamoDB is een volledig beheerde, serverloze, wide-column keyvaluestore, wat betekent dat het veel items (of rijen) in een tabel ondersteunt, maar die items niet per se dezelfde attributen hebben. Ondersteunde datatypen zijn:

  • Scalaire typen: string, number, binary, boolean, null
  • Documenttypen: complexe structuur met geneste attributen (bijv. JSON)
  • Settypen: string set, number set, binary set

Zoals eerder genoemd, gebruik je NoSQL wanneer de toegangspatronen van de applicatie goed bekend zijn en er behoefte is aan veel TPS. DynamoDB kan naar elke workload schalen en levert voorspelbaar snelle en consistente responstijden tot 4 miljoen TPS met lage latency (10 - 20 ms).

Hoe werkt DynamoDB?

Elk item (of rij) heeft een partition key die dat item uniek identificeert en de datadistributie over de onderliggende opslag bepaalt.

Items kunnen optioneel een sort key hebben, die bepaalt in welke volgorde de data binnen een partitie op schijf wordt opgeslagen. Sort keys stellen ons in staat partities te bevragen met complexe range- en filteroperaties (al worden filteroperaties toegepast in de clientlaag, niet in de databaselaag).

Een partition key wordt gebruikt om een hash-index te maken, waarna elk item volgens deze hash wordt geplaatst over een virtuele keyspace. Deze virtuele keyspace is verdeeld in segmenten die mappen naar fysieke opslagapparaten en die dynamisch kunnen groeien en krimpen afhankelijk van het volume opgeslagen data.

Dit is de reden dat DynamoDB snel en consistent is op elke schaal: de database-engine fungeert als een routeringsservice van gehashte keys naar de fysieke opslag.

Datamodellering met NoSQL

In deze tutorial modelleren we een onlineshop met de naam Daintree.com.

(Leuk weetje: het Daintree-regenwoud, in Queensland, Australië, is een van de oudste regenwouden op aarde en wordt geschat op zo’n 180 miljoen jaar oud!)

Laten we eerst een sterk vereenvoudigd Entity Relationship Diagram (ERD) maken, zodat we kunnen visualiseren wat we gaan modelleren.

entiteit-relatiediagram (ERD)

Hieruit kunnen we afleiden dat een klant een winkelmand kan aanmaken en meerdere producten met verschillende hoeveelheden kan toevoegen.

Een product heeft een categorie, die op zijn beurt bovenliggende categorieën kan hebben.

Mandjes kunnen bestellingen worden die in meerdere betalingen kunnen worden opgesplitst.

Zoals gezegd is dit een zeer vereenvoudigd beeld van de relaties en attributen van de entiteiten die een online shoppingapplicatie vormen, maar het volstaat voor deze tutorial.

Definieer de toegangspatronen

Bij een relationele database hoeven we ons over deze stap geen zorgen te maken. SQL is een enorm krachtige taal die, mits het schema correct is ontworpen, ons de flexibiliteit geeft om onze data te bevragen en te manipuleren zoals we willen. Dat is niet het geval bij het single-table-ontwerppatroon in NoSQL, omdat we onze data gedenormaliseerd en klaar voor directe consumptie door de applicatie moeten opslaan.

Dit zijn de toegangspatronen die we voor onze onlineshop willen ondersteunen:

  • Alle producten opvragen per categorie
  • Subcategorieën opvragen voor een bovenliggende categorie
  • Alle bestellingen opvragen per klant
  • Alle betalingen opvragen voor een bestelling
  • Een specifieke bestelling opvragen voor een klant
  • Alle manditems opvragen voor een winkelmand van een klant
  • Alle producten opvragen op basis van bestelgeschiedenis van de klant

De tabel modelleren

NoSQL Workbench is een gratis tool van AWS om NoSQL-tabellen voor DynamoDB te modelleren.

Ga naar de Data Modeler en maak een nieuw datamodel met een nieuwe tabel:

image5.png

We houden de primaire sleutelattributen generiek als strings PK en SK:

image8.png

Voor extra attributen slaan we het entity_type op, samen met twee GSI PK- en SK-primaire sleutelattributen.

Een GSI is een global secondary index: een kopie van je tabel die DynamoDB synchroon houdt (asynchroon!!) maar opslaat met alternatieve paren van primaire sleutelattributen. Dit is het kernmechanisme waarmee één tabel meerdere toegangspatronen kan ondersteunen. Later meer hierover; onthoud voorlopig dat deze keys de primaire sleutels van gerelateerde entiteiten opslaan.

De andere extra attributen die we moeten toevoegen, zijn alle niet-sleutelattributen van onze data (let op: deze stap is alleen nodig in de ontwerpfase en wordt niet expliciet gedefinieerd wanneer we de daadwerkelijke tabel inrichten):

image11.png

Vervolgens voegen we de twee GSI’s toe met de keys die we in de vorige stap hebben gedefinieerd:

image9.png

Klik op opslaan en vervolgens op visualize the data model. Je ziet nu je nieuw aangemaakte maar lege tabel. Klik op edit table en dan rechtsboven op edit data; hiermee kun je nieuwe rijen toevoegen:

image7.png

Dataprefixen

Voordat we data gaan toevoegen, moeten we onze primaire sleutelprefixen per entiteit definiëren:

product

p#

categorie

c#

klant (gebruiker)

u#

mand

b#

manditem

bi#

bestelling

o#

bestelregel

ol#

betaling

py#

factuur

i#

De data modelleren

Klantgegevens

Het merendeel van de toegangspatronen draait om de klant, dus voor onze primaire index gebruiken we de klant-ID als de hoofdpartitie, waarbij het daadwerkelijke klantrecord te herkennen is doordat de sort key ook de klant-ID is.

We kunnen dan eenvoudig relaties toevoegen van klanten naar bestellingen en van klanten naar manden door dezelfde partition key te gebruiken maar met een unieke sort key.

Onze geaggregeerde view ziet er nu zo uit:

image12.png

We kunnen nu alle drie de records in één commando opvragen door de volledige partitie van die klant te bevragen:

export const getAllCustomerRecords = async (id: string) =>
  dynamoClient
    .query({
      TableName: TABLE_NAME,
      KeyConditionExpression: "pk=:pk",
      ExpressionAttributeValues: {
        ":pk": valueToAttributeValue(addPrefix(id, CUSTOMER_PREFIX)),
      },
    })
    .then((result) => result.Items);

Naarmate de tabel groeit, is dit soort query mogelijk niet het meest efficiënt, omdat hij elke bestelling die de klant ooit heeft geplaatst retourneert, samen met alle andere klantrecords, zoals manden en facturen. Maar het is goed om te weten dat we alle klantdata in één query kunnen openen als dat nodig is.

Een kleine aanpassing aan de expression attribute values om een gedeeltelijke match (begins_with) op de sort key op te nemen, stelt ons in staat te bevragen:

  • Alle klant-mandrecords
  • Alle klant-bestelrecords

De belangrijkste les is hoe we één-op-één- en één-op-veel-relaties in NoSQL kunnen modelleren.

Categorieën

Deze entiteiten vertegenwoordigen opnieuw een één-op-veel-relatie, waarbij één categorie veel producten kan bevatten.

Categorieën hebben ook een hiërarchische relatie waarbij een categorie veel subcategorieën kan hebben, wat betekent dat producten overal in de categoriehiërarchie kunnen bestaan.

Laten we dit modelleren met boeken, onderverdeeld in fictie en non-fictie.

image18.png

Let op hoe we de GSI-attribuutkeys voor de subcategorieën hebben ingevuld, waarbij we GSI1_PK instellen als de bovenliggende categorie-ID en GSI1_SK als de subcategorie-ID. Dit stelt ons in staat om alle subcategorieën per categorie op te vragen via GSI1:

image19.png

Dezelfde index, GSI1, kan niet alleen worden gebruikt voor het in kaart brengen van één-op-veel-relaties. Stel dat we de klant op e-mailadres willen opzoeken; het huidige model ondersteunt dit niet direct, tenzij we een kleine wijziging aanbrengen in hoe we de data opslaan:

image1.png

Door het e-mailadres van de klant en het entiteitstype op te slaan als GSI PK en SK:

  • Waarborgen we dat het e-mailadres van de klant uniek is (alleen voor deze GSI; om uniciteit op de primaire index af te dwingen is een andere oplossing nodig)
  • Maken we het mogelijk dat klanten bijvoorbeeld ook als verkopers voorkomen (als we dat als entiteitstype zouden hebben)
  • Kunnen we een klant op e-mailadres opzoeken in plaats van op ID

Om de datadistributie in DynamoDB te ondersteunen, zouden we nog een stap verder kunnen gaan door het e-mailadres als hash op te slaan.

export const getCustomerByEmail = async (email: string) =>
  dynamoClient
    .query({
      TableName: TABLE_NAME,
      IndexName: "gsi1",
      KeyConditionExpression: "gsi1_pk = :email AND gsi1_sk = :entityType",
      ExpressionAttributeValues: {
        ":email": valueToAttributeValue(email),
        ":entityType": valueToAttributeValue(entityType),
      },
    })
    .then(({ Items }) => Items?.[0]);

Tot nu toe vroeg je je misschien af waarom alle indexen generieke namen hadden met generiek genaamde sleutelattributen, gewoon PK en SK. Het antwoord zou nu duidelijk moeten zijn: de data die we opslaan en de indexen die we bijhouden zijn flexibel en kunnen meerdere toegangspatronen ondersteunen, afhankelijk van het entiteitstype.

De Terraform-configuratie voor de tabel die we tot nu toe gebruiken, is als volgt:

resource "aws_dynamodb_table" "tutorial-1" {
  name = "tutorial-1"
  billing_mode = "PAY_PER_REQUEST"
  hash_key = "pk"
  range_key = "sk"


  attribute {
    name = "pk"
    type = "S"
  }


  attribute {
    name = "sk"
    type = "S"
  }


  attribute {
    name = "gsi1_pk"
    type = "S"
  }


  attribute {
    name = "gsi1_sk"
    type = "S"
  }


  attribute {
    name = "gsi2_pk"
    type = "S"
  }


  attribute {
    name = "gsi2_sk"
    type = "S"
  }


  global_secondary_index {
    name = "gsi1"
    hash_key = "gsi1_pk"
    range_key = "gsi1_sk"
    projection_type = "ALL"
  }


  global_secondary_index {
    name = "gsi2"
    hash_key = "gsi2_pk"
    range_key = "gsi2_sk"
    projection_type = "ALL"
  }
}

Producten

Het opslaan van producten volgt hetzelfde één-op-veel-patroon, waarbij de partition key de categorie-ID is en de sort key de product-ID:

image3.png

Door de bovenliggende categorie toe te voegen aan GSI1 kunnen we alle boeken in onze database opvragen:

image17.png

In een productieapplicatie kunnen we DynamoDB Streams gebruiken om data naar een Elasticsearch-cluster te pushen, zodat we op boektitel kunnen zoeken. Maar hoe zouden we met ons bestaande model op producttitel kunnen bevragen?

We hebben GSI1 al gebruikt, dus we zouden hiervoor GSI2 kunnen gebruiken:

image2.png

Onthoud dat je bij het bevragen van DynamoDB de volledige, exacte partition key moet opgeven, dus hoewel deze implementatie misschien niet de meest optimale is, illustreert ze wel hoe drie indexen voor verschillende toegangspatronen kunnen worden gebruikt.

In mijn volgende tutorial laat ik zien hoe je dit kunt oplossen door te integreren met een zoekindex.

Mandinhoud en bestelregels

Zowel manditems als bestelregels worden opgeslagen in de klantpartitie; we kunnen de GSI’s gebruiken om alle producten in een mand, alle producten in een bestelling en alle producten die een klant ooit heeft besteld op te vragen.

image13.png

image4.png

image16.png

Dit laatste voorbeeld toont de bestelgeschiedenis van een klant met twee producten afkomstig uit twee verschillende bestel-ID’s.

Voor wie geïnteresseerd is in hoe datamodellering werkt in SQL-databases: ons webinar Data Modeling in SQL biedt waardevolle inzichten die je begrip van NoSQL-datamodellering kunnen aanvullen.

Uitdagingen bij single-table-ontwerp

Werken met een single-table-ontwerp in DynamoDB biedt veel voordelen, maar brengt ook de nodige uitdagingen en aandachtspunten met zich mee:

Complexiteit en applicatielogica

Al je data in één tabel beheren kan complex worden, zeker naarmate je applicatie groeit en evolueert.

Je moet je data zorgvuldig plannen en structureren om verschillende querypatronen te accommoderen. Daarnaast is de effectiviteit van een single-table-ontwerp nauw verweven met je applicatielogica; die speelt een cruciale rol in soepele data-toegang en efficiënte queryverwerking.

Een zorgvuldige afweging van zowel datastructuur als applicatielogica is essentieel om een single-table-ontwerp effectief te laten werken.

Index-overhead

Hoewel DynamoDB Global Secondary Indexes biedt om verschillende querypatronen te ondersteunen, moet je deze indexen inrichten en onderhouden, wat kosten en complexiteit kan verhogen.

Keuze van partition key

Het kiezen van de juiste partition key is cruciaal voor een gelijkmatige datadistributie en queryprestaties. Een ongeschikte partition key kan leiden tot hot partitions en prestatieknelpunten.

Grootte en kosten

Naarmate je datavolume toeneemt, kan je enkele tabel flink in omvang groeien. Dit kan zowel de opslagkosten als de kosten voor lezen en schrijven beïnvloeden.

Geen full-text search

DynamoDB ondersteunt niet native full-text search, dus voor dergelijke functionaliteit moet je integreren met andere services zoals Elasticsearch.

Schemawijzigingen

Wijzigingen aan je tabelschema doorvoeren kan lastig zijn, zeker in productieomgevingen. Je moet mogelijk bestaande data migreren of je applicatielogica aanpassen om schemawijzigingen op te vangen.

Consistentie-overwegingen

Afhankelijk van je keuze voor leesconsistentie (strong of eventual) moet je rekening houden met mogelijke dataconsistentie-uitdagingen, met in het achterhoofd dat GSI’s asynchroon worden gerepliceerd.

Beperkte queryflexibiliteit

De querymogelijkheden van DynamoDB zijn krachtig, maar niet zo flexibel als SQL-queries.

Wil je beter begrijpen hoe je effectief databases ontwerpt en met bijbehorende uitdagingen omgaat? Bekijk dan onze cursus Database Design.

Conclusie

Als je met een NoSQL-database werkt, is het cruciaal om los te komen van de bekende wereld van relationele ontwerppatronen; je moet afleren wat je hebt aangeleerd.

Kiezen voor het single-table-ontwerppatroon biedt de belofte van een wereldwijd schaalbare database die grote hoeveelheden verkeer aankan met betrouwbare en voorspelbare prestaties.

Die prestatiewinst is echter niet gratis; het vraagt om nauwgezette planning en doordachte implementatie van je databasestructuur en applicatielogica, al in een vroeg stadium.

Verdiep je kennis met onze cursus NoSQL-concepten!

Gary Alway

Auteur

Gary Alway

Ik ben een full‑stack software engineer en solutions architect met een passie voor leren en data!

Onderwerpen

Gerelateerd