Architettura di dati e sistemi, dalle fondamenta Lezione 17 / 80

Database relazionali: quando SQL è la risposta giusta

Postgres come default. ACID, schemi, join, la piattaforma stabile da vent'anni che fa girare in silenzio la maggior parte dei sistemi transazionali del mondo.

Benvenuto al Modulo 3. Il modulo precedente parlava di cosa cambia nel momento in cui smetti di stare su una macchina sola: le reti falliscono, gli orologi non vanno d’accordo, il consenso costa caro, l’exactly-once delivery è in gran parte uno slogan di marketing che l’idempotenza riesce a salvare. Con quelle fondamenta a posto, possiamo finalmente parlare del data layer con un vocabolario adulto.

Il Modulo 3 copre le grandi famiglie di data storage, un modello per lezione, e le lega insieme nella lezione 24 sotto il titolo della polyglot persistence. Cominciamo da dove la maggior parte dei sistemi di produzione comincia, dove resta, e dove la maggior parte dei team dovrebbe partire di default nel 2026: il database relazionale. Alla fine di questa lezione dovresti sapere perché Postgres è la risposta noiosa e corretta per la maggior parte dei carichi transazionali, cosa promette davvero ACID, dove il modello relazionale non è quello adatto, e perché “abbiamo superato Postgres” è una frase che dovrebbe insospettirti prima di prenderla per buona.

Il modello relazionale, in breve

Edgar F. Codd pubblicò “A Relational Model of Data for Large Shared Data Banks” nel 1970. L’articolo sosteneva che i dati dovessero essere memorizzati come relazioni (insiemi di tuple con attributi nominati), che le operazioni dovessero essere espresse in un linguaggio dichiarativo invece che camminando lungo i puntatori, e che il layout fisico dovesse essere nascosto all’applicazione. Quell’articolo oggi ha 56 anni. Il vocabolario si è spostato (tabelle, righe e colonne invece di relazioni, tuple e attributi), ma il modello è lo stesso che gira oggi in produzione.

I pezzi che usi davvero: tabelle con schemi fissi di colonne tipizzate; righe con un valore per colonna; primary key che identificano una riga in modo univoco; foreign key che puntano a una primary key altrove con la referential integrity garantita dall’engine; join che combinano righe su condizioni di matching, con il query planner che decide l’algoritmo; indici (per lo più B-tree, talvolta hash, GIN, GiST, BRIN) per lookup veloci; e SQL, il linguaggio di query dichiarativo.

Il modello è dominante da oltre cinquant’anni per una buona ragione: separa in modo pulito la domanda logica (quali dati voglio) dalla domanda fisica (come sono memorizzati e recuperati). Quella separazione permette al database di evolvere indipendentemente dall’applicazione, permette agli stessi dati di rispondere a query che il progettista dello schema non aveva mai immaginato, e permette all’operatore di aggiungere un indice martedì prossimo per risolvere una query lenta senza cambiare una sola riga di codice applicativo.

ACID, cosa significa davvero

Ogni database relazionale fa pubblicità ad ACID. L’acronimo è così familiare da essere diventato rumore di fondo, ma ogni lettera è una promessa specifica che costa qualcosa all’engine per essere mantenuta.

Atomicity. Una transazione è tutto o niente. Se una transazione contiene cinque scritture e la terza fallisce, le prime due vengono annullate e lo stato del database appare come se la transazione non fosse mai partita. Il meccanismo è il write-ahead log (WAL): ogni cambiamento viene appeso a un log durevole prima di essere applicato ai data file, e al riavvio il database rigioca le transazioni committate e scarta quelle non committate.

Consistency. Ogni transazione porta il database da uno stato valido a un altro. “Valido” è definito dai constraint che hai dichiarato: not-null, unique, check, foreign-key, exclusion. Il database rifiuta di committare una transazione che violerebbe un constraint. Questa C, tra parentesi, non è la stessa C di CAP; è molto più ristretta. La CAP-consistency è “ogni read vede l’ultima write attraverso le repliche”. L’ACID-consistency è “le regole di integrità che ho dichiarato continuano a valere”.

Isolation. Le transazioni concorrenti non interferiscono tra loro in un modo che produca un risultato che lo schedule seriale non avrebbe prodotto. Il problema è che la strict serializability è costosa, e lo standard SQL definisce quattro livelli di isolation con garanzie progressivamente più deboli: read uncommitted, read committed, repeatable read, serializable. Postgres usa di default read committed; la maggior parte dei sistemi in produzione gira su read committed o repeatable read. I fenomeni che puoi incontrare a livelli più deboli (dirty read, non-repeatable read, phantom read, write skew) hanno nomi specifici perché hanno causato bug specifici nel mondo reale. Sapere a quale livello girano le tue transazioni, e quali anomalie quel livello permette, fa parte dello scrivere codice applicativo corretto. La maggior parte degli ingegneri non ci pensa abbastanza spesso.

Durability. Una volta che una transazione è committata, il cambiamento sopravvive a un crash. Il meccanismo è, di nuovo, il WAL: il commit ritorna solo dopo che la voce di log è stata flushata su storage durevole. Su una singola macchina questo significa fsync su disco. In un setup replicato di solito significa che il WAL ha raggiunto almeno una replica sincrona.

I quattro insieme sono la ragione per cui un trasferimento bancario funziona: il debit e il credit sono atomic (uno o l’altro, mai metà), consistent (i constraint di integrità reggono per tutto il tempo), isolated (una transazione concorrente non può vedere lo stato applicato a metà), e durable (una volta che la ricevuta esce dalla stampante, il denaro si è davvero mosso). Nessun coordinamento a livello applicativo richiesto. Il database fa il lavoro.

Perché il modello relazionale è “il default giusto” nel 2026

Il tooling è maturo: migrations, ORM, query analyzer, strumenti di backup, framework di replica, offerte cloud-managed, integrazioni di observability. Le caratteristiche operative sono ben comprese, con runbook per ogni shape di carico comune. Il pool di assunzioni è enorme: ogni backend engineer ha scritto SQL. La documentazione è esaustiva (il manuale Postgres da solo supera le tremila pagine). E il costo dello sbagliare è basso: se Postgres non va bene, puoi spostare i dati fuori, mentre gli store esotici ti intrappolano nel momento in cui il tuo data model si è solidificato.

Il modello relazionale è anche un moltiplicatore di conoscenza. Un ingegnere che impara Postgres impara skill trasferibili: SQL, indexing, query plan, lock, il WAL. Un ingegnere che impara uno store NoSQL proprietario impara quello store, e ricomincia da capo al lavoro successivo.

I due dominatori open-source

Nel 2026 due database relazionali open-source fanno girare la maggior parte del carico transazionale di produzione su internet: PostgreSQL e MySQL. Non sono intercambiabili, ma le differenze contano meno di un tempo.

Postgres è il più aderente allo standard SQL dei due. Supporta un set di tipi più ricco out of the box: array, range, enum, full-text search, JSON e JSONB, hstore, tipi geografici via PostGIS. Ha extension point più forti: puoi aggiungere nuovi tipi di dato, nuovi metodi di indice, nuovi hook del query planner, tutto senza forkare il sorgente. Materialized view, common table expression, window function, query ricorsive sono cittadini di prima classe e lo sono da un decennio. Il livello di isolation di default è read committed; serializable, quando ti serve, è implementato bene. Per i nuovi progetti che partono nel 2026, Postgres è il default senza ambiguità.

MySQL è il più operativamente semplice dei due, soprattutto nella sua forma di lungo corso InnoDB-con-row-replication. La storia della replica è più vecchia e battle-tested su distanze molto lunghe; molta infrastruttura globale legacy gira su MySQL perché nel 2010 niente altro replicava così bene attraverso i continenti. Il query planner è meno sofisticato di quello di Postgres, il sistema di tipi è più ristretto, e il supporto JSON, pur presente, non è alla pari con il JSONB di Postgres. MySQL è ancora enorme nelle codebase legacy (WordPress, Drupal, gran parte del web della prima era PHP) e il fork MariaDB lo ha tenuto in salute. Scegli MySQL oggi solo se hai una forte ragione operativa o un footprint MySQL esistente che non vuoi migrare. Altrimenti Postgres.

La realtà cloud-managed

Quasi nessuno fa girare Postgres in proprio ormai. Si fa girare un’offerta cloud-managed e si tratta il database sottostante come se fosse un prodotto vendor. Amazon RDS offre Postgres vanilla su infrastruttura AWS-managed con backup, patching, failover e read replica. Aurora Postgres è la reimplementazione AWS dello storage layer sotto il query engine di Postgres, con failover più rapido ed economia migliore su scala. Google Cloud SQL e Azure Database for PostgreSQL sono gli equivalenti sugli altri due cloud. Neon è Postgres con un’interfaccia serverless e branching-friendly; Supabase è Postgres più una API auto-generata e un layer realtime; Crunchy Bridge e Timescale Cloud completano il panorama degli host specializzati.

Le offerte cloud restringono ciò che puoi fare (niente extension arbitrarie, niente tuning di ogni parametro del server), ma ti risparmiano il carico operativo che era il lavoro a tempo pieno di un DBA. Per la maggior parte dei team il trade vale la pena.

Quando SQL è la risposta giusta, e quando non lo è

Il modello relazionale calza quando le transazioni contano (denaro, inventario, identità, audit), quando i dati hanno relazioni naturali (gli utenti hanno ordini, gli ordini hanno line item, le line item referenziano prodotti), quando avrai bisogno di query ad-hoc che non avevi pianificato (il team di reporting, il team di operations, il nuovo product manager), e quando il reporting ha bisogno di join. Se la maggior parte di queste cose si applica, vuoi un database relazionale, Postgres in particolare.

È quello sbagliato quando il pattern di accesso è lookup single-key a throughput estremo (usa un KV store, lezione 18), quando lo schema evolve in modo selvaggio attraverso le righe e uno schema fisso sarebbe una bugia (considera un document store, lezione 19), per time-series con regole di retention (TimescaleDB, InfluxDB, ClickHouse), quando la graph traversal è la query dominante (i grafi grandi vogliono un database a grafo), o per scan analitici massivi (gli store a colonne come BigQuery, Snowflake, ClickHouse, Redshift sono di un ordine di grandezza più economici su scala).

Il test onesto: elenca le top cinque query che la tua applicazione farà girare, per frequenza. Se sono tutte get single-key, vuoi un KV store. Se sono join complessi, vuoi SQL. Se sono miste, la risposta è di solito SQL più una cache.

La tesi “Postgres è abbastanza”

La lezione 8 sosteneva che la maggior parte dei team over-engineerizza la propria prima architettura. Il corollario Postgres è più affilato: la maggior parte dei team sotto i dieci milioni di utenti non ha bisogno di niente di più sofisticato di un Postgres ben tunato, una read replica o due, un’indicizzazione attenta, e un connection pooler.

In modo conservativo, un’istanza moderna di Postgres su una macchina dimensionata sensatamente (64 vCPU, 256 GB RAM, storage NVMe) gestisce decine di migliaia basse di transazioni di scrittura al secondo e centinaia di migliaia di query di lettura al secondo, con latenza mediana in millisecondi a singola cifra. Le read replica dietro PgBouncer moltiplicano linearmente il throughput di lettura. L’autovacuum tunato tiene il bloat sotto controllo. Il partizionamento nativo gestisce tabelle nelle centinaia di milioni di righe senza dirupi di performance.

Stripe, Notion, GitLab, i monoliti Rails dell’era Heroku, gran parte del mondo SaaS small e mid-size ha girato e continua a girare su questa shape. La conversazione “ci serve sharding” arriva di solito molto più tardi di quanto i diagrammi di architettura suggeriscano, e anche allora, lo sharding dentro Postgres (per tenant, per customer, per region) di solito batte lo spostarsi su un engine diverso.

Uno schema lavorato

Ecco un piccolo schema e-commerce come lo progetteresti davvero. Quattro tabelle: users, products, orders, line item. Le foreign key impongono la referential integrity. Le primary key sono ID surrogati. Il modello non ha niente di notevole, ed è proprio questo il punto.

erDiagram
    USERS ||--o{ ORDERS : places
    ORDERS ||--|{ LINE_ITEMS : contains
    PRODUCTS ||--o{ LINE_ITEMS : "appears in"
    USERS {
        bigint id PK
        text email
        text name
        timestamp created_at
    }
    PRODUCTS {
        bigint id PK
        text sku
        text name
        numeric price
        int stock
    }
    ORDERS {
        bigint id PK
        bigint user_id FK
        text status
        numeric total
        timestamp placed_at
    }
    LINE_ITEMS {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        numeric unit_price
    }

Una query che chiede “cosa ha comprato l’utente 42 negli ultimi 30 giorni, con i nomi dei prodotti e i totali” è un solo join attraverso quattro tabelle, espresso in forse dieci righe di SQL, eseguito in millisecondi con i giusti indici. Prova a scrivere quella query in un key-value store e guarda cosa ti casca dalle mani.

Dove atterra questa lezione

Il modello relazionale è il default noioso e corretto per i carichi transazionali. Postgres è il default noioso e corretto dentro al modello relazionale. La maggior parte dei team non avrà mai bisogno di niente altro per il system of record, e i team che ne avranno bisogno lo sapranno per dolore misurato, non per moda architetturale.

Le prossime due lezioni coprono le famiglie che battono davvero il modello relazionale su carichi specifici: i key-value store (lezione 18) e i document store (lezione 19). Il Modulo 3 fa un giro tra tutte le grandi famiglie prima che la lezione 24 le metta insieme come polyglot persistence: usa Postgres come system of record, più un paio di store specializzati per i carichi dove Postgres non è lo strumento giusto.

Riferimenti e letture aggiuntive

  • Edgar F. Codd, “A Relational Model of Data for Large Shared Data Banks”, Communications of the ACM, June 1970. The founding document. Worth reading once just to see how much of the modern vocabulary is already there in the original.
  • The PostgreSQL documentation, https://www.postgresql.org/docs/ (retrieved 2026-05-01). The most thorough free database manual on the internet. Start with the Concurrency Control and Performance Tips chapters.
  • Jim Gray and Andreas Reuter, “Transaction Processing: Concepts and Techniques” (Morgan Kaufmann, 1992). Still the reference text for ACID, isolation levels, and the WAL.
  • Stripe Engineering, “Online Migrations at Scale”, https://stripe.com/blog/online-migrations (retrieved 2026-05-01). The companion piece to the Stripe case study from lesson 8: how to evolve a Postgres schema without downtime.
  • Markus Winand, “Use The Index, Luke”, https://use-the-index-luke.com/ (retrieved 2026-05-01). The best free resource on SQL indexing, applicable across engines.
Cerca