Моят добър приятел Аарон Бертран ме вдъхнови да напиша тази статия. Той ми напомни как понякога приемаме нещата за даденост, когато ни изглеждат очевидни и не винаги си правим труда да проверим цялата история зад тях. Значението за T-SQL е, че понякога приемаме, че знаем всичко, което трябва да знаем за определени функции на T-SQL, и не винаги си правим труда да проверяваме документацията, за да видим дали има повече за тях. В тази статия разглеждам редица функции на T-SQL, които или често са напълно пренебрегвани, или поддържат параметри или възможности, които често се пренебрегват. Ако имате собствени примери за T-SQL скъпоценни камъни, които често се пренебрегват, моля, споделете ги в секцията за коментари на тази статия.
Преди да започнете да четете тази статия, запитайте се какво знаете за следните функции на T-SQL:EOMONTH, TRANSLATE, TRIM, CONCAT и CONCAT_WS, LOG, променливи на курсора и MERGE с OUTPUT.
В моите примери ще използвам примерна база данни, наречена TSQLV5. Тук можете да намерите скрипта, който създава и попълва тази база данни, както и нейната диаграма за ER тук.
EOMONTH има втори параметър
Функцията EOMONTH беше въведена в SQL Server 2012. Много хора смятат, че поддържа само един параметър, съдържащ входна дата, и че просто връща датата на края на месеца, която съответства на датата на въвеждане.
Помислете за малко по-сложна нужда да изчислите края на предходния месец. Да предположим например, че трябва да направите заявка в таблицата Sales.Orders и да върнете поръчки, направени в края на предходния месец.
Един от начините да постигнете това е да приложите функцията EOMONTH към SYSDATETIME, за да получите датата на края на месеца на текущия месец, и след това да приложите функцията DATEADD, за да извадите месец от резултата, както следва:
USE TSQLV5; SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));
Имайте предвид, че ако действително изпълните тази заявка в примерната база данни TSQLV5, ще получите празен резултат, тъй като последната дата на поръчка, записана в таблицата, е 6 май 2019 г. Въпреки това, ако таблицата имаше поръчки с дата на поръчка, която пада на последната ден от предходния месец, заявката щеше да ги върне.
Това, което много хора не осъзнават, е, че EOMONTH поддържа втори параметър, където посочвате колко месеца да добавите или извадите. Ето [напълно документиран] синтаксис на функцията:
EOMONTH ( start_date [, month_to_add ] )
Нашата задача може да бъде постигната по-лесно и естествено, като просто посочим -1 като втори параметър на функцията, така:
SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(SYSDATETIME(), -1);
TRANSLATE понякога е по-прост от REPLACE
Много хора са запознати с функцията REPLACE и как работи. Използвате го, когато искате да замените всички срещания на един подниз с друг във входен низ. Понякога обаче, когато имате множество замествания, които трябва да приложите, използването на REPLACE е малко сложно и води до заплетени изрази.
Като пример, да предположим, че ви е даден входен низ @s, който съдържа число с испанско форматиране. В Испания използват точка като разделител за групи от хиляди и запетая като десетичен разделител. Трябва да преобразувате въведеното в американско форматиране, където запетая се използва като разделител за групи от хиляди и точка като десетичен разделител.
Използвайки едно извикване на функцията REPLACE, можете да замените само всички поява на един знак или подниз с друг. За да приложите две замествания (точка към запетаи и запетаи към точки), трябва да вложите извиквания на функции. Трудната част е, че ако използвате REPLACE веднъж, за да промените точките в запетаи, и след това втори път срещу резултата, за да промените запетаите на точки, в крайна сметка ще получите само точки. Опитайте:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');
Получавате следния изход:
123.456.789.00
Ако искате да се придържате към използването на функцията REPLACE, имате нужда от три извиквания на функции. Един за замяна на точки с неутрален знак, който знаете, че не може нормално да се появи в данните (да речем, ~). Друга срещу резултата да се заменят всички запетаи с точки. Друго срещу резултата за замяна на всички появявания на временния символ (~ в нашия пример) със запетаи. Ето пълния израз:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');
Този път получавате правилния изход:
123,456,789.00
Донякъде е изпълнимо, но води до дълго и объркано изражение. Ами ако трябваше да кандидатствате за повече замествания?
Много хора не са наясно, че SQL Server 2017 въведе нова функция, наречена TRANSLATE, която опростява много подобни замествания. Ето синтаксиса на функцията:
TRANSLATE ( inputString, characters, translations )
Вторият вход (знаци) е низ със списъка на отделните знаци, които искате да замените, а третият вход (преводи) е низ със списъка на съответните знаци, с които искате да замените изходните знаци. Това естествено означава, че вторият и третият параметър трябва да имат еднакъв брой знаци. Важното за функцията е, че тя не прави отделни проходи за всяка от заместванията. Ако беше така, това потенциално щеше да доведе до същия бъг като в първия пример, който показах, използвайки двете извиквания на функцията REPLACE. Следователно, справянето с нашата задача става безпроблемно:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT TRANSLATE(@s, '.,', ',.');
Този код генерира желания изход:
123,456,789.00
Това е доста добре!
TRIM е повече от LTRIM(RTRIM())
SQL Server 2017 въведе поддръжка за функцията TRIM. Много хора, включително и аз, първоначално просто приемат, че това е не повече от обикновен пряк път към LTRIM (RTRIM (вход)). Въпреки това, ако проверите документацията, ще разберете, че тя всъщност е по-мощна от това.
Преди да вляза в подробностите, помислете за следната задача:като се даде входен низ @s, премахнете водещите и крайните наклонени черти (назад и напред). Като пример, да предположим, че @s съдържа следния низ:
//\\ remove leading and trailing backward (\) and forward (/) slashes \\//
Желаният изход е:
remove leading and trailing backward (\) and forward (/) slashes
Имайте предвид, че изходът трябва да запази началните и крайните интервали.
Ако не сте знаели за пълните възможности на TRIM, ето един начин, по който може да сте решили задачата:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ') AS outputstring;
Решението започва с използване на TRANSLATE за замяна на всички интервали с неутрален знак (~) и наклонени черти с интервали, след което се използва TRIM за отрязване на водещи и крайни интервали от резултата. Тази стъпка по същество отрязва водещите и задните наклонени черти напред, като временно използва ~ вместо оригинални интервали. Ето резултата от тази стъпка:
\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\
След това втората стъпка използва TRANSLATE за замяна на всички интервали с друг неутрален знак (^) и обратни наклонени черти с интервали, след което използва TRIM за отрязване на водещи и крайни интервали от резултата. Тази стъпка по същество отрязва водещите и задните наклонени черти назад, като временно използва ^ вместо междинни интервали. Ето резултата от тази стъпка:
~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~
Последната стъпка използва TRANSLATE за замяна на интервали с наклонени черти назад, ^ с наклонени черти напред и ~ с интервали, генерирайки желания резултат:
remove leading and trailing backward (\) and forward (/) slashes
Като упражнение опитайте да решите тази задача с предварително съвместимо с SQL Server 2017 решение, където не можете да използвате TRIM и TRANSLATE.
Обратно към SQL Server 2017 и по-нова версия, ако сте си направили труда да проверите документацията, щяхте да откриете, че TRIM е по-сложен от това, което си мислехте първоначално. Ето синтаксиса на функцията:
TRIM ( [ characters FROM ] string )
Незадължителните символи ОТ part ви позволява да посочите един или повече символи, които искате да бъдат изрязани от началото и края на входния низ. В нашия случай всичко, което трябва да направите, е да посочите '/\' като тази част, така:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRIM( '/\' FROM @s) AS outputstring;
Това е доста значително подобрение в сравнение с предишното решение!
CONCAT и CONCAT_WS
Ако работите с T-SQL от известно време, знаете колко неудобно е да се справяте с NULL, когато трябва да свържете низове. Като пример разгледайте данните за местоположението, записани за служителите в таблицата HR.Employees:
SELECT empid, country, region, city FROM HR.Employees;
Тази заявка генерира следния изход:
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
Забележете, че за някои служители регионалната част е ирелевантна и ирелевантният регион е представен с NULL. Да предположим, че трябва да конкатенирате частите за местоположение (държава, регион и град), като използвате запетая като разделител, но игнорирате NULL региони. Когато регионът е подходящ, искате резултатът да има формата <coutry>,<region>,<city>
и когато регионът е без значение, искате резултатът да има формата <country>,<city>
. Обикновено конкатенирането на нещо с NULL води до NULL резултат. Можете да промените това поведение, като изключите опцията за сесия CONCAT_NULL_YIELDS_NULL, но не бих препоръчал да активирате нестандартно поведение.
Ако не знаехте за съществуването на функциите CONCAT и CONCAT_WS, вероятно бихте използвали ISNULL или COALESCE, за да замените NULL с празен низ, като така:
SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location FROM HR.Employees;
Ето изхода от тази заявка:
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 въведе функцията CONCAT. Тази функция приема списък с въведени символни низове и ги конкатенира, като при това игнорира NULL. Така че с помощта на CONCAT можете да опростите решението по следния начин:
SELECT empid, CONCAT(country, ',' + region, ',', city) AS location FROM HR.Employees;
Все пак трябва изрично да посочите разделителите като част от входовете на функцията. За да направи живота ни още по-лесен, SQL Server 2017 въведе подобна функция, наречена CONCAT_WS, където започвате с посочване на разделителя, последвано от елементите, които искате да обедините. С тази функция решението е допълнително опростено така:
SELECT empid, CONCAT_WS(',', country, region, city) AS location FROM HR.Employees;
Следващата стъпка, разбира се, е четене на мисли. На 1 април 2020 г. Microsoft планира да пусне CONCAT_MR. Функцията ще приеме празен вход и ще разбере автоматично кои елементи искате да се конкатенира, като чете мислите ви. След това заявката ще изглежда така:
SELECT empid, CONCAT_MR() AS location FROM HR.Employees;
LOG има втори параметър
Подобно на функцията EOMONTH, много хора не осъзнават, че започвайки вече със SQL Server 2012, функцията LOG поддържа втори параметър, който ви позволява да посочите основата на логаритъма. Преди това T-SQL поддържаше функцията LOG(вход), която връща естествения логаритъм на входа (използвайки константата e като основа), и LOG10(вход), която използва 10 като основа.
Без да знаят за съществуването на втория параметър на функцията LOG, когато хората искаха да изчислят Logb (x), където b е основа, различна от e и 10, те често го правеха по дългия път. Можете да разчитате на следното уравнение:
Дневникb (x) =Loga (x)/Loga (б)Като пример, за изчисляване на Log2 (8), разчитате на следното уравнение:
Дневник2 (8) =Loge (8)/Дневникe (2)Преведено на T-SQL, прилагате следното изчисление:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x) / LOG(@b);
След като разберете, че LOG поддържа втори параметър, където посочвате основата, изчислението просто става:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x, @b);
Променлива на курсора
Ако сте работили с T-SQL от известно време, вероятно сте имали много шансове да работите с курсори. Както знаете, когато работите с курсор, обикновено използвате следните стъпки:
- Декларирайте курсора
- Отворете курсора
- Прегледайте записите на курсора
- Затворете курсора
- Освободете курсора
Като пример, да предположим, че трябва да изпълните някаква задача за база данни във вашия екземпляр. Използвайки курсор, обикновено използвате код, подобен на следния:
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;
Командата CLOSE освобождава текущия набор от резултати и освобождава заключванията. Командата DEALLOCATE премахва препратка към курсора и когато последната препратка бъде освободена, освобождава структурите от данни, съдържащи курсора. Ако опитате да изпълните горния код два пъти без командите CLOSE и DEALLOCATE, ще получите следната грешка:
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.
Уверете се, че изпълнявате командите CLOSE и DEALLOCATE, преди да продължите.
Много хора не осъзнават, че когато трябва да работят с курсор само в една партида, което е най-често срещаният случай, вместо да използвате обикновен курсор, можете да работите с променлива на курсора. Както всяка променлива, обхватът на променливата на курсора е само партидата, където е декларирана. Това означава, че веднага щом една партида приключи, всички променливи изтичат. Използвайки курсорна променлива, след като пакетът приключи, SQL Server автоматично го затваря и освобождава, спестявайки ви необходимостта да изпълнявате изрично командите CLOSE и DEALLOCATE.
Ето преработения код, използващ този път променлива на курсора:
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;
Чувствайте се свободни да го изпълните няколко пъти и забележете, че този път не получавате никакви грешки. Просто е по-чисто и не е нужно да се притеснявате да запазите ресурсите на курсора, ако сте забравили да затворите и освободите курсора.
СЛИВАНЕ с ИЗХОД
От началото на клаузата OUTPUT за изрази за модификация в SQL Server 2005, тя се оказа много практичен инструмент, когато искате да върнете данни от модифицирани редове. Хората използват тази функция редовно за цели като архивиране, одит и много други случаи на употреба. Едно от досадните неща за тази функция обаче е, че ако я използвате с изрази INSERT, имате право да връщате данни само от вмъкнатите редове, като поставяте префикс на изходните колони с вмъкнати . Нямате достъп до колоните на изходната таблица, въпреки че понякога се налага да връщате колони от източника заедно с колони от целта.
Като пример, разгледайте таблиците T1 и T2, които създавате и попълвате, като изпълнявате следния код:
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');
Забележете, че за генериране на ключовете и в двете таблици се използва свойство за идентичност.
Да предположим, че трябва да копирате някои редове от T1 до T2; да речем, тези, при които keycol % 2 =1. Искате да използвате клаузата OUTPUT, за да върнете новогенерираните ключове в T2, но също така искате да върнете заедно с тези ключове съответните изходни ключове от T1. Интуитивното очакване е да се използва следната инструкция 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;
За съжаление обаче, както беше споменато, клаузата OUTPUT не ви позволява да се позовавате на колони от изходната таблица, така че получавате следната грешка:
Съобщение 4104, ниво 16, състояние 1, ред 2Идентификаторът от няколко части "T1.keycol" не може да бъде обвързан.
Много хора не осъзнават, че странно това ограничение не важи за изявлението MERGE. Така че, въпреки че е малко неудобно, можете да преобразувате своя оператор INSERT в оператор MERGE, но за да го направите, трябва предикатът MERGE винаги да е false. Това ще активира клаузата WHEN NOT MATCHED и ще приложи единственото поддържано действие INSERT там. Можете да използвате фиктивно фалшиво условие, като 1 =2. Ето пълния преобразуван код:
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;
Този път кодът работи успешно, произвеждайки следния изход:
T1_keycol T2_keycol ----------- ----------- 1 1 3 2 5 3
Надяваме се, че Microsoft ще подобри поддръжката на клаузата OUTPUT в другите оператори за модификация, за да позволи връщането на колони и от изходната таблица.
Заключение
Не предполагайте, и RTFM! :-)