Denormalizzazione nei database: quando e come usarla
Scopri come la denormalizzazione migliora le prestazioni in lettura riducendo i join e semplificando le query. Capisci compromessi, tecniche e casi d’uso che la rendono uno strumento potente per sistemi di analytics e reporting.
Ho scritto molto sulla normalizzazione e sul perché sia una base così affidabile per l’integrità dei dati. Non mi sto contraddicendo. Continuo a credere che uno schema ben normalizzato sia il punto di partenza giusto per la maggior parte dei sistemi transazionali. Questo articolo tratta invece della scelta deliberata di andare contro quella “purezza” in situazioni molto specifiche in cui le prestazioni in lettura contano più della rigorosa forma normale.
La denormalizzazione non è saltare la normalizzazione. È un’ottimizzazione delle prestazioni che applichi a un modello normalizzato quando query reali, utenti reali e SLA reali ti dicono che join e calcoli al volo sono troppo lenti o costosi. In pratica, scambi letture più veloci e query più semplici con più spazio di archiviazione, scritture più complesse e lavoro extra per mantenere la coerenza.
In questo articolo ti mostrerò dove la denormalizzazione è utile e quando non lo è, e come implementarla in sicurezza nei database SQL. L’obiettivo non è abbandonare il buon design, ma aggiungervi scorciatoie quando il carico di lavoro lo giustifica.
Che cos’è la denormalizzazione nei database?
Se conosci la normalizzazione e il design dei database, ecco la risposta breve:
Denormalizzazione è l’atto deliberato di aggiungere dati ridondanti a uno schema precedentemente normalizzato per accelerare le letture e semplificare le query. È un’ottimizzazione mirata delle prestazioni, non una scusa per saltare il buon modelling!
Se sei alle prime armi con il design dei database, può valere la pena scomporre il significato di “normalizzato”, “denormalizzato” e “non normalizzato”. Online si sentono spesso questi 3 termini, ed è importante non confonderli.
Normalizzato: I dati sono divisi in tabelle ben strutturate che minimizzano la ridondanza e proteggono l’integrità (vedi 3NF/BCNF).
Denormalizzato: Reintroduci una ridondanza selettiva con colonne extra, valori precomputati o tabelle pre-joinate, sopra quel modello normalizzato, per rendere più veloci le letture comuni. Con la denormalizzazione, mantieni una singola fonte di verità (le tabelle normalizzate), poi gestisci una o più rappresentazioni più veloci per i percorsi “caldi”, come dashboard, listing di prodotti, ricerca, ecc. Scambi spazio + complessità di scrittura per velocità di lettura + query più semplici.
Non normalizzato: Dati grezzi, ad hoc o disordinati in cui struttura e vincoli non sono mai stati progettati correttamente. Non è ciò che facciamo qui.
Esempio in SQL
Ecco un piccolo esempio in SQL per aiutarti a visualizzare la differenza tra modellazione normalizzata e denormalizzata.
Modellazione normalizzata
-- Source of truthCREATETABLE customers (
customer_id BIGINTPRIMARYKEY,
name TEXTNOTNULL,
tier TEXTNOTNULL);CREATETABLE orders (
order_id BIGINTPRIMARYKEY,
customer_id BIGINTNOTNULLREFERENCES customers(customer_id),
order_total NUMERIC(12,2)NOTNULL,
created_at TIMESTAMPNOTNULLDEFAULTnow());-- Typical report needs a joinSELECT c.name, c.tier,SUM(o.order_total)AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
GROUPBY c.name, c.tier;Offerto da
Denormalizzato per un percorso di reporting
-- Add redundant fields for faster readsALTERTABLE orders
ADDCOLUMN customer_name TEXT,ADDCOLUMN customer_tier TEXT;-- Now most reports avoid the joinSELECT customer_name, customer_tier,SUM(order_total)AS revenue
FROM orders
GROUPBY customer_name, customer_tier;Offerto da
In questo esempio, manterresti customers come fonte di verità e ti assicureresti che orders.customer_name / orders.customer_tierrestino sincronizzati (ad es. trigger, job CDC o un backfill pianificato). Stessa verità, due rappresentazioni, ognuna ottimizzata per un lavoro diverso.
La denormalizzazione è un cattivo design?
C’è un equivoco diffuso secondo cui la denormalizzazione sia solo il risultato di un cattivo design. Non è così, se è misurata, circoscritta e mantenuta. Sì, la denormalizzazione spesso viola forme normali più alte (è il punto), ma è intenzionale e supportata da un piano per preservare la coerenza. Il cattivo design è saltare del tutto la normalizzazione o spargere ridondanza senza una strategia di sincronizzazione.
Ecco il confronto affiancato. Non è esaustivo ma ti aiuterà a capire i compromessi di ciascun approccio.
Aspetto
Normalizzazione
Denormalizzazione
Obiettivo principale
Integrità, ridondanza minima, aggiornamenti facili e corretti
Letture più veloci, query più semplici sui percorsi caldi
Ideale per
Sistemi OLTP con scritture/aggiornamenti frequenti
Dashboard a carico di lettura, pagine di ricerca/listing, reporting/analytics
Prestazioni in lettura
Spesso richiede join, buone con gli indici giusti
Meno join, può essere molto più veloce e prevedibile
Complessità in scrittura
Semplice: singola fonte di verità
Maggiore: devi aggiornare/sincronizzare copie o aggregati ridondanti
Storage
Snello
Più grande (colonne/tabelle extra, viste precompute)
Integrità dei dati
Forte per design (vincoli 3NF/BCNF)
Richiede meccanismi per evitare drift (trigger, CDC, job)
Velocità di cambiamento
Rinomine/aggiornamenti di colonne sono localizzati
I cambiamenti possono propagarsi su dati duplicati
Overhead operativo
Più basso: meno parti in movimento
Più alto: politiche di refresh, backfill, monitoraggio
Modalità di guasto
Query N+1, join lenti, indici mancanti
Dati stantii, inconsistenza, amplificazione delle scritture
Evoluzione dello schema
Prevedibile, adatta al refactoring
Servono piani di migrazione per le rappresentazioni ridondanti
Esempi tipici
Ordini, clienti, transazioni
Listing di prodotti pre-joinati; tabelle di vendite aggregate; viste materializzate/indicizzate
Perché e quando denormalizzare
La denormalizzazione diventa davvero utile quando utenti reali e query reali sono bloccati da join, aggregazioni o lookup ripetuti. Una volta confermato che indicizzazione, tuning delle query e caching non bastano, potresti ricorrere alla denormalizzazione per ottimizzare letture rapide su pattern di accesso prevedibili.
Quando la denormalizzazione può aiutare
Carichi pesanti in lettura:latenze p95/p99 dominate da join o aggregazioni, CPU spesa in hash/merge join, alto churn della buffer cache.
Forme di query stabili:Le stesse dashboard/endpoint girano tutto il giorno con filtri simili (es. vendite di ieri per categoria).
Join a ventaglio:Una tabella “calda” fa join con 3-5 altre solo per renderizzare una card o una lista.
Hotspot di aggregazione:Ricalcoli ripetutamente totali, conteggi o valori più recenti su ampi intervalli.
Pressione da SLA:Il prodotto richiede risposte sotto i 200 ms dove i piani attuali spillano su disco o scandiscono troppo.
Casi d’uso classici
Dashboard e reporting BI (OLAP/analytics):Precalcolare aggregati giornalieri/mensili, mantenere viste materializzate di group-by costosi o archiviare tabelle di fatti denormalizzate per slice comuni.
E-commerce/catalogo e pagine di ricerca/listing:Duplicare category_name, brand_name, price_with_tax o una proiezione di prodotto pre-joinata per liste e filtri veloci.
CMS/blog/feed di notizie:Archiviare author_name, primary_topic o rendered_excerpt sulla tabella degli articoli/post per evitare join o trasformazioni a runtime.
Feed attività e contatori:Mantenere like_count, follower_count, o latest_comment_at come attributi derivati invece di ricalcolarli.
Analytics di eventi/log su larga scala (OLAP/NoSQL):Appiattire i dati annidati per gli archivi colonnari e mantenere righe larghe adatte al partizionamento per rendere prevedibili le scansioni.
Quando non denormalizzare e usare prima qualcos’altro
OLTP con carico di scrittura elevato e coerenza rigorosa (ordini, pagamenti, aggiustamenti di inventario).
Il collo di bottiglia sono indici mancanti/scadenti, query N+1 o ORM loquace. Correggi prima quelli.
Campi ad alta volatilità (es. disponibilità prodotto che cambia ogni secondo) in cui la duplicazione amplifica il churn.
Team senza ownership chiara e piano di sync (trigger, CDC/job, politiche di refresh, monitoraggio del drift). Senza questo, rischi di fare più danni che benefici!
Il dataset è abbastanza piccolo che un indice “covering” o una cache lo rendono già veloce.
Vuoi davvero usare la denormalizzazione dove ti dà la massima velocità in lettura con il minimo onere operativo aggiuntivo, e solo dopo aver escluso soluzioni più economiche.
Se ti sei reso conto che la denormalizzazione non è proprio ciò di cui hai bisogno ora, resta qui! Nella prossima sezione esploreremo le alternative alla denormalizzazione.
Alternative alla denormalizzazione
La denormalizzazione non è la risposta a tutti i tuoi problemi. Prima di aggiungere ridondanza, devi spremere tutto ciò che puoi dal motore e dalla tua app. Queste soluzioni sono più economiche da mantenere e spesso offrono gli stessi benefici.
1) Indicizzazione
Indici compositi/covering: Nelle query SQL, metti prima le colonne di filtro, poi le colonne di GROUP BY/ORDER BY. Includo le colonne della select list così che il motore possa servire la query dal solo indice.
Indici filtrati/parziali: Indicizza solo la fetta calda (es. status = 'ACTIVE'), mantenendo l’indice piccolo e veloce.
Indici su espressione/funzionali: Indicizza su LOWER(email) o date_trunc('day', created_at) per evitare scansioni calcolate.
2) Tuning delle query e paginazione
Evita SELECT *. Recupera solo ciò che mostri.
Sostituisci join non necessari con EXISTS/SEMI quando devi solo verificare la presenza.
Spingi i predicati in basso: filtra presto, aggrega tardi.
Usa keyset pagination (WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) per uno scrolling stabile e veloce.
3) Caching
Usa cache applicativa (es. Redis) per query calde e frammenti renderizzati.
Usa caching HTTP (ETag/Last-Modified) per pagine pubbliche e dashboard.
Cache di breve durata (30–120 s) spesso eliminano la necessità di modifiche allo schema.
4) Repliche di sola lettura
Scarica le letture pesanti sulle repliche. Ottimo per dashboard ed export.
5) Partizionamento e pruning
Partiziona per range/hash le tabelle grandi così che le scansioni tocchino solo le partizioni rilevanti (es. ultimi 30 giorni). Questo non è denormalizzare, sia chiaro: è semplicemente ridurre i dati scansionati.
6) Archivi colonnari/OLAP
Sposta analytics pesanti su Snowflake/BigQuery/ClickHouse (via ELT/dbt). Tieni l’OLTP normalizzato e lascia che il warehouse gestisca forme larghe e adatte alle scansioni.
7) Igiene dell’ORM
Elimina le query N+1 (caricamento eager o batch), imposta select list sensate e limita le dimensioni pagina. Uno strato ORM pulito può eliminare la necessità di denormalizzare.
8) Colonne calcolate/generate (gestite dal DB)
Lascia che il DB mantenga i valori derivati (es. price_with_tax) come colonne generate/calcolate o tramite indici su espressione. Otterrai letture veloci senza logica di sync a livello applicativo.
Tecniche di denormalizzazione
Ecco i modi più comuni per aggiungere ridondanza controllata a un modello normalizzato. Per ciascuno, mostrerò cosa fa, quando usarlo e come mantenerlo sincronizzato.
Setup: supponi un core normalizzato con customers, orders, order_items, products.
1) Tabelle “proiezione” appiattite/pre-joinate
Cosa: Crea una tabella che pre-joini le colonne necessarie per letture veloci (es. listing prodotti o dashboard ordini).
Quando: Il tuo percorso caldo fa join tra 3 e 5 tabelle in modi prevedibili.
Come (PostgreSQL):
-- Read-optimised projection for a typical orders listCREATETABLE orders_projection (
order_id BIGINTPRIMARYKEY,
created_day DATENOTNULL,
customer_id BIGINTNOTNULL,
customer_name TEXTNOTNULL,
total_amount NUMERIC(12,2)NOTNULL);-- Initial backfillINSERTINTO orders_projection (order_id, created_day, customer_id, customer_name, total_amount)SELECT o.order_id,
o.created_at::dateAS created_day,
c.customer_id,
c.name AS customer_name,SUM(oi.quantity * oi.unit_price)AS total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
GROUPBY o.order_id, created_day, c.customer_id, c.name;-- Index for fast filtering by day/customerCREATEINDEXON orders_projection (created_day, customer_id);Offerto da
Cosa: Duplica un pugno di attributi letti di frequente su un’altra tabella per evitare join (es. orders.customer_name).
Quando: Ti servono solo 1 o 2 valori e non vuoi una proiezione completa.
Come (esempio di trigger PostgreSQL):
ALTERTABLE orders
ADDCOLUMN customer_name TEXT,ADDCOLUMN customer_tier TEXT;-- Keep the redundant fields correct on insert/updateCREATEORREPLACEFUNCTION sync_order_customer_fields()RETURNSTRIGGERAS $
BEGINSELECT name, tier INTO NEW.customer_name, NEW.customer_tier
FROM customers WHERE customer_id = NEW.customer_id;RETURN NEW;END;
$ LANGUAGE plpgsql;CREATETRIGGER trg_orders_sync_customer
BEFORE INSERTORUPDATEOF customer_id ON orders
FOR EACH ROWEXECUTEFUNCTION sync_order_customer_fields();-- Propagate customer name/tier changes to existing ordersCREATEORREPLACEFUNCTION propagate_customer_changes()RETURNSTRIGGERAS $
BEGINUPDATE orders
SET customer_name = NEW.name,
customer_tier = NEW.tier
WHERE customer_id = NEW.customer_id;RETURN NEW;END;
$ LANGUAGE plpgsql;CREATETRIGGER trg_customer_propagate
AFTERUPDATEOF name, tier ON customers
FOR EACH ROWEXECUTEFUNCTION propagate_customer_changes();Offerto da
Compromesso: Letture semplici, ma le scritture si propagano quando un cliente viene aggiornato.
-- Generated column example (Postgres 12+; same-table expressions)ALTERTABLE products
ADDCOLUMN price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS(price *1.20) STORED;-- Counter maintained by triggers (likes per post)ALTERTABLE posts ADDCOLUMN like_count INTEGERNOTNULLDEFAULT0;CREATEORREPLACEFUNCTION inc_like_count()RETURNSTRIGGERAS $
BEGINUPDATE posts SET like_count = like_count +1WHERE post_id = NEW.post_id;RETURN NEW;END; $ LANGUAGE plpgsql;CREATETRIGGER trg_like_insert
AFTERINSERTON post_likes
FOR EACH ROWEXECUTEFUNCTION inc_like_count();CREATEORREPLACEFUNCTION dec_like_count()RETURNSTRIGGERAS $
BEGINUPDATE posts SET like_count = GREATEST(like_count -1,0)WHERE post_id = OLD.post_id;RETURN OLD;END; $ LANGUAGE plpgsql;CREATETRIGGER trg_like_delete
AFTERDELETEON post_likes
FOR EACH ROWEXECUTEFUNCTION dec_like_count();Offerto da
4) Tabelle aggregate/di riepilogo
Cosa: Precalcolare rollup come ricavi giornalieri, utenti attivi per giorno, ordini per categoria.
Quando: Le dashboard ripetono gli stessi group-by e le scansioni raw sono costose.
CREATETABLE daily_sales (
sales_day DATEPRIMARYKEY,
gross_amount NUMERIC(14,2)NOTNULL,
order_count INTEGERNOTNULL);-- Incremental upsert for "yesterday" (run hourly/15-min)INSERTINTO daily_sales (sales_day, gross_amount, order_count)SELECT(o.created_at AT TIME ZONE 'UTC')::dateAS sales_day,SUM(oi.quantity * oi.unit_price)AS gross_amount,COUNT(DISTINCT o.order_id)AS order_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at >= date_trunc('day',now()-interval'1 day')AND o.created_at < date_trunc('day',now())GROUPBY sales_day
ON CONFLICT (sales_day)DOUPDATESET gross_amount = EXCLUDED.gross_amount,
order_count = EXCLUDED.order_count;Offerto da
Opzioni di sync: job pianificato (cron/dbt), aggiornamenti in streaming (CDC) o aggregatori event-driven.
5) Viste materializzate (risultati di query persistiti)
Cosa: Archivia l’output di una query costosa come tabella fisica che puoi indicizzare.
Quando: Il result set è costoso, relativamente stabile e puoi tollerare finestre di refresh.
-- Expensive reportCREATE MATERIALIZED VIEW mv_category_sales ASSELECT p.category_id,
date_trunc('day', o.created_at)ASday,SUM(oi.quantity * oi.unit_price)AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
GROUPBY p.category_id, date_trunc('day', o.created_at);-- Make it fast to queryCREATEINDEXON mv_category_sales (category_id,day);-- Refresh patterns-- Full, blocking:
REFRESH MATERIALIZED VIEW mv_category_sales;-- Non-blocking reads (requires unique index on the MV):-- 1) ensure a unique index exists (e.g., (category_id, day))-- 2) then:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_sales;Offerto da
Compromesso: Dovrai gestire la cadenza di refresh perché il refresh incrementale non è nativo in Postgres. Detto ciò, è abbastanza semplice usando dbt o pattern personalizzati “append-only + rebuild finestrato”.
6) Proiezioni “documento” JSON precompute
Cosa: Archivia un blob JSON denormalizzato che corrisponde alle esigenze della tua API/UI (es. product card), insieme a indici JSON mirati.
Quando: La tua API serve ripetutamente la stessa forma e vuoi una sola lettura.
CREATETABLE product_cards (
product_id BIGINTPRIMARYKEY,
card_json JSONB NOTNULL);-- Build or rebuild card documentsINSERTINTO product_cards (product_id, card_json)SELECT p.product_id,
jsonb_build_object('id', p.product_id,'name', p.name,'brand', b.name,'price_with_tax', p.price *1.20,'category', c.name
)FROM products p
JOIN brands b ON b.brand_id = p.brand_id
JOIN categories c ON c.category_id = p.category_id
ON CONFLICT (product_id)DOUPDATESET card_json = EXCLUDED.card_json;-- GIN index for JSON queries if neededCREATEINDEX idx_product_cards_gin ON product_cards USING GIN (card_json);Offerto da
Opzioni di sync: rebuild event-driven su cambi di prodotto/marca/categoria, o refresh batch frequenti per i prodotti aggiornati di recente.
Scegliere una strategia di sync (guida rapida)
Trigger: Sono immediati e garantiscono coerenza transazionale. In generale ottimi per fan-out ridotto e basso volume di scrittura, ma se abusati possono peggiorare la latenza in scrittura.
Doppia scrittura applicativa: L’app scrive sia sulla fonte sia sulla denormalizzazione. È semplice ma più rischioso. Puoi mitigare con retry idempotenti e pattern outbox/CDC.
CDC → worker: Questa strategia di sync è affidabile e scalabile poiché propaga i cambiamenti in modo asincrono. Ideale quando l’eventual consistency è accettabile.
Job pianificati: Opzione più semplice per aggregati e viste materializzate. Scegli finestre di refresh in linea con la tolleranza della tua UX.
Ti consiglio di integrare sempre l’osservabilità nella soluzione. Non c’è niente di peggio che non sapere se le modifiche siano state processate o replicate correttamente. Aiuta anche a gestire i controlli di drift e gli script di backfill.
Come implementare la denormalizzazione passo dopo passo
Come sempre, intervenire sul database è piuttosto rischioso. Ti raccomando vivamente di replicare il database e provare a implementare la denormalizzazione prima sulla replica, per assicurarti che tutto si comporti come ti aspetti. Se hai un ambiente di sviluppo, ancora meglio!
Il processo è abbastanza semplice: misura → cambia il meno possibile → mantienilo corretto → verifica → monitora → ripeti.
1) Profila e imposta un target di successo
Cattura la query esatta (testo + parametri) e la sua frequenza.
Registra una baseline: piano EXPLAIN (ANALYZE, BUFFERS), latenza p95, CPU/IO, righe scansionate.
Concorda un criterio di successo (per esempio “p95 < 120 ms con costo di scrittura ≤1,2×”).
Conferma la tolleranza di consistenza (es. “le analytics possono essere stantie di 5 min”).
2) Scegli la denormalizzazione più piccola che funziona
Idealmente prova una colonna ridondante o una tabella di riepilogo prima di una grande proiezione.
Abbozza la nuova forma:
Colonne ridondanti? (quali, perché)
Tabella aggregata o vista materializzata? (granularità, chiavi, finestra di refresh)
Decidi il modello di coerenza:
Forte (trigger/transazionale) vs eventuale (CDC/job/refresh vista materializzata).
Scrivi il budget di amplificazione delle scritture (quante scritture extra per evento sono accettabili).
L’app scrive sulle tabelle sorgente e una riga outbox nella stessa transazione.
Un worker legge l’outbox e aggiorna idempotentemente il target denormalizzato.
Job pianificati / refresh MV (ottimi per aggregati):
Definisci cadenza di refresh, windowing e strategia di backfill.
L’idempotenza non si discute. Gli aggiornamenti devono essere ripetibili in sicurezza (ad es. UPSERT con ricalcolo deterministico). Fidati, ti ringrazierai più avanti.
4) Crea, backfill e valida
Crea le strutture (tabelle, colonne, indici, trigger/job) ma lascia invariata l’app.
Esegui il backfill dalla fonte di verità.
Valida la parità con invarianti:
-- Example parity check: orders_projection vs live joinSELECTCOUNT(*)AS mismatches
FROM orders_projection p
JOIN orders o ON o.order_id = p.order_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE p.customer_name <> c.name
OR p.total_amount <>(SELECTSUM(oi.quantity * oi.unit_price)FROM order_items oi WHERE oi.order_id = o.order_id
);Offerto da
4. Correggi le discrepanze; riesegui fino a zero (o entro un budget di errore concordato).
5) Passa in produzione in sicurezza
Rilascia il cambiamento dell’app dietro un feature flag o canary (es. il 10% del traffico legge dal percorso denormalizzato).
Esegui shadow read: calcola il vecchio risultato in background e confronta hash/aggregati per un sottoinsieme di richieste.
Mantieni un rollback rapido (torna istantaneamente al percorso normalizzato).
6) Monitora le cose giuste
Crea una piccola dashboard nel tuo strumento di osservabilità preferito:
Letture: latenza p50/p95, righe lette, buffer hits, stabilità dei piani di query.
Scritture: tempo extra di scrittura, errori di trigger/job, lag della coda (CDC), timestamp ultimo refresh MV.
Qualità dei dati: contatori di drift (controlli di parità giornalieri), numero di righe backfillate, % di discrepanze.
Costo/impronta: dimensioni tabelle, bloat degli indici, crescita dimensione MV.
7) Operare e iterare
Assegna l’ownership (team/sistema di reperibilità) e un runbook (come ricostruire, fare backfill, riparare). Spesso viene trascurato, ma vorrai poter reagire il più rapidamente possibile quando succede un incidente.
Rivaluta trimestralmente: serve ancora? È ancora la forma giusta? Possiamo semplificare? Documenta la decisione e linka alle dashboard.
Pro e contro della denormalizzazione
Abbiamo trattato i dettagli nella tabella Normalizzazione vs Denormalizzazione in una sezione precedente, quindi ecco un rapido riepilogo.
Cosa guadagni
Letture più veloci e latenze p95/p99 più prevedibili
Query più semplici sui percorsi caldi (meno join/aggregazioni)
CPU/IO più basse per lettura e query analitiche più economiche
Cosa costa
Amplificazione delle scritture (update/insert extra)
Rischio di consistenza (dati stantii/in deriva a meno che non sincronizzi bene)
Crescita dello storage (colonne/tabelle/viste duplicate)
Propagazione dei cambiamenti (update di schema/logica da rispecchiare)
Strumenti e tecnologie che supportano la denormalizzazione
Non tutti i database aiutano a denormalizzare allo stesso modo. Alcuni offrono funzionalità di prima classe per persistere risultati di query costose, altri si aspettano che tu costruisca con job, trigger o pipeline.
PostgreSQL
Postgres ti offre diversi modi per materializzare forme ottimizzate per la lettura.
Le viste materializzate persistono il risultato di una query, quindi le letture sono immediate. Scegli tu quando fare il refresh (a orario o ad hoc) e puoi persino fare refresh “concurrently” così i lettori non vengono bloccati.
Per duplicazioni leggere, colonne generate e indici su espressione consentono al database di mantenere valori derivati e accelerare filtri comuni senza scrivere logica di sync extra.
Quando ti serve coerenza forte tra la fonte e una copia denormalizzata, puoi usare i trigger per tenere tutto allineato (ma ricorda, aggiungono lavoro al percorso di scrittura!). Per refresh event-driven o batch, puoi abbinare Postgres a uno scheduler (es. pg_cron o un job runner esterno) o alla replica logica/CDC per streammare i cambiamenti in un worker che aggiorna asincronamente le tue tabelle denormalizzate.
Se vuoi metterti all’opera e creare i tuoi database e testare la denormalizzazione, dai un’occhiata al nostro corso su PostgreSQL.
SQL Server
SQL Server punta molto sulle viste indicizzate, che sono una sorta di viste materializzate “always-on”, dove il motore mantiene la vista sincronizzata a ogni insert/update/delete, quindi le letture sono super rapide. Il rovescio della medaglia è che le scritture ora mantengono sia le tabelle base sia la vista, quindi carichi OLTP pesanti possono rallentare.
Oltre a ciò, puoi usare colonne calcolate persistenti per coprire attributi derivati su singola riga.
Per il reporting su larga scala, gli indici columnstore sono un’alternativa alla denormalizzazione: comprimono e scansionano dataset grandi in modo molto efficiente, il che può eliminare la necessità di duplicare i dati.
Oracle
Le viste materializzate di Oracle sono un’opzione matura con refresh FAST che usa change log per aggiornare solo ciò che è cambiato, o refresh COMPLETE quando va bene ricostruire tutto.
Con Query Rewrite, l’ottimizzatore può usare in modo trasparente la tua vista materializzata quando qualcuno interroga le tabelle sottostanti, così i team ottengono il boost senza cambiare la SQL dell’applicazione. Come sempre, letture più veloci significano lavoro extra altrove: mantenere log e fare refresh delle viste aumenta l’overhead di scrittura e operativo.
MySQL / MariaDB
MySQL non ha viste materializzate native, quindi la denormalizzazione di solito si costruisce con tabelle fisiche più job pianificati o trigger per tenerle aggiornate.
Le colonne generate aiutano con valori derivati semplici. Funziona bene per dashboard e riepiloghi prevedibili, ma di nuovo, è utile essere intenzionali su cadenza di refresh e complessità dei trigger per non tassare inavvertitamente i percorsi critici di scrittura. Molti team combinano questo con repliche di lettura per scaricare completamente le query di reporting.
Snowflake / BigQuery (livello analytics)
I warehouse colonnari sono costruiti per dati larghi, adatti alla scansione e denormalizzati. In genere modellerai ampie tabelle di fatti con attributi utili già inclusi, poi farai affidamento su partizionamento/clustering per ridurre ciò che viene scansionato.
Entrambe le piattaforme supportano viste materializzate e task/query pianificate per mantenere freschi gli aggregati senza toccare l’OLTP. Scambi storage e costi di refresh per letture molto prevedibili ed economiche su larga scala. È perfetto per dashboard e BI.
Questo è un po’ più avanzato del nostro corso su PostgreSQL, quindi se ti senti già a tuo agio con i database puoi provare la nostra introduzione al data modeling con Snowflake.
dbt (modellazione e orchestrazione)
dbt è conosciuto come lo strato di “infrastruttura come codice” per analytics denormalizzate. Definisci i modelli una volta, scegli una materializzazione (tabella, vista, incrementale) e lascia che dbt gestisca build, dipendenze e test. I modelli incrementali sono particolarmente utili per tabelle di riepilogo che devono solo aggiungere e unire nuovi dati. È facilmente uno dei miei strumenti preferiti!
CDC e data pipeline
Quando vuoi che i cambiamenti applicativi fluiscano in modo affidabile in strutture denormalizzate, il Change Data Capture è la scelta giusta. Strumenti come Debezium (self-hosted) o connettori gestiti (Fivetran, Airbyte) streammano cambiamenti a livello riga dall’OLTP verso worker o warehouse che aggiornano tabelle di proiezione, contatori o aggregati. In genere è eventualmente consistente (ottimo per dashboard e feed) e scala molto meglio che infilare tutto nei trigger. Rendi gli update idempotenti, monitora il lag e mantieni un percorso di backfill per eventi in ritardo o mancati.
Punti chiave e considerazioni finali
La denormalizzazione è un’ottimizzazione pragmatica sovrapposta a un design normalizzato, non un suo rifiuto. Stai scambiando storage e complessità di scrittura per letture più veloci e semplici su percorsi specifici e ben compresi.
Ricorda:
Parti normalizzato. Usa la denormalizzazione solo dove una query nominata e misurata ne ha bisogno.
Esaurisci prima i rimedi più economici (indici, riscritture di query, cache, repliche, OLAP).
Scegli la denormalizzazione più piccola che rende (una colonna ridondante, un piccolo aggregato o una vista materializzata).
Sii esplicito sulla freschezza (consistenza forte vs eventuale) e costruisci una sync idempotente.
Misura prima/dopo, monitora il drift e tieni a portata di mano un interruttore di rollback.
Tratta le tabelle normalizzate come fonte di verità! I pezzi denormalizzati sono copie ottimizzate per la lettura che puoi ricostruire.
Autore
Marie Fayard
Sono una tech lead orientata al prodotto, specializzata nel far crescere startup nelle fasi iniziali, dal primo prototipo al product-market fit e oltre. Sono instancabilmente curiosa di come le persone usano la tecnologia e adoro lavorare a stretto contatto con founder e team cross‑funzionali per dare vita a idee ambiziose. Quando non costruisco prodotti, inseguo l’ispirazione in angoli nuovi del mondo o scarico le tensioni in una sala yoga.
Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Ho scritto molto sulla normalizzazione e sul perché sia una base così affidabile per l’integrità dei dati. Non mi sto contraddicendo. Continuo a credere che uno schema ben normalizzato sia il punto di partenza giusto per la maggior parte dei sistemi transazionali. Questo articolo tratta invece della scelta deliberata di andare contro quella “purezza” in situazioni molto specifiche in cui le prestazioni in lettura contano più della rigorosa forma normale.
La denormalizzazione non è saltare la normalizzazione. È un’ottimizzazione delle prestazioni che applichi a un modello normalizzato quando query reali, utenti reali e SLA reali ti dicono che join e calcoli al volo sono troppo lenti o costosi. In pratica, scambi letture più veloci e query più semplici con più spazio di archiviazione, scritture più complesse e lavoro extra per mantenere la coerenza.
In questo articolo ti mostrerò dove la denormalizzazione è utile e quando non lo è, e come implementarla in sicurezza nei database SQL. L’obiettivo non è abbandonare il buon design, ma aggiungervi scorciatoie quando il carico di lavoro lo giustifica.
Che cos’è la denormalizzazione nei database?
Se conosci la normalizzazione e il design dei database, ecco la risposta breve:
Denormalizzazione è l’atto deliberato di aggiungere dati ridondanti a uno schema precedentemente normalizzato per accelerare le letture e semplificare le query. È un’ottimizzazione mirata delle prestazioni, non una scusa per saltare il buon modelling!
Se sei alle prime armi con il design dei database, può valere la pena scomporre il significato di “normalizzato”, “denormalizzato” e “non normalizzato”. Online si sentono spesso questi 3 termini, ed è importante non confonderli.
Normalizzato: I dati sono divisi in tabelle ben strutturate che minimizzano la ridondanza e proteggono l’integrità (vedi 3NF/BCNF).
Denormalizzato: Reintroduci una ridondanza selettiva con colonne extra, valori precomputati o tabelle pre-joinate, sopra quel modello normalizzato, per rendere più veloci le letture comuni. Con la denormalizzazione, mantieni una singola fonte di verità (le tabelle normalizzate), poi gestisci una o più rappresentazioni più veloci per i percorsi “caldi”, come dashboard, listing di prodotti, ricerca, ecc. Scambi spazio + complessità di scrittura per velocità di lettura + query più semplici.
Non normalizzato: Dati grezzi, ad hoc o disordinati in cui struttura e vincoli non sono mai stati progettati correttamente. Non è ciò che facciamo qui.
Esempio in SQL
Ecco un piccolo esempio in SQL per aiutarti a visualizzare la differenza tra modellazione normalizzata e denormalizzata.
Modellazione normalizzata
Denormalizzato per un percorso di reporting
In questo esempio, manterresti customers come fonte di verità e ti assicureresti che
orders.customer_name / orders.customer_tierrestino sincronizzati (ad es. trigger, job CDC o un backfill pianificato). Stessa verità, due rappresentazioni, ognuna ottimizzata per un lavoro diverso.La denormalizzazione è un cattivo design?
C’è un equivoco diffuso secondo cui la denormalizzazione sia solo il risultato di un cattivo design. Non è così, se è misurata, circoscritta e mantenuta. Sì, la denormalizzazione spesso viola forme normali più alte (è il punto), ma è intenzionale e supportata da un piano per preservare la coerenza. Il cattivo design è saltare del tutto la normalizzazione o spargere ridondanza senza una strategia di sincronizzazione.
Se vuoi saperne di più sul design dei database, il nostro corso per principianti sull’argomento è un buon punto di partenza!
Normalizzazione vs Denormalizzazione
Ecco il confronto affiancato. Non è esaustivo ma ti aiuterà a capire i compromessi di ciascun approccio.
Aspetto
Normalizzazione
Denormalizzazione
Obiettivo principale
Integrità, ridondanza minima, aggiornamenti facili e corretti
Letture più veloci, query più semplici sui percorsi caldi
Ideale per
Sistemi OLTP con scritture/aggiornamenti frequenti
Dashboard a carico di lettura, pagine di ricerca/listing, reporting/analytics
Prestazioni in lettura
Spesso richiede join, buone con gli indici giusti
Meno join, può essere molto più veloce e prevedibile
Complessità in scrittura
Semplice: singola fonte di verità
Maggiore: devi aggiornare/sincronizzare copie o aggregati ridondanti
Storage
Snello
Più grande (colonne/tabelle extra, viste precompute)
Integrità dei dati
Forte per design (vincoli 3NF/BCNF)
Richiede meccanismi per evitare drift (trigger, CDC, job)
Velocità di cambiamento
Rinomine/aggiornamenti di colonne sono localizzati
I cambiamenti possono propagarsi su dati duplicati
Overhead operativo
Più basso: meno parti in movimento
Più alto: politiche di refresh, backfill, monitoraggio
Modalità di guasto
Query N+1, join lenti, indici mancanti
Dati stantii, inconsistenza, amplificazione delle scritture
Evoluzione dello schema
Prevedibile, adatta al refactoring
Servono piani di migrazione per le rappresentazioni ridondanti
Esempi tipici
Ordini, clienti, transazioni
Listing di prodotti pre-joinati; tabelle di vendite aggregate; viste materializzate/indicizzate
Perché e quando denormalizzare
La denormalizzazione diventa davvero utile quando utenti reali e query reali sono bloccati da join, aggregazioni o lookup ripetuti. Una volta confermato che indicizzazione, tuning delle query e caching non bastano, potresti ricorrere alla denormalizzazione per ottimizzare letture rapide su pattern di accesso prevedibili.
Quando la denormalizzazione può aiutare
Casi d’uso classici
Quando non denormalizzare e usare prima qualcos’altro
Vuoi davvero usare la denormalizzazione dove ti dà la massima velocità in lettura con il minimo onere operativo aggiuntivo, e solo dopo aver escluso soluzioni più economiche.
Se ti sei reso conto che la denormalizzazione non è proprio ciò di cui hai bisogno ora, resta qui! Nella prossima sezione esploreremo le alternative alla denormalizzazione.
Alternative alla denormalizzazione
La denormalizzazione non è la risposta a tutti i tuoi problemi. Prima di aggiungere ridondanza, devi spremere tutto ciò che puoi dal motore e dalla tua app. Queste soluzioni sono più economiche da mantenere e spesso offrono gli stessi benefici.
1) Indicizzazione
Indici compositi/covering: Nelle query SQL, metti prima le colonne di filtro, poi le colonne di
GROUP BY/ORDER BY. Includo le colonne della select list così che il motore possa servire la query dal solo indice.Indici filtrati/parziali: Indicizza solo la fetta calda (es.
status = 'ACTIVE'), mantenendo l’indice piccolo e veloce.Indici su espressione/funzionali: Indicizza su
LOWER(email)odate_trunc('day', created_at)per evitare scansioni calcolate.2) Tuning delle query e paginazione
Evita
SELECT *. Recupera solo ciò che mostri.Sostituisci join non necessari con
EXISTS/SEMIquando devi solo verificare la presenza.Spingi i predicati in basso: filtra presto, aggrega tardi.
Usa keyset pagination (
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) per uno scrolling stabile e veloce.3) Caching
4) Repliche di sola lettura
5) Partizionamento e pruning
6) Archivi colonnari/OLAP
7) Igiene dell’ORM
8) Colonne calcolate/generate (gestite dal DB)
Lascia che il DB mantenga i valori derivati (es.
price_with_tax) come colonne generate/calcolate o tramite indici su espressione. Otterrai letture veloci senza logica di sync a livello applicativo.Tecniche di denormalizzazione
Ecco i modi più comuni per aggiungere ridondanza controllata a un modello normalizzato. Per ciascuno, mostrerò cosa fa, quando usarlo e come mantenerlo sincronizzato.
Setup: supponi un core normalizzato con customers, orders, order_items, products.
1) Tabelle “proiezione” appiattite/pre-joinate
Cosa: Crea una tabella che pre-joini le colonne necessarie per letture veloci (es. listing prodotti o dashboard ordini).
Quando: Il tuo percorso caldo fa join tra 3 e 5 tabelle in modi prevedibili.
Come (PostgreSQL):
Opzioni di sync:
2) Colonne ridondanti (copiare alcuni attributi)
Cosa: Duplica un pugno di attributi letti di frequente su un’altra tabella per evitare join (es. orders.customer_name).
Quando: Ti servono solo 1 o 2 valori e non vuoi una proiezione completa.
Come (esempio di trigger PostgreSQL):
Compromesso: Letture semplici, ma le scritture si propagano quando un cliente viene aggiornato.
3) Attributi derivati (archiviare valori calcolati)
Cosa: Conserva valori che altrimenti calcoleresti al volo (es.
price_with_tax,latest_comment_at,item_count).Quando: Il calcolo è eseguito continuamente ed è deterministico.
Due pattern comuni:
4) Tabelle aggregate/di riepilogo
Cosa: Precalcolare rollup come ricavi giornalieri, utenti attivi per giorno, ordini per categoria.
Quando: Le dashboard ripetono gli stessi group-by e le scansioni raw sono costose.
Opzioni di sync: job pianificato (cron/dbt), aggiornamenti in streaming (CDC) o aggregatori event-driven.
5) Viste materializzate (risultati di query persistiti)
Cosa: Archivia l’output di una query costosa come tabella fisica che puoi indicizzare.
Quando: Il result set è costoso, relativamente stabile e puoi tollerare finestre di refresh.
Compromesso: Dovrai gestire la cadenza di refresh perché il refresh incrementale non è nativo in Postgres. Detto ciò, è abbastanza semplice usando dbt o pattern personalizzati “append-only + rebuild finestrato”.
6) Proiezioni “documento” JSON precompute
Cosa: Archivia un blob JSON denormalizzato che corrisponde alle esigenze della tua API/UI (es. product card), insieme a indici JSON mirati.
Quando: La tua API serve ripetutamente la stessa forma e vuoi una sola lettura.
Opzioni di sync: rebuild event-driven su cambi di prodotto/marca/categoria, o refresh batch frequenti per i prodotti aggiornati di recente.
Scegliere una strategia di sync (guida rapida)
Ti consiglio di integrare sempre l’osservabilità nella soluzione. Non c’è niente di peggio che non sapere se le modifiche siano state processate o replicate correttamente. Aiuta anche a gestire i controlli di drift e gli script di backfill.
Come implementare la denormalizzazione passo dopo passo
Come sempre, intervenire sul database è piuttosto rischioso. Ti raccomando vivamente di replicare il database e provare a implementare la denormalizzazione prima sulla replica, per assicurarti che tutto si comporti come ti aspetti. Se hai un ambiente di sviluppo, ancora meglio!
Il processo è abbastanza semplice: misura → cambia il meno possibile → mantienilo corretto → verifica → monitora → ripeti.
1) Profila e imposta un target di successo
2) Scegli la denormalizzazione più piccola che funziona
3) Progetta il percorso di sync
L’idempotenza non si discute. Gli aggiornamenti devono essere ripetibili in sicurezza (ad es.
UPSERTcon ricalcolo deterministico). Fidati, ti ringrazierai più avanti.4) Crea, backfill e valida
4. Correggi le discrepanze; riesegui fino a zero (o entro un budget di errore concordato).
5) Passa in produzione in sicurezza
6) Monitora le cose giuste
Crea una piccola dashboard nel tuo strumento di osservabilità preferito:
7) Operare e iterare
Pro e contro della denormalizzazione
Abbiamo trattato i dettagli nella tabella Normalizzazione vs Denormalizzazione in una sezione precedente, quindi ecco un rapido riepilogo.
Cosa guadagni
Cosa costa
Strumenti e tecnologie che supportano la denormalizzazione
Non tutti i database aiutano a denormalizzare allo stesso modo. Alcuni offrono funzionalità di prima classe per persistere risultati di query costose, altri si aspettano che tu costruisca con job, trigger o pipeline.
PostgreSQL
Postgres ti offre diversi modi per materializzare forme ottimizzate per la lettura.
Le viste materializzate persistono il risultato di una query, quindi le letture sono immediate. Scegli tu quando fare il refresh (a orario o ad hoc) e puoi persino fare refresh “concurrently” così i lettori non vengono bloccati.
Per duplicazioni leggere, colonne generate e indici su espressione consentono al database di mantenere valori derivati e accelerare filtri comuni senza scrivere logica di sync extra.
Quando ti serve coerenza forte tra la fonte e una copia denormalizzata, puoi usare i trigger per tenere tutto allineato (ma ricorda, aggiungono lavoro al percorso di scrittura!). Per refresh event-driven o batch, puoi abbinare Postgres a uno scheduler (es. pg_cron o un job runner esterno) o alla replica logica/CDC per streammare i cambiamenti in un worker che aggiorna asincronamente le tue tabelle denormalizzate.
Se vuoi metterti all’opera e creare i tuoi database e testare la denormalizzazione, dai un’occhiata al nostro corso su PostgreSQL.
SQL Server
SQL Server punta molto sulle viste indicizzate, che sono una sorta di viste materializzate “always-on”, dove il motore mantiene la vista sincronizzata a ogni insert/update/delete, quindi le letture sono super rapide. Il rovescio della medaglia è che le scritture ora mantengono sia le tabelle base sia la vista, quindi carichi OLTP pesanti possono rallentare.
Oltre a ciò, puoi usare colonne calcolate persistenti per coprire attributi derivati su singola riga.
Per il reporting su larga scala, gli indici columnstore sono un’alternativa alla denormalizzazione: comprimono e scansionano dataset grandi in modo molto efficiente, il che può eliminare la necessità di duplicare i dati.
Oracle
Le viste materializzate di Oracle sono un’opzione matura con refresh
FASTche usa change log per aggiornare solo ciò che è cambiato, o refreshCOMPLETEquando va bene ricostruire tutto.Con Query Rewrite, l’ottimizzatore può usare in modo trasparente la tua vista materializzata quando qualcuno interroga le tabelle sottostanti, così i team ottengono il boost senza cambiare la SQL dell’applicazione. Come sempre, letture più veloci significano lavoro extra altrove: mantenere log e fare refresh delle viste aumenta l’overhead di scrittura e operativo.
MySQL / MariaDB
MySQL non ha viste materializzate native, quindi la denormalizzazione di solito si costruisce con tabelle fisiche più job pianificati o trigger per tenerle aggiornate.
Le colonne generate aiutano con valori derivati semplici. Funziona bene per dashboard e riepiloghi prevedibili, ma di nuovo, è utile essere intenzionali su cadenza di refresh e complessità dei trigger per non tassare inavvertitamente i percorsi critici di scrittura. Molti team combinano questo con repliche di lettura per scaricare completamente le query di reporting.
Snowflake / BigQuery (livello analytics)
I warehouse colonnari sono costruiti per dati larghi, adatti alla scansione e denormalizzati. In genere modellerai ampie tabelle di fatti con attributi utili già inclusi, poi farai affidamento su partizionamento/clustering per ridurre ciò che viene scansionato.
Entrambe le piattaforme supportano viste materializzate e task/query pianificate per mantenere freschi gli aggregati senza toccare l’OLTP. Scambi storage e costi di refresh per letture molto prevedibili ed economiche su larga scala. È perfetto per dashboard e BI.
Questo è un po’ più avanzato del nostro corso su PostgreSQL, quindi se ti senti già a tuo agio con i database puoi provare la nostra introduzione al data modeling con Snowflake.
dbt (modellazione e orchestrazione)
dbt è conosciuto come lo strato di “infrastruttura come codice” per analytics denormalizzate. Definisci i modelli una volta, scegli una materializzazione (tabella, vista, incrementale) e lascia che dbt gestisca build, dipendenze e test. I modelli incrementali sono particolarmente utili per tabelle di riepilogo che devono solo aggiungere e unire nuovi dati. È facilmente uno dei miei strumenti preferiti!
CDC e data pipeline
Quando vuoi che i cambiamenti applicativi fluiscano in modo affidabile in strutture denormalizzate, il Change Data Capture è la scelta giusta. Strumenti come Debezium (self-hosted) o connettori gestiti (Fivetran, Airbyte) streammano cambiamenti a livello riga dall’OLTP verso worker o warehouse che aggiornano tabelle di proiezione, contatori o aggregati. In genere è eventualmente consistente (ottimo per dashboard e feed) e scala molto meglio che infilare tutto nei trigger. Rendi gli update idempotenti, monitora il lag e mantieni un percorso di backfill per eventi in ritardo o mancati.
Punti chiave e considerazioni finali
La denormalizzazione è un’ottimizzazione pragmatica sovrapposta a un design normalizzato, non un suo rifiuto. Stai scambiando storage e complessità di scrittura per letture più veloci e semplici su percorsi specifici e ben compresi.
Ricorda: