sql >> Databáze >  >> RDS >> Database

Přehlížené klenoty T-SQL

K napsání tohoto článku mě inspiroval můj dobrý přítel Aaron Bertrand. Připomněl mi, jak někdy bereme věci jako samozřejmost, když se nám zdají samozřejmé, a neobtěžujeme se vždy kontrolovat celý příběh za nimi. Pro T-SQL je důležité, že někdy předpokládáme, že o určitých funkcích T-SQL víme vše, co je třeba vědět, a neobtěžujeme se vždy kontrolou dokumentace, abychom zjistili, zda v nich není něco víc. V tomto článku se zabývám řadou funkcí T-SQL, které jsou buď často zcela přehlíženy, nebo které podporují parametry nebo schopnosti, které jsou často přehlíženy. Pokud máte příklady svých vlastních drahokamů T-SQL, které jsou často přehlíženy, podělte se o ně v sekci komentářů tohoto článku.

Než začnete číst tento článek, zeptejte se sami sebe, co víte o následujících funkcích T-SQL:EOMONTH, TRANSLATE, TRIM, CONCAT a CONCAT_WS, LOG, kurzorové proměnné a MERGE with OUTPUT.

Ve svých příkladech použiji ukázkovou databázi nazvanou TSQLV5. Skript, který vytváří a naplňuje tuto databázi, najdete zde a její ER diagram zde.

EOMONTH má druhý parametr

Funkce EOMONTH byla představena v SQL Server 2012. Mnoho lidí si myslí, že podporuje pouze jeden parametr obsahující vstupní datum a že jednoduše vrací datum konce měsíce, které odpovídá vstupnímu datu.

Zvažte trochu sofistikovanější potřebu počítat konec předchozího měsíce. Předpokládejme například, že potřebujete dotaz na tabulku Sales.Orders a vrátit objednávky, které byly zadány na konci předchozího měsíce.

Jedním ze způsobů, jak toho dosáhnout, je použít funkci EOMONTH na SYSDATETIME, abyste získali datum konce měsíce aktuálního měsíce, a poté použít funkci DATEADD k odečtení měsíce od výsledku, například takto:

POUŽÍVEJTE TSQLV5; SELECT orderid, orderdateFROM Sales.OrdersWHERE orderdate =EOMONTH(DATEADD(měsíc, -1, SYSDATETIME()));

Upozorňujeme, že pokud skutečně spustíte tento dotaz ve vzorové databázi TSQLV5, získáte prázdný výsledek, protože datum poslední objednávky zaznamenané v tabulce je 6. května 2019. Pokud však tabulka obsahovala objednávky s datem objednávky, které připadá na poslední den předchozího měsíce, dotaz by je vrátil.

Mnoho lidí si neuvědomuje, že EOMONTH podporuje druhý parametr, kde uvádíte, kolik měsíců se má přidat nebo odečíst. Zde je [plně zdokumentovaná] syntaxe funkce:

EOMONTH ( start_date [, month_to_add ] )

Náš úkol lze dosáhnout snadněji a přirozeněji, když jednoduše zadáte -1 jako druhý parametr funkce, například takto:

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

TRANSLATE je někdy jednodušší než REPLACE

Mnoho lidí zná funkci REPLACE a její fungování. Používáte jej, když chcete nahradit všechny výskyty jednoho podřetězce jiným ve vstupním řetězci. Někdy, když máte více záměn, které potřebujete použít, je použití REPLACE trochu složitější a vede ke spletitým výrazům.

Předpokládejme například, že jste dostali vstupní řetězec @s, který obsahuje číslo ve španělském formátu. Ve Španělsku používají jako oddělovač pro skupiny tisíců tečku a jako oddělovač desetinných míst čárku. Vstup musíte převést na americké formátování, kde se pro skupiny tisíců používá čárka jako oddělovač a jako oddělovač desetinných míst tečka.

Pomocí jednoho volání funkce REPLACE můžete nahradit pouze všechny výskyty jednoho znaku nebo podřetězce jiným. Chcete-li použít dvě nahrazení (tečky na čárky a čárky na tečky), musíte vnořit volání funkcí. Záludná část je v tom, že pokud použijete REPLACE jednou pro změnu teček na čárky a poté podruhé proti výsledku pro změnu čárek na tečky, skončíte pouze s tečkami. Zkuste to:

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

Získáte následující výstup:

123 456 789,00

Pokud se chcete držet používání funkce REPLACE, potřebujete tři volání funkce. Jeden pro nahrazení teček neutrálním znakem, o kterém víte, že se normálně v datech nemůže objevit (řekněme ~). Další proti výsledku nahradit všechny čárky tečkami. Další proti výsledku nahradit všechny výskyty dočasného znaku (~ v našem příkladu) čárkami. Zde je úplný výraz:

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

Tentokrát získáte správný výstup:

123 456 789,00

Je to trochu proveditelné, ale výsledkem je dlouhý a spletitý výraz. Co kdybyste mohli požádat o více náhradníků?

Mnoho lidí si neuvědomuje, že SQL Server 2017 představil novou funkci nazvanou TRANSLATE, která takové náhrady značně zjednodušuje. Zde je syntaxe funkce:

TRANSLATE ( inputString, characters, translations )

Druhý vstup (znaky) je řetězec se seznamem jednotlivých znaků, které chcete nahradit, a třetí vstup (překlady) je řetězec se seznamem odpovídajících znaků, kterými chcete nahradit zdrojové znaky. To přirozeně znamená, že druhý a třetí parametr musí mít stejný počet znaků. Na této funkci je důležité, že neprovádí samostatné průchody pro každou z náhrad. Pokud by tomu tak bylo, potenciálně by to vedlo ke stejné chybě jako v prvním příkladu, který jsem ukázal pomocí dvou volání funkce REPLACE. V důsledku toho se zvládání našeho úkolu stává bez rozmyslu:

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

Tento kód generuje požadovaný výstup:

123 456 789,00

To je pěkné!

TRIM je více než LTRIM(RTRIM())

SQL Server 2017 zavedl podporu pro funkci TRIM. Mnoho lidí, včetně mě, zpočátku jen předpokládá, že to není nic víc než jednoduchá zkratka k LTRIM(RTRIM(input)). Pokud však zkontrolujete dokumentaci, zjistíte, že je ve skutečnosti výkonnější.

Než se pustím do podrobností, zvažte následující úkol:zadáte-li vstupní řetězec @s, odstraňte úvodní a koncová lomítka (dozadu a vpřed). Předpokládejme například, že @s obsahuje následující řetězec:

//\\ odstranit úvodní a koncová zpětná (\) a dopředná (/) lomítka \\//

Požadovaný výstup je:

 odstranit úvodní a koncová zpětná (\) a dopředná (/) lomítka 

Všimněte si, že výstup by měl zachovat úvodní a koncové mezery.

Pokud jste nevěděli o všech možnostech TRIM, zde je jeden způsob, jak jste mohli tento úkol vyřešit:

DECLARE @s AS VARCHAR(100) ='//\\ odstranit úvodní a koncová zpětná (\) a dopředná (/) lomítka \\//'; SELECT TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ') AS výstupní řetězec;

Řešení začíná tím, že pomocí TRANSLATE nahradíte všechny mezery neutrálním znakem (~) a lomítka mezerami a poté pomocí TRIM oříznete úvodní a koncové mezery z výsledku. Tento krok v podstatě ořízne úvodní a koncová lomítka a dočasně použije místo původních mezer ~. Zde je výsledek tohoto kroku:

\\~odstranit~vedouci~a~konci~dozadu~(\)~a~vpřed~( )~lomítka~\\

Druhý krok pak pomocí TRANSLATE nahradí všechny mezery jiným neutrálním znakem (^) a zpětná lomítka mezerami a poté pomocí TRIM ořízne úvodní a koncové mezery z výsledku. Tento krok v podstatě ořízne úvodní a koncová zpětná lomítka a dočasně použije ^ místo mezilehlých mezer. Zde je výsledek tohoto kroku:

~odstranit~vedoucí~a~trailing~dozadu~( )~a~vpřed~(^)~lomítka~

Poslední krok používá TRANSLATE k nahrazení mezer zpětnými lomítky, ^ dopřednými lomítky a ~ mezerami, čímž se generuje požadovaný výstup:

 odstranit úvodní a koncová zpětná (\) a dopředná (/) lomítka 

Jako cvičení si zkuste tento úkol vyřešit pomocí řešení kompatibilního s předchozí verzí SQL Server 2017, kde nemůžete použít TRIM a TRANSLATE.

Zpátky k SQL Server 2017 a vyšším, pokud byste se obtěžovali kontrolou dokumentace, zjistili byste, že TRIM je sofistikovanější, než jste si původně mysleli. Zde je syntaxe funkce:

TRIM ( [ znaků FROM ] řetězec )

Volitelné znaky FROM část umožňuje zadat jeden nebo více znaků, které chcete oříznout od začátku a konce vstupního řetězce. V našem případě vše, co musíte udělat, je zadat '/\' jako tuto část, například takto:

DECLARE @s AS VARCHAR(100) ='//\\ odstranit úvodní a koncová zpětná (\) a dopředná (/) lomítka \\//'; SELECT TRIM( '/\' FROM @s) AS výstupní řetězec;

To je docela významné zlepšení ve srovnání s předchozím řešením!

CONCAT a CONCAT_WS

Pokud už nějakou dobu pracujete s T-SQL, víte, jak nepříjemné je vypořádat se s NULL, když potřebujete zřetězit řetězce. Jako příklad zvažte údaje o poloze zaznamenané pro zaměstnance v tabulce HR.Employees:

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

Tento dotaz generuje následující výstup:

empid country region city----------- --------------- --------------- -- -------------1 USA WA Seattle2 USA WA Tacoma3 USA WA Kirkland4 USA WA Redmond5 UK NULL Londýn6 UK NULL Londýn7 UK NULL Londýn8 USA WA Seattle9 UK NULL Londýn

Všimněte si, že pro některé zaměstnance je část regionu irelevantní a nerelevantní region je reprezentován NULL. Předpokládejme, že potřebujete zřetězit části umístění (země, region a město) pomocí čárky jako oddělovače, ale ignorovat oblasti NULL. Když je region relevantní, chcete, aby výsledek měl tvar <coutry>,<region>,<city> a když je region irelevantní, chcete, aby výsledek měl tvar <country>,<city> . Normálně zřetězení něčeho s NULL vytvoří výsledek NULL. Toto chování můžete změnit vypnutím možnosti relace CONCAT_NULL_YIELDS_NULL, ale nedoporučoval bych povolit nestandardní chování.

Pokud byste nevěděli o existenci funkcí CONCAT a CONCAT_WS, pravděpodobně byste použili ISNULL nebo COALESCE k nahrazení NULL prázdným řetězcem, například takto:

SELECT empid, country + ISNULL(',' + region, '') + ',' + město AS locationFROM HR.Employees;

Zde je výstup tohoto dotazu:

empid location----------- ---------------------------------- -------------1 USA,WA,Seattle2 USA,WA,Tacoma3 USA,WA,Kirkland4 USA,WA,Redmond5 Velká Británie,Londýn6 Velká Británie,Londýn7 Velká Británie,Londýn8 USA,WA,Seattle9 Velká Británie, Londýn

SQL Server 2012 představil funkci CONCAT. Tato funkce přijímá seznam vstupů znakového řetězce a zřetězuje je, a přitom ignoruje hodnoty NULL. Takže pomocí CONCAT můžete zjednodušit řešení takto:

SELECT empid, CONCAT(země, ',' + region, ',', město) AS locationFROM HR.Employees;

Přesto musíte explicitně zadat oddělovače jako součást vstupů funkce. Aby byl náš život ještě jednodušší, zavedl SQL Server 2017 podobnou funkci nazvanou CONCAT_WS, kde začínáte uvedením oddělovače, za nímž následují položky, které chcete zřetězit. Pomocí této funkce je řešení dále zjednodušeno takto:

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

Dalším krokem je samozřejmě čtení myšlenek. 1. dubna 2020 Microsoft plánuje vydat CONCAT_MR. Funkce přijme prázdný vstup a automaticky zjistí, které prvky chcete zřetězit čtením vaší mysli. Dotaz pak bude vypadat takto:

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

LOG má druhý parametr

Podobně jako u funkce EOMONTH si mnoho lidí neuvědomuje, že počínaje SQL Serverem 2012 funkce LOG podporuje druhý parametr, který vám umožňuje označit základ logaritmu. Předtím T-SQL podporovalo funkci LOG (vstup), která vrací přirozený logaritmus vstupu (za použití konstanty e jako základ), a LOG10 (vstup), která používá jako základ 10.

Nevědomí si existence druhého parametru funkce LOG, když lidé chtěli vypočítat Logb (x), kde b je základ jiný než e a 10, často to dělali daleko. Můžete se spolehnout na následující rovnici:

Zaznamenatb (x) =Loga (x)/Loga (b)

Například pro výpočet Log2 (8), spoléháte na následující rovnici:

Zaznamenat2 (8) =Loge (8)/Loge (2)

V překladu do T-SQL použijete následující výpočet:

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

Jakmile si uvědomíte, že LOG podporuje druhý parametr, kde uvádíte základnu, výpočet se jednoduše změní na:

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

Proměnná kurzoru

Pokud už nějakou dobu pracujete s T-SQL, pravděpodobně jste měli spoustu příležitostí pracovat s kurzory. Jak víte, při práci s kurzorem obvykle používáte následující kroky:

  • Deklarujte kurzor
  • Otevřete kurzor
  • Iterujte záznamy kurzoru
  • Zavřete kurzor
  • Přidělit kurzor

Předpokládejme například, že potřebujete provést nějakou úlohu pro každou databázi ve vaší instanci. Pomocí kurzoru byste normálně použili kód podobný následujícímu:

DECLARE @dbname JAKO sysname; DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT name FROM sys.databases; OTEVŘENO C; NAČÍST DALŠÍ Z C DO @dbname; WHILE @@FETCH_STATUS =0BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... zde udělejte svou věc ... */ NAČÍST DALŠÍ Z C DO @dbname;END; CLOSE C;DEALOCATE C;

Příkaz CLOSE uvolní aktuální sadu výsledků a uvolní zámky. Příkaz DEALLOCATE odebere odkaz kurzoru, a když se uvolní poslední odkaz, uvolní datové struktury obsahující kurzor. Pokud se pokusíte spustit výše uvedený kód dvakrát bez příkazů CLOSE a DEALLOCATE, zobrazí se následující chyba:

Zpráva 16915, úroveň 16, stav 1, řádek 4A kurzor s názvem 'C' již existuje. Zpráva 16905, úroveň 16, stav 1, řádek 6 Kurzor je již otevřený.

Než budete pokračovat, ujistěte se, že jste spustili příkazy CLOSE a DEALLOCATE.

Mnoho lidí si neuvědomuje, že když potřebují pracovat s kurzorem pouze v jedné dávce, což je nejběžnější případ, místo použití běžného kurzoru můžete pracovat s proměnnou kurzoru. Jako každá proměnná je rozsah kurzorové proměnné pouze dávka, kde byla deklarována. To znamená, že jakmile dávka skončí, platnost všech proměnných vyprší. Pomocí proměnné kurzoru se po dokončení dávky SQL Server zavře a automaticky ji uvolní, takže nemusíte spouštět příkazy CLOSE a DEALLOCATE explicitně.

Zde je revidovaný kód tentokrát využívající kurzorovou proměnnou:

DECLARE @dbname JAKO sysname, @C JAKO CURSOR; SET @C =CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT jméno FROM sys.databases; OPEN @C; NAČÍST DALŠÍ Z @C DO @dbname; WHILE @@FETCH_STATUS =0BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... zde udělejte svou věc ... */ NAČÍST DALŠÍ Z @C DO @dbname;END;

Klidně to spusťte vícekrát a všimněte si, že tentokrát se neobjeví žádné chyby. Je jen čistší a nemusíte se starat o ponechání zdrojů kurzoru, pokud zapomenete kurzor zavřít a uvolnit místo.

SLOUČIT s VÝSTUPEM

Od vzniku klauzule OUTPUT pro modifikační příkazy v SQL Server 2005 se ukázalo, že je to velmi praktický nástroj, kdykoli chcete vrátit data z upravených řádků. Lidé tuto funkci pravidelně používají pro účely, jako je archivace, audit a mnoho dalších případů použití. Jednou z nepříjemných věcí na této funkci však je, že pokud ji použijete s příkazy INSERT, můžete vracet data pouze z vložených řádků a výstupním sloupcům předponu inserted . Nemáte přístup ke sloupcům zdrojové tabulky, i když někdy potřebujete vrátit sloupce ze zdroje vedle sloupců z cíle.

Jako příklad zvažte tabulky T1 a T2, které vytvoříte a naplníte spuštěním následujícího kódu:

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

Všimněte si, že ke generování klíčů v obou tabulkách se používá vlastnost identity.

Předpokládejme, že potřebujete zkopírovat některé řádky z T1 do T2; řekněme ty, kde klíč % 2 =1. Chcete použít klauzuli OUTPUT k vrácení nově vygenerovaných klíčů v T2, ale chcete také vrátit vedle těchto klíčů příslušné zdrojové klíče z T1. Intuitivním očekáváním je použití následujícího příkazu INSERT:

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

Bohužel, jak již bylo zmíněno, klauzule OUTPUT vám neumožňuje odkazovat na sloupce ze zdrojové tabulky, takže se zobrazí následující chyba:

Zpráva 4104, úroveň 16, stav 1, řádek 2
Vícedílný identifikátor "T1.keycol" nelze svázat.

Mnoho lidí si neuvědomuje, že toto omezení se kupodivu nevztahuje na příkaz MERGE. Takže i když je to trochu nešikovné, můžete svůj příkaz INSERT převést na příkaz MERGE, ale k tomu potřebujete, aby predikát MERGE byl vždy nepravdivý. Tím se aktivuje klauzule WHEN NOT MATCHED a tam se použije jediná podporovaná akce INSERT. Můžete použít falešnou podmínku, jako je 1 =2. Zde je úplný převedený kód:

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;

Tentokrát se kód spustí úspěšně a vytvoří následující výstup:

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

Doufejme, že Microsoft posílí podporu pro klauzuli OUTPUT v ostatních modifikačních příkazech, aby umožnil vracet sloupce i ze zdrojové tabulky.

Závěr

Nepředpokládejte a RTFM! :-)


  1. Může SQL Server odeslat webový požadavek?

  2. 5 způsobů, jak získat krátký název měsíce z data na serveru SQL

  3. ScaleGrid přidává Oracle Cloud pro Managed Database Hosting

  4. Různé způsoby, jak naplnit uživatele MySQL