Oggi è la prima lezione che riguarda davvero la scrittura di T-SQL. Tutto quello che è venuto prima era preparazione. Da qui in poi, ogni lezione parte da una domanda che il business di Runehold si pone davvero — “quali prodotti hanno venduto di più in Germania la settimana scorsa?”, “quanti ordini abbiamo spedito puntuali a marzo?”, “qual è il tasso di abbandono delle Runebox in Italia?” — e ricava l’SQL per rispondere.
L’istruzione che passeremo la maggior parte della nostra vita a scrivere è SELECT. Sembra semplice. Ha almeno nove parole chiave che possono apparirvi dentro. Ha un ordine di esecuzione logico che non corrisponde all’ordine in cui la digiti. Ha una parola chiave (*) che rovina le query in modi invisibili finché non lo sono più. Oggi copriremo tutto questo.
Le sei parti di un SELECT
Ogni SELECT ha fino a sei clausole principali, in quest’ordine quando le scrivi:
SELECT <colonne>
FROM <tabella>
WHERE <filtro di riga>
GROUP BY <raggruppamento>
HAVING <filtro di gruppo>
ORDER BY <ordinamento>
Più una manciata di modificatori opzionali (TOP, DISTINCT, OFFSET ... FETCH, join, funzioni finestra) che si attaccano a una di quelle clausole.
Il SELECT legale più semplice ha solo due clausole:
SELECT CustomerId, Email FROM Sales.Customer;
Il SELECT legale più semplice senza una tabella ne ha una:
SELECT GETDATE();
-- Ritorna l'ora corrente. Nessun FROM necessario.
La maggior parte delle query di produzione usa tutte e sei.
L’ordine logico non è l’ordine scritto
Ecco il singolo fatto più importante su SELECT che nessuno ti spiega mai esplicitamente.
L’ordine in cui SCRIVI le clausole non è l’ordine in cui SQL Server le ESEGUE.
L’ordine di elaborazione logica di SQL Server va più o meno così:
1. FROM (prendi la tabella o le tabelle)
2. ON (applica la condizione di JOIN)
3. JOIN (combina le righe dalle tabelle unite)
4. WHERE (filtra le righe)
5. GROUP BY (collassa le righe in gruppi)
6. HAVING (filtra i gruppi)
7. SELECT (proietta le colonne)
8. DISTINCT (deduplica)
9. ORDER BY (ordina)
10. TOP / OFFSET (limita)
Tutto comincia con FROM. La lista di SELECT — le colonne che stai proiettando — è quasi l’ultima cosa valutata. Sembra strano perché scriviamo SELECT per primo. Ma spiega in una frase una dozzina di errori da principiante:
- Perché non puoi usare un alias di colonna nella clausola
WHERE(l’alias viene creato al passo 7;WHEREgira al passo 4) - Perché puoi usare un alias di colonna in
ORDER BY(il passo 9 è dopo il passo 7) - Perché
GROUP BYrichiede il nome senza alias - Perché un filtro
WHEREsu un aggregato non funziona — devi usareHAVING
Tieni a mente quest’ordine. Ti risparmierà dieci ore di “perché non funziona” nel primo anno.
Proiezione: scegliere le colonne
Nelle query reali, mai SELECT *. Ti spiegherò il perché tra un attimo. Invece, elenca sempre le colonne che vuoi:
USE Runehold;
SELECT CustomerId,
Name,
Country,
CreatedAt
FROM Sales.Customer;
Ogni riga che corrisponde alla query produce una riga in output, con le colonne richieste nell’ordine richiesto.
Puoi anche calcolare nuove colonne al volo, chiamate espressioni:
SELECT CustomerId,
Name,
UPPER(Country) AS CountryCode, -- trasformazione case
DATEDIFF(DAY, CreatedAt, GETDATE()) AS DaysSinceSignup,
CreatedAt
FROM Sales.Customer;
AS <alias> dà all’espressione un nome di colonna. Senza alias, le colonne calcolate appaiono nei risultati come (No column name) — brutto e inutilizzabile per il codice applicativo. Usa sempre un alias.
SELECT * è un odore professionale
SELECT * significa “ogni colonna della tabella, nell’ordine in cui la tabella le definisce.” Ogni tutorial SQL inizia con quello perché è facile da digitare. Ogni guida di produzione SQL lo vieta.
Motivi:
- I cambi di schema rompono il tuo codice. Qualcuno aggiunge una colonna; il tuo
INSERT INTO ... SELECT *ora ha il numero di colonne sbagliato. Il mapping fortemente tipizzato dei risultati della tua applicazione si rompe. Eccoti un 500 il lunedì mattina. - Performance.
SELECT *costringe SQL Server a leggere ogni colonna, il che esclude gli indici non clustered di tipo covering. Una query che potrebbe fare un seek su un indice e tornare in 2ms ora scansiona l’intera tabella. - Leggibilità.
SELECT Name, Email, Countrydice al lettore di cosa parla questa query.SELECT *non dice nulla. - Costo di rete. Restituire colonne che l’app non usa spreca banda tra il database e il server applicativo.
Tre eccezioni in cui SELECT * è accettabile:
- Esplorazione ad-hoc in SSMS (“cosa c’è in questa tabella?”). Usa-e-getta.
- Subquery
EXISTS:WHERE EXISTS (SELECT * FROM ... WHERE ...)— il*non viene mai materializzato, è una convenzione. COUNT(*)— non è in realtà unSELECT *, è una forma speciale che significa “conta le righe ignorando NULL.”
Elenca sempre le colonne. Il te del futuro ti ringrazierà.
Alias: la feature che migliora la qualità della vita
Gli alias di colonna rinominano colonne calcolate o verbose:
SELECT o.OrderId AS Id,
o.Total AS AmountEUR,
o.CountryCode AS Country,
o.Total * o.VatRate AS VatAmount,
o.Total - (o.Total * o.VatRate / (1 + o.VatRate)) AS NetAmount
FROM Sales.Orders AS o;
Due stili: o.OrderId AS Id (con AS) e o.OrderId Id (spazio invece di AS). Funzionano entrambi. Usa sempre AS — è esplicito e visivamente non ambiguo. La forma con lo spazio mi ha morso quando ho accidentalmente scritto SELECT OrderId Total FROM ... e SQL Server l’ha interpretato come “dammi OrderId con alias Total,” non “dammi OrderId e Total.” Confondente, silenzioso, fastidioso.
Gli alias di tabella rinominano la tabella in una query, di solito in qualcosa di breve:
SELECT o.OrderId, c.Name, c.Country
FROM Sales.Orders AS o
JOIN Sales.Customer AS c ON c.CustomerId = o.CustomerId;
o per Orders, c per Customer. Gli alias ti permettono di scrivere o.OrderId invece di Sales.Orders.OrderId. Aliasa sempre le tabelle nelle query multi-tabella. Scrivi sempre o.OrderId invece del solo OrderId quando più di una tabella è in gioco, anche se il nome della colonna è inequivocabile oggi. Domani qualcuno aggiunge una colonna OrderId a Customer e la tua query smette di compilare.
TOP, OFFSET ... FETCH, e DISTINCT
TOP (n) — restituisce le prime n righe. Senza un ORDER BY è un lancio della moneta capire quali n righe ottieni:
SELECT TOP (10) OrderId, Total FROM Sales.Orders ORDER BY OrderDate DESC;
-- I 10 ordini più recenti
OFFSET ... FETCH — la forma di paginazione standard SQL. Richiede ORDER BY:
SELECT OrderId, Total
FROM Sales.Orders
ORDER BY OrderDate DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;
-- Salta 20, prendi 10 — "pagina 3 di 10-per-pagina"
DISTINCT — deduplica le righe in base a tutte le colonne proiettate:
SELECT DISTINCT Country FROM Sales.Customer;
-- Ogni paese appare una volta
DISTINCT è costoso (SQL Server deve ordinare o fare hash per deduplicare). Se ti accorgi di scriverlo spesso, di solito la vera soluzione è GROUP BY o un pattern di join diverso.
L’abitudine della lista completa di colonne
Prendiamo una query realistica di Runehold: “elenca i dieci ordini più grandi dei clienti italiani in questo trimestre, dal più recente, mostrando ID dell’ordine, nome del cliente, totale in EUR e la data.”
SELECT TOP (10)
o.OrderId,
c.Name AS CustomerName,
o.Total AS TotalEUR,
CAST(o.OrderDate AS DATE) AS OrderDay
FROM Sales.Orders AS o
JOIN Sales.Customer AS c
ON c.CustomerId = o.CustomerId
WHERE o.CountryCode = 'IT'
AND o.OrderDate >= '2026-01-01'
AND o.OrderDate < '2026-04-01'
ORDER BY o.Total DESC;
Ogni colonna è nominata. Ogni tabella ha un alias. Il filtro su OrderDate usa un intervallo semi-aperto (>= all’inizio, < alla fine), che è il modo giusto di filtrare su una colonna datetime — ne riparleremo nella lezione 15, ma è l’unica forma sia corretta sia index-friendly. L’ORDER BY è esplicito; senza, TOP (10) sarebbe un campione casuale di 10 righe.
Questa è la forma di una query di produzione. Prendici confidenza.
Commenti: lascia briciole di pane
T-SQL ha due stili di commento:
-- Commento su una sola riga, come questo
/*
Commento multi-riga.
Utile per spiegazioni lunghe o
per disabilitare temporaneamente un blocco di codice.
*/
Scrivi commenti per il contesto di business, non per quello che l’SQL ovviamente fa:
-- Commento cattivo: ti dice quello che già leggi
-- Seleziona i clienti
SELECT Name FROM Sales.Customer;
-- Commento buono: ti dice il perché
-- Clienti italiani, usati dalla campagna mailing trimestrale
-- "famiglie italiane" del team marketing. Aggiornato 2026-03-15.
SELECT Name FROM Sales.Customer WHERE Country = 'IT';
Una query che sarà letta da una dozzina di persone in cinque anni ha bisogno di commenti. Un’esplorazione usa-e-getta no. Usa il giudizio.
Errori comuni dei principianti, con correzioni
Errore 1: “Invalid column name” su un alias che sembra valido.
-- ERRORE: 'Country' non è valido in WHERE
SELECT UPPER(CountryCode) AS Country FROM Sales.Orders WHERE Country = 'IT';
WHERE gira prima di SELECT nell’ordine logico. L’alias non esiste ancora al passo 4. Soluzione:
SELECT UPPER(CountryCode) AS Country FROM Sales.Orders WHERE UPPER(CountryCode) = 'IT';
-- o meglio:
SELECT UPPER(CountryCode) AS Country FROM Sales.Orders WHERE CountryCode = 'IT';
La seconda forma mantiene utilizzabile l’indice su CountryCode (nessuna funzione sulla colonna).
Errore 2: Ordinare per una colonna non nella lista SELECT.
In realtà questo funziona benissimo:
SELECT OrderId, Total FROM Sales.Orders ORDER BY OrderDate DESC;
ORDER BY gira al passo 9 e ha accesso a tutto quello che FROM ha prodotto, non solo a quello che SELECT ha scelto. L’output mostra solo le colonne che hai proiettato, ma puoi ordinare per qualsiasi cosa dalle tabelle sorgente. Utile e spesso dimenticato.
Errore 3: DISTINCT come cerotto per i duplicati.
Se la tua query ritorna righe duplicate che non ti aspettavi, la causa solita è un JOIN che produce più righe di quanto pensavi. Aggiungere DISTINCT nasconde il sintomo senza risolvere la causa. Sistema il join.
Esegui questo sulla tua macchina
Finalmente metteremo dei dati in Runehold e li interrogheremo. Incolla l’intero blocco in una volta sola — si appoggia al database della lezione 3.
USE Runehold;
GO
-- Crea la tabella Orders se non esiste
IF OBJECT_ID(N'Sales.Orders', N'U') IS NULL
BEGIN
CREATE TABLE Sales.Orders (
OrderId BIGINT IDENTITY(1,1) NOT NULL,
CustomerId INT NOT NULL,
OrderDate DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
Total DECIMAL(19, 4) NOT NULL,
Currency CHAR(3) NOT NULL DEFAULT 'EUR',
CountryCode CHAR(2) NOT NULL,
VatRate DECIMAL(5, 4) NOT NULL,
CONSTRAINT pk_Orders PRIMARY KEY CLUSTERED (OrderId),
CONSTRAINT fk_Orders_Customer FOREIGN KEY (CustomerId)
REFERENCES Sales.Customer (CustomerId)
);
END;
-- Inserisci alcuni ordini realistici (CustomerId 1-4 dalla lezione 3)
INSERT INTO Sales.Orders (CustomerId, OrderDate, Total, CountryCode, VatRate)
VALUES (1, '2026-03-05 10:32:00', 59.00, 'NL', 0.2100),
(1, '2026-03-18 14:22:00', 29.00, 'NL', 0.2100),
(2, '2026-02-15 09:15:00', 149.00, 'IT', 0.2200),
(2, '2026-03-22 11:40:00', 89.50, 'IT', 0.2200),
(3, '2026-03-10 16:05:00', 199.00, 'DE', 0.1900),
(4, '2026-03-28 08:12:00', 42.42, 'RO', 0.1900);
-- Query 1: SELECT semplice con alias
SELECT OrderId,
CustomerId,
Total AS TotalEUR,
CountryCode AS Country
FROM Sales.Orders
ORDER BY Total DESC;
-- Query 2: calcola al volo lo scorporo IVA
SELECT OrderId,
Total AS GrossEUR,
Total / (1 + VatRate) AS NetEUR,
Total - (Total / (1 + VatRate)) AS VatEUR,
CAST(VatRate * 100 AS DECIMAL(5,2)) AS VatPercent
FROM Sales.Orders
ORDER BY OrderDate;
-- Query 3: i top-3 ordini più grandi in Italia (join tra tabelle)
SELECT TOP (3)
o.OrderId,
c.Name AS CustomerName,
o.Total AS TotalEUR,
CAST(o.OrderDate AS DATE) AS OrderDay
FROM Sales.Orders AS o
JOIN Sales.Customer AS c ON c.CustomerId = o.CustomerId
WHERE o.CountryCode = 'IT'
ORDER BY o.Total DESC;
-- Query 4: nota l'ordine logico — l'alias funziona in ORDER BY, non in WHERE
SELECT o.OrderId,
o.Total * 100 AS TotalInCents
FROM Sales.Orders AS o
WHERE o.Total > 50 -- il filtro usa la colonna originale
ORDER BY TotalInCents DESC; -- ORDER BY può usare l'alias
Leggi i risultati. Scrivi qualche query tua. Rompile di proposito per vedere gli errori.
Prossima lezione: WHERE, NULL, e perché i tuoi filtri ogni tanto mentono. È più corta di questa, e contiene l’unico fatto T-SQL che inciampa ogni persona che abbia mai imparato SQL.