La gestione delle stringhe in SQL Server è passata da “scriviti la tua funzione CLR” a “c’è una built-in che lo fa” da qualche parte tra il 2016 e il 2022. La lezione di oggi è lo stato dell’arte attuale: le funzioni che userai davvero nel T-SQL moderno per nomi, indirizzi, codici SKU, import CSV, e gli altri quaranta posti in cui le stringhe spuntano nei dati di Runehold.
Concatenazione: CONCAT e CONCAT_WS
L’operatore + concatena stringhe, ma ha due trappole:
NULL + qualsiasi cosaèNULL. Un solo valore null azzera l’intero risultato.- Mischiare i tipi richiede cast espliciti:
'Order #' + CAST(OrderId AS NVARCHAR(20)).
CONCAT() risolve entrambe:
-- NULL-safe, fa cast automatico
SELECT CONCAT('Order #', o.OrderId, ' - €', o.Total, ' from ', c.Name) AS description
FROM Sales.Orders AS o
JOIN Sales.Customer AS c ON c.CustomerId = o.CustomerId;
CONCAT tratta NULL come una stringa vuota e converte implicitamente gli argomenti non-stringa. Molto più sicuro.
CONCAT_WS — With Separator — aggiunge un delimitatore tra ogni argomento non-NULL:
SELECT CONCAT_WS(', ',
c.Name,
c.AddressLine1,
c.AddressLine2, -- le righe NULL vengono saltate silenziosamente
c.PostalCode,
c.City,
c.Country) AS mailing_label
FROM Sales.Customer AS c;
Niente virgole finali, niente delimitatori doppi quando un campo è NULL. Etichette di indirizzo, righe CSV, breadcrumb — CONCAT_WS è fatto esattamente per questo.
STRING_AGG: aggregare righe in una stringa delimitata
L’altra metà di quel superpotere CSV. Combina molte righe di una colonna in una sola stringa:
-- Elenca tutti i prodotti in ogni ordine, separati da virgola
SELECT o.OrderId,
STRING_AGG(p.Name, ', ') WITHIN GROUP (ORDER BY p.Name) AS products
FROM Sales.OrderLine AS ol
JOIN Sales.Orders AS o ON o.OrderId = ol.OrderId
JOIN Catalog.Product AS p ON p.ProductId = ol.ProductId
GROUP BY o.OrderId
ORDER BY o.OrderId;
STRING_AGG(col, separator) WITHIN GROUP (ORDER BY col) — concatena tutti i valori non-NULL all’interno di ogni gruppo, ordinati come vuoi.
Prima di STRING_AGG (aggiunto in SQL Server 2017), questo richiedeva il famigerato trucco FOR XML PATH — circa 15 righe di codice illeggibile. STRING_AGG lo fa in una riga. Se vedi FOR XML PATH('') in vecchie query, stai guardando un workaround pre-2017 che merita di essere rifattorizzato.
STRING_SPLIT: trasformare una stringa delimitata in righe
L’opposto: dividere una stringa CSV in righe individuali.
-- Split semplice
SELECT value FROM STRING_SPLIT('NL,IT,DE,FR,ES', ',');
-- Restituisce 5 righe.
-- Con ordinale (SQL Server 2022+, richiede enable_ordinal)
SELECT value, ordinal
FROM STRING_SPLIT('apple,banana,cherry', ',', 1);
-- Restituisce le righe con la loro posizione
Utile per:
- Spezzare liste di tag in singoli tag.
- Parsare import CSV.
- Accettare un parametro “lista” in una stored procedure.
Pattern classico per un parametro:
CREATE PROCEDURE Sales.GetOrdersByCountries
@CountryCsv NVARCHAR(500)
AS
BEGIN
SELECT o.OrderId, o.Total, o.CountryCode
FROM Sales.Orders AS o
WHERE o.CountryCode IN (SELECT value FROM STRING_SPLIT(@CountryCsv, ','));
END;
-- Chiamato dall'app:
EXEC Sales.GetOrdersByCountries @CountryCsv = 'NL,IT,DE';
Un modo semplice di passare una lista di valori come singolo parametro. Non l’unico — i table-valued parameter sono più efficienti per liste grandi — ma è pulito fino a qualche decina di valori.
TRIM e amici
SELECT LTRIM(RTRIM(' hello world ')); -- 'hello world', vecchio stile
SELECT TRIM(' hello world '); -- 'hello world', dal 2017
SELECT TRIM(',' FROM ',,,trimmed,,,'); -- 'trimmed', un carattere qualsiasi (2022+)
TRIM di default rimuove i whitespace. Puoi anche specificare un set di caratteri da rimuovere:
SELECT TRIM(' ,.;' FROM ' , .Mr. Bloch; ');
-- 'Mr. Bloch'
Utile per pulire dati importati con padding incoerente, virgole vaganti o caratteri BOM.
Pattern matching: LIKE, PATINDEX, CHARINDEX
LIKE
-- Email che terminano con @gmail.com
WHERE Email LIKE '%@gmail.com';
-- Nomi che iniziano con 'Anne'
WHERE Name LIKE 'Anne%';
-- Esattamente 5 caratteri
WHERE Code LIKE '_____';
-- Classi di caratteri (un singolo carattere)
WHERE Code LIKE '[A-Z]%'; -- inizia con una lettera maiuscola
WHERE Code LIKE '[^0-9]%'; -- inizia con un non-cifra
Wildcard: % corrisponde a qualsiasi stringa (anche vuota), _ corrisponde esattamente a un carattere, [abc] corrisponde a uno dei caratteri elencati, [^abc] corrisponde a un carattere non nel set.
Promemoria sulla SARGability (lezione 7): LIKE 'prefisso%' è SARGable (può fare seek su un indice). LIKE '%suffisso' e LIKE '%middle%' non lo sono — forzano una scan. Se ti serve la ricerca full-text, usa la feature Full-Text Search di SQL Server.
PATINDEX e CHARINDEX
Trova la posizione di un pattern o di una sottostringa:
SELECT CHARINDEX('@', Email) AS at_position FROM Sales.Customer;
-- Posizione di '@' nell'email.
SELECT PATINDEX('%[0-9]%', 'order 123') AS first_digit_pos;
-- 7. Pattern match come LIKE, restituisce la posizione.
Restituisce 0 se non trovato. Utile per spezzare le stringhe a mano quando STRING_SPLIT non basta.
Operazioni comuni sulle stringhe
-- Cambio caso
SELECT UPPER(Name), LOWER(Email) FROM Sales.Customer;
-- Sottostringa
SELECT SUBSTRING(Email, 1, CHARINDEX('@', Email) - 1) AS localpart,
SUBSTRING(Email, CHARINDEX('@', Email) + 1, 4000) AS domain
FROM Sales.Customer;
-- Replace
SELECT REPLACE('Apples, oranges, bananas', ',', ';') AS csv_semi;
-- Lunghezza
SELECT LEN(Name) AS length_char, -- conteggio caratteri (rimuove gli spazi finali)
DATALENGTH(Name) AS length_bytes -- conteggio byte (2x per NVARCHAR)
FROM Sales.Customer;
-- Reverse (raramente utile, ogni tanto comodo)
SELECT REVERSE('goblin'); -- 'nilbog'
-- Padding
SELECT RIGHT(REPLICATE('0', 10) + CAST(OrderId AS NVARCHAR(10)), 10) AS padded_id
FROM Sales.Orders;
-- Padding di zeri a 10 caratteri.
-- Oppure FORMAT, più lento ma più pulito
SELECT FORMAT(OrderId, '0000000000') AS padded_id
FROM Sales.Orders;
LEN vs DATALENGTH conta: LEN rimuove gli spazi finali e restituisce il conteggio dei caratteri; DATALENGTH restituisce i byte (quindi 2x per Unicode). Se ti interessa lo storage o stai lavorando con VARBINARY, usa DATALENGTH. Altrimenti LEN.
FORMAT è comodo ma noto per essere lento — chiama il framework System.Globalization di .NET per ogni riga. Per dashboard e query leggere va bene; per query calde, usa CONVERT + pattern espliciti.
Espressioni regolari (la cattiva notizia)
SQL Server non ha regex native. LIKE è pattern matching limitato, non regex.
Le tue opzioni:
- Simulare i pattern comuni con
LIKE. Funziona per l’80% dei casi. - Scrivere una funzione CLR — possibile, raro, aggiunge complessità di deploy.
- SQL Server 2025+ ha
REGEXP_LIKE,REGEXP_REPLACE,REGEXP_INSTR,REGEXP_SUBSTRnativi. Se sei sul 2025, festeggia e usali. - Parsare nell’applicazione — per esigenze di regex davvero complesse, il codice applicativo è spesso il posto giusto.
-- SQL Server 2025+
SELECT Email FROM Sales.Customer WHERE REGEXP_LIKE(Email, '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$');
Se erediti un database con funzioni CLR utente chiamate RegexMatch, RegexReplace, eccetera — quello era il workaround pre-2025. Quando migri al 2025, sostituiscile con le funzioni native.
Collation e case sensitivity nelle stringhe
Ricordi la collation dalla lezione 4: influenza l’ordinamento e i confronti di uguaglianza per le stringhe.
-- Controllo case-sensitive su una colonna case-insensitive
SELECT Email FROM Sales.Customer
WHERE Email = 'Anne@Example.COM' COLLATE Latin1_General_CS_AS;
-- Questo matcherà esattamente 'Anne@Example.COM', non 'anne@example.com'.
COLLATE <nome> sovrascrive la collation per un singolo confronto. Utile per forzare la case-sensitivity in un database altrimenti case-insensitive.
Utili-ma-abusati: ISJSON ed estrazione JSON
T-SQL dal 2016 ha ISJSON, JSON_VALUE, JSON_QUERY, OPENJSON per parsare JSON memorizzato in colonne NVARCHAR. SQL Server 2025 aggiunge un tipo JSON nativo.
-- Attributi prodotto memorizzati come JSON
SELECT ProductId,
JSON_VALUE(Attributes, '$.color') AS color,
JSON_VALUE(Attributes, '$.material') AS material,
JSON_VALUE(Attributes, '$.dimensions.cm') AS dim_cm
FROM Catalog.Product
WHERE JSON_VALUE(Attributes, '$.color') = 'black';
Utile quando il tuo schema ha attributi flessibili. Attenzione: la chiamata a JSON_VALUE non è SARGable su quella colonna a meno che tu non crei una colonna calcolata + indice. Per query con molti filtri, estrai in una colonna vera.
Esegui questo sulla tua macchina
USE Runehold;
GO
-- 1. CONCAT e CONCAT_WS
SELECT CustomerId,
CONCAT_WS(' - ', Name, Country, Email) AS label
FROM Sales.Customer;
-- 2. STRING_AGG: lista prodotti per ordine
-- (richiede le tabelle OrderLine e Product; qui una versione leggera)
SELECT CustomerId,
STRING_AGG(CAST(OrderId AS NVARCHAR(20)), ', ')
WITHIN GROUP (ORDER BY OrderId) AS orders_csv,
COUNT(*) AS order_count
FROM Sales.Orders
GROUP BY CustomerId
ORDER BY order_count DESC;
-- 3. STRING_SPLIT: filtra per un parametro CSV
DECLARE @countries NVARCHAR(100) = 'NL,IT,DE';
SELECT OrderId, CountryCode, Total
FROM Sales.Orders
WHERE CountryCode IN (SELECT value FROM STRING_SPLIT(@countries, ','))
ORDER BY CountryCode;
-- 4. Estrai parti dell'email con SUBSTRING + CHARINDEX
SELECT Email,
SUBSTRING(Email, 1, CHARINDEX('@', Email) - 1) AS local_part,
SUBSTRING(Email, CHARINDEX('@', Email) + 1, 4000) AS domain
FROM Sales.Customer
WHERE CHARINDEX('@', Email) > 0;
-- 5. Report di frequenza dei domini
SELECT SUBSTRING(Email, CHARINDEX('@', Email) + 1, 4000) AS domain,
COUNT(*) AS customers
FROM Sales.Customer
WHERE CHARINDEX('@', Email) > 0
GROUP BY SUBSTRING(Email, CHARINDEX('@', Email) + 1, 4000)
ORDER BY customers DESC;
-- 6. TRIM con un set di caratteri
SELECT TRIM(' ,.;' FROM ' ;Piotr Nowak, ') AS cleaned;
Esegui ognuna. Guarda gli output. Nota quanto sono più corte e pulite di prima dell’esistenza delle funzioni moderne.
Prossima lezione: date e orari. La palude dei timezone. Perché esiste DATETIMEOFFSET, cosa fa AT TIME ZONE, e i bug che nascono dal salvare l’ora locale ingenuamente.