förbises T-SQL Gems

min gode vän Aaron Bertrand inspirerade mig att skriva den här artikeln. Han påminde mig om hur vi ibland tar saker för givet när de verkar uppenbara för oss och inte alltid bry sig om att kontrollera hela historien bakom dem. Relevansen för T-SQL är att vi ibland antar att vi vet allt som finns att veta om vissa T-SQL-funktioner, och stör inte alltid att kontrollera dokumentationen för att se om det finns mer för dem. I den här artikeln täcker jag ett antal T-SQL-funktioner som antingen ofta helt förbises, eller som stöder parametrar eller funktioner som ofta förbises. Om du har exempel på din egen av T-SQL pärlor som ofta förbises, Vänligen dela dem i kommentarerna i denna artikel.

innan du börjar läsa den här artikeln, fråga dig själv vad du vet om följande T-SQL-funktioner: EOMONTH, TRANSLATE, TRIM, CONCAT och CONCAT_WS, LOG, cursor variables och MERGE with OUTPUT.

i mina exempel använder jag en provdatabas som heter TSQLV5. Du kan hitta skriptet som skapar och fyller denna databas här, och dess ER diagram här.

EOMONTH har en andra parameter

eomonth-funktionen introducerades i SQL Server 2012. Många tror att det bara stöder en parameter som håller ett inmatningsdatum och att det helt enkelt returnerar månadsdatumet som motsvarar inmatningsdatumet.

Tänk på ett något mer sofistikerat behov av att beräkna slutet av föregående månad. Antag till exempel att du måste fråga försäljningen.Ordertabell och returorder som placerades i slutet av föregående månad.

ett sätt att uppnå detta är att tillämpa EOMONTH-funktionen på SYSDATETIME för att få månadens slutdatum för den aktuella månaden och sedan tillämpa DATEADD-funktionen för att subtrahera en månad från resultatet, så här:

USE TSQLV5; SELECT orderid, orderdateFROM Sales.OrdersWHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

Observera att om du faktiskt kör den här frågan i tsqlv5-exempeldatabasen får du ett tomt resultat sedan det senaste orderdatumet som registrerats i tabellen är 6 maj 2019. Men om tabellen hade order med ett orderdatum som faller på den sista dagen i föregående månad, skulle frågan ha returnerat dem.

vad många inte inser är att EOMONTH stöder en andra parameter där du anger hur många månader du ska lägga till eller subtrahera. Här är syntaxen för funktionen:

EOMONTH ( start_date )

vår uppgift kan uppnås lättare och naturligt genom att helt enkelt ange -1 som den andra parametern till funktionen, som så:

SELECT orderid, orderdateFROM Sales.OrdersWHERE orderdate = EOMONTH(SYSDATETIME(), -1);

TRANSLATE är ibland enklare än ersätt

många känner till funktionen Ersätt och hur den fungerar. Du använder den när du vill ersätta alla förekomster av en delsträng med en annan i en inmatningssträng. Ibland, men när du har flera ersättningar som du behöver använda, är det lite knepigt att använda Ersätt och resulterar i invecklade uttryck.

anta som exempel att du får en inmatningssträng @s som innehåller ett tal med spansk formatering. I Spanien använder de en period som separator för grupper om tusentals och ett komma som decimalavgränsare. Du måste konvertera inmatningen till Amerikansk formatering, där ett komma används som separator för grupper om tusentals och en period som decimalavgränsare.

med ett anrop till funktionen ersätt kan du bara ersätta alla förekomster av ett tecken eller substring med ett annat. För att tillämpa två ersättningar (perioder till kommatecken och kommatecken till perioder) måste du nest funktionsanrop. Den knepiga delen är att om du använder ersätt en gång för att ändra perioder till kommatecken, och sedan en andra gång mot resultatet för att ändra kommatecken till perioder, slutar du med bara perioder. Prova det:

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

du får följande utgång:

123.456.789.00

om du vill hålla fast vid att använda funktionen ersätt behöver du tre funktionssamtal. En för att ersätta perioder med ett neutralt tecken som du vet som normalt inte kan visas i data (säg ~). En annan mot resultatet för att ersätta alla kommatecken med perioder. En annan mot resultatet för att ersätta alla förekomster av den tillfälliga karaktären (~ i vårt exempel) med kommatecken. Här är det fullständiga uttrycket:

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

den här gången får du rätt utgång:

123,456,789.00

det är ganska genomförbart, men det resulterar i ett långt och invecklat uttryck. Vad händer om du hade fler ersättare att ansöka?

många är inte medvetna om att SQL Server 2017 introducerade en ny funktion som heter TRANSLATE som förenklar sådana ersättningar mycket. Här är funktionens syntax:

TRANSLATE ( inputString, characters, translations )

den andra ingången (tecken) är en sträng med listan över de enskilda tecknen som du vill ersätta, och den tredje ingången (översättningar) är en sträng med listan över motsvarande tecken som du vill ersätta källtecknen med. Detta betyder naturligtvis att den andra och tredje parametrarna måste ha samma antal tecken. Vad som är viktigt med funktionen är att det inte gör separata pass för var och en av ersättningarna. Om det gjorde det skulle det potentiellt ha resulterat i samma fel som i det första exemplet som jag visade med de två samtalen till ersätt-funktionen. Följaktligen blir hanteringen av vår uppgift en no-brainer:

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

denna kod genererar önskad utgång:

123,456,789.00

det är ganska snyggt!

TRIM är mer än LTRIM(RTRIM())

SQL Server 2017 infört stöd för funktionen TRIM. Många människor, inklusive mig själv, antar initialt bara att det inte är mer än en enkel genväg till LTRIM(RTRIM(input)). Men om du kontrollerar dokumentationen inser du att den faktiskt är kraftfullare än så.

innan jag går in i detaljerna, överväga följande uppgift: ge en inmatningssträng @s, ta bort Ledande och efterföljande snedstreck (bakåt och framåt). Anta till exempel att @s innehåller följande sträng:

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

önskad utgång är:

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

Observera att utgången ska behålla de ledande och bakre utrymmena.

Om du inte kände till trims fulla kapacitet, är det här ett sätt du kanske har löst uppgiften:

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

lösningen börjar med att använda TRANSLATE för att ersätta alla mellanslag med ett neutralt tecken (~) och snedstreck framåt med mellanslag, använd sedan trim för att trimma inledande och efterföljande mellanslag från resultatet. Detta steg trimmar i huvudsak ledande och efterföljande snedstreck, tillfälligt med ~ istället för ursprungliga mellanslag. Här är resultatet av detta steg:

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

det andra steget använder sedan TRANSLATE för att ersätta alla mellanslag med ett annat neutralt tecken (^) och bakåt snedstreck med mellanslag, sedan använder TRIM för att trimma inledande och efterföljande mellanslag från resultatet. Detta steg trimmar i huvudsak ledande och bakre snedstreck, tillfälligt med ^ istället för mellanliggande utrymmen. Här är resultatet av detta steg:

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

det sista steget använder TRANSLATE för att ersätta mellanslag med bakåt snedstreck, ^ med framåt snedstreck och ~ med mellanslag, vilket genererar önskad utgång:

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

som en övning kan du försöka lösa den här uppgiften med en pre-SQL Server 2017-kompatibel lösning där du inte kan använda TRIM och TRANSLATE.

tillbaka till SQL Server 2017 och senare, om du störde att kontrollera dokumentationen, skulle du ha upptäckt att TRIM är mer sofistikerat än vad du trodde ursprungligen. Här är funktionens Syntax:

TRIM ( string )

de valfria tecknen från delen låter dig ange ett eller flera tecken som du vill klippa från början och slutet av inmatningssträngen. I vårt fall är allt du behöver göra att ange ’/ \ ’ som den här delen, så här:

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

det är en ganska signifikant förbättring jämfört med den tidigare lösningen!

CONCAT och CONCAT_WS

Om du har arbetat med T-SQL ett tag vet du hur besvärligt det är att hantera NULLs när du behöver sammanfoga strängar. Tänk exempel på platsdata som registrerats för anställda i HR.Employees-tabellen:

SELECT empid, country, region, cityFROM HR.Employees;

denna fråga genererar följande utdata:

empid country region city----------- --------------- --------------- ---------------1 USA WA Seattle2 USA WA Tacoma3 USA WA Kirkland4 USA WA Redmond5 UK NULL London6 UK NULL London7 UK NULL London8 USA WA Seattle9 UK NULL London

Observera att för vissa anställda är regiondelen irrelevant och en irrelevant Region representeras av en null. Antag att du måste sammanfoga platsdelarna (land, region och stad), med ett komma som separator, men ignorera NULL-regioner. När regionen är relevant vill du att resultatet ska ha formen <coutry>,<region>,<city> och när regionen är irrelevant vill du att resultatet ska ha formen <country>,<city>. Normalt ger sammanslagning av något med ett NULL ett NULL-resultat. Du kan ändra detta beteende genom att stänga av sessionsalternativet CONCAT_NULL_YIELDS_NULL, men jag skulle inte rekommendera att aktivera icke-standardbeteende.

Om du inte visste om förekomsten av CONCAT-och CONCAT_WS-funktionerna, skulle du antagligen ha använt ISNULL eller COALESCE för att ersätta en NULL med en tom sträng, så här:

div >

här är resultatet av denna fråga:

empid location----------- -----------------------------------------------1 USA,WA,Seattle2 USA,WA,Tacoma3 USA,WA,Kirkland4 USA,WA,Redmond5 UK,London6 UK,London7 UK,London8 USA,WA,Seattle9 UK,London

SQL Server 2012 introducerade funktionen concat. Den här funktionen accepterar en lista med teckensträngingångar och sammanfogar dem, och samtidigt ignorerar den nollor. Så med CONCAT kan du förenkla lösningen så här:

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

ändå måste du uttryckligen ange separatorerna som en del av funktionens ingångar. För att göra våra liv ännu enklare introducerade SQL Server 2017 en liknande funktion som heter CONCAT_WS där du börjar med att indikera separatorn, följt av de objekt som du vill sammanfoga. Med denna funktion förenklas lösningen ytterligare som så:

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

nästa steg är naturligtvis mindreading. Den 1 April 2020 planerar Microsoft att släppa CONCAT_MR. funktionen accepterar en tom ingång och räknar automatiskt ut vilka element du vill att den ska sammanfoga genom att läsa ditt sinne. Frågan kommer då att se ut så här:

SELECT empid, CONCAT_MR() AS locationFROM HR.Employees;

LOG har en andra parameter

liknande eomonth-funktionen inser många inte att det redan börjar med SQL Server 2012 stöder loggfunktionen en andra parameter som låter dig ange logaritmens bas. Innan dess stödde T-SQL funktionen LOG (input) som returnerar den naturliga logaritmen för ingången(med konstanten e som bas) och LOG10 (input) som använder 10 som bas.

att inte vara medveten om förekomsten av den andra parametern till loggfunktionen, när folk ville beräkna Logb(x), där b är en annan bas än e och 10, gjorde de det ofta långt. Du kan lita på följande ekvation:

Logb (x) = Loga (x) / Loga (b)

som ett exempel, för att beräkna Log2 (8), litar du på följande ekvation:

Log2 (8) = Loge (8) / Loge (2)

översatt till T-SQL tillämpar du följande beräkning:

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

När du inser att loggen stöder en andra parameter där du anger basen blir beräkningen helt enkelt:

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

markörvariabel

om du har arbetat med T-SQL ett tag hade du förmodligen många chanser att arbeta med markörer. Som du vet använder du vanligtvis följande steg när du arbetar med en markör:

  • deklarera markören
  • öppna markören
  • iterera genom markörposterna
  • Stäng markören
  • Avfördela markören

Antag till exempel att du måste utföra någon uppgift per databas i din instans. Med en markör använder du normalt kod som liknar följande:

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 = 0BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... do your thing here ... */ FETCH NEXT FROM C INTO @dbname;END; CLOSE C;DEALLOCATE C;

stäng-kommandot släpper den aktuella resultatuppsättningen och frigör lås. Kommandot AVALLOKERA tar bort en markörreferens, och när den sista referensen avallokeras frigörs datastrukturerna som består av markören. Om du försöker köra ovanstående kod två gånger utan kommandona stäng och AVFÖRDELA, får du följande felmeddelande:

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

se till att du kör kommandona stäng och AVFÖRDELA innan du fortsätter.

många människor inser inte att när de behöver arbeta med en markör i endast en sats, vilket är det vanligaste fallet, istället för att använda en vanlig markör kan du arbeta med en markörvariabel. Liksom alla variabler är omfattningen av en markörvariabel endast den sats där den deklarerades. Detta innebär att så snart en sats är klar löper alla variabler ut. Med hjälp av en markörvariabel, när en sats är klar, stängs SQL Server och avallokerar den automatiskt, vilket sparar behovet av att köra kommandot Stäng och AVALLOKERA explicit.

här är den reviderade koden med en markörvariabel den här gången:

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 = 0BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... do your thing here ... */ FETCH NEXT FROM @C INTO @dbname;END;

kör gärna det flera gånger och märka att den här gången får du inga fel. Det är bara renare, och du behöver inte oroa dig för att hålla markörresurser om du glömde att stänga och avfördela markören.

sammanfoga med OUTPUT

Sedan starten av output-klausulen för modifieringssatser i SQL Server 2005 visade det sig vara ett mycket praktiskt verktyg när du ville returnera data från modifierade rader. Människor använder den här funktionen regelbundet för ändamål som arkivering, revision och många andra användningsfall. En av de irriterande sakerna med den här funktionen är dock att om du använder den med INSERT-satser får du bara returnera data från de infogade raderna och prefixa utgångskolumnerna med infogad. Du har inte tillgång till källtabellens kolumner, även om du ibland behöver returnera kolumner från källan tillsammans med kolumner från målet.

tänk exempel på tabellerna T1 och T2, som du skapar och fyller i genom att köra följande kod:

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');

Observera att en identitetsegenskap används för att generera nycklarna i båda tabellerna.

Antag att du måste kopiera några rader från T1 till T2; säg de där keycol % 2 = 1. Du vill använda output-klausulen för att returnera de nyligen genererade nycklarna i T2, men du vill också returnera respektive källtangenter från T1 tillsammans med dessa nycklar. Den intuitiva förväntan är att använda följande infoga uttalande:

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;

div > msg 4104, nivå 16, tillstånd 1, Linje 2
Den flerdelade identifieraren ”T1.keycol ” kunde inte vara bunden.

många människor inser inte att konstigt denna begränsning inte gäller för sammanslagningen. Så även om det är lite besvärligt kan du konvertera ditt INSERT-uttalande till ett MERGE-uttalande, men för att göra det måste du sammanfoga predikat för att alltid vara falskt. Detta aktiverar satsen när den inte matchas och tillämpar den enda stödda insatsen där. Du kan använda ett falskt falskt tillstånd som 1 = 2. Här är den fullständiga konverterade koden:

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

den här gången körs koden framgångsrikt och producerar följande utgång:

T1_keycol T2_keycol----------- -----------1 13 25 3

Lämna ett svar

Din e-postadress kommer inte publiceras.