Vai al contenuto principale

Prestazioni e scalabilità senza limiti: padroneggiare il design di database a tabella singola con DynamoDB

Una tabella per domarle tutte: semplifica, scala e potenzia il tuo database NoSQL!
· 12 min leggi

Scopri con l'IA

Benvenuto nel mondo di DynamoDB, dove efficienza e scalabilità illimitata convergono in un'unica tabella. In questo tutorial completo, intraprenderemo un percorso stimolante per svelare il vero potenziale del design di database NoSQL a tabella singola.

Scopri l'arte di semplificare modelli di dati complessi sfruttando tutta la potenza di una struttura unificata, in grado di accogliere senza sforzo le tue esigenze in continua crescita.

Che tu sia uno sviluppatore esperto di database NoSQL o che tu stia appena iniziando il tuo percorso nel mondo dei database, preparati a sbloccare un nuovo livello di scalabilità per le tue applicazioni con una potente architettura dati!

Che cos'è il NoSQL?

Prima di iniziare, è importante capire la differenza tra NoSQL e database relazionali, in quali scenari useremmo ciascuno e perché queste tecnologie sono state inventate. Questo richiede una breve occhiata alla storia dell'elaborazione dei dati.

Puoi leggere di più sull'importanza dei database NoSQL nella data science in un post separato.

Database relazionali

Il database relazionale è una tecnologia ben nota e ampiamente utilizzata sin dagli anni '70. Due dei principali problemi che il database relazionale risolve sono l'archiviazione e l'integrità referenziale.

I dati sono "normalizzati", tipicamente alla terza forma normale, il che riduce l'impronta dei dati e garantisce che eventuali aggiornamenti restino coerenti con il resto dei dati.

La dimensione dell'impronta dei dati persistenti era cruciale perché, storicamente, il componente più costoso nel data center era il disco rigido. Oggi non è più così: la CPU è diventata la componente più costosa e l'archiviazione dei dati si è drasticamente ridotta di costo. Questo non significa che il database relazionale sia obsoleto; ha applicazioni valide per:

  • OLAP
  • Data warehouse
  • Query ad hoc
  • Applicazioni in cui i pattern di accesso non sono ben definiti
  • CMS / headless CMS

Database NoSQL

La pressione sui dati è la capacità di un sistema di elaborare una determinata quantità di dati a costi e/o tempi ragionevoli.

I database NoSQL sono stati inventati alla fine degli anni '90 per affrontare i problemi di pressione dei dati che influivano sulle prestazioni dei database relazionali a causa dei volumi sempre crescenti di dati applicativi.

La CPU in un database relazionale può essere considerata uno dei principali colli di bottiglia, poiché il server relazionale si affanna a recuperare e ricomporre dati normalizzati nelle viste denormalizzate che le applicazioni in genere consumano.

Questo può essere alleviato aggiungendo server replica di sola lettura, ma NoSQL adotta un approccio diverso e memorizza i dati applicativi in forma denormalizzata, pronta per essere consumata dalle applicazioni. Ciò significa che l'impronta dei dati è maggiore rispetto a un database relazionale, ma alleggerisce il carico sulla CPU, rendendo di fatto il server del database un semplice router che usa un algoritmo di hash per puntare a una posizione su un disco nel data center. Gli utilizzi tipici dei database NoSQL sono:

  • OLTP
  • Applicazioni con pattern di accesso ben noti
  • Applicazioni che devono scalare orizzontalmente per supportare grandi volumi di domanda a livello regionale o globale

Numerosi studi ci hanno fornito abbastanza dati oggettivi sull'argomento per presentare un grafico che rappresenta le prestazioni di SQL vs NoSQL su larga scala.

grafico di confronto delle operazioni di massa

Fonte dell'immagine

L'ultimo punto da chiarire è che i dati archiviati in un database NoSQL sono comunque dati relazionali. Se i dati non fossero relazionali, li archivieremmo in un semplice filestore.

Antipattern NoSQL

Non modelliamo un database NoSQL nello stesso modo in cui modelliamo un database relazionale, quindi è un antipattern tradurre un design normalizzato direttamente in un database NoSQL usando più tabelle collegate tra loro, perché NoSQL non ha l'operatore di join né l'integrità referenziale.

Una tabella in NoSQL è l'equivalente di un catalogo in un database relazionale. Invece, modella i dati per essere memorizzati in forma denormalizzata, pronti per il consumo da parte delle applicazioni.

Un altro antipattern sono le Hot Key. Significa che la maggior parte, se non tutte, le richieste di accesso colpiscono un singolo nodo di archiviazione, il che implica che lo spazio delle chiavi non è stato progettato correttamente.

La heatmap degli accessi apparirà più o meno così:

image14.png

DynamoDB

AWS DynamoDB è un archivio key-value a colonne larghe, completamente gestito e serverless, il che significa che supporta molti item, o righe, in una tabella, ma questi item non hanno necessariamente gli stessi attributi. I tipi di dato supportati sono i seguenti:

  • Tipi scalari: stringa, numero, binario, booleano, null
  • Tipi documento: struttura complessa con attributi annidati (cioè JSON)
  • Tipi set: set di stringhe, set di numeri, set binari

Come detto in precedenza, NoSQL andrebbe usato quando i pattern di accesso dell'applicazione sono ben noti e c'è bisogno di supportare un elevato numero di TPS. DynamoDB può scalare per qualsiasi carico di lavoro e garantisce tempi di risposta prevedibilmente rapidi e costanti fino a 4 milioni di TPS con bassa latenza (10 - 20 ms).

Come funziona DynamoDB?

Ogni item (o riga) ha una chiave di partizione che lo identifica in modo univoco e determina la distribuzione dei dati sull'archiviazione sottostante.

Gli item possono avere facoltativamente una chiave di ordinamento, che determina l'ordine in cui i dati sono memorizzati su disco all'interno di una partizione. Le chiavi di ordinamento ci consentono di interrogare le partizioni con operazioni complesse di range e filtro (anche se i filtri sono applicati a livello di client, non di database).

La chiave di partizione viene usata per creare un indice hash e poi ogni item viene disposto secondo questo hash su uno spazio chiavi virtuale. Questo spazio chiavi virtuale è diviso in segmenti che mappano su dispositivi di archiviazione fisici, che possono crescere e ridursi dinamicamente a seconda del volume di dati memorizzati.

Ecco perché DynamoDB è veloce e coerente a qualsiasi scala: perché il motore del database agisce come un servizio di instradamento dalle chiavi con hash allo storage fisico.

Data modeling con NoSQL

In questo tutorial modelleremo un negozio online chiamato Daintree.com.

(Curiosità: la foresta pluviale di Daintree, nel Queensland, Australia, è una delle più antiche al mondo, con un'età stimata di circa 180 milioni di anni!)

Per prima cosa, creiamo un diagramma ERD (entity relationship diagram) molto semplificato per visualizzare ciò che andremo a modellare.

diagramma delle relazioni tra entità (ERD)

Da questo, possiamo dedurre che un cliente può creare un carrello e aggiungere più prodotti con quantità variabili.

Un prodotto ha una categoria, che a sua volta può avere categorie padre.

I carrelli possono diventare ordini che possono essere suddivisi in più pagamenti.

Come già detto, questa è una visione altamente semplificata delle relazioni e degli attributi delle entità che compongono un'applicazione di shopping online, ma è sufficiente allo scopo di questo tutorial.

Definire i pattern di accesso

Non dobbiamo preoccuparci di questo passaggio quando implementiamo un database relazionale. SQL è un linguaggio incredibilmente potente che, a patto che lo schema sia stato progettato correttamente, ci consente la flessibilità di interrogare e manipolare i dati come meglio crediamo. Non è così quando si usa il pattern a tabella singola in NoSQL, perché dobbiamo archiviare i dati in forma denormalizzata e pronti per essere consumati direttamente dalla nostra applicazione.

Ecco i pattern di accesso che potremmo voler supportare per la nostra applicazione di shopping online:

  • Interrogare tutti i prodotti per categoria
  • Interrogare le sottocategorie di una categoria padre
  • Interrogare tutti gli ordini per cliente
  • Interrogare tutti i pagamenti per un ordine
  • Interrogare un ordine specifico per un cliente
  • Interrogare tutti gli articoli del carrello per il carrello di un cliente
  • Interrogare tutti i prodotti in base alla cronologia ordini di un cliente

Modellare la tabella

NoSQL workbench è uno strumento gratuito fornito da AWS per modellare tabelle NoSQL per DynamoDB.

Vai al Data modeler e crea un nuovo modello di dati con una nuova tabella:

image5.png

Terremo gli attributi della chiave primaria come stringhe generiche PK e SK:

image8.png

Per gli attributi aggiuntivi, memorizzeremo l'entity_type insieme a due attributi di chiave primaria per le GSI PK e SK.

Una GSI è un'indice secondario globale, cioè una copia della tua tabella che DynamoDB mantiene sincronizzata (in modo asincrono!!) ma archiviata usando coppie alternative di attributi di chiave primaria. Questo è il meccanismo chiave con cui una tabella singola può supportare molteplici pattern di accesso. Ne parleremo più avanti; per ora, ti basta capire che queste chiavi memorizzeranno le chiavi primarie delle entità correlate.

Gli altri attributi aggiuntivi da aggiungere sono tutti gli attributi non chiave dei nostri dati (nota che questo passaggio è necessario solo in fase di progettazione e non verrà definito esplicitamente quando effettivamente approvvigioneremo la tabella):

image11.png

Successivamente, aggiungiamo le due GSI utilizzando le chiavi definite nel passaggio precedente:

image9.png

Clicca su salva e poi su visualizza il modello di dati. Ora dovresti vedere la tabella appena creata ma vuota. Clicca su modifica tabella, quindi in alto a destra su modifica dati; questo ti permetterà di aggiungere nuove righe:

image7.png

Prefissi dei dati

Prima di iniziare ad aggiungere dati, dobbiamo definire i prefissi della chiave primaria per ogni entità:

prodotto

p#

categoria

c#

cliente (utente)

u#

carrello

b#

articolo del carrello

bi#

ordine

o#

riga d'ordine

ol#

pagamento

py#

fattura

i#

Modellare i dati

Dati del cliente

La maggior parte dei pattern di accesso ruota attorno al cliente, quindi per il nostro indice primario useremo l'ID cliente come partizione principale, con il record effettivo del cliente identificabile grazie alla chiave di ordinamento che è anch'essa l'ID cliente.

Possiamo poi aggiungere facilmente le relazioni cliente-ordini e cliente-carrelli usando la stessa chiave di partizione ma con una chiave di ordinamento univoca.

La nostra vista aggregata ora appare così:

image12.png

Ora possiamo interrogare tutti e tre i record con un unico comando, interrogando l'intera partizione di quel cliente:

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

Man mano che la tabella cresce, questo tipo di query potrebbe non essere il più efficiente, dato che restituirà ogni ordine del cliente mai effettuato insieme a tutti gli altri record del cliente, come carrelli e fatture, ma è utile sapere che possiamo accedere a tutti i dati di un cliente con una singola query se necessario.

Un piccolo aggiustamento ai valori dell'espressione degli attributi per includere una corrispondenza parziale (begins_with) sulla chiave di ordinamento ci consente di interrogare:

  • Tutti i record dei carrelli del cliente
  • Tutti i record degli ordini del cliente

Il punto chiave è come possiamo modellare relazioni uno-a-uno e uno-a-molti in NoSQL.

Categorie

Queste entità rappresentano un'altra relazione uno-a-molti in cui una categoria può contenere molti prodotti.

Le categorie hanno anche una relazione gerarchica in cui una categoria può avere molte sottocategorie, il che significa che i prodotti possono esistere ovunque nell'albero gerarchico delle categorie.

Modelliamo questo con i libri suddivisi in narrativa e saggistica.

image18.png

Nota come abbiamo popolato gli attributi chiave della GSI per le sottocategorie, impostando GSI1_PK come l'ID della categoria padre e GSI1_SK come l'ID della sottocategoria. Questo ci permette di interrogare tutte le sottocategorie per categoria usando GSI1:

image19.png

Lo stesso indice, GSI1, può essere usato non solo per mappare relazioni uno-a-molti. Poniamo per esempio di voler cercare il cliente per email; l'attuale modello non lo supporta direttamente a meno che non apportiamo una piccola modifica al modo in cui salviamo i dati:

image1.png

Memorizzare l'indirizzo email del cliente e il tipo di entità come GSI PK e SK;

  • Garantisce che l'indirizzo email del cliente sia univoco (solo per questa GSI; imporre l'unicità sull'indice primario richiederebbe un'altra soluzione)
  • Consente, ad esempio, che i clienti compaiano anche come venditori (se avessimo quel tipo di entità)
  • Ci permette di cercare un cliente per email invece che per ID

Per aiutare la distribuzione dei dati in DynamoDB, potremmo fare un ulteriore passo e memorizzare l'indirizzo email come hash.

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

Fino ad ora, potresti esserti chiesto perché tutti gli indici avessero nomi generici con attributi chiave genericamente chiamati semplicemente PK e SK. Ora la risposta dovrebbe essere chiara: i dati che memorizziamo e gli indici che manteniamo sono flessibili e in grado di supportare molteplici pattern di accesso a seconda del tipo di entità.

Il terraform per la tabella che abbiamo usato finora è il seguente:

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

Prodotti

La memorizzazione dei prodotti segue lo stesso pattern uno-a-molti, dove la chiave di partizione è l'ID della categoria e la chiave di ordinamento è l'ID del prodotto:

image3.png

Aggiungere la categoria padre alla GSI1 ci consente di interrogare tutti i libri nel nostro database:

image17.png

In un'applicazione in produzione, possiamo usare gli stream di DynamoDB per inviare i dati a un cluster Elasticsearch, ottenendo così la possibilità di cercare per titolo del libro; ma come potremmo usare il modello esistente per interrogare per titolo del prodotto?

Abbiamo già usato GSI1, quindi potremmo usare GSI2 per questo:

image2.png

Ricorda che, quando interroghi DynamoDB, devi fornire l'intera chiave di partizione esatta, quindi, sebbene questa implementazione possa non essere la più ottimale, illustra come tre indici possano essere usati per diversi pattern di accesso.

Nel mio prossimo tutorial, mostrerò come risolvere questo aspetto integrandolo con un indice di ricerca.

Contenuti del carrello e righe d'ordine

Sia gli articoli del carrello sia le righe d'ordine sono memorizzati nella partizione del cliente; possiamo usare le GSI per consentirci di interrogare tutti i prodotti in un carrello, tutti i prodotti in un ordine e tutti i prodotti che un cliente ha mai ordinato.

image13.png

image4.png

image16.png

Questo ultimo esempio mostra la cronologia ordini di un cliente con due prodotti effettuati da due diversi ID ordine.

Per chi è interessato a come funziona il data modeling nei database SQL, il nostro webinar Data Modeling in SQL offre spunti preziosi che possono completare la comprensione del data modeling NoSQL.

Sfide associate al design a tabella singola

Usare un design a tabella singola con DynamoDB offre molti vantaggi, ma presenta anche diverse sfide e considerazioni:

Complessità e logica applicativa

Gestire tutti i tuoi dati in un'unica tabella può diventare complesso, soprattutto man mano che l'applicazione cresce ed evolve.

Devi pianificare e strutturare con attenzione i dati per accogliere vari pattern di query. Inoltre, l'efficacia di un design a tabella singola è strettamente legata alla logica della tua applicazione; essa svolge un ruolo fondamentale nel garantire un accesso fluido ai dati e un'elaborazione efficiente delle query.

Una considerazione attenta sia della struttura dei dati sia della logica applicativa è essenziale affinché un design a tabella singola funzioni in modo efficace.

Overhead degli indici

Sebbene DynamoDB consenta indici secondari globali per supportare diversi pattern di query, devi predisporre e mantenere questi indici, il che può aumentare costi e complessità.

Scelta della chiave di partizione

Scegliere la giusta chiave di partizione è fondamentale per una distribuzione uniforme dei dati e per le prestazioni delle query. Una chiave di partizione inadatta può portare a partizioni calde e colli di bottiglia prestazionali.

Dimensioni e costi

All'aumentare del volume dei dati, la tua tabella singola può crescere significativamente in dimensioni. Questo può influire sia sul costo di archiviazione sia sul costo di lettura e scrittura dei dati.

Assenza di ricerca full-text

DynamoDB non supporta nativamente la ricerca full-text, quindi implementare tale funzionalità richiede l'integrazione con altri servizi come Elasticsearch.

Modifiche allo schema

Apportare modifiche allo schema della tabella può essere impegnativo, soprattutto nei sistemi in produzione. Potresti dover migrare i dati esistenti o modificare la logica applicativa per accogliere i cambiamenti di schema.

Considerazioni sulla consistenza

A seconda della scelta della consistenza di lettura (forte o eventuale), potresti dover tenere conto di potenziali sfide di consistenza dei dati, tenendo presente che le GSI sono replicate in modo asincrono.

Flessibilità di query limitata

Le capacità di query di DynamoDB sono potenti ma non flessibili come le query SQL.

Per comprendere meglio come progettare efficacemente i database e affrontare le sfide associate, dai un'occhiata al nostro corso di Database Design.

Conclusione

Quando lavori con un database NoSQL, è fondamentale allontanarsi dal territorio familiare dei pattern di progettazione dei database relazionali; devi disimparare ciò che hai imparato.

Scegliere il pattern a tabella singola offre la promessa di un database scalabile a livello globale, pronto a gestire grandi volumi di traffico con prestazioni affidabili e prevedibili.

Tuttavia, questo vantaggio prestazionale non è gratuito; richiede una pianificazione meticolosa e un'attenta implementazione della struttura del database e della logica applicativa fin dall'inizio.

Approfondisci le tue conoscenze con il nostro corso sui concetti NoSQL!

Gary Alway

Autore

Gary Alway

Sono un Full Stack Software Engineer e Solutions Architect con una passione per l'apprendimento e i dati!

Argomenti

Correlato