modulo 08 / lezione 42
Debugging di dati, backend e integrazioni
Query, migrazioni e dati sporchi
Riconoscere bug causati da ordinamenti, join, vincoli mancanti, migrazioni incomplete e record incoerenti.
In parole semplici
Quando il bug non è nel codice ma nei dati, la cura cambia: non basta correggere una funzione, serve capire e ripulire ciò che è già salvato. Query mal scritte, migrazioni incomplete, vincoli mancanti producono dati incoerenti che si comportano “stranamente” anche con codice corretto.
Parole nuove
- query: interrogazione al database (SELECT, INSERT, UPDATE, DELETE).
- migrazione: modifica strutturale del database (nuova colonna, indice, vincolo).
- vincolo (constraint): regola che il database fa rispettare (NOT NULL, UNIQUE, FK).
- dati sporchi: record che violano regole implicite o esplicite.
- EXPLAIN: piano di esecuzione di una query.
Cosa impari
- Diagnosticare query lente o errate con
EXPLAIN. - Riconoscere migrazioni applicate parzialmente.
- Trovare record che violano vincoli “morali” non scritti.
Anti-pattern delle query
1. ORDER BY assente
SELECT * FROM ordini WHERE utente_id = 42 LIMIT 10;
Senza ORDER BY, il DB ritorna un sottoinsieme. Non sempre lo stesso. Bug intermittente garantito.
Fix:
SELECT * FROM ordini WHERE utente_id = 42 ORDER BY data_creazione DESC LIMIT 10;
2. JOIN che produce duplicati
SELECT u.nome, o.totale
FROM utenti u
JOIN ordini o ON u.id = o.utente_id;
Se un utente ha 3 ordini, appare 3 volte. Quando il client vuole una riga per utente:
SELECT u.nome, SUM(o.totale)
FROM utenti u LEFT JOIN ordini o ON u.id = o.utente_id
GROUP BY u.id, u.nome;
3. NULL in confronti
SELECT * FROM utenti WHERE email != 'admin@x.com';
-- non ritorna utenti con email NULL
NULL in SQL è “sconosciuto”, non “vuoto”. Confronti con NULL ritornano NULL, non true. Per includere:
WHERE email != 'admin@x.com' OR email IS NULL
4. SQL injection (lato sicurezza)
db.query(`SELECT * FROM utenti WHERE id = ${id}`); // BUG GRAVE
id può contenere 1 OR 1=1. Sempre parametri:
db.query("SELECT * FROM utenti WHERE id = ?", [id]);
EXPLAIN: come il DB esegue la query
EXPLAIN SELECT * FROM ordini WHERE utente_id = 42 ORDER BY data DESC;
Output mostra:
- Sequential scan: legge tutta la tabella. Lento per tabelle grandi.
- Index scan: usa un indice. Veloce.
- Hash join, Nested loop: strategie di join con costi diversi.
Quando una query è lenta:
EXPLAIN ANALYZEper vedere tempi reali.- Se “Seq Scan” su tabella grande con condizione → manca indice.
- Se “Hash join” con merge su milioni di righe → query ristrutturabile.
Migrazioni incomplete
Scenario: una migrazione aggiunge la colonna email_verificata BOOLEAN. Il DB nuovo ha la colonna. Ambienti vecchi no. Codice che legge email_verificata fallisce su uno solo.
Diagnosi:
- Vedi lo stato delle migrazioni applicate (tabella
schema_migrationso equivalente). - Confronta tra ambienti.
- Riapplica.
Bug correlato: dati “rolled back” senza dati. La migrazione aggiunge la colonna ma non popola gli esistenti. Per evitare:
ALTER TABLE utenti ADD COLUMN email_verificata BOOLEAN DEFAULT false NOT NULL;
UPDATE utenti SET email_verificata = false WHERE email_verificata IS NULL;
Oppure migrazione in due fasi:
- Aggiungi colonna nullable.
- Backfill.
- Imposta NOT NULL.
Vincoli “morali” non scritti
Il codice assume:
- Ogni utente ha un’email.
- Ogni ordine ha un utente esistente.
- Le date sono in UTC.
Se non c’è vincolo NOT NULL, foreign key, controllo formato, prima o poi un dato “sporco” entra e il codice esplode.
Strumenti:
- NOT NULL: per campi obbligatori.
- FOREIGN KEY: per garantire integrità referenziale.
- CHECK: per condizioni semantiche (
CHECK (eta >= 0)). - UNIQUE: per chiavi alternative.
Trovare dati sporchi
Query di scoperta:
-- utenti senza email
SELECT id FROM utenti WHERE email IS NULL OR email = '';
-- ordini con utente_id che non esiste (FK mancante)
SELECT o.id FROM ordini o LEFT JOIN utenti u ON o.utente_id = u.id WHERE u.id IS NULL;
-- date nel futuro
SELECT id FROM eventi WHERE data > NOW();
-- duplicati
SELECT email, COUNT(*) FROM utenti GROUP BY email HAVING COUNT(*) > 1;
Una volta trovati, decidi: ripulisci, accetta, o riformula il vincolo.
Anonimizzazione per debug
Per indagare bug con dati reali senza esportarli:
- Dump anonimizzato: esporta una vista con
random()su campi sensibili. - Subset: solo i 10 record che causano il bug.
Mai mettere dati di produzione nei repo. Sempre anonimizzare.
Esercizio guidato
- In un DB locale (anche SQLite), crea due tabelle:
utentieordinicon foreign key. - Inserisci dati sporchi: utente con email NULL, ordine con utente_id inesistente.
- Scrivi query che li trovano.
- Aggiungi i vincoli mancanti con
ALTER TABLE. Cosa succede sui dati sporchi? - Scrivi una migrazione “due fasi” per aggiungere una nuova colonna
creato_il TIMESTAMP NOT NULLretroattivamente.
Errori frequenti del principiante
- Saltare
EXPLAINsu query lente. - Migrazioni a una sola fase con
NOT NULLsenza backfill. - Confidare in vincoli “del codice” quando il DB non li enforce.
- Confronti con NULL che ritornano NULL e si comportano come “false”.