sql >> Databasteknik >  >> RDS >> Database

Förbisedda T-SQL-ädelstenar

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 bryr oss om att kolla 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 att vi inte alltid bryr oss om att kontrollera dokumentationen för att se om det finns mer i 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 egna exempel på T-SQL-ädelstenar som ofta förbises, vänligen dela dem i kommentarsavsnittet i den här artikeln.

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, markörvariabler och MERGE with OUTPUT.

I mina exempel kommer jag att använda en exempeldatabas 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

Funktionen EOMONTH introducerades i SQL Server 2012. Många tror att den bara stöder en parameter som innehåller ett inmatningsdatum och att den helt enkelt returnerar månadens slutdatum som motsvarar inmatningsdatumet.

Tänk på ett lite mer sofistikerat behov av att beräkna slutet av föregående månad. Anta till exempel att du behöver fråga i tabellen Sales.Orders och returnera beställningar som gjordes i slutet av föregående månad.

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

USE TSQLV5; 
 
SELECT orderid, orderdate
FROM Sales.Orders
WHERE 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 eftersom det senaste beställningsdatumet som registrerades i tabellen är den 6 maj 2019. Men om tabellen hade beställningar med ett beställningsdatum som infaller på det sista. dagen i föregående månad, skulle frågan ha returnerat dessa.

Vad många inte inser är att EOMONTH stöder en andra parameter där du anger hur många månader som ska läggas till eller subtraheras. Här är den [fullständigt dokumenterade] syntaxen för funktionen:

EOMONTH ( start_date [, month_to_add ] )

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

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

ÖVERSÄTT är ibland enklare än REPLACE

Många är bekanta med REPLACE-funktionen och hur den fungerar. Du använder det när du vill ersätta alla förekomster av en delsträng med en annan i en inmatningssträng. Men ibland, när du har flera ersättningar som du måste använda, är det lite knepigt att använda REPLACE och resulterar i krystade uttryck.

Anta som ett exempel att du får en inmatningssträng @s som innehåller ett tal med spansk formatering. I Spanien använder de en punkt som avgränsare för grupper om tusentals och ett kommatecken som decimaltecken. Du måste konvertera indata till amerikansk formatering, där ett kommatecken används som avgränsare för grupper om tusentals och en punkt som decimalavgränsare.

Genom att använda ett anrop till REPLACE-funktionen kan du endast ersätta alla förekomster av ett tecken eller delsträng med en annan. För att tillämpa två ersättningar (punkter till kommatecken och kommatecken till punkter) måste du kapsla funktionsanrop. Det knepiga är att om du använder REPLACE en gång för att ändra punkter till kommatecken, och sedan en andra gång mot resultatet för att ändra kommatecken till punkter, slutar du med bara punkter. Prova det:

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

Du får följande utdata:

123.456.789.00

Om du vill hålla dig till att använda REPLACE-funktionen behöver du tre funktionsanrop. En för att ersätta punkter 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 punkter. En annan mot resultatet för att ersätta alla förekomster av det tillfälliga tecknet (~ 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 utdata:

123,456,789.00

Det är lite genomförbart, men det resulterar i ett långt och invecklat uttryck. Tänk om du hade fler ersättare att ansöka om?

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

TRANSLATE ( inputString, characters, translations )

Den andra inmatningen (tecken) är en sträng med listan över de enskilda tecken 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 innebär naturligtvis att den andra och tredje parametrarna måste ha samma antal tecken. Det som är viktigt med funktionen är att den inte gör separata pass för var och en av ersättningarna. Om det gjorde det, skulle det potentiellt ha resulterat i samma bugg som i det första exemplet jag visade med de två anropen till REPLACE-funktionen. Följaktligen blir det enkelt att hantera vår uppgift:

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

Denna kod genererar önskad utdata:

123,456,789.00

Det är ganska snyggt!

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

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

Innan jag går in på detaljerna, överväg följande uppgift:givet en inmatningssträng @s, ta bort inledande och efterföljande snedstreck (bakåt och framåt). Som ett exempel, anta att @s innehåller följande sträng:

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

Den önskade utgången är:

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

Observera att utgången ska behålla de inledande och efterföljande utrymmena.

Om du inte kände till TRIMs fulla kapacitet, här är ett sätt du kan ha 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, och sedan använda TRIM för att trimma inledande och efterföljande mellanslag från resultatet. Detta steg klipper i huvudsak främre och efterföljande snedstreck, tillfälligt med ~ istället för originalmellanslag. Här är resultatet av det här steget:

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

Det andra steget använder sedan TRANSLATE för att ersätta alla blanksteg med ett annat neutralt tecken (^) och snedstreck bakåt med mellanslag, och använder sedan TRIM för att trimma inledande och efterföljande mellanslag från resultatet. Det här steget trimmar huvudsakligen inledande och efterföljande snedstreck, tillfälligt med ^ istället för mellanslag. Här är resultatet av det här steget:

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

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

 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 brydde dig om att kontrollera dokumentationen, skulle du ha upptäckt att TRIM är mer sofistikerat än vad du trodde från början. Här är funktionens syntax:

TRIM ( [ characters FROM ] string )

De valfria tecken FRÅN del låter dig ange ett eller flera tecken som du vill trimma från början och slutet av inmatningssträngen. I vårt fall är allt du behöver göra att ange '/\' som denna del, som så:

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

Det är en ganska betydande 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 NULL när du behöver sammanfoga strängar. Som ett exempel, betrakta platsdata som registrerats för anställda i tabellen HR.Employees:

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

Den här frågan genererar följande utdata:

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

Observera att för vissa anställda är regiondelen irrelevant och en irrelevant region representeras av en NULL. Anta att du behöver sammanfoga platsdelarna (land, region och stad), med ett kommatecken som avgränsare, 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 en sammanlänkning av något med en 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-standardiserat beteende.

Om du inte visste att funktionerna CONCAT och CONCAT_WS fanns, skulle du förmodligen ha använt ISNULL eller COALESCE för att ersätta en NULL med en tom sträng, som så:

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

Här är resultatet av den här frågan:

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 introducerade funktionen CONCAT. Den här funktionen accepterar en lista med teckensträngsinmatningar och sammanfogar dem, och medan den gör det ignorerar den NULL. Så med CONCAT kan du förenkla lösningen så här:

SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
FROM 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 ange separatorn, följt av de objekt som du vill sammanfoga. Med denna funktion förenklas lösningen ytterligare så här:

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

Nästa steg är förstås 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 dina tankar. Frågan kommer då att se ut så här:

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

LOG har en andra parameter

I likhet med EOMONTH-funktionen inser många människor inte att redan från och med SQL Server 2012 stöder LOG-funktionen en andra parameter som låter dig indikera logaritmens bas. Innan dess stödde T-SQL funktionen LOG(ingång) som returnerar den naturliga logaritmen för ingången (med konstanten e som bas), och LOG10(ingång) som använder 10 som bas.

Att inte vara medveten om existensen av den andra parametern till LOG-funktionen, när folk ville beräkna Logb (x), där b är en annan bas än e och 10, de gjorde det ofta den långa vägen. Du kan lita på följande ekvation:

Loggb (x) =Logga (x)/Logga (b)

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

Logg2 (8) =Logge (8)/Logge (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 väl inser att LOG 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 har du förmodligen haft 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:

  • Ange markören
  • Öppna markören
  • Iterera genom markörposterna
  • Stäng markören
  • Avallokera markören

Anta som ett exempel att du behöver utföra någon uppgift per databas i din instans. Med hjälp av en markör skulle du normalt använda 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 = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM C INTO @dbname;
END;
 
CLOSE C;
DEALLOCATE C;

Kommandot CLOSE släpper den aktuella resultatuppsättningen och frigör lås. Kommandot DEALLOCATE tar bort en markörreferens, och när den sista referensen avallokeras frigörs datastrukturerna som utgör markören. Om du försöker köra ovanstående kod två gånger utan kommandona CLOSE och DEALLOCATE får du följande felmeddelande:

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.

Se till att du kör kommandona CLOSE och DEALLOCATE innan du fortsätter.

Många människor inser inte att när de behöver arbeta med en markör i bara en batch, vilket är det vanligaste fallet, kan du istället för att använda en vanlig markör arbeta med en markörvariabel. Precis som vilken variabel som helst är omfattningen av en markörvariabel endast den batch där den deklarerades. Det betyder att så fort en batch är klar upphör alla variabler. Med hjälp av en markörvariabel, när en batch är klar, stängs och avallokerar SQL Server den automatiskt, vilket gör att du inte behöver köra kommandot CLOSE och DEALLOCATE explicit.

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

Kör gärna flera gånger och lägg märke till att du inte får några fel den här gången. Det är bara renare, och du behöver inte oroa dig för att behålla markörresurser om du glömde att stänga och deallokera markören.

SAMMANSLUT med OUTPUT

Sedan starten av OUTPUT-satsen för modifieringssatser i SQL Server 2005, visade det sig vara ett mycket praktiskt verktyg närhelst 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 prefixet utdatakolumnerna med inserted . 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.

Som ett exempel, betrakta 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.

Anta att du behöver kopiera några rader från T1 till T2; säg de där keycol % 2 =1. Du vill använda OUTPUT-satsen för att returnera de nyligen genererade nycklarna i T2, men du vill också returnera respektive källnycklar från T1 tillsammans med dessa nycklar. Den intuitiva förväntan är att använda följande INSERT-sats:

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;

Tyvärr tillåter, som nämnts, OUTPUT-satsen dig inte att referera till kolumner från källtabellen, så du får följande fel:

Msg 4104, Level 16, State 1, Line 2
Den flerdelade identifieraren "T1.keycol" kunde inte bindas.

Många människor inser inte att den här begränsningen konstigt nog inte gäller MERGE-påståendet. Så även om det är lite besvärligt kan du konvertera din INSERT-sats till en MERGE-sats, men för att göra det behöver du att MERGE-predikatet alltid är falskt. Detta kommer att aktivera WHEN NOT MATCHED-satsen och tillämpa den enda INSERT-åtgärden som stöds där. Du kan använda ett falskt dummy-villkor som 1 =2. Här är den fullständiga konverterade koden:

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;

Den här gången körs koden framgångsrikt och ger följande utdata:

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

Förhoppningsvis kommer Microsoft att förbättra stödet för OUTPUT-satsen i de andra modifieringssatserna för att även tillåta att kolumner returneras från källtabellen.

Slutsats

Anta inte, och RTFM! :-)


  1. Hur returnerar man flera värden i en kolumn (T-SQL)?

  2. Skaffa id för en infogning i samma uttalande

  3. uppdatera tabellrader i postgres med hjälp av subquery

  4. Intervjutips för SQL-databasadministratör