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.

60 min Debugging

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:

  1. EXPLAIN ANALYZE per vedere tempi reali.
  2. Se “Seq Scan” su tabella grande con condizione → manca indice.
  3. 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_migrations o 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:

  1. Aggiungi colonna nullable.
  2. Backfill.
  3. 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

  1. In un DB locale (anche SQLite), crea due tabelle: utenti e ordini con foreign key.
  2. Inserisci dati sporchi: utente con email NULL, ordine con utente_id inesistente.
  3. Scrivi query che li trovano.
  4. Aggiungi i vincoli mancanti con ALTER TABLE. Cosa succede sui dati sporchi?
  5. Scrivi una migrazione “due fasi” per aggiungere una nuova colonna creato_il TIMESTAMP NOT NULL retroattivamente.

Errori frequenti del principiante

  • Saltare EXPLAIN su query lente.
  • Migrazioni a una sola fase con NOT NULL senza backfill.
  • Confidare in vincoli “del codice” quando il DB non li enforce.
  • Confronti con NULL che ritornano NULL e si comportano come “false”.