PostgreSQL per sviluppatori PHP: quando sceglierlo rispetto a MySQL e come migrare

PostgreSQL per sviluppatori PHP: quando sceglierlo rispetto a MySQL e come migrare

Nel 2024 e 2025 ho migrato due applicazioni Laravel da MySQL a PostgreSQL per altrettanti clienti - un sistema di gestione contratti per un'azienda del settore servizi finanziari e una piattaforma di catalogo prodotti per un'azienda del settore distribuzione industriale. In entrambi i casi, la migrazione non è stata motivata da un problema di performance di MySQL (che con il tuning corretto è un database eccellente per la maggior parte dei workload), ma da requisiti funzionali specifici che PostgreSQL gestisce nativamente e MySQL no: nel primo caso, la necessità di transazioni ACID con isolamento serializable per operazioni finanziarie concorrenti che MySQL InnoDB gestiva con lock eccessivi; nel secondo caso, la necessità di JSONB nativo per dati di catalogo semi-strutturati (specifiche tecniche che variano per categoria di prodotto) e di full-text search in italiano senza dipendere da un'istanza Elasticsearch separata.

La decisione di migrare un database in produzione non è mai leggera - è un'operazione ad alto rischio che tocca il cuore dell'applicazione. Ma la decisione di non migrare quando i requisiti lo richiedono è peggiore: significa costruire workaround sempre più fragili sopra un database che non è progettato per quel caso d'uso. In questo articolo condivido le differenze pratiche tra MySQL e PostgreSQL che ho sperimentato nel codice quotidiano - non la teoria accademica sui modelli relazionali, ma le situazioni concrete dove la scelta del database cambia il modo in cui scrivi codice PHP e la robustezza del risultato.

Quando ha senso scegliere PostgreSQL rispetto a MySQL per un'applicazione PHP?

La risposta non è "PostgreSQL è meglio" - è "PostgreSQL è meglio per certi casi d'uso." MySQL resta la scelta eccellente per la maggioranza delle applicazioni web PHP: è più semplice da installare e gestire, ha un ecosistema di hosting più ampio (quasi ogni provider condiviso offre MySQL, non tutti offrono PostgreSQL), la community PHP lo conosce meglio, e per i workload OLTP standard - CRUD, e-commerce, CMS, gestionali con traffico medio - le prestazioni sono equivalenti o superiori a PostgreSQL grazie al tuning maturo di InnoDB.

PostgreSQL diventa la scelta migliore in quattro scenari specifici che ho incontrato nella pratica:

Il primo scenario è quando hai bisogno di dati semi-strutturati con query. Il tipo JSONB di PostgreSQL non è un semplice campo di testo con JSON dentro (come il JSON type di MySQL 5.7+) - è un formato binario indicizzabile che supporta operatori di query, indici GIN per ricerche efficienti all'interno del JSON, e aggiornamenti parziali del documento senza riscrivere l'intero campo. Per il catalogo prodotti del cliente distribuzione, ogni prodotto aveva 15-80 specifiche tecniche che variavano completamente per categoria: un motore elettrico ha potenza, voltaggio, RPM e classe di isolamento; un cuscinetto ha diametro interno, esterno, tipo di lubrificazione e carico dinamico. Con MySQL, le opzioni erano una tabella EAV (Entity-Attribute-Value, un antipattern di performance noto) o N tabelle per N categorie. Con PostgreSQL JSONB, le specifiche vivono in un singolo campo specifiche jsonb indicizzato, e la query WHERE specifiche->>'potenza_kw' > '5.5' è veloce grazie all'indice GIN.

Il secondo scenario è la full-text search nativa. PostgreSQL ha un motore di full-text search integrato con supporto per stemming multilingua (incluso l'italiano), ranking dei risultati e highlight dei match - funzionalità che con MySQL richiedono Elasticsearch o Meilisearch come servizio separato. Per il catalogo con 45.000 prodotti, la ricerca testuale su nome, descrizione e specifiche tecniche funziona con una singola query PostgreSQL e un indice tsvector, senza nessun servizio esterno da mantenere, sincronizzare e monitorare. Il costo eliminato: il server Elasticsearch che il cliente pagava 25 euro al mese e che richiedeva manutenzione separata.

Il terzo scenario riguarda le transazioni con isolamento forte. MySQL InnoDB supporta i livelli di isolamento READ COMMITTED e REPEATABLE READ, ma il livello SERIALIZABLE (che garantisce che le transazioni concorrenti producano lo stesso risultato come se fossero eseguite in sequenza) è implementato con lock esclusivi che degradano pesantemente le prestazioni sotto carico concorrente. PostgreSQL implementa SERIALIZABLE con un meccanismo di Serializable Snapshot Isolation (SSI) basato sulla rilevazione dei conflitti, non sui lock - il che significa che le transazioni concorrenti procedono senza bloccarsi e vengono serializzate solo se rilevano un conflitto reale. Per il sistema di gestione contratti con operazioni finanziarie concorrenti, questa differenza era critica: con MySQL SERIALIZABLE, due operazioni sullo stesso conto si bloccavano reciprocamente; con PostgreSQL SSI, procedevano in parallelo e solo in caso di conflitto reale una veniva ritentata.

Il quarto scenario è quando hai bisogno di funzionalità SQL avanzate che MySQL non supporta o supporta parzialmente: CTE ricorsive con materializzazione controllata, window function con frame specification completa, lateral join, array nativi come tipo di colonna, range type per intervalli temporali, e generated column con espressioni complesse. Nel mio profilo professionale trovi il dettaglio dell'esperienza su entrambi i database - la scelta tra MySQL e PostgreSQL è una decisione architetturale che faccio basandomi sui requisiti specifici del progetto, non su preferenze personali.

Le differenze pratiche nel codice Laravel quotidiano

La migrazione del driver di database in Laravel è una modifica di configurazione nel .env (DB_CONNECTION=pgsql invece di DB_CONNECTION=mysql) - Laravel astrae le differenze del database attraverso il query builder e Eloquent. Ma l'astrazione non è completa: ci sono differenze nel comportamento che emergono nel codice quotidiano e che, se non gestite, causano bug sottili.

La prima differenza è il case sensitivity. MySQL con la collation di default (utf8mb4_0900_ai_ci) confronta le stringhe in modo case-insensitive: WHERE nome = 'Mario' trova anche 'mario' e 'MARIO'. PostgreSQL confronta le stringhe in modo case-sensitive per default: WHERE nome = 'Mario' non trova 'mario'. Se la tua applicazione si appoggia al case-insensitive di MySQL nelle query di ricerca (cosa molto comune), la migrazione a PostgreSQL rompe tutte quelle query. La fix è usare ILIKE invece di LIKE e lower() nei confronti - in Laravel, ->where('nome', 'ilike', "%{$search}%") funziona solo con il driver PostgreSQL, mentre ->whereRaw('lower(nome) = lower(?)', [$search]) è portabile.

La seconda differenza è il group by strict. PostgreSQL richiede che tutte le colonne nel SELECT che non sono aggregate siano presenti nel GROUP BY - un requisito dello standard SQL che MySQL nella modalità di default ignora (restituendo un valore arbitrario per le colonne non raggruppate). Se hai query Eloquent con groupBy('categoria_id')->select(['categoria_id', 'nome_categoria', DB::raw('SUM(importo)')]), PostgreSQL lancia un errore perché nome_categoria non è nel GROUP BY e non è aggregata. La fix è aggiungere tutte le colonne non aggregate al GROUP BY o usare DISTINCT ON (una feature esclusiva di PostgreSQL che non esiste in MySQL).

La terza differenza è il boolean handling. MySQL tratta i booleani come TINYINT(1) - i valori sono 0 e 1. PostgreSQL ha un tipo BOOLEAN nativo con valori true e false. Se il tuo codice confronta un campo booleano con === 1 o === 0, funziona con MySQL ma fallisce con PostgreSQL perché il valore è true/false, non 1/0. Eloquent gestisce la conversione con il cast 'boolean', ma le query raw e i confronti diretti devono essere aggiornati.

Il processo di migrazione: come ho spostato 2,4 milioni di record senza downtime

La migrazione del database di produzione del sistema contratti (2,4 milioni di record, 45 tabelle, 89 foreign key) ha seguito un processo in cinque fasi che ha garantito zero downtime per gli utenti.

La prima fase è stata la preparazione dello schema PostgreSQL. Non ho usato pg_dump o tool di conversione automatica - ho rigenerato lo schema da zero con le migration Laravel, eseguendole contro il driver pgsql. Le migration Laravel sono (quasi) portabili tra database, ma alcune richiedono adattamenti: i campi UNSIGNED non esistono in PostgreSQL, le colonne ENUM di MySQL diventano check constraint o enum type in PostgreSQL, e gli indici fulltext di MySQL diventano indici GIN su colonne tsvector. Ho adattato 12 migration su 67 - il 18% del totale.

La seconda fase è stata il dump e import dei dati con pgloader - uno strumento open source che legge da MySQL e scrive in PostgreSQL con conversione automatica dei tipi, gestione delle sequenze (auto-increment diventa SERIAL), e parallelismo configurabile. Per 2,4 milioni di record, il trasferimento ha richiesto 8 minuti su una connessione locale tra i due database sullo stesso server.

La terza fase è stata il test funzionale completo dell'applicazione su PostgreSQL - ogni endpoint, ogni form, ogni report. Ho trovato 14 incompatibilità (tutte nelle categorie descritte sopra: case sensitivity, group by strict, boolean handling, e 3 query raw con sintassi MySQL-specific) che ho corretto in 6 ore di lavoro.

La quarta fase è stata il dual-write temporaneo: l'applicazione scriveva su entrambi i database (MySQL e PostgreSQL) per 48 ore, con un job di riconciliazione notturno che verificava che i dati fossero identici. Nessuna discrepanza trovata dopo 48 ore - la migrazione era pronta.

La quinta fase è stata lo switch: cambiare DB_CONNECTION=pgsql nel .env, riavviare FPM, e disabilitare il dual-write. Il downtime totale: 3 secondi per il reload di PHP-FPM. Ho descritto un approccio analogo alla migrazione con zero downtime nel mio articolo sulla migrazione sicura di VPS senza interruzione del servizio - il principio è identico: preparazione parallela, test completo, switch istantaneo, rollback pronto.

JSONB in pratica: il catalogo prodotti con specifiche variabili

Per il catalogo del cliente distribuzione, la colonna JSONB ha trasformato il modello dati. Invece di una struttura EAV con tre tabelle (prodotti, attributi, valori_attributi) e query con N JOIN per ricostruire le specifiche di un prodotto, ho una singola tabella prodotti con una colonna specifiche jsonb che contiene le specifiche tecniche come documento JSON:

-- Query PostgreSQL su campo JSONB con indice GIN
-- Trova i motori elettrici con potenza > 5.5 kW e classe IE3
SELECT nome, codice, specifiche->>'potenza_kw' AS potenza
FROM prodotti
WHERE categoria = 'motori_elettrici'
  AND (specifiche->>'potenza_kw')::numeric > 5.5
  AND specifiche->>'classe_efficienza' = 'IE3'
ORDER BY (specifiche->>'potenza_kw')::numeric DESC;

In Laravel Eloquent, la stessa query diventa:

$motori = Prodotto::where('categoria', 'motori_elettrici')
    ->whereRaw("(specifiche->>'potenza_kw')::numeric > ?", [5.5])
    ->where('specifiche->classe_efficienza', 'IE3')
    ->orderByRaw("(specifiche->>'potenza_kw')::numeric DESC")
    ->get();

L'indice GIN sulla colonna specifiche (CREATE INDEX idx_specifiche ON prodotti USING GIN (specifiche)) rende queste query efficienti anche su tabelle con centinaia di migliaia di record - il tempo di risposta per la query sopra su 45.000 prodotti è di 12 ms, comparabile a un join tradizionale su tabelle normalizzate. Il vantaggio rispetto all'EAV non è solo nelle prestazioni: è nella semplicità del modello. Aggiungere una nuova specifica tecnica per una nuova categoria di prodotto non richiede migration, non richiede aggiunta di righe nella tabella attributi, non richiede modifiche al codice - basta inserire il campo nel JSON. La documentazione ufficiale di PostgreSQL su JSONB descrive in dettaglio gli operatori e le funzioni disponibili - un set molto più ricco di quello offerto da MySQL JSON.

La scelta tra MySQL e PostgreSQL non è una questione di fede - è una questione di requisiti. Se la tua applicazione ha bisogno di dati semi-strutturati, full-text search nativo, transazioni con isolamento forte o funzionalità SQL avanzate, PostgreSQL è probabilmente la scelta migliore. Per tutto il resto - e "il resto" copre la maggioranza delle applicazioni web PHP - MySQL è perfettamente adeguato e spesso preferibile per la semplicità operativa. Se stai valutando una migrazione da MySQL a PostgreSQL per la tua applicazione Laravel, contattami per un assessment: in una giornata analizziamo i requisiti, identifichiamo le incompatibilità nel codice, e stimiamo tempi e rischi della migrazione con un piano dettagliato che include test funzionali, dual-write e strategia di rollback per ogni fase del processo.

Ultima modifica: