Database
 sql >> Baza danych >  >> RDS >> Database

Przeoczone perełki T-SQL

Mój dobry przyjaciel Aaron Bertrand zainspirował mnie do napisania tego artykułu. Przypomniał mi, jak czasami bierzemy rzeczy za pewnik, kiedy wydają się nam oczywiste i nie zawsze zawracamy sobie głowę sprawdzaniem całej historii za nimi. Znaczenie dla T-SQL polega na tym, że czasami zakładamy, że wiemy wszystko, co trzeba wiedzieć o pewnych funkcjach T-SQL, i nie zawsze zawracamy sobie głowę sprawdzaniem dokumentacji, aby sprawdzić, czy jest w nich coś więcej. W tym artykule omówię szereg funkcji T-SQL, które są albo często całkowicie pomijane, albo obsługują parametry lub możliwości, które często są pomijane. Jeśli masz własne przykłady klejnotów T-SQL, które często są pomijane, podziel się nimi w sekcji komentarzy tego artykułu.

Zanim zaczniesz czytać ten artykuł, zadaj sobie pytanie, co wiesz o następujących funkcjach T-SQL:EOMONTH, TRANSLATE, TRIM, CONCAT i CONCAT_WS, LOG, zmienne kursora i MERGE with OUTPUT.

W moich przykładach użyję przykładowej bazy danych o nazwie TSQLV5. Skrypt, który tworzy i wypełnia tę bazę danych, oraz jego diagram ER można znaleźć tutaj.

EOMONTH ma drugi parametr

Funkcja EOMONTH została wprowadzona w SQL Server 2012. Wiele osób uważa, że ​​obsługuje tylko jeden parametr przechowujący datę wejściową i po prostu zwraca datę końca miesiąca, która odpowiada dacie wejściowej.

Rozważ nieco bardziej wyrafinowaną potrzebę obliczenia końca poprzedniego miesiąca. Załóżmy na przykład, że musisz wykonać zapytanie do tabeli Sales.Orders i zwrócić zamówienia złożone pod koniec poprzedniego miesiąca.

Jednym ze sposobów na osiągnięcie tego jest zastosowanie funkcji EOMONTH do SYSDATETIME, aby uzyskać datę końca miesiąca bieżącego miesiąca, a następnie zastosowanie funkcji DATEADD, aby odjąć miesiąc od wyniku, na przykład:

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

Zwróć uwagę, że jeśli faktycznie uruchomisz to zapytanie w przykładowej bazie danych TSQLV5, otrzymasz pusty wynik, ponieważ ostatnia data zamówienia zarejestrowana w tabeli to 6 maja 2019 r. Jeśli jednak tabela zawiera zamówienia z datą zamówienia, która przypada na ostatnią dnia poprzedniego miesiąca zapytanie zwróciłoby je.

Wiele osób nie zdaje sobie sprawy, że EOMONTH obsługuje drugi parametr, w którym wskazujesz, ile miesięcy należy dodać lub odjąć. Oto [w pełni udokumentowana] składnia funkcji:

EOMONTH ( start_date [, month_to_add ] )

Nasze zadanie można osiągnąć łatwiej i naturalnie, po prostu określając -1 jako drugi parametr funkcji, na przykład:

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

TRANSLATE jest czasami prostsze niż REPLACE

Wiele osób zna funkcję REPLACE i jej działanie. Używasz go, gdy chcesz zastąpić wszystkie wystąpienia jednego podciągu innym w ciągu wejściowym. Czasami jednak, gdy masz wiele zamienników, które musisz zastosować, użycie opcji ZAMIEŃ jest nieco trudne i skutkuje zawiłymi wyrażeniami.

Jako przykład załóżmy, że otrzymujesz ciąg wejściowy @s, który zawiera liczbę z formatowaniem hiszpańskim. W Hiszpanii używa się kropki jako separatora dla grup tysięcy i przecinka jako separatora dziesiętnego. Musisz przekonwertować dane wejściowe na formatowanie amerykańskie, gdzie przecinek jest używany jako separator dla grup tysięcy, a kropka jako separator dziesiętny.

Używając jednego wywołania funkcji REPLACE, możesz zastąpić tylko wszystkie wystąpienia jednego znaku lub podciągu innym. Aby zastosować dwie zamiany (kropki na przecinki i przecinki na kropki), musisz zagnieździć wywołania funkcji. Trudne jest to, że jeśli użyjesz opcji ZAMIEŃ raz, aby zamienić kropki na przecinki, a następnie po raz drugi w celu zmiany przecinków na kropki, otrzymasz same kropki. Wypróbuj:

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

Otrzymasz następujące dane wyjściowe:

123.456.789.00

Jeśli chcesz trzymać się funkcji REPLACE, potrzebujesz trzech wywołań funkcji. Jeden, aby zastąpić kropki neutralnym znakiem, o którym wiesz, że normalnie nie może pojawić się w danych (powiedzmy, ~). Kolejny przeciwko wynikowi, aby zastąpić wszystkie przecinki kropkami. Kolejny w stosunku do wyniku, aby zastąpić wszystkie wystąpienia znaku tymczasowego (w naszym przykładzie ~) przecinkami. Oto pełne wyrażenie:

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

Tym razem uzyskasz właściwy wynik:

123,456,789.00

Jest to wykonalne, ale skutkuje długim i zawiłym wyrazem twarzy. Co by było, gdybyś miał więcej zamienników do zastosowania?

Wiele osób nie zdaje sobie sprawy, że SQL Server 2017 wprowadził nową funkcję o nazwie TRANSLATE, która znacznie upraszcza takie zastępowanie. Oto składnia funkcji:

TRANSLATE ( inputString, characters, translations )

Drugie dane wejściowe (znaki) to ciąg z listą poszczególnych znaków, które chcesz zastąpić, a trzecie dane wejściowe (tłumaczenia) to ciąg z listą odpowiednich znaków, którymi chcesz zastąpić znaki źródłowe. To oczywiście oznacza, że ​​drugi i trzeci parametr muszą mieć tę samą liczbę znaków. Ważne w tej funkcji jest to, że nie wykonuje osobnych przejść dla każdej z podmian. Gdyby tak było, potencjalnie spowodowałoby to ten sam błąd, co w pierwszym przykładzie, który pokazałem, używając dwóch wywołań funkcji REPLACE. W związku z tym obsługa naszego zadania staje się oczywista:

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

Ten kod generuje pożądane dane wyjściowe:

123,456,789.00

To całkiem fajne!

TRIM to więcej niż LTRIM(RTRIM())

W SQL Server 2017 wprowadzono obsługę funkcji TRIM. Wiele osób, w tym ja, początkowo po prostu zakłada, że ​​jest to tylko prosty skrót do LTRIM(RTRIM(input)). Jeśli jednak sprawdzisz dokumentację, zdasz sobie sprawę, że jest ona w rzeczywistości potężniejsza.

Zanim przejdę do szczegółów, rozważ następujące zadanie:mając podany ciąg wejściowy @s, usuń początkowe i końcowe ukośniki (wstecz i do przodu). Jako przykład załóżmy, że @s zawiera następujący ciąg:

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

Żądany wynik to:

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

Zwróć uwagę, że dane wyjściowe powinny zachować spacje wiodące i końcowe.

Jeśli nie wiedziałeś o pełnych możliwościach TRIM, oto jeden ze sposobów, w jaki mogłeś rozwiązać zadanie:

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

Rozwiązanie zaczyna się od użycia TRANSLATE do zastąpienia wszystkich spacji znakiem neutralnym (~) i ukośników spacją, a następnie użycia TRIM do przycięcia początkowych i końcowych spacji z wyniku. Ten krok zasadniczo przycina początkowe i końcowe ukośniki, tymczasowo używając znaku ~ zamiast oryginalnych spacji. Oto wynik tego kroku:

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

W drugim kroku używa się TRANSLATE, aby zastąpić wszystkie spacje innym neutralnym znakiem (^) i ukośnikami odwrotnymi ze spacjami, a następnie za pomocą TRIM przycinamy początkowe i końcowe spacje z wyniku. Ten krok zasadniczo przycina początkowe i końcowe ukośniki od tyłu, tymczasowo używając ^ zamiast spacji pośrednich. Oto wynik tego kroku:

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

Ostatni krok używa TRANSLATE do zastąpienia spacji ukośnikami odwrotnymi, ^ ukośnikami przednimi i ~ spacjami, generując żądane dane wyjściowe:

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

Jako ćwiczenie spróbuj rozwiązać to zadanie za pomocą rozwiązania zgodnego z wersją pre-SQL Server 2017, w której nie można używać funkcji TRIM i TRANSLATE.

Wracając do SQL Server 2017 i nowszych wersji, gdybyś zadał sobie trud sprawdzenia dokumentacji, odkryłbyś, że TRIM jest bardziej wyrafinowany niż początkowo myślałeś. Oto składnia funkcji:

TRIM ( [ characters FROM ] string )

Opcjonalne znaki OD part pozwala określić jeden lub więcej znaków, które mają zostać przycięte od początku i końca ciągu wejściowego. W naszym przypadku wystarczy podać „/\” jako tę część, na przykład:

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

To całkiem znacząca poprawa w porównaniu z poprzednim rozwiązaniem!

CONCAT i CONCAT_WS

Jeśli od jakiegoś czasu pracujesz z T-SQL, wiesz, jak niezręcznie jest radzić sobie z wartościami NULL, gdy trzeba łączyć łańcuchy. Jako przykład rozważ dane lokalizacji zarejestrowane dla pracowników w tabeli HR.Pracownicy:

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

To zapytanie generuje następujące dane wyjściowe:

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

Zauważ, że dla niektórych pracowników część regionu jest nieistotna, a nieistotny region jest reprezentowany przez NULL. Załóżmy, że musisz połączyć części lokalizacji (kraj, region i miasto), używając przecinka jako separatora, ale ignorując regiony NULL. Gdy region jest istotny, chcesz, aby wynik miał postać ,, a gdy region jest nieistotny, chcesz, aby wynik miał postać , . Zwykle połączenie czegoś z wartością NULL daje wynik NULL. Możesz zmienić to zachowanie, wyłączając opcję sesji CONCAT_NULL_YIELDS_NULL, ale nie polecam włączania niestandardowego zachowania.

Gdybyś nie wiedział o istnieniu funkcji CONCAT i CONCAT_WS, prawdopodobnie użyłbyś ISNULL lub COALESCE, aby zastąpić NULL pustym ciągiem, na przykład:

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

Oto wynik tego zapytania:

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 wprowadził funkcję CONCAT. Ta funkcja akceptuje listę wejściowych ciągów znaków i łączy je, a jednocześnie ignoruje wartości NULL. Używając CONCAT, możesz uprościć rozwiązanie w następujący sposób:

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

Mimo to musisz jawnie określić separatory jako część danych wejściowych funkcji. Aby uczynić nasze życie jeszcze łatwiejszym, SQL Server 2017 wprowadził podobną funkcję o nazwie CONCAT_WS, w której zaczynasz od wskazania separatora, a następnie elementów, które chcesz połączyć. Dzięki tej funkcji rozwiązanie jest jeszcze bardziej uproszczone:

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

Następnym krokiem jest oczywiście czytanie myśli. 1 kwietnia 2020 r. Microsoft planuje wydać CONCAT_MR. Funkcja zaakceptuje puste dane wejściowe i automatycznie zorientuje się, które elementy chcesz połączyć, czytając w twoim umyśle. Zapytanie będzie wtedy wyglądać tak:

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

LOG ma drugi parametr

Podobnie jak w przypadku funkcji EOMONTH, wiele osób nie zdaje sobie sprawy, że już od SQL Server 2012 funkcja LOG obsługuje drugi parametr, który pozwala wskazać podstawę logarytmu. Wcześniej T-SQL obsługiwał funkcję LOG(wejście), która zwraca logarytm naturalny z wejścia (przy użyciu stałej e jako podstawy) oraz LOG10(wejście), która wykorzystuje 10 jako podstawę.

Nie zdając sobie sprawy z istnienia drugiego parametru funkcji LOG, gdy ludzie chcieli obliczyć Logb (x), gdzie b jest bazą inną niż e i 10, często robili to na dłuższą metę. Możesz polegać na następującym równaniu:

Zalogujb (x) =Dziennika (x)/Zaloguja (b)

Jako przykład, aby obliczyć Log2 (8), polegasz na następującym równaniu:

Zaloguj2 (8) =Zaloguje (8)/Zaloguje (2)

W tłumaczeniu na T-SQL stosuje się następujące obliczenia:

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

Gdy zdasz sobie sprawę, że LOG obsługuje drugi parametr, w którym wskazujesz podstawę, obliczenie po prostu wygląda następująco:

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

Zmienna kursora

Jeśli od jakiegoś czasu pracujesz z T-SQL, prawdopodobnie miałeś wiele okazji do pracy z kursorami. Jak wiesz, podczas pracy z kursorem zazwyczaj wykonujesz następujące czynności:

  • Zadeklaruj kursor
  • Otwórz kursor
  • Iteruj przez rekordy kursora
  • Zamknij kursor
  • Odblokuj kursor

Jako przykład załóżmy, że musisz wykonać jakieś zadanie na bazę danych w swojej instancji. Używając kursora, normalnie użyjesz kodu podobnego do następującego:

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;

Polecenie ZAMKNIJ zwalnia bieżący zestaw wyników i zwalnia blokady. Polecenie DEALLOCATE usuwa odwołanie do kursora, a kiedy ostatnie odwołanie jest zwalniane, zwalnia struktury danych zawierające kursor. Jeśli spróbujesz uruchomić powyższy kod dwa razy bez poleceń CLOSE i DEALLOCATE, otrzymasz następujący błąd:

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.

Upewnij się, że uruchomiłeś polecenia CLOSE i DEALLOCATE przed kontynuowaniem.

Wiele osób nie zdaje sobie sprawy, że kiedy muszą pracować z kursorem tylko w jednej partii, co jest najczęstszym przypadkiem, zamiast zwykłego kursora można pracować ze zmienną kursora. Jak każda zmienna, zakres zmiennej kursora to tylko partia, w której została zadeklarowana. Oznacza to, że po zakończeniu partii wszystkie zmienne tracą ważność. Używając zmiennej kursora, po zakończeniu partii, SQL Server zamyka ją i zwalnia ją automatycznie, oszczędzając Ci potrzeby jawnego uruchamiania poleceń CLOSE i DEALLOCATE.

Oto poprawiony kod używający tym razem zmiennej kursora:

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;

Możesz go wykonać wiele razy i zauważ, że tym razem nie pojawią się żadne błędy. Jest po prostu czystszy i nie musisz się martwić o utrzymanie zasobów kursora, jeśli zapomnisz zamknąć i zwolnić kursor.

MERGE z WYJŚCIEM

Od momentu powstania klauzuli OUTPUT dla instrukcji modyfikacji w SQL Server 2005 okazało się, że jest to bardzo praktyczne narzędzie, gdy chcesz zwrócić dane ze zmodyfikowanych wierszy. Ludzie regularnie korzystają z tej funkcji do celów takich jak archiwizacja, audyt i wiele innych przypadków użycia. Jedną z irytujących rzeczy związanych z tą funkcją jest jednak to, że jeśli używasz jej z instrukcjami INSERT, możesz zwrócić dane tylko z wstawionych wierszy, poprzedzając kolumny wyjściowe przedrostkiem wstawiony . Nie masz dostępu do kolumn tabeli źródłowej, chociaż czasami musisz zwrócić kolumny ze źródła obok kolumn z celu.

Jako przykład rozważ tabele T1 i T2, które tworzysz i wypełniasz, uruchamiając następujący 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');

Zauważ, że do generowania kluczy w obu tabelach używana jest właściwość tożsamości.

Załóżmy, że musisz skopiować niektóre wiersze z T1 do T2; powiedzmy te, w których keycol % 2 =1. Chcesz użyć klauzuli OUTPUT, aby zwrócić nowo wygenerowane klucze w T2, ale chcesz również zwrócić obok tych kluczy odpowiednie klucze źródłowe z T1. Intuicyjnym oczekiwaniem jest użycie następującej instrukcji INSERT:

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;

Niestety, jak wspomniano, klauzula OUTPUT nie pozwala na odwoływanie się do kolumn z tabeli źródłowej, więc pojawia się następujący błąd:

Msg 4104, Poziom 16, Stan 1, Wiersz 2
Nie można powiązać wieloczęściowego identyfikatora „T1.keycol”.

Wiele osób nie zdaje sobie sprawy, że to ograniczenie, co dziwne, nie dotyczy oświadczenia MERGE. Więc nawet jeśli jest to trochę niezręczne, możesz przekonwertować instrukcję INSERT na instrukcję MERGE, ale aby to zrobić, potrzebujesz predykatu MERGE, aby zawsze był fałszywy. Spowoduje to aktywację klauzuli WHEN NOT MATCHED i zastosowanie tam jedynej obsługiwanej akcji INSERT. Możesz użyć fałszywego warunku, takiego jak 1 =2. Oto kompletny przekonwertowany kod:

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;

Tym razem kod działa pomyślnie, dając następujące dane wyjściowe:

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

Mamy nadzieję, że Microsoft ulepszy obsługę klauzuli OUTPUT w innych instrukcjach modyfikacji, aby umożliwić zwracanie kolumn również z tabeli źródłowej.

Wniosek

Nie zakładaj i RTFM! :-)


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Migracja schematu:relacja do gwiazdy

  2. Model danych zarządzania zdarzeniami

  3. Model danych platformy pożyczkowej typu peer-to-peer

  4. Prawe połączenie SQL

  5. Omówienie polecenia DBCC SHRINKFILE