sql >> Database >  >> RDS >> Database

T-SQL-edelstenen over het hoofd gezien

Mijn goede vriend Aaron Bertrand inspireerde me tot het schrijven van dit artikel. Hij herinnerde me eraan dat we dingen soms als vanzelfsprekend beschouwen als ze voor ons vanzelfsprekend lijken en niet altijd de moeite nemen om het volledige verhaal erachter te controleren. De relevantie voor T-SQL is dat we soms aannemen dat we alles weten wat er te weten valt over bepaalde T-SQL-functies, en niet altijd de moeite nemen om de documentatie te controleren om te zien of er meer aan de hand is. In dit artikel bespreek ik een aantal T-SQL-functies die ofwel vaak volledig over het hoofd worden gezien, of die parameters of mogelijkheden ondersteunen die vaak over het hoofd worden gezien. Als je zelf voorbeelden hebt van T-SQL-edelstenen die vaak over het hoofd worden gezien, deel deze dan alsjeblieft in het opmerkingengedeelte van dit artikel.

Vraag uzelf, voordat u dit artikel begint te lezen, af wat u weet over de volgende T-SQL-functies:EOMONTH, TRANSLATE, TRIM, CONCAT en CONCAT_WS, LOG, cursorvariabelen en MERGE met OUTPUT.

In mijn voorbeelden gebruik ik een voorbeelddatabase met de naam TSQLV5. U vindt het script dat deze database maakt en vult hier, en het ER-diagram hier.

EOMONTH heeft een tweede parameter

De functie EOMONTH is geïntroduceerd in SQL Server 2012. Veel mensen denken dat deze slechts één parameter met een invoerdatum ondersteunt, en dat deze eenvoudigweg de einddatum van de maand retourneert die overeenkomt met de invoerdatum.

Overweeg een iets geavanceerdere behoefte om het einde van de vorige maand te berekenen. Stel dat u bijvoorbeeld de tabel Sales.Orders moet doorzoeken en bestellingen moet retourneren die aan het einde van de vorige maand zijn geplaatst.

Een manier om dit te bereiken is door de functie EOMONTH toe te passen op SYSDATETIME om de datum aan het einde van de maand van de huidige maand te krijgen, en vervolgens de functie DATEADD toe te passen om een ​​maand van het resultaat af te trekken, zoals:

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

Houd er rekening mee dat als u deze query daadwerkelijk uitvoert in de TSQLV5-voorbeelddatabase, u een leeg resultaat krijgt, aangezien de laatste besteldatum in de tabel 6 mei 2019 is. Als de tafel echter bestellingen had met een besteldatum die op de laatste dag van de vorige maand, zou de zoekopdracht deze hebben geretourneerd.

Wat veel mensen zich niet realiseren is dat EOMONTH een tweede parameter ondersteunt waarbij je aangeeft hoeveel maanden je moet optellen of aftrekken. Hier is de [volledig gedocumenteerde] syntaxis van de functie:

EOMONTH ( start_date [, month_to_add ] )

Onze taak kan gemakkelijker en natuurlijker worden bereikt door simpelweg -1 op te geven als de tweede parameter van de functie, zoals:

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

VERTALEN is soms eenvoudiger dan VERVANGEN

Veel mensen zijn bekend met de REPLACE-functie en hoe deze werkt. U gebruikt het wanneer u alle exemplaren van een subtekenreeks wilt vervangen door een andere in een invoertekenreeks. Soms echter, wanneer je meerdere vervangingen hebt die je moet toepassen, is het gebruik van REPLACE een beetje lastig en resulteert dit in ingewikkelde uitdrukkingen.

Stel dat u als voorbeeld een invoertekenreeks @s krijgt die een getal met Spaanse opmaak bevat. In Spanje gebruiken ze een punt als scheidingsteken voor groepen van duizenden en een komma als decimaalteken. U moet de invoer converteren naar Amerikaanse opmaak, waarbij een komma wordt gebruikt als scheidingsteken voor groepen van duizenden, en een punt als decimaalteken.

Door één aanroep van de REPLACE-functie te gebruiken, kunt u alleen alle exemplaren van een teken of subtekenreeks door een ander vervangen. Om twee vervangingen toe te passen (punten voor komma's en komma's voor punten) moet u functieaanroepen nesten. Het lastige is dat als je eenmaal REPLACE gebruikt om punten in komma's te veranderen, en dan een tweede keer tegen het resultaat om komma's in punten te veranderen, je alleen punten krijgt. Probeer het:

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

U krijgt de volgende uitvoer:

123,456,789.00

Als je de REPLACE-functie wilt blijven gebruiken, heb je drie functieaanroepen nodig. Een om punten te vervangen door een neutraal teken waarvan u weet dat het normaal gesproken niet in de gegevens kan voorkomen (bijvoorbeeld ~). Nog een tegen het resultaat om alle komma's te vervangen door punten. Nog een tegen het resultaat om alle exemplaren van het tijdelijke teken (~ in ons voorbeeld) te vervangen door komma's. Hier is de volledige uitdrukking:

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

Deze keer krijg je de juiste output:

123,456,789.00

Het is een beetje te doen, maar het resulteert in een lange en ingewikkelde uitdrukking. Wat als u meer vervangingen had om toe te passen?

Veel mensen zijn zich er niet van bewust dat SQL Server 2017 een nieuwe functie genaamd TRANSLATE heeft geïntroduceerd die dergelijke vervangingen aanzienlijk vereenvoudigt. Dit is de syntaxis van de functie:

TRANSLATE ( inputString, characters, translations )

De tweede invoer (tekens) is een string met de lijst van de individuele tekens die u wilt vervangen, en de derde invoer (vertalingen) is een reeks met de lijst van de corresponderende tekens waarmee u de brontekens wilt vervangen. Dit betekent natuurlijk dat de tweede en derde parameter hetzelfde aantal karakters moeten hebben. Wat belangrijk is aan de functie, is dat deze geen afzonderlijke passen doet voor elk van de vervangingen. Als dat zo was, zou dit mogelijk tot dezelfde bug hebben geleid als in het eerste voorbeeld dat ik liet zien met behulp van de twee aanroepen van de REPLACE-functie. Bijgevolg wordt het afhandelen van onze taak een no-brainer:

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

Deze code genereert de gewenste output:

123,456,789.00

Dat is best netjes!

TRIM is meer dan LTRIM(RTRIM())

SQL Server 2017 introduceerde ondersteuning voor de functie TRIM. Veel mensen, waaronder ikzelf, gaan er in eerste instantie vanuit dat het niet meer is dan een simpele snelkoppeling naar LTRIM(RTRIM(input)). Als u echter de documentatie bekijkt, realiseert u zich dat het eigenlijk krachtiger is dan dat.

Voordat ik inga op de details, overweeg dan de volgende taak:gegeven een invoerstring @s, verwijder voorloop- en achterslashes (achteruit en vooruit). Stel bijvoorbeeld dat @s de volgende tekenreeks bevat:

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

De gewenste uitvoer is:

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

Merk op dat de uitvoer de voorloop- en volgspaties moet behouden.

Als u niet op de hoogte was van de volledige mogelijkheden van TRIM, kunt u de taak als volgt hebben opgelost:

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

De oplossing begint met het gebruik van TRANSLATE om alle spaties te vervangen door een neutraal teken (~) en voorwaartse slashes door spaties, en vervolgens met TRIM om voorloop- en volgspaties uit het resultaat te verwijderen. Deze stap trimt in wezen voor- en achterwaartse schuine strepen, waarbij tijdelijk ~ wordt gebruikt in plaats van originele spaties. Dit is het resultaat van deze stap:

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

De tweede stap gebruikt vervolgens VERTALEN om alle spaties te vervangen door een ander neutraal teken (^) en schuine strepen naar achteren met spaties, en vervolgens TRIM te gebruiken om voorloop- en volgspaties uit het resultaat te verwijderen. Deze stap trimt in wezen voor- en achterwaartse schuine strepen, waarbij tijdelijk ^ wordt gebruikt in plaats van tussenliggende spaties. Dit is het resultaat van deze stap:

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

De laatste stap gebruikt TRANSLATE om spaties te vervangen door schuine strepen naar achteren, ^ door schuine strepen naar voren en ~ door spaties, waardoor de gewenste uitvoer wordt gegenereerd:

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

Probeer als oefening deze taak op te lossen met een pre-SQL Server 2017-compatibele oplossing waarbij u TRIM en TRANSLATE niet kunt gebruiken.

Terug naar SQL Server 2017 en hoger, als u de moeite had genomen om de documentatie te controleren, zou u hebben ontdekt dat TRIM geavanceerder is dan u aanvankelijk dacht. Dit is de syntaxis van de functie:

TRIM ( [ characters FROM ] string )

De optionele tekens FROM part kunt u een of meer tekens specificeren die u wilt bijsnijden vanaf het begin en einde van de invoerreeks. In ons geval hoef je alleen maar '/\' op te geven als dit deel, zoals:

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

Dat is een behoorlijk significante verbetering vergeleken met de vorige oplossing!

CONCAT en CONCAT_WS

Als je al een tijdje met T-SQL werkt, weet je hoe lastig het is om met NULL's om te gaan wanneer je strings moet samenvoegen. Neem als voorbeeld de locatiegegevens die voor werknemers zijn vastgelegd in de tabel HR.Medewerkers:

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

Deze query genereert de volgende uitvoer:

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

Merk op dat voor sommige werknemers het regiogedeelte niet relevant is en dat een irrelevante regio wordt weergegeven door een NULL. Stel dat u de locatiedelen (land, regio en stad) moet samenvoegen, met een komma als scheidingsteken, maar waarbij u NULL-regio's negeert. Als de regio relevant is, wilt u dat het resultaat de vorm <coutry>,<region>,<city> heeft en wanneer de regio niet relevant is, wilt u dat het resultaat de vorm heeft <country>,<city> . Normaal gesproken levert het aaneenschakelen van iets met een NULL een NULL-resultaat op. U kunt dit gedrag wijzigen door de CONCAT_NULL_YIELDS_NULL sessie-optie uit te schakelen, maar ik zou niet aanraden om niet-standaard gedrag in te schakelen.

Als u niet wist van het bestaan ​​van de CONCAT- en CONCAT_WS-functies, zou u waarschijnlijk ISNULL of COALESCE hebben gebruikt om een ​​NULL te vervangen door een lege tekenreeks, zoals:

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

Dit is de uitvoer van deze zoekopdracht:

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 introduceerde de functie CONCAT. Deze functie accepteert een lijst met invoer van tekenreeksen en voegt ze samen, en negeert daarbij NULL's. Dus met CONCAT kunt u de oplossing als volgt vereenvoudigen:

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

Toch moet u de scheidingstekens expliciet specificeren als onderdeel van de invoer van de functie. Om ons leven nog gemakkelijker te maken, heeft SQL Server 2017 een vergelijkbare functie genaamd CONCAT_WS geïntroduceerd, waarbij u begint met het aangeven van het scheidingsteken, gevolgd door de items die u wilt samenvoegen. Met deze functie wordt de oplossing als volgt verder vereenvoudigd:

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

De volgende stap is natuurlijk mindreading. Op 1 april 2020 is Microsoft van plan CONCAT_MR uit te brengen. De functie accepteert een lege invoer en zoekt automatisch uit welke elementen je wilt laten samenvoegen door je gedachten te lezen. De zoekopdracht ziet er dan als volgt uit:

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

LOG heeft een tweede parameter

Net als de EOMONTH-functie realiseren veel mensen zich niet dat de LOG-functie al vanaf SQL Server 2012 een tweede parameter ondersteunt waarmee u de basis van de logaritme kunt aangeven. Daarvoor ondersteunde T-SQL de functie LOG(invoer) die de natuurlijke logaritme van de invoer retourneert (met de constante e als basis), en LOG10 (invoer) die 10 als basis gebruikt.

Zich niet bewust zijn van het bestaan ​​van de tweede parameter van de LOG-functie, toen mensen Logb wilden berekenen (x), waar b een ander grondtal is dan e en 10, deden ze het vaak op de lange weg. U kunt vertrouwen op de volgende vergelijking:

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

Om bijvoorbeeld Log2 . te berekenen (8), vertrouwt u op de volgende vergelijking:

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

Vertaald naar T-SQL past u de volgende berekening toe:

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

Zodra u zich realiseert dat LOG een tweede parameter ondersteunt waarin u de basis aangeeft, wordt de berekening eenvoudig:

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

Cursorvariabele

Als je al een tijdje met T-SQL werkt, heb je waarschijnlijk veel kansen gehad om met cursors te werken. Zoals u weet, gebruikt u meestal de volgende stappen wanneer u met een cursor werkt:

  • Declareer de cursor
  • Open de cursor
  • Doorloop de cursorrecords
  • Sluit de cursor
  • De toewijzing van de cursor ongedaan maken

Stel bijvoorbeeld dat u een taak per database in uw instantie moet uitvoeren. Als u een cursor gebruikt, zou u normaal gesproken code gebruiken die lijkt op de volgende:

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;

Het CLOSE-commando geeft de huidige resultaatset vrij en maakt vergrendelingen vrij. Het DEALLOCATE-commando verwijdert een cursorverwijzing, en wanneer de laatste verwijzing wordt opgeheven, worden de gegevensstructuren waaruit de cursor bestaat, vrijgemaakt. Als u de bovenstaande code twee keer probeert uit te voeren zonder de opdrachten CLOSE en DEALLOCATE, krijgt u de volgende foutmelding:

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.

Zorg ervoor dat u de opdrachten CLOSE en DEALLOCATE uitvoert voordat u doorgaat.

Veel mensen realiseren zich niet dat wanneer ze met een cursor in slechts één batch moeten werken, wat het meest voorkomende geval is, in plaats van een gewone cursor te gebruiken, je met een cursorvariabele kunt werken. Zoals elke variabele is het bereik van een cursorvariabele alleen de batch waarin deze is gedeclareerd. Dit betekent dat zodra een batch is afgelopen, alle variabelen verlopen. Met behulp van een cursorvariabele, zodra een batch is voltooid, wordt deze door SQL Server automatisch gesloten en ongedaan gemaakt, zodat u de opdracht CLOSE en DEALLOCATE niet expliciet hoeft uit te voeren.

Hier is de herziene code met deze keer een cursorvariabele:

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;

Voel je vrij om het meerdere keren uit te voeren en merk op dat je deze keer geen fouten krijgt. Het is gewoon schoner en u hoeft zich geen zorgen te maken over het behouden van cursorbronnen als u bent vergeten de cursor te sluiten en de toewijzing ongedaan te maken.

MERGE MET OUTPUT

Sinds de introductie van de OUTPUT-clausule voor wijzigingsinstructies in SQL Server 2005, bleek het een zeer praktisch hulpmiddel te zijn wanneer u gegevens uit gewijzigde rijen wilde retourneren. Mensen gebruiken deze functie regelmatig voor doeleinden zoals archivering, auditing en vele andere use-cases. Een van de vervelende dingen van deze functie is echter dat als je het gebruikt met INSERT-instructies, je alleen gegevens mag retourneren uit de ingevoegde rijen, waarbij de uitvoerkolommen worden voorafgegaan door ingevoegd . U heeft geen toegang tot de kolommen van de brontabel, ook al moet u soms kolommen van de bron naast kolommen van het doel retourneren.

Beschouw als voorbeeld de tabellen T1 en T2, die u maakt en vult door de volgende code uit te voeren:

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

Merk op dat een identiteitseigenschap wordt gebruikt om de sleutels in beide tabellen te genereren.

Stel dat u enkele rijen van T1 naar T2 moet kopiëren; zeg, degenen waar keycol % 2 =1. U wilt de OUTPUT-clausule gebruiken om de nieuw gegenereerde sleutels in T2 te retourneren, maar u wilt naast die sleutels ook de respectieve bronsleutels van T1 retourneren. De intuïtieve verwachting is om de volgende INSERT-instructie te gebruiken:

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;

Helaas staat de OUTPUT-clausule u helaas niet toe om naar kolommen uit de brontabel te verwijzen, dus u krijgt de volgende foutmelding:

Msg 4104, Level 16, State 1, Line 2
De meerdelige identifier "T1.keycol" kan niet worden gebonden.

Veel mensen realiseren zich niet dat deze beperking vreemd genoeg niet van toepassing is op de MERGE-instructie. Dus ook al is het een beetje onhandig, je kunt je INSERT-instructie omzetten in een MERGE-instructie, maar om dit te doen, moet het MERGE-predikaat altijd onwaar zijn. Hierdoor wordt de WHEN NOT MATCHED-clausule geactiveerd en wordt de enige ondersteunde INSERT-actie daar toegepast. U kunt een dummy-false voorwaarde gebruiken, zoals 1 =2. Hier is de volledige geconverteerde code:

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;

Deze keer wordt de code succesvol uitgevoerd en wordt de volgende uitvoer geproduceerd:

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

Hopelijk zal Microsoft de ondersteuning voor de OUTPUT-clausule in de andere wijzigingsinstructies verbeteren, zodat ook kolommen uit de brontabel kunnen worden geretourneerd.

Conclusie

Ga er niet vanuit, en RTFM! :-)


  1. Herhaal een string meerdere keren in MySQL - REPEAT()

  2. Hoe de Ln()-functie werkt in PostgreSQL

  3. Hoe LOG10() werkt in MariaDB

  4. Simulatie van CONNECT BY PRIOR van Oracle in SQL Server