Database Non Relazionali NoSQL - ACID vs BASE, CAP, 4 Modelli | Programmazione Anno 4 Modulo 2 Lezione 12 | Prof. Giagnotti
Programmazione • Anno 5 • Modulo 2 • Lezione 6
Docente: Prof. Giagnotti

Database Non Relazionali NoSQL: Modelli, Teoremi e Scelte Architetturali

Da ACID a BASE, dal singolo server al cluster planetario: quando lasciare il relazionale e perché

⏱ 65 min 📘 NoSQL / Big Data 🧩 Modelli Document, Key-Value, Column, Graph 🔧 Polyglot Persistence
🎯 Obiettivi di apprendimento
  • Comprendere i limiti del modello relazionale nel contesto Big Data e i motivi dell'emergere dei database NoSQL
  • Distinguere le proprietà ACID (RDBMS) dalle proprietà BASE (NoSQL) e capirne il trade-off architetturale
  • Riconoscere le quattro categorie NoSQL (Document, Key-Value, Column-family, Graph) con esempi reali e casi d'uso
  • Scegliere consapevolmente tra SQL e NoSQL in base ai requisiti di consistenza, scalabilità e pattern di accesso

1. Perché il modello relazionale non basta più?

I database relazionali sono nati negli anni '70 per gestire dati strutturati: banche, ERP, prenotazioni aeree. Hanno dominato per 40 anni. Poi è arrivato il Web 2.0, Instagram con miliardi di foto, sensori IoT che sparano 10.000 misure al secondo, log di click infiniti. E lì il modello relazionale ha iniziato a scricchiolare.

Io lo spiego sempre così: in un RDBMS tutto deve stare in tabelle rettangolari, con schema rigido. Se domani un ordine e-commerce deve contenere un array di coupon, un oggetto regalo con messaggio e tre timestamp diversi, con SQL devi fare ALTER TABLE e aggiungere tabelle ponte. Con un documento JSON, aggiungi un campo.

📘 Definizione chiave — NoSQL

NoSQL = Not Only SQL. Non significa "contro SQL". Significa: esistono scenari dove il modello tabellare non è ottimale. Il termine esplode nel 2009 in un meetup a San Francisco organizzato da Johan Oskarsson per discutere di DB distribuiti open source.

I quattro limiti strutturali che ho visto io in produzione:

  • Schema rigido: ALTER TABLE su una tabella da 2 miliardi di righe in MySQL può bloccare il DB per ore. In NoSQL lo schema è flessibile.
  • JOIN costosi: su dataset enormi, un JOIN tra 3 tabelle da 100M di righe richiede shuffle e I/O ingestibili. NoSQL denormalizza per evitare il JOIN.
  • Scalabilità verticale vs orizzontale: un RDBMS scala facendo più potente un singolo server (scale-up) ma arrivi a un tetto fisico ed economico. NoSQL è nato per lo scale-out: aggiungi nodi commodity.
  • Impedance mismatch: nel codice avete oggetti con array annidati. In SQL dovete appiattirli. Con Document Store, salvate l'oggetto quasi identico a come lo avete in memoria.
🎯 Tip del docente — Perché l'ho strutturata così

Io ho pensato di partire dai limiti del relazionale perché altrimenti sembra che NoSQL sia "figo" per moda. Io lo farei così: prima vi faccio soffrire con un esercizio di ALTER TABLE su 50 milioni di righe simulate, poi vi mostro come MongoDB aggiunge un campo senza battere ciglio. Così capite il perché, non solo il come.

2. ACID vs BASE: due filosofie a confronto

I RDBMS garantiscono ACID. I NoSQL sacrificano un po' di consistenza immediata per ottenere disponibilità e velocità, e adottano BASE. Non è meglio o peggio: è un trade-off.

ProprietàACID — RDBMS tradizionaleBASE — NoSQL distribuito
A / BAAtomicità: tutto o niente, rollback completo se una parte fallisceBasically Available: il sistema risponde SEMPRE, anche con dati non aggiornati
C / SConsistenza: da stato valido a stato valido, vincoli mai violatiSoft State: lo stato può cambiare anche senza input, per propagazione replica
I / EIsolamento: transazioni concorrenti come se fossero serialiEventually Consistent: col tempo tutti i nodi convergono allo stesso valore
DDurabilità: dopo il commit, sopravvive anche al crash (WAL)Durabilità tunable: W=1 veloce ma rischioso, W=majority più sicuro
🔧 Esempio concreto — Io lo farei capire così

Aggiorni lo stato su Facebook. Un amico in Australia per 2-3 secondi vede ancora il vecchio stato. È acceptable. Quello è eventually consistent. Se invece fai un bonifico da 10.000€, non è acceptable che per 3 secondi il saldo sia sballato. Lì serve ACID puro. Io ho pensato di farvi scegliere voi: per quale feature accettereste BASE e per quale no?

Concetto // ACID vs BASE nella pratica
// RDBMS - Transazione bancaria classica ACID BEGIN TRANSACTION; UPDATE conto SET saldo = saldo - 100 WHERE id = 42; UPDATE conto SET saldo = saldo + 100 WHERE id = 99; -- Se una delle due fallisce -> ROLLBACK automatico COMMIT; // durabile su WAL// NoSQL - Contatore like stile Redis (BASE) -- SET like:post:123 100 // disponibilità immediata -- Se il nodo master crasha prima della replica? -- Per 1 secondo due nodi possono avere valori diversi -- Ma il sistema non blocca mai la scrittura
⚠️ Errore comune che vedo sempre

Pensare che BASE significhi "perde dati". No. Significa che per un breve intervallo (millisecondi/secondi) nodi diversi possono vedere valori diversi. Alla fine convergono. L'errore all'esame è dire "NoSQL non è affidabile". Io lo correggerei così: NoSQL è affidabile, ma con garanzie diverse. Se ti serve consistenza immediata, usi write concern majority o quorum.

3. Il teorema CAP di Brewer

📘 Teorema CAP — Formulazione

Un sistema distribuito può garantire al massimo 2 su 3 tra: Consistency (tutti i nodi vedono gli stessi dati nello stesso momento), Availability (ogni richiesta riceve una risposta non errore), Partition Tolerance (sistema funzionante anche se la rete si spezza).

Poiché le partizioni di rete in un sistema reale sono inevitabili, la scelta è CP vs AP. I RDBMS tradizionali monolitici sono CA: funzionano bene fino a quando c'è una rete perfetta. Ma su cluster planetario, CA non esiste.

ClasseSistemaScelta pratica
CA — Consistenza + DisponibilitàMySQL / PostgreSQL singolo nodoOttimo fino a ~10TB, poi scale-up costoso
CP — Consistenza + PartizionamentoMongoDB (majority write), HBase, ZookeeperSe la rete si parte, alcune letture possono fallire per mantenere consistenza
AP — Disponibilità + PartizionamentoCassandra, CouchDB, DynamoDB, Redis Cluster APSempre disponibile, ma letture possono dare valori vecchi
🎯 Nota di metodo — Come l'ho insegnata io

Io ho pensato di farvi disegnare il triangolo CAP alla lavagna. Io lo farei così: prendete un e-commerce. Durante il Black Friday la rete tra datacenter EU e US si spezza. Vuoi che un utente possa ancora aggiungere al carrello (AP) anche se il magazzino potrebbe essere oversellato per 5 sec, o vuoi bloccare tutto (CP) per non vendere ciò che non hai? Non c'è risposta giusta universale, c'è contesto.

4. Le 4 categorie NoSQL: panoramica

Non esiste "un NoSQL". Esistono quattro famiglie completamente diverse. Scegliere la famiglia sbagliata è peggio che restare su SQL.

ModelloEsempiPunto di forzaQuando evitarlo
Document StoreMongoDB, CouchDB, FirestoreSchema flessibile, embedding, query JSONTransazioni complesse multi-document
Key-ValueRedis, DynamoDB, MemcachedO(1) in RAM, TTL, contatoriQuery su campi del valore
Column-FamilyCassandra, HBase, ScyllaDBScritture massive, petabyte, time-seriesRicerca ad-hoc, JOIN
GraphNeo4j, Neptune, ArangoDBTraverse relazioni profonde velociAggregazioni tabellari massive

5. Document Store — MongoDB e JSON annidato

Ogni record è un documento BSON (JSON binario) con struttura libera. I documenti stanno in collezioni. Ogni documento può avere campi diversi. È il modello più vicino a come programmate in JavaScript/PHP con array associativi.

JSON / BSON — Documento cliente con ordini embedded
{ "_id": ObjectId("64a1f2c9e4b0d123456789"), "nome": "Mario", "cognome": "Rossi", "email": "mario@example.com", "indirizzo": { "citta": "Milano", "cap": "20100", "coordinate": [45.4642, 9.1900] }, "ordini": [ // <-- embedding: evito JOIN! { "id": 101, "prodotto": "Laptop", "prezzo": 999, "data": "2026-01-12" }, { "id": 102, "prodotto": "Mouse", "prezzo": 29, "tags": ["wireless","ergonomico"] } ], "premium": true }// Query MongoDB — molto più vicina al codice db.clienti.find({ "indirizzo.citta": "Milano", premium: true }) .sort({ "ordini.prezzo": -1 }) .limit(10)
✅ Quando usarlo — Caso reale

E-commerce prodotto con attributi variabili: una scarpa ha taglia/colore, un laptop ha RAM/CPU/GPU, un libro ha ISBN/autore. Con SQL fareste EAV (entity-attribute-value) infernale. Con MongoDB ogni prodotto ha campi diversi, naturale. Io lo farei così nel vostro progetto di 4a: catalogo in MongoDB, ordini transazionali in PostgreSQL.

Embedding vs Referencing — scelta critica

StrategiaCome funzionaProContro
EmbeddingDati correlati dentro lo stesso documentoLettura O(1), niente JOIN, atomicoDuplicazione, documento >16MB problematico
ReferencingID di altro documento + $lookupNiente duplicati, normalizzazioneDue query, simile a JOIN, più lento
⚠️ Errore comune — Embedding selvaggio

Vedo spesso studenti che embeddano tutto. Se un utente ha 10.000 ordini, il documento esplode e superare i 16MB di limite BSON blocca tutto. Io ho pensato di darvi questa regola pratica che uso io: embedding se rapporto 1-pochi e dati letti sempre insieme; referencing se 1-molti o molti-molti. Ordini di un cliente con < 100? Embed. Post di un utente in un social? Reference.

6. Key-Value Store — Redis e l'accesso O(1)

Il modello più semplice che esista: chiave → valore. Redis tiene tutto in RAM, quindi legge e scrive in microsecondi. Il valore può essere stringa, lista, set, hash, sorted set.

REDIS CLI — comandi che uso ogni giorno
# Cache sessione utente con scadenza automatica (TTL) SET sessione:user42 '{"nome":"Mario","ruolo":"admin"}' EX 3600 GET sessione:user42 # -> '{"nome":"Mario","ruolo":"admin"}'# Contatori in tempo reale O(1) - niente SELECT COUNT(*) INCR contatore:visite:pagina:home INCRBY like:post:123 1 GET like:post:123# Code di lavoro per microservizi LPUSH coda:email "mario@example.com" BRPOP coda:email 0 -- blocking pop, worker pronto# Classifica real-time con sorted set ZADD leaderboard 1200 "player:42" ZREVRANGE leaderboard 0 9 WITHSCORES -- top 10# TTL automatico elimina cache vecchia TTL sessione:user42
🎯 Tip del docente — Io lo farei così

Io ho pensato di farvi iniziare da Redis perché è il più gratificante: installi, lanci due comandi e vedi O(1). Io lo farei così nei vostri progetti: MySQL per i dati veri, Redis come cache davanti. Prima query costosa su MySQL → salvo risultato in Redis con SET ... EX 300 → le prossime 299 letture sono in RAM. Risparmiate il 90% di CPU sul DB. È il pattern che vedrete in ogni azienda.

Casi d'uso che non si possono fare efficientemente in SQL:

  • Cache applicativa: query SQL da 200ms diventa risposta Redis da 0.5ms
  • Sessioni: EX 3600 fa scadere automaticamente la sessione, niente cron
  • Pub/Sub real-time: notifiche tra microservizi senza Kafka per casi semplici
  • Rate limiting: INCR + EXPIRE per limitare 100 richieste/min per IP

7. Column-Family — Cassandra e la scrittura massiva

I dati sono organizzati per colonne, non per righe. Ogni riga ha una chiave e famiglie di colonne con numero arbitrario di colonne. Nasce per scrivere moltissimo e leggere poche colonne su petabyte. Architettura masterless: tutti i nodi sono uguali, niente single point of failure. Scrittura = commit log + memtable → flush su SSTable immutabile.

📘 Perché è diverso?

In SQL leggi una riga intera anche se ti serve una colonna. In Column-Family leggi solo le colonne che ti servono. Se hai tabella da 500 colonne IoT (temperatura, umidità, pressione...) e vuoi solo temperatura dell'ultimo mese, Cassandra legge solo quella famiglia di colonne.

CQL — Cassandra Query Language
-- Creazione famiglia colonne per metriche IoT CREATE TABLE iot_metrics ( sensore_id uuid, giorno date, timestamp timeuuid, temperatura double, umidita double, PRIMARY KEY ((sensore_id, giorno), timestamp) ) WITH CLUSTERING ORDER BY (timestamp DESC);-- Scrittura massiva - 1M writes/sec possibili su cluster INSERT INTO iot_metrics (sensore_id, giorno, timestamp, temperatura) VALUES (uuid(), '2026-05-12', now(), 23.5);-- Lettura efficiente solo per colonna e giorno SELECT temperatura FROM iot_metrics WHERE sensore_id = ? AND giorno = '2026-05-12' ORDER BY timestamp DESC LIMIT 100;-- TTL per dato che scade automaticamente dopo 30gg INSERT INTO iot_metrics (...) VALUES (...) USING TTL 2592000;
🔧 Esempio che uso io in classe

Timeline di Twitter/X: ogni tweet va scritto una volta ma letto milioni di volte da follower diversi. Con RDBMS fareste JOIN timeline enorme. Con Cassandra denormalizzate: scrivete il tweet in una tabella per ogni follower (scrittura amplificata ma lettura O(1)). Io lo farei così per farvi capire: 300M di utenti × scritture veloci ok, lettura timeline in <10ms.

⚠️ Errore comune — Pensare CQL = SQL

CQL assomiglia a SQL ma non lo è. In Cassandra non puoi fare WHERE su colonna non parte della PRIMARY KEY senza ALLOW FILTERING che è un killer. Molti fanno SELECT * WHERE temperatura > 30 senza indice → full scan cluster → timeout. Io ho pensato di farvi sbattere la testa su questo errore una volta sola in lab, così non lo dimenticate più.

8. Graph Database — Neo4j e le relazioni come cittadini di prima classe

Nei graph DB, nodi (entità) e archi (relazioni) hanno proprietà proprie e le relazioni sono fisicamente puntatori memorizzati, non calcolate a runtime con JOIN. Su traverse profonde (amici degli amici di amici) è 100-1000× più veloce di SQL.

CYPHER — Neo4j linguaggio di query
// Creazione nodi e relazioni CREATE (mario:Persona {nome:'Mario', eta:25}) CREATE (luca:Persona {nome:'Luca', eta:27}) CREATE (anna:Persona {nome:'Anna', eta:24}) CREATE (mario)-[:CONOSCE {dal:2020}]->(luca) CREATE (luca)-[:CONOSCE {dal:2019}]->(anna)// Query: amici degli amici di Mario (2 hop) MATCH (mario:Persona {nome:'Mario'})-[:CONOSCE]->(amico)-[:CONOSCE]->(amico2) RETURN amico2.nome// Query reale: "persone che potresti conoscere" con peso MATCH (io:Persona {nome:'Mario'})-[:CONOSCE]->(amico)-[:CONOSCE]->(consigliato) WHERE NOT (io)-[:CONOSCE]->(consigliato) AND consigliato <> io RETURN consigliato.nome, COUNT(amico) AS amici_in_comune ORDER BY amici_in_comune DESC LIMIT 5// Fraud detection: anello di transazioni sospette MATCH path = (a:Conto)-[:TRASFERISCE*3..5]->(a) RETURN path // cerchi chiusi = riciclaggio potenziale

Con SQL equivalente servirebbero 3 JOIN ricorsivi con CTE ricorsiva, esponenzialmente più lenti perché ogni JOIN deve scansionare indici.

🎯 Perché l'ho inserito qui

Io ho pensato di mettere i Graph per ultimi perché sono i meno intuitivi ma i più potenti per certi problemi. Io lo farei così: vi chiedo di modellare su carta una rete di 1M utenti con relazioni CONOSCE. Con SQL avete tabella Persona + tabella Amicizia. Con Neo4j avete nodi + archi fisici. Quando dovete trovare tutti i percorsi di 4 livelli tra due persone, Neo4j è nativo, SQL muore.

9. Quando scegliere SQL vs NoSQL? Tabella decisionale

La domanda non è "qual è meglio?" ma "qual è giusto per questo requisito?". Io ho pensato di darvi questa tabella che uso io in consulenza per decidere al volo.

CriterioScegli SQLScegli NoSQL
Schema dei datiFisso, relazioni chiare, normalizzatoFlessibile, variabile per record, semi-strutturato
ConsistenzaCritica (banca, prenotazioni, fatture)Eventual consistency ok (like, feed, cache)
ScalabilitàVerticale, fino a 10TB, scale-upOrizzontale, da 10TB a PB, scale-out + sharding
QueryJOIN multipli, aggregazioni GROUP BY complesseAccesso per chiave, range scan, traverse grafo
VolumeKB → decine di GB, migliaia di scritture/secGB → petabyte, milioni di scritture/sec
Velocità scritturaModerata (overhead lock ACID)Molto alta (no lock globale, SSTable append)
EsempiERP, gestionale, banca, e-commerce transazionaleSocial, IoT, gaming, real-time analytics
🔧 Mini esercizio decisionale che vi do io

Vi do tre app: A) app per studio medico con fatture e ricette, B) app per monopattini con 50k mezzi che inviano GPS ogni 3 sec, C) LinkedIn clone. Io lo farei così: A = PostgreSQL rigoroso ACID, B = Cassandra + Redis, C = Neo4j per grafo connessioni + MongoDB per profili + Redis per feed. Se scegliete tutto con un solo DB, state forzando.

10. Polyglot Persistence: il mondo reale

In produzione nessuna grande app usa un solo database. Si usa Polyglot Persistence: il DB giusto per ogni tipo di dato nella stessa architettura.

Architettura — Stack reale e-commerce moderno
Frontend -> API Gateway ├── PostgreSQL (transazionale): │ ├── ordini, pagamenti, utenti (ACID critico) │ └── fatture, magazzino (JOIN complessi) ├── MongoDB (catalogo): │ └── prodotti con attributi variabili, recensioni embedded ├── Redis (velocità): │ ├── cache query costose -> SET prodotto:123 EX 600 │ ├── sessioni login -> SET sessione:xyz EX 3600 │ └── carrello temporaneo + rate limiter ├── Elasticsearch (ricerca): │ └── ricerca full-text "scarpe rosse taglia 42" ├── Cassandra (eventi): │ └── log click, visite, metriche IoT magazzino └── Neo4j (raccomandazioni): └── "chi ha comprato X ha comprato Y" via grafoIo lo farei così anche per il vostro progetto finale: SQL per ciò che non può perdere consistenza + Redis per ciò che deve essere velocissimo + Document per ciò che cambia forma spesso.
🎯 Il mio consiglio finale da docente

Io ho pensato di chiudere con questo perché è quello che mi ha insegnato il campo: non innamoratevi di un DB. Io lo farei così: partite sempre da PostgreSQL, che con JSONB ormai fa anche il document-like. Poi aggiungete Redis il primo giorno che sentite lentezza. Poi, solo se avete davvero scale-out o grafi profondi, aggiungete Cassandra o Neo4j. La regola che uso io: aggiungi complessità solo quando il dolore è misurabile, non per hype.

  • Limiti relazionale: schema rigido costoso su Big Data, JOIN inefficienti su miliardi di righe, scale-up con tetto fisico, impedance mismatch tra oggetti e tabelle
  • ACID vs BASE: ACID = Atomicità, Consistenza, Isolamento, Durabilità (correttezza assoluta). BASE = Basically Available, Soft State, Eventually Consistent (disponibilità + scalabilità, consistenza finale)
  • CAP: in un sistema distribuito puoi garantire al massimo 2 tra Consistency, Availability, Partition Tolerance → nella pratica CP (MongoDB majority) vs AP (Cassandra, DynamoDB)
  • Document Store (MongoDB): JSON/BSON annidato, embedding vs referencing, ideale per cataloghi e profili con schema flessibile, query per campo con indice
  • Key-Value (Redis): O(1) in RAM, TTL automatico, contatori con INCR, liste per job queue, sorted set per leaderboard — la cache universale
  • Column-Family (Cassandra): scritture massive append-only su SSTable, masterless, letture per famiglia di colonne, TTL nativo, time-series IoT e timeline social
  • Graph (Neo4j): nodi + archi con proprietà, relazioni come puntatori fisici, Cypher per traverse amici-degli-amici e fraud detection ad anello, 100× più veloce di JOIN ricorsivi su grafi profondi
  • Scelta: SQL quando consistenza immediata e JOIN complessi; NoSQL quando scala orizzontale, schema variabile, velocità di scrittura estrema o relazioni profonde
  • Polyglot persistence: architettura reale = PostgreSQL transazionale + Redis cache + MongoDB catalogo + Elasticsearch ricerca + eventuale Cassandra/Neo4j se serve scale o grafo

📚 Fine Modulo

A questo punto hai imparato come implementare database relazionali e NoSQL in contesti diversi. Ora sei in grado di scegliere il modello più adatto alle tue esigenze di performance, scalabilità e consistenza.