Hai mai dovuto aggiungere un campo a un database relazionale solo per un cliente, e ti sei ritrovato con venti colonne sparse, metà delle quali nulle? Succede quando i dati non hanno una forma fissa. E-commerce con attributi prodotto variabili, log di eventi eterogenei, metadati utente che cambiano senza preavviso — il mondo reale non si incastra in uno schema rigido. Noi, di Meteora Web, ci siamo passati decine di volte: gestire un magazzino con stagioni diverse, sconti e promozioni che nascono e muoiono, oppure tracciare conversioni pubblicitarie con campi che ogni piattaforma chiama in modo diverso.
PostgreSQL offre una soluzione che non è un compromesso: JSONB. Un formato binario per dati JSON che si integra con il motore relazionale, supporta vincoli, transazioni e indici avanzati. Niente database NoSQL aggiuntivi, niente doppia verità. In questa guida vediamo come usare JSONB per schemi flessibili senza sacrificare le performance. Codice reale, query vere, niente slide da conferenza.
Perché JSONB e non un semplice TEXT o JSON
PostgreSQL ha due tipi per dati JSON: json e jsonb. La differenza è tutt'altro che accademica. json salva il testo esatto, compresi spazi e ordine delle chiavi. jsonb lo converte in una rappresentazione binaria decompressa e normalizzata: ordina le chiavi e rimuove i duplicati. Significa che:
- Le query sono più veloci: non serve rieseguire il parsing a ogni accesso.
- Si possono creare indici GIN, GiST e B-tree su espressioni.
- Supporta operatori nativi come
@>,?,?|,?&per interrogare la struttura. - Consuma più spazio su disco (circa il 10-20% in più del corrispondente testo), ma il guadagno in performance vale la pena per qualsiasi carico di lavoro realistico.
Regola pratica: usa sempre jsonb tranne quando devi preservare l'ordine esatto delle chiavi (casi rari di hash di firma).
Creare e popolare una tabella con JSONB
Partiamo da un esempio concreto. Immagina un'applicazione che gestisce contratti per clienti diversi: ogni contratto ha campi fissi (cliente, data) e una sezione variabile (clausole personalizzate, allegati, date di rinnovo).
CREATE TABLE contratti (
id SERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clienti(id),
data_stipula DATE NOT NULL,
dati_variabili JSONB NOT NULL DEFAULT '{}'
);
-- Inseriamo un contratto con clausole variabili
INSERT INTO contratti (cliente_id, data_stipula, dati_variabili)
VALUES (
42,
'2026-03-01',
'{ "tipo": "annuale", "rinnovo_automatico": true, "sconto_fedelta": 15, "allegati": ["condizioni_generali.pdf", "informativa_privacy.pdf"] }'
);Nota: il valore JSONB è un oggetto con tipi misti (booleano, numero, array). Postgres li accetta senza problemi. Nessun migration, nessuna colonna aggiuntiva.
Operatori fondamentali per interrogare JSONB
Ecco gli operatori che usiamo ogni giorno:
-> (text)→ restituisce il campo come JSON (null, oggetto, array).->> (text)→ restituisce il campo come testo (stringa).#> (text[])→ percorso annidato, restituisce JSON.#>> (text[])→ percorso annidato, restituisce testo.@>→ verifica se un documento JSONB contiene un altro (substrato).?→ esiste una chiave al primo livello.?|→ esiste almeno una delle chiavi.?&→ esistono tutte le chiavi.
Qualche query sul nostro esempio:
-- Contratti con rinnovo automatico
SELECT * FROM contratti
WHERE dati_variabili -> 'rinnovo_automatico' = 'true'::jsonb;
-- Contratti con sconto fedeltà maggiore di 10
SELECT * FROM contratti
WHERE (dati_variabili ->> 'sconto_fedelta')::numeric > 10;
-- Contratti che contengono l'allegato 'condizioni_generali.pdf'
SELECT * FROM contratti
WHERE dati_variabili @> '{ "allegati": ["condizioni_generali.pdf"] }'::jsonb;
Attenzione: l'operatore @> verifica la presenza dell'intero substrato, non una singola occorrenza in un array. Per cercare all'interno di array usa jsonb_array_elements() oppure un indice GIN con jsonb_path_ops.
Indicizzare JSONB per non farci male
Senza indice, ogni query JSONB fa una scansione sequenziale. Se hai migliaia di righe, il database diventa un materasso. Postgres offre due tipi di indice GIN per JSONB:
- Default (
jsonb_ops): indicizza ogni chiave, valore e percorso. Supporta@>,?,?|,?&. jsonb_path_ops: indicizza solo i percorsi come B-tree compresso. Migliore per le query@>(da 2 a 5 volte più veloce), ma non supporta operatori di esistenza chiave (?).
-- Indice GIN jsonb_ops (default)
CREATE INDEX idx_contratti_dati_jsonb ON contratti USING GIN (dati_variabili);
-- Indice GIN jsonb_path_ops (più veloce per @>)
CREATE INDEX idx_contratti_dati_path ON contratti USING GIN (dati_variabili jsonb_path_ops);
Se hai query che usano sia @> sia ?, tieni l'indice default. Se invece cerchi solo sotto-strutture (tipico quando ogni documento ha una forma ben definita ma variabile), jsonb_path_ops è la scelta giusta.
Manipolare JSONB: aggiornare e modificare
Anche con uno schema flessibile, a volte devi aggiornare un singolo campo senza riscrivere l'intero documento. Postgres mette a disposizione funzioni come jsonb_set() e jsonb_insert().
-- Aggiornare il valore di sconto_fedelta
UPDATE contratti
SET dati_variabili = jsonb_set(
dati_variabili,
'{sconto_fedelta}',
'20'::jsonb,
true -- crea la chiave se non esiste
)
WHERE id = 1;
-- Aggiungere un nuovo allegato
UPDATE contratti
SET dati_variabili = jsonb_set(
dati_variabili,
'{allegati}',
dati_variabili -> 'allegati' || '"nuovo_documento.pdf"'::jsonb,
true
)
WHERE id = 1;
Attenzione: ogni modifica a un campo JSONB riscrive l'intero valore. Per documenti grandi e aggiornamenti frequenti, considera di normalizzare la parte variabile in righe separate con una tabella attributi_contratto.
Pitfall comuni con JSONB
1. Fingere che JSONB sia un database documentale puro
JSONB non è MongoDB. Non supporta join nativi tra documenti, non ha atomicità cross-document e le query complesse su array nidificati possono essere lente anche con indice. Noi abbiamo visto progetti in cui hanno spinto tutto in un unico campo JSONB, per poi dover riscrivere l'applicazione dopo sei mesi perché le performance degradavano. Usa JSONB per dati che cambiano forma, non per relazioni.
2. Ignorare i tipi nelle query
Quando usi ->> ottieni sempre testo. Se confronti con un numero, devi castare. Dimenticarselo porta a confronti lessicografici sbagliati.
-- SBAGLIATO: sconto_fedelta come testo
WHERE dati_variabili ->> 'sconto_fedelta' > '10'
-- Restituisce '9' se presente? Sì! Perché '9' > '10' in ordine stringa.
-- CORRETTO: cast esplicito
WHERE (dati_variabili ->> 'sconto_fedelta')::numeric > 10
3. Creare indici su campi non usati nelle query
Un indice GIN su JSONB non serve se le tue query filtrano solo su colonne relazionali. Misura sempre con EXPLAIN ANALYZE prima e dopo.
Quando invece meglio una colonna relazionale classica?
JSONB è fantastico, ma non è la risposta a tutto. Non usarlo per:
- Dati che hanno una struttura fissa e stabile (es. nome, cognome, email).
- Campi che servono in chiave esterna o vincoli di unicità (posta una colonna separata).
- Attributi che vengono interrogati frequentemente come filtro principale (meglio colonne indicizzate con B-tree).
La regola che seguiamo noi: relazionale per le entità canoniche, JSONB per le eccezioni e la configurazione dinamica. Un prodotto ha nome e prezzo in colonne fisse (per poter fare aggregazioni e join), e attributi extra (colore, taglio, materiali) in JSONB.
In sintesi — Cosa fare adesso
- Analizza i tuoi dati variabili: ci sono campi che cambiano per tipologia di record? Se sì, spostali in una colonna JSONB.
- Crea un indice GIN subito dopo il deploy. Meglio
jsonb_path_opsse usi@>. - Scrivi le query con il cast esplicito per i numeri, e usa
EXPLAIN ANALYZEper verificare che l'indice venga usato. - Non normalizzare tutto in JSONB: tieni le colonne relazionali per i dati strutturati e indicizzati.
- Progetta l'accesso: se devi aggiornare spesso un singolo campo JSONB, considera una tabella separata con chiave-valore.
JSONB è uno strumento potente se usato con disciplina. Noi lo abbiamo integrato in piattaforme che gestiscono cataloghi di migliaia di prodotti con attributi diversi per categoria, e funziona. Come dicevamo: un sito si misura in fatturato, non in complimenti. Un database che scala senza dolore è parte di quel fatturato. Se vuoi approfondire, il manuale ufficiale di PostgreSQL su JSON è la bibbia. E se hai un progetto in cui JSONB potrebbe essere la soluzione, parliamone.
Sponsored Protocol