Cos'è SQL: il linguaggio per interrogare i dati
Cos'è SQL e perché è un linguaggio dichiarativo: SELECT, WHERE, i JOIN spiegati bene, GROUP BY, gli indici e gli errori che fanno perdere più tempo.
Hai imparato un linguaggio di programmazione, sai scrivere cicli e funzioni, e poi apri il primo file .sql e ti sembra di leggere un'altra materia. La sensazione è giusta: SQL funziona secondo un principio diverso da tutto il resto. In questo articolo trovi qual è quel principio, le operazioni che userai davvero e i due o tre punti in cui si perde più tempo.
Cos'è SQL
SQL è un linguaggio dichiarativo per interrogare e modificare i dati di un database relazionale: descrivi il risultato che vuoi, non i passaggi per ottenerlo.
Sta per Structured Query Language, si pronuncia "esse-qu-elle" oppure "sìquel", e in italiano si usano entrambe senza che nessuno si offenda.
Qui parliamo del linguaggio. Se non ti è chiaro cosa sia il contenitore che interroga, parti da cos'è un database; se invece stai decidendo tra relazionale e non relazionale, quella discussione è in SQL vs NoSQL.
Dichiarativo: la differenza che conta
È il concetto che va afferrato prima di qualunque sintassi, perché è l'unico punto in cui SQL si comporta in modo davvero diverso dai linguaggi che conosci.
In JavaScript, Python o Java descrivi la procedura. Per trovare i clienti di Milano ordinati per fatturato: apri la lista, scorri ogni elemento, controlli la città, metti da parte quelli buoni, poi ordini. Dici al computer come fare.
In SQL descrivi il risultato:
SELECT nome, fatturato
FROM clienti
WHERE citta = 'Milano'
ORDER BY fatturato DESC;
Nessun ciclo, nessuna variabile temporanea. Hai detto cosa vuoi. Il come lo decide il database.
E lo decide bene. Dentro c'è un componente chiamato query planner che, per ogni interrogazione, valuta più strategie possibili — scorrere tutta la tabella, usare un indice, quale tabella leggere per prima in un join — stima il costo di ciascuna sulla base delle statistiche che ha sui dati, e sceglie. La stessa identica query può essere eseguita in due modi completamente diversi a distanza di un mese, perché nel frattempo la tabella è cresciuta e la strategia migliore è cambiata.
Questa è anche la ragione per cui ottimizzare SQL è un'attività particolare: non riscrivi l'algoritmo, dai al database informazioni e strumenti migliori — un indice, statistiche aggiornate — perché ne scelga uno migliore da solo.
Le operazioni di base
SELECT, WHERE, ORDER BY
Lo scheletro di ogni interrogazione:
SELECT nome, email, data_registrazione
FROM utenti
WHERE attivo = true
AND data_registrazione >= '2026-01-01'
ORDER BY data_registrazione DESC
LIMIT 20;
Da leggere così: prendi queste colonne, dalla tabella utenti, dove valgono queste condizioni, ordinando per data decrescente, fermandoti a venti risultati.
Qualche filtro che userai continuamente:
WHERE citta IN ('Milano', 'Roma', 'Torino')
WHERE prezzo BETWEEN 10 AND 50
WHERE email LIKE '%@gmail.com'
WHERE telefono IS NULL
Nota IS NULL e non = NULL. In SQL NULL significa "valore sconosciuto", e un confronto con uno sconosciuto non restituisce né vero né falso: restituisce sconosciuto. citta = NULL non è mai vero, nemmeno per le righe dove la città manca. È l'errore che fa impazzire tutti la prima volta.
I JOIN: lo scoglio vero
Qui si ferma la maggior parte di chi impara SQL, e vale la pena rallentare.
I dati relazionali vivono divisi in tabelle collegate. Gli utenti stanno in utenti, i loro ordini in ordini, e ogni ordine ha una colonna utente_id che dice a chi appartiene. Un JOIN li rimette insieme in un'unica risposta.
Prendiamo tre utenti e due ordini:
| utenti | ordini | ||||
|---|---|---|---|---|---|
| id | nome | id | utente_id | totale | |
| 1 | Mario | 10 | 1 | 49.90 | |
| 2 | Lucia | 11 | 1 | 120.00 | |
| 3 | Anna |
Lucia e Anna non hanno mai ordinato.
INNER JOIN — solo le corrispondenze:
SELECT u.nome, o.totale
FROM utenti u
INNER JOIN ordini o ON o.utente_id = u.id;
| nome | totale |
|---|---|
| Mario | 49.90 |
| Mario | 120.00 |
Lucia e Anna spariscono. Non hanno ordini, quindi non hanno corrispondenza, quindi non compaiono. Nota anche che Mario compare due volte: una riga per ogni ordine. Un join non restituisce "gli utenti", restituisce le combinazioni.
LEFT JOIN — tutte le righe di sinistra, comunque:
SELECT u.nome, o.totale
FROM utenti u
LEFT JOIN ordini o ON o.utente_id = u.id;
| nome | totale |
|---|---|
| Mario | 49.90 |
| Mario | 120.00 |
| Lucia | NULL |
| Anna | NULL |
Tutti gli utenti restano. Dove manca l'ordine, le colonne di destra valgono NULL.
La differenza in una frase: INNER JOIN risponde a "chi ha ordinato", LEFT JOIN risponde a "tutti gli utenti, e cosa hanno ordinato se hanno ordinato". Scegliere quello sbagliato non genera un errore: genera un risultato plausibile e falso, che è molto peggio.
Una trappola conseguente: un WHERE su una colonna della tabella destra dopo un LEFT JOIN lo trasforma di fatto in un INNER JOIN, perché NULL non supera la condizione. Se vuoi filtrare tenendo le righe senza corrispondenza, la condizione va messa nell'ON.
Esistono anche RIGHT JOIN (lo stesso al contrario, si usa poco) e FULL OUTER JOIN (tutto da entrambe le parti, raro).
GROUP BY e le aggregazioni
Servono a rispondere a domande di sintesi: quanti, quanto, la media.
SELECT u.nome,
COUNT(o.id) AS numero_ordini,
SUM(o.totale) AS speso_totale,
AVG(o.totale) AS scontrino_medio
FROM utenti u
LEFT JOIN ordini o ON o.utente_id = u.id
GROUP BY u.id, u.nome
HAVING COUNT(o.id) > 0
ORDER BY speso_totale DESC;
GROUP BY raggruppa le righe, le funzioni di aggregazione (COUNT, SUM, AVG, MIN, MAX) calcolano un valore per ogni gruppo.
WHERE e HAVING non sono sinonimi: WHERE filtra le righe prima del raggruppamento, HAVING filtra i gruppi dopo. Non puoi scrivere WHERE COUNT(...) > 0 perché nel momento in cui WHERE agisce il conteggio non esiste ancora.
INSERT, UPDATE, DELETE
Le tre operazioni che modificano.
INSERT INTO utenti (nome, email, attivo)
VALUES ('Giulia', 'giulia@esempio.it', true);
UPDATE utenti
SET attivo = false
WHERE ultimo_accesso < '2025-01-01';
DELETE FROM sessioni
WHERE scadenza < NOW();
Il WHERE su UPDATE e DELETE non è opzionale nella pratica. Ometterlo è sintatticamente lecito e colpisce ogni riga della tabella. È l'incidente più famoso del mestiere, ed è successo a chiunque abbia lavorato abbastanza.
L'abitudine che salva: scrivi prima la stessa condizione con SELECT, guarda cosa torna, e solo dopo sostituisci SELECT * con UPDATE ... SET o DELETE.
Gli indici: perché la query è lenta
Quasi ogni volta che una query "improvvisamente" diventa lenta, la causa è la stessa: manca un indice.
Senza indice, per trovare le righe che soddisfano WHERE email = 'x@y.it' il database deve leggere tutta la tabella, riga per riga. Su mille righe è istantaneo, su cinque milioni no. Un indice è una struttura ordinata a parte che permette di saltare direttamente al punto giusto — concettualmente è la stessa idea dell'indice analitico in fondo a un libro.
CREATE INDEX idx_utenti_email ON utenti (email);
Per capire cosa sta facendo davvero il database, esiste lo strumento giusto:
EXPLAIN ANALYZE
SELECT * FROM utenti WHERE email = 'mario@esempio.it';
Ti mostra la strategia scelta e i tempi reali. Se leggi Seq Scan su una tabella grande, hai trovato il problema.
Gli indici non sono gratuiti: occupano spazio e rallentano le scritture, perché ogni inserimento deve aggiornarli. Indicizza le colonne che compaiono in WHERE, JOIN e ORDER BY, non tutte per prudenza. Il tema più ampio del rendere veloci le letture è in cos'è il caching, e la scelta del prodotto in quale database scegliere.
SQL injection, in una riga
Se costruisci una query concatenando stringhe con dati che arrivano da un utente, quell'utente può riscrivere la tua query. Usa sempre query parametrizzate — mai concatenazione — e la spiegazione completa con gli esempi è in cos'è la SQL injection.
Errori che fanno perdere ore
| Errore | Cosa succede |
|---|---|
= NULL invece di IS NULL | Zero risultati, nessun errore |
| INNER al posto di LEFT JOIN | Righe che spariscono senza avviso |
WHERE su colonna destra dopo LEFT JOIN | Il LEFT JOIN diventa INNER |
UPDATE/DELETE senza WHERE | Tutta la tabella. Senza conferme |
Filtro in WHERE invece di HAVING | Errore di sintassi, o filtro sbagliato |
| Nessun indice su colonne filtrate | Query da secondi invece che millisecondi |
| Query dentro un ciclo del codice | Mille query invece di una con JOIN |
L'ultimo merita una nota perché è il più frequente in chi arriva dalla programmazione: caricare una lista e poi, per ogni elemento, fare un'altra query. Si chiama problema "N+1", è il classico regalo di un ORM usato senza guardare le query che genera, e la soluzione è quasi sempre un JOIN. È anche un buon esempio del perché conoscere SQL serva anche a chi non lo scrive a mano: vale la pena leggere cosa produce il tuo strumento, nello spirito di cos'è il clean code.
SQL non è opzionale
La tesi, senza giri di parole: qualunque strada tu prenda nello sviluppo, userai SQL.
Backend, evidentemente. Ma anche frontend quando devi capire perché un'API restituisce dati strani, analisi dati dove è lo strumento principale, DevOps quando indaghi su un servizio lento, e mobile appena il database locale entra in gioco. Chi fa backend lo usa quotidianamente e chi lavora sui dati ancora di più — vedi quanto guadagna un data scientist in Italia.
E c'è un argomento che nessun altro linguaggio può vantare: SQL è in giro dagli anni Settanta ed è sostanzialmente lo stesso. Una query scritta trent'anni fa gira oggi. I framework che hai imparato l'anno scorso saranno superati, SQL no. È probabilmente la competenza tecnica col miglior rapporto tra tempo di apprendimento e durata nel tempo che puoi acquisire.
Le basi si coprono in un pomeriggio; i JOIN chiedono qualche giorno di pratica vera; il resto si impara sul lavoro.
In sintesi
SQL è dichiarativo: dici cosa vuoi, il database decide come ottenerlo. È l'unica differenza concettuale profonda rispetto agli altri linguaggi, e capirla cambia il modo in cui scrivi le query.
Le cose da padroneggiare sono poche: SELECT/WHERE/ORDER BY per leggere, i JOIN per mettere insieme tabelle, GROUP BY con le aggregazioni per le sintesi, e INSERT/UPDATE/DELETE per modificare.
I JOIN sono lo scoglio reale. INNER tiene solo le corrispondenze, LEFT tiene tutto ciò che sta a sinistra: sbagliare non genera un errore, genera un risultato credibile e sbagliato.
Quando una query è lenta, cerca prima l'indice mancante — con EXPLAIN ANALYZE in mano, non a intuito.
E soprattutto: non è una competenza rimandabile. È stabile da cinquant'anni, funziona ovunque, e la userai qualunque cosa tu decida di fare.
Per collocare SQL rispetto agli altri linguaggi, la mappa completa è in tutti i linguaggi di programmazione; se stai ancora scegliendo da dove partire, vedi quale linguaggio imparare nel 2026.