Min gode ven Aaron Bertrand inspirerede mig til at skrive denne artikel. Han mindede mig om, hvordan vi nogle gange tager ting for givet, når de virker indlysende for os og ikke altid gider tjekke hele historien bag dem. Relevansen for T-SQL er, at vi nogle gange antager, at vi ved alt, hvad der er at vide om visse T-SQL-funktioner, og ikke altid gider tjekke dokumentationen for at se, om der er mere i dem. I denne artikel dækker jeg en række T-SQL-funktioner, som enten ofte er helt overset, eller som understøtter parametre eller muligheder, som ofte overses. Hvis du har eksempler på dine egne T-SQL-perler, som ofte overses, så del venligst dem i kommentarfeltet i denne artikel.
Inden du begynder at læse denne artikel, spørg dig selv, hvad ved du om følgende T-SQL-funktioner:EOMONTH, TRANSLATE, TRIM, CONCAT og CONCAT_WS, LOG, markørvariabler og FLÉNING med OUTPUT.
I mine eksempler vil jeg bruge en prøvedatabase kaldet TSQLV5. Du kan finde scriptet, der opretter og udfylder denne database her, og dets ER-diagram her.
EOMONTH har en anden parameter
EOMONTH-funktionen blev introduceret i SQL Server 2012. Mange mennesker tror, at den kun understøtter én parameter, der indeholder en inputdato, og at den blot returnerer den slutning af måneden, der svarer til inputdatoen.
Overvej et lidt mere sofistikeret behov for at beregne slutningen af den foregående måned. Antag for eksempel, at du skal forespørge i Sales.Orders-tabellen og returnere ordrer, der blev afgivet i slutningen af den foregående måned.
En måde at opnå dette på er at anvende EOMONTH-funktionen på SYSDATETIME for at få månedens slutningsdato for den aktuelle måned og derefter anvende DATEADD-funktionen for at trække en måned fra resultatet, som sådan:
USE TSQLV5; SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));
Bemærk, at hvis du rent faktisk kører denne forespørgsel i TSQLV5-eksempeldatabasen, vil du få et tomt resultat, da den sidste ordredato, der er registreret i tabellen, er den 6. maj 2019. Men hvis tabellen havde ordrer med en ordredato, der falder på den sidste dag i den foregående måned, ville forespørgslen have returneret disse.
Hvad mange mennesker ikke er klar over er, at EOMONTH understøtter en anden parameter, hvor du angiver, hvor mange måneder der skal lægges til eller trækkes fra. Her er den [fuldstændige dokumenterede] syntaks for funktionen:
EOMONTH ( start_date [, month_to_add ] )
Vores opgave kan opnås lettere og mere naturligt ved blot at angive -1 som den anden parameter til funktionen, som sådan:
SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(SYSDATETIME(), -1);
OVERSÆT er nogle gange enklere end REPLACE
Mange mennesker kender til REPLACE-funktionen og hvordan den virker. Du bruger det, når du vil erstatte alle forekomster af en understreng med en anden i en inputstreng. Nogle gange, når du har flere erstatninger, som du skal anvende, er det dog en smule vanskeligt at bruge REPLACE og resulterer i indviklede udtryk.
Antag som et eksempel, at du får en inputstreng @s, der indeholder et tal med spansk formatering. I Spanien bruger de et punktum som skilletegn for grupper af tusinder og et komma som decimalseparator. Du skal konvertere input til amerikansk formatering, hvor et komma bruges som separator for grupper af tusinder, og et punktum som decimal separator.
Ved at bruge ét kald til REPLACE-funktionen kan du kun erstatte alle forekomster af et tegn eller en understreng med en anden. For at anvende to erstatninger (punktum til kommaer og kommaer til punktummer) skal du indlejre funktionskald. Den vanskelige del er, at hvis du bruger REPLACE én gang til at ændre punktum til komma, og derefter en anden gang mod resultatet for at ændre kommaer til punktum, ender du med kun punktum. Prøv det:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');
Du får følgende output:
123.456.789.00
Hvis du vil blive ved med at bruge REPLACE-funktionen, skal du bruge tre funktionskald. En til at erstatte punktum med et neutralt tegn, som du ved, der normalt ikke kan forekomme i dataene (f.eks. ~). En anden mod resultatet for at erstatte alle kommaer med punktum. En anden mod resultatet for at erstatte alle forekomster af den midlertidige karakter (~ i vores eksempel) med kommaer. Her er det komplette udtryk:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');
Denne gang får du det rigtige output:
123,456,789.00
Det er sådan set muligt, men det resulterer i et langt og indviklet udtryk. Hvad hvis du havde flere erstatninger at ansøge?
Mange mennesker er ikke klar over, at SQL Server 2017 introducerede en ny funktion kaldet TRANSLATE, der forenkler sådanne udskiftninger en hel del. Her er funktionens syntaks:
TRANSLATE ( inputString, characters, translations )
Det andet input (tegn) er en streng med listen over de enkelte tegn, som du ønsker at erstatte, og det tredje input (oversættelser) er en streng med listen over de tilsvarende tegn, som du ønsker at erstatte kildetegnene med. Det betyder naturligvis, at den anden og tredje parameter skal have samme antal tegn. Det, der er vigtigt ved funktionen, er, at den ikke laver separate afleveringer for hver af udskiftningerne. Hvis det gjorde det, ville det potentielt have resulteret i den samme fejl som i det første eksempel, jeg viste ved at bruge de to kald til REPLACE-funktionen. Som følge heraf bliver det nemt at håndtere vores opgave:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT TRANSLATE(@s, '.,', ',.');
Denne kode genererer det ønskede output:
123,456,789.00
Det er ret pænt!
TRIM er mere end LTRIM(RTRIM())
SQL Server 2017 introducerede understøttelse af funktionen TRIM. Mange mennesker, inklusiv mig selv, antager i starten bare, at det ikke er mere end en simpel genvej til LTRIM(RTRIM(input)). Men hvis du tjekker dokumentationen, indser du, at den faktisk er mere kraftfuld end som så.
Før jeg går ind i detaljerne, skal du overveje følgende opgave:givet en inputstreng @s, fjern indledende og efterfølgende skråstreger (tilbage og frem). Antag som et eksempel, at @s indeholder følgende streng:
//\\ remove leading and trailing backward (\) and forward (/) slashes \\//
Det ønskede output er:
remove leading and trailing backward (\) and forward (/) slashes
Bemærk, at udgangen skal beholde de forreste og efterfølgende mellemrum.
Hvis du ikke kendte til TRIMs fulde muligheder, er her en måde, du måske har løst opgaven på:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ') AS outputstring;
Løsningen starter med at bruge TRANSLATE til at erstatte alle mellemrum med et neutralt tegn (~) og skråstreger frem med mellemrum, og derefter bruge TRIM til at trimme forreste og efterfølgende mellemrum fra resultatet. Dette trin beskærer hovedsageligt førende og efterfølgende skråstreger ved midlertidigt at bruge ~ i stedet for originale mellemrum. Her er resultatet af dette trin:
\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\
Det andet trin bruger derefter TRANSLATE til at erstatte alle mellemrum med et andet neutralt tegn (^) og skråstreger bagud med mellemrum, hvorefter man bruger TRIM til at trimme forreste og efterfølgende mellemrum fra resultatet. Dette trin trimmer hovedsageligt førende og bagudgående skråstreger ved midlertidigt at bruge ^ i stedet for mellemrum. Her er resultatet af dette trin:
~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~
Det sidste trin bruger TRANSLATE til at erstatte mellemrum med skråstreger bagud, ^ med skråstreger frem og ~ med mellemrum, hvilket genererer det ønskede output:
remove leading and trailing backward (\) and forward (/) slashes
Som en øvelse kan du prøve at løse denne opgave med en præ-SQL Server 2017-kompatibel løsning, hvor du ikke kan bruge TRIM og TRANSLATE.
Tilbage til SQL Server 2017 og nyere, hvis du gad at tjekke dokumentationen, ville du have opdaget, at TRIM er mere sofistikeret, end du troede i begyndelsen. Her er funktionens syntaks:
TRIM ( [ characters FROM ] string )
De valgfrie tegn FRA del giver dig mulighed for at angive et eller flere tegn, som du ønsker trimmet fra begyndelsen og slutningen af inputstrengen. I vores tilfælde er alt hvad du skal gøre at angive '/\' som denne del, som sådan:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRIM( '/\' FROM @s) AS outputstring;
Det er en ret betydelig forbedring i forhold til den tidligere løsning!
CONCAT og CONCAT_WS
Hvis du har arbejdet med T-SQL i et stykke tid, ved du, hvor akavet det er at håndtere NULL'er, når du skal sammenkæde strenge. Som et eksempel kan du overveje lokationsdataene, der er registreret for medarbejdere i HR.Employees-tabellen:
SELECT empid, country, region, city FROM HR.Employees;
Denne forespørgsel genererer følgende 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
Bemærk, at for nogle medarbejdere er regionsdelen irrelevant, og en irrelevant region er repræsenteret med en NULL. Antag, at du skal sammenkæde lokationsdelene (land, region og by), ved at bruge et komma som separator, men ignorere NULL-regioner. Når regionen er relevant, ønsker du, at resultatet skal have formen
og når regionen er irrelevant, ønsker du, at resultatet skal have formen
Hvis du ikke kendte til eksistensen af CONCAT- og CONCAT_WS-funktionerne, ville du sandsynligvis have brugt ISNULL eller COALESCE til at erstatte en NULL med en tom streng, som sådan:
SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location FROM HR.Employees;
Her er outputtet af denne forespørgsel:
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 introducerede funktionen CONCAT. Denne funktion accepterer en liste over tegnstrengsinput og sammenkæder dem, og mens den gør det, ignorerer den NULL. Så ved at bruge CONCAT kan du forenkle løsningen sådan her:
SELECT empid, CONCAT(country, ',' + region, ',', city) AS location FROM HR.Employees;
Alligevel skal du eksplicit angive separatorerne som en del af funktionens input. For at gøre vores liv endnu nemmere, introducerede SQL Server 2017 en lignende funktion kaldet CONCAT_WS, hvor du starter med at angive separatoren, efterfulgt af de elementer, du vil sammenkæde. Med denne funktion forenkles løsningen yderligere sådan:
SELECT empid, CONCAT_WS(',', country, region, city) AS location FROM HR.Employees;
Det næste trin er selvfølgelig mindreading. Den 1. april 2020 planlægger Microsoft at frigive CONCAT_MR. Funktionen accepterer et tomt input og finder automatisk ud af, hvilke elementer du vil have den til at sammenkæde ved at læse dit sind. Forespørgslen vil så se således ud:
SELECT empid, CONCAT_MR() AS location FROM HR.Employees;
LOG har en anden parameter
I lighed med EOMONTH-funktionen er mange mennesker ikke klar over, at allerede fra SQL Server 2012, understøtter LOG-funktionen en anden parameter, der giver dig mulighed for at angive logaritmens basis. Forinden understøttede T-SQL funktionen LOG(input), som returnerer den naturlige logaritme af input (ved hjælp af konstanten e som basis), og LOG10(input), som bruger 10 som basis.
Ikke at være klar over eksistensen af den anden parameter til LOG-funktionen, når folk ønskede at beregne Logb (x), hvor b er en anden base end e og 10, gjorde de det ofte den lange vej. Du kan stole på følgende ligning:
Logb (x) =Loga (x)/Loga (b)Som et eksempel, at beregne Log2 (8), stoler du på følgende ligning:
Log2 (8) =Loge (8)/Loge (2)Oversat til T-SQL anvender du følgende beregning:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x) / LOG(@b);
Når du først indser, at LOG understøtter en anden parameter, hvor du angiver basen, bliver beregningen simpelthen:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x, @b);
Markørvariabel
Hvis du har arbejdet med T-SQL i et stykke tid, har du sandsynligvis haft masser af chancer for at arbejde med markører. Som du ved, bruger du typisk følgende trin, når du arbejder med en markør:
- Erklærer markøren
- Åbn markøren
- Gentag gennem markørposterne
- Luk markøren
- Deallokér markøren
Antag som et eksempel, at du skal udføre en opgave pr. database i din instans. Ved at bruge en markør vil du normalt bruge kode, der ligner følgende:
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;
CLOSE-kommandoen frigiver det aktuelle resultatsæt og frigør låse. DEALLOCATE-kommandoen fjerner en markørreference, og når den sidste reference er deallokeret, frigøres de datastrukturer, der omfatter markøren. Hvis du prøver at køre ovenstående kode to gange uden CLOSE og DEALLOCATE kommandoerne, får du følgende fejl:
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.
Sørg for at køre kommandoerne CLOSE og DEALLOCATE, før du fortsætter.
Mange mennesker er ikke klar over, at når de kun skal arbejde med en markør i én batch, hvilket er det mest almindelige tilfælde, kan du i stedet for at bruge en almindelig markør arbejde med en markørvariabel. Som enhver variabel er omfanget af en markørvariabel kun den batch, hvor den blev erklæret. Det betyder, at så snart en batch er færdig, udløber alle variabler. Ved at bruge en markørvariabel, når en batch er færdig, lukker og deallokerer SQL Server den automatisk, hvilket sparer dig for behovet for eksplicit at køre CLOSE og DEALLOCATE-kommandoen.
Her er den reviderede kode, der bruger en markørvariabel denne gang:
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;
Du er velkommen til at udføre det flere gange og bemærk, at denne gang får du ingen fejl. Det er bare renere, og du behøver ikke bekymre dig om at beholde markørens ressourcer, hvis du har glemt at lukke og tildele markøren.
SAMLET med OUTPUT
Siden starten af OUTPUT-sætningen for modifikationssætninger i SQL Server 2005, har det vist sig at være et meget praktisk værktøj, når du vil returnere data fra ændrede rækker. Folk bruger denne funktion regelmæssigt til formål som arkivering, revision og mange andre use cases. En af de irriterende ting ved denne funktion er dog, at hvis du bruger den sammen med INSERT-sætninger, har du kun lov til at returnere data fra de indsatte rækker, og præfikser outputkolonnerne med indsat . Du har ikke adgang til kildetabellens kolonner, selvom du nogle gange skal returnere kolonner fra kilden sammen med kolonner fra målet.
Som et eksempel kan du overveje tabellerne T1 og T2, som du opretter og udfylder ved at køre følgende kode:
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');
Bemærk, at en identitetsegenskab bruges til at generere nøglerne i begge tabeller.
Antag, at du skal kopiere nogle rækker fra T1 til T2; sige dem, hvor keycol % 2 =1. Du vil bruge OUTPUT-sætningen til at returnere de nygenererede nøgler i T2, men du vil også returnere de respektive kildenøgler fra T1 ved siden af disse nøgler. Den intuitive forventning er at bruge følgende INSERT-sætning:
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;
Som nævnt tillader OUTPUT-sætningen dig desværre ikke at henvise til kolonner fra kildetabellen, så du får følgende fejl:
Msg 4104, Level 16, State 1, Line 2Den flerdelte identifikator "T1.keycol" kunne ikke bindes.
Mange mennesker indser ikke, at denne begrænsning mærkeligt nok ikke gælder for MERGE-erklæringen. Så selvom det er lidt akavet, kan du konvertere dit INSERT-udsagn til et MERGE-udsagn, men for at gøre det skal du have MERGE-prædikatet til altid at være falsk. Dette vil aktivere WHEN NOT MATCHED-sætningen og anvende den eneste understøttede INSERT-handling der. Du kan bruge en falsk tilstand som 1 =2. Her er den komplette konverterede kode:
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;
Denne gang kører koden med succes og producerer følgende output:
T1_keycol T2_keycol ----------- ----------- 1 1 3 2 5 3
Forhåbentlig vil Microsoft forbedre understøttelsen af OUTPUT-sætningen i de andre modifikationssætninger for også at tillade returnering af kolonner fra kildetabellen.
Konklusion
Gå ikke ud fra, og RTFM! :-)