Database
 sql >> Database >  >> RDS >> Database

Gemme T-SQL trascurate

Il mio buon amico Aaron Bertrand mi ha ispirato a scrivere questo articolo. Mi ha ricordato come a volte diamo per scontate le cose quando ci sembrano ovvie e non sempre ci preoccupiamo di controllare l'intera storia dietro di esse. La rilevanza per T-SQL è che a volte assumiamo di sapere tutto ciò che c'è da sapere su alcune funzionalità di T-SQL e non ci preoccupiamo sempre di controllare la documentazione per vedere se c'è di più. In questo articolo tratterò una serie di funzionalità di T-SQL che sono spesso del tutto trascurate o che supportano parametri o funzionalità che sono spesso trascurate. Se hai esempi di gemme T-SQL che vengono spesso trascurate, condividili nella sezione commenti di questo articolo.

Prima di iniziare a leggere questo articolo chiediti cosa sai delle seguenti funzionalità di T-SQL:EOMONTH, TRANSLATE, TRIM, CONCAT e CONCAT_WS, LOG, variabili cursor e MERGE with OUTPUT.

Nei miei esempi userò un database di esempio chiamato TSQLV5. Puoi trovare lo script che crea e popola questo database qui e il suo diagramma ER qui.

EOMONTH ha un secondo parametro

La funzione EOMONTH è stata introdotta in SQL Server 2012. Molte persone pensano che supporti un solo parametro contenente una data di input e che restituisca semplicemente la data di fine mese che corrisponde alla data di input.

Considera una necessità leggermente più sofisticata per calcolare la fine del mese precedente. Si supponga, ad esempio, di dover interrogare la tabella Sales.Orders e restituire gli ordini effettuati alla fine del mese precedente.

Un modo per ottenere ciò è applicare la funzione EOMONTH a SYSDATETIME per ottenere la data di fine mese del mese corrente, quindi applicare la funzione DATAADD per sottrarre un mese dal risultato, in questo modo:

USE TSQLV5; 
 
SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

Tieni presente che se esegui effettivamente questa query nel database di esempio TSQLV5 otterrai un risultato vuoto poiché la data dell'ultimo ordine registrata nella tabella è il 6 maggio 2019. Tuttavia, se la tabella aveva ordini con una data dell'ordine che cade l'ultima giorno del mese precedente, la query li avrebbe restituiti.

Ciò che molte persone non si rendono conto è che EOMONTH supporta un secondo parametro in cui indichi quanti mesi aggiungere o sottrarre. Ecco la sintassi [completamente documentata] della funzione:

EOMONTH ( start_date [, month_to_add ] )

Il nostro compito può essere svolto in modo più semplice e naturale semplicemente specificando -1 come secondo parametro della funzione, in questo modo:

SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(SYSDATETIME(), -1);

TRADURRE è talvolta più semplice di SOSTITUIRE

Molte persone hanno familiarità con la funzione REPLACE e come funziona. Lo usi quando vuoi sostituire tutte le occorrenze di una sottostringa con un'altra in una stringa di input. A volte, però, quando devi applicare più sostituzioni, l'uso di REPLACE è un po' complicato e si traduce in espressioni contorte.

Ad esempio, supponiamo di ricevere una stringa di input @s che contiene un numero con formattazione spagnola. In Spagna usano un punto come separatore per gruppi di migliaia e una virgola come separatore decimale. Devi convertire l'input nella formattazione USA, dove una virgola viene utilizzata come separatore per i gruppi di migliaia e un punto come separatore decimale.

Utilizzando una chiamata alla funzione REPLACE, è possibile sostituire solo tutte le occorrenze di un carattere o di una sottostringa con un altro. Per applicare due sostituzioni (punti alle virgole e virgole ai punti) è necessario annidare le chiamate di funzione. La parte difficile è che se usi REPLACE una volta per cambiare i punti in virgole, e poi una seconda volta contro il risultato per cambiare le virgole in punti, finisci con solo punti. Provalo:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
 
SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');

Ottieni il seguente output:

123.456.789.00

Se vuoi continuare a usare la funzione REPLACE, hai bisogno di tre chiamate di funzione. Uno per sostituire i punti con un carattere neutro che sai che normalmente non può apparire nei dati (diciamo, ~). Un altro contro il risultato per sostituire tutte le virgole con punti. Un altro contro il risultato per sostituire tutte le occorrenze del carattere temporaneo (~ nel nostro esempio) con virgole. Ecco l'espressione completa:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');

Questa volta ottieni l'output giusto:

123,456,789.00

È un po' fattibile, ma si traduce in un'espressione lunga e contorta. E se avessi più sostituti da applicare?

Molte persone non sanno che SQL Server 2017 ha introdotto una nuova funzione chiamata TRANSLATE che semplifica notevolmente tali sostituzioni. Ecco la sintassi della funzione:

TRANSLATE ( inputString, characters, translations )

Il secondo input (caratteri) è una stringa con l'elenco dei singoli caratteri che si desidera sostituire e il terzo input (traduzioni) è una stringa con l'elenco dei caratteri corrispondenti con cui si desidera sostituire i caratteri di origine. Ciò significa naturalmente che il secondo e il terzo parametro devono avere lo stesso numero di caratteri. L'importante della funzione è che non esegue passaggi separati per ciascuna delle sostituzioni. In tal caso, avrebbe potenzialmente provocato lo stesso bug del primo esempio che ho mostrato utilizzando le due chiamate alla funzione REPLACE. Di conseguenza, gestire il nostro compito diventa un gioco da ragazzi:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT TRANSLATE(@s, '.,', ',.');

Questo codice genera l'output desiderato:

123,456,789.00

È abbastanza carino!

TRIM è più di LTRIM(RTRIM())

SQL Server 2017 ha introdotto il supporto per la funzione TRIM. Molte persone, me compreso, inizialmente presumono che non sia altro che una semplice scorciatoia per LTRIM(RTRIM(input)). Tuttavia, se controlli la documentazione, ti rendi conto che in realtà è più potente di così.

Prima di entrare nei dettagli, considera il seguente compito:data una stringa di input @s, rimuovi le barre iniziali e finali (indietro e avanti). Ad esempio, supponiamo che @s contenga la seguente stringa:

//\\ remove leading and trailing backward (\) and forward (/) slashes \\//

L'output desiderato è:

 remove leading and trailing backward (\) and forward (/) slashes 

Tieni presente che l'output deve mantenere gli spazi iniziali e finali.

Se non conoscevi tutte le funzionalità di TRIM, ecco un modo in cui potresti aver risolto il problema:

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT
  TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ')
    AS outputstring;

La soluzione inizia utilizzando TRANSLATE per sostituire tutti gli spazi con un carattere neutro (~) e le barre in avanti con spazi, quindi utilizzando TRIM per tagliare gli spazi iniziali e finali dal risultato. Questo passaggio essenzialmente taglia le barre iniziali e finali, utilizzando temporaneamente ~ invece degli spazi originali. Ecco il risultato di questo passaggio:

\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\

Il secondo passaggio usa quindi TRANSLATE per sostituire tutti gli spazi con un altro carattere neutro (^) e barre all'indietro con spazi, quindi usa TRIM per tagliare gli spazi iniziali e finali dal risultato. Questo passaggio essenzialmente taglia le barre iniziali e finali all'indietro, utilizzando temporaneamente ^ invece degli spazi intermedi. Ecco il risultato di questo passaggio:

~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~

L'ultimo passaggio utilizza TRANSLATE per sostituire gli spazi con barre rovesciate, ^ con barre in avanti e ~ con spazi, generando l'output desiderato:

 remove leading and trailing backward (\) and forward (/) slashes 

Come esercizio, prova a risolvere questa attività con una soluzione compatibile precedente a SQL Server 2017 in cui non puoi utilizzare TRIM e TRANSLATE.

Tornando a SQL Server 2017 e versioni successive, se ti fossi preso la briga di controllare la documentazione, avresti scoperto che TRIM è più sofisticato di quello che pensavi inizialmente. Ecco la sintassi della funzione:

TRIM ( [ characters FROM ] string )

I caratteri DA facoltativi parte consente di specificare uno o più caratteri che si desidera ritagliare dall'inizio e dalla fine della stringa di input. Nel nostro caso, tutto ciò che devi fare è specificare '/\' come questa parte, in questo modo:

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT TRIM( '/\' FROM @s) AS outputstring;

È un miglioramento piuttosto significativo rispetto alla soluzione precedente!

CONCAT e CONCAT_WS

Se hai lavorato con T-SQL per un po', sai quanto sia imbarazzante gestire i NULL quando devi concatenare le stringhe. Ad esempio, considera i dati sulla posizione registrati per i dipendenti nella tabella HR.Employees:

SELECT empid, country, region, city
FROM HR.Employees;

Questa query genera il seguente output:

empid       country         region          city
----------- --------------- --------------- ---------------
1           USA             WA              Seattle
2           USA             WA              Tacoma
3           USA             WA              Kirkland
4           USA             WA              Redmond
5           UK              NULL            London
6           UK              NULL            London
7           UK              NULL            London
8           USA             WA              Seattle
9           UK              NULL            London

Si noti che per alcuni dipendenti la parte della regione è irrilevante e una regione irrilevante è rappresentata da un NULL. Si supponga di dover concatenare le parti della posizione (paese, regione e città), utilizzando una virgola come separatore, ma ignorando le regioni NULL. Quando la regione è rilevante, vuoi che il risultato abbia il formato <coutry>,<region>,<city> e quando la regione è irrilevante vuoi che il risultato abbia la forma <country>,<city> . Normalmente, la concatenazione di qualcosa con un NULL produce un risultato NULL. Puoi modificare questo comportamento disattivando l'opzione di sessione CONCAT_NULL_YIELDS_NULL, ma non consiglierei di abilitare un comportamento non standard.

Se non sapessi dell'esistenza delle funzioni CONCAT e CONCAT_WS, probabilmente avresti usato ISNULL o COALESCE per sostituire un NULL con una stringa vuota, in questo modo:

SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location
FROM HR.Employees;

Ecco l'output di questa query:

empid       location
----------- -----------------------------------------------
1           USA,WA,Seattle
2           USA,WA,Tacoma
3           USA,WA,Kirkland
4           USA,WA,Redmond
5           UK,London
6           UK,London
7           UK,London
8           USA,WA,Seattle
9           UK,London

SQL Server 2012 ha introdotto la funzione CONCAT. Questa funzione accetta un elenco di input di stringhe di caratteri e li concatena e, mentre lo fa, ignora i NULL. Quindi usando CONCAT puoi semplificare la soluzione in questo modo:

SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
FROM HR.Employees;

Tuttavia, devi specificare esplicitamente i separatori come parte degli input della funzione. Per semplificarci ulteriormente la vita, SQL Server 2017 ha introdotto una funzione simile denominata CONCAT_WS in cui si inizia indicando il separatore, seguito dagli elementi che si desidera concatenare. Con questa funzione la soluzione è ulteriormente semplificata in questo modo:

SELECT empid, CONCAT_WS(',', country, region, city) AS location
FROM HR.Employees;

Il passo successivo è ovviamente la lettura del pensiero. Il 1 aprile 2020 Microsoft prevede di rilasciare CONCAT_MR. La funzione accetterà un input vuoto e scoprirà automaticamente quali elementi vuoi concatenare leggendo la tua mente. La query sarà quindi simile a questa:

SELECT empid, CONCAT_MR() AS location
FROM HR.Employees;

LOG ha un secondo parametro

Simile alla funzione EOMONTH, molte persone non si rendono conto che già a partire da SQL Server 2012, la funzione LOG supporta un secondo parametro che consente di indicare la base del logaritmo. In precedenza, T-SQL supportava la funzione LOG(input) che restituisce il logaritmo naturale dell'input (utilizzando la costante e come base) e LOG10(input) che utilizza 10 come base.

Non essendo a conoscenza dell'esistenza del secondo parametro della funzione LOG, quando le persone volevano calcolare Logb (x), dove b è una base diversa da e e 10, spesso lo facevano in modo lungo. Potresti fare affidamento sulla seguente equazione:

Accedib (x) =Registroa (x)/Registroa (b)

Ad esempio, per calcolare Log2 (8), ti affidi alla seguente equazione:

Accedi2 (8) =Registroe (8)/Registroe (2)

Tradotto in T-SQL, si applica il seguente calcolo:

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x) / LOG(@b);

Una volta che ti rendi conto che LOG supporta un secondo parametro in cui indichi la base, il calcolo diventa semplicemente:

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x, @b);

Variabile cursore

Se hai lavorato con T-SQL per un po', probabilmente hai avuto molte possibilità di lavorare con i cursori. Come sai, quando lavori con un cursore, in genere utilizzi i seguenti passaggi:

  • Dichiara il cursore
  • Apri il cursore
  • Scorri i record del cursore
  • Chiudi il cursore
  • Dealloca il cursore

Ad esempio, supponi di dover eseguire alcune attività per database nella tua istanza. Utilizzando un cursore, normalmente utilizzeresti un codice simile al seguente:

DECLARE @dbname AS sysname;
 
DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN C;
 
FETCH NEXT FROM C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM C INTO @dbname;
END;
 
CLOSE C;
DEALLOCATE C;

Il comando CHIUDI rilascia il set di risultati corrente e libera i blocchi. Il comando DEALLOCATE rimuove un riferimento al cursore e, quando l'ultimo riferimento viene deallocato, libera le strutture di dati che compongono il cursore. Se provi a eseguire il codice precedente due volte senza i comandi CLOSE e DEALLOCATE, riceverai il seguente errore:

Msg 16915, Level 16, State 1, Line 4
A cursor with the name 'C' already exists.
Msg 16905, Level 16, State 1, Line 6
The cursor is already open.

Assicurati di eseguire i comandi CLOSE e DEALLOCATE prima di continuare.

Molte persone non si rendono conto che quando hanno bisogno di lavorare con un cursore in un solo batch, che è il caso più comune, invece di usare un cursore normale puoi lavorare con una variabile cursore. Come ogni variabile, l'ambito di una variabile cursore è solo il batch in cui è stata dichiarata. Ciò significa che non appena un batch termina, tutte le variabili scadono. Utilizzando una variabile cursore, una volta terminato un batch, SQL Server lo chiude e lo dealloca automaticamente, risparmiando la necessità di eseguire il comando CLOSE e DEALLOCATE in modo esplicito.

Ecco il codice rivisto utilizzando una variabile cursore questa volta:

DECLARE @dbname AS sysname, @C AS CURSOR;
 
SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN @C;
 
FETCH NEXT FROM @C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM @C INTO @dbname;
END;

Sentiti libero di eseguirlo più volte e nota che questa volta non ottieni errori. È solo più pulito e non devi preoccuparti di mantenere le risorse del cursore se hai dimenticato di chiudere e deallocare il cursore.

UNISCI con USCITA

Dall'inizio della clausola OUTPUT per le istruzioni di modifica in SQL Server 2005, si è rivelato uno strumento molto pratico ogni volta che si desidera restituire dati da righe modificate. Le persone usano questa funzione regolarmente per scopi come l'archiviazione, il controllo e molti altri casi d'uso. Una delle cose fastidiose di questa funzione, tuttavia, è che se la usi con le istruzioni INSERT, puoi restituire i dati solo dalle righe inserite, anteponendo alle colonne di output inserted . Non hai accesso alle colonne della tabella di origine, anche se a volte devi restituire colonne dall'origine insieme a colonne dalla destinazione.

Ad esempio, considera le tabelle T1 e T2, che crei e popola eseguendo il codice seguente:

DROP TABLE IF EXISTS dbo.T1, dbo.T2;
GO
 
CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');

Si noti che una proprietà identity viene utilizzata per generare le chiavi in ​​entrambe le tabelle.

Supponiamo di dover copiare alcune righe da T1 a T2; ad esempio, quelli in cui keycol % 2 =1. Si desidera utilizzare la clausola OUTPUT per restituire le chiavi appena generate in T2, ma si desidera anche restituire insieme a quelle chiavi le rispettive chiavi di origine da T1. L'aspettativa intuitiva è quella di utilizzare la seguente istruzione INSERT:

INSERT INTO dbo.T2(datacol)
    OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol
  SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;

Sfortunatamente però, come accennato, la clausola OUTPUT non ti consente di fare riferimento a colonne dalla tabella di origine, quindi ottieni il seguente errore:

Msg 4104, livello 16, stato 1, riga 2
Impossibile associare l'identificatore in più parti "T1.keycol".

Molte persone non si rendono conto che stranamente questa limitazione non si applica all'istruzione MERGE. Quindi, anche se è un po' imbarazzante, puoi convertire la tua istruzione INSERT in un'istruzione MERGE, ma per farlo, è necessario che il predicato MERGE sia sempre falso. Ciò attiverà la clausola WHEN NOT MATCHED e applicherà l'unica azione INSERT supportata lì. Puoi utilizzare una condizione falsa fittizia come 1 =2. Ecco il codice convertito completo:

MERGE INTO dbo.T2 AS TGT
USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC 
  ON 1 = 2
WHEN NOT MATCHED THEN
  INSERT(datacol) VALUES(SRC.datacol)
OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;

Questa volta il codice viene eseguito correttamente, producendo il seguente output:

T1_keycol   T2_keycol
----------- -----------
1           1
3           2
5           3

Si spera che Microsoft migliorerà il supporto per la clausola OUTPUT nelle altre istruzioni di modifica per consentire anche la restituzione di colonne dalla tabella di origine.

Conclusione

Non dare per scontato e RTFM! :-)