SQL Server, dalle fondamenta Lezione 14 / 40

Funzioni stringa: STRING_AGG, STRING_SPLIT, TRIM

Gestione moderna delle stringhe in T-SQL. Concatenazione, split, pattern matching, trimming, più la morte delle funzioni CLR per il lavoro sulle stringhe.

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:

  1. NULL + qualsiasi cosa è NULL. Un solo valore null azzera l’intero risultato.
  2. 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_WSWith 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:

  1. Simulare i pattern comuni con LIKE. Funziona per l’80% dei casi.
  2. Scrivere una funzione CLR — possibile, raro, aggiunge complessità di deploy.
  3. SQL Server 2025+ ha REGEXP_LIKE, REGEXP_REPLACE, REGEXP_INSTR, REGEXP_SUBSTR nativi. Se sei sul 2025, festeggia e usali.
  4. 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.

Cerca