SQL CASE? Парче торта!
Наистина ли?
Не и докато не се сблъскате с 3 обезпокоителни проблема, които могат да причинят грешки по време на изпълнение и бавна производителност.
Ако се опитвате да сканирате подзаглавията, за да видите какви са проблемите, не мога да ви виня. Читателите, включително и аз, са нетърпеливи.
Вярвам, че вече знаете основите на SQL CASE, така че няма да ви отегчавам с дълги въведения. Нека се задълбочим в по-задълбочено разбиране на това, което се случва под капака.
1. SQL CASE не винаги оценява последователно
Изразите в оператора CASE на Microsoft SQL се оценяват предимно последователно или отляво надясно. Това е различна история обаче, когато го използвате с агрегатни функции. Нека имаме пример:
-- aggregate function evaluated first and generated an error
DECLARE @value INT = 0;
SELECT CASE WHEN @value = 0 THEN 1 ELSE MAX(1/@value) END;
Горният код изглежда нормален. Ако ви попитам какъв е резултатът от тези твърдения, вероятно ще кажете 1. Визуалната проверка ни казва, че тъй като @value е зададен на 0. Когато @value е 0, резултатът е 1.
Но тук не е така. Вижте реалния резултат от SQL Server Management Studio:
Msg 8134, Level 16, State 1, Line 4
Divide by zero error encountered.
Но защо?
Когато условните изрази използват агрегатни функции като MAX() в SQL CASE, той се оценява първо. По този начин MAX(1/@value) ще причини грешка при деление на нула, тъй като @value е нула.
Тази ситуация е по-обезпокоителна, когато е скрита. Ще го обясня по-късно.
2. Прост SQL CASE израз оценява няколко пъти
И какво?
Добър въпрос. Истината е, че няма никакви проблеми, ако използвате литерали или прости изрази. Но ако използвате подзаявки като условен израз, ще получите голяма изненада.
Преди да опитате примера по-долу, може да искате да възстановите копие на базата данни от тук. Ще го използваме за останалите примери.
Сега помислете за тази много проста заявка:
SELECT TOP 1 manufacturerID FROM SportsCars
Много е просто, нали? Връща 1 ред с 1 колона данни. STATISTICS IO разкрива минимални логически четения.
Бърза бележка :За непосветените, наличието на по-високи логически четения прави заявката бавна. Прочетете това за повече подробности.
Планът за изпълнение също така разкрива прост процес:
Сега нека поставим тази заявка в CASE израз като подзаявка:
-- Using a subquery in a SQL CASE
DECLARE @manufacturer NVARCHAR(50)
SET @manufacturer = (CASE (SELECT TOP 1 manufacturerID FROM SportsCars)
WHEN 6 THEN 'Alfa Romeo'
WHEN 21 THEN 'Aston Martin'
WHEN 64 THEN 'Ferrari'
WHEN 108 THEN 'McLaren'
ELSE 'Others'
END)
SELECT @manufacturer;
Анализ
Стискайте палци, защото това ще издуха логическите четения 4 пъти.
Изненада! В сравнение с Фигура 1 само с 2 логически показания, това е 4 пъти по-високо. Така заявката е 4 пъти по-бавна. Как може да се случи това? Видяхме подзаявката само веднъж.
Но това не е краят на историята. Вижте плана за изпълнение:
Виждаме 4 екземпляра на операторите Top и Index Scan на фигура 4. Ако всяко горно и индексно сканиране консумират 2 логически четения, това обяснява защо логическите четения са станали 8 на фигура 3. И тъй като всяко сканиране отгоре и индексно сканиране имат 25% цена , това също ни казва, че са еднакви.
Но това не свършва дотук. Свойствата на оператора Compute Scalar разкриват как се третира цялото изявление.
Виждаме 4 израза CASE WHEN, идващи от Дефинирани стойности на оператора Compute Scalar. Изглежда, че нашият прост CASE израз се превърна в търсен CASE израз по следния начин:
DECLARE @manufacturer NVARCHAR(50)
SET @manufacturer = (CASE
WHEN (SELECT TOP 1 manufacturerID FROM SportsCars) = 6 THEN 'Alfa Romeo'
WHEN (SELECT TOP 1 manufacturerID FROM SportsCars) = 21 THEN 'Aston Martin'
WHEN (SELECT TOP 1 manufacturerID FROM SportsCars) = 64 THEN 'Ferrari'
WHEN (SELECT TOP 1 manufacturerID FROM SportsCars) = 108 THEN 'McLaren'
ELSE 'Others'
END)
SELECT @manufacturer;
Нека обобщим. Имаше 2 логически четения за всеки оператор за сканиране отгоре и индекс. Това умножено по 4 прави 8 логически четения. Видяхме и 4 израза CASE WHEN в оператора Compute Scalar.
В крайна сметка подзаявката в простия израз CASE беше оценена 4 пъти. Това ще изостави заявката ви.
Как да избегнем множество оценки на подзаявка в прост CASE израз
За да избегнем такъв проблем с производителността като множество оператори CASE в SQL, трябва да пренапишем заявката.
Първо, поставете резултата от подзаявката в променлива. След това използвайте тази променлива в условието на простия израз CASE на SQL Server, както следва:
DECLARE @manufacturer NVARCHAR(50)
DECLARE @ManufacturerID INT -- create a new variable
-- store the result of the subquery in a variable
SET @ManufacturerID = (SELECT TOP 1 manufacturerID FROM SportsCars)
-- use the new variable in the simple CASE expression
SET @manufacturer = (CASE @ManufacturerID
WHEN 6 THEN 'Alfa Romeo'
WHEN 21 THEN 'Aston Martin'
WHEN 64 THEN 'Ferrari'
WHEN 108 THEN 'McLaren'
ELSE 'Others'
END)
SELECT @manufacturer;
Това добро решение ли е? Нека видим логическите показания в STATISTICS IO:
Виждаме по-ниски логически четения от модифицираната заявка. Изваждането на подзаявката и присвояването на резултата на променлива е много по-добре. Какво ще кажете за плана за изпълнение? Вижте го по-долу.
Операторът Top и Index Scan се появи само веднъж, а не 4 пъти. Прекрасно!
Вземане за вкъщи :Не използвайте подзаявка като условие в израза CASE. Ако трябва да извлечете стойност, първо поставете резултата от подзаявката в променлива. След това използвайте тази променлива в израза CASE.
3. Тези 3 вградени функции тайно се трансформират в SQL CASE
Има тайна и изявлението CASE на SQL Server има нещо общо с него. Ако не знаете как се държат тези 3 функции, няма да знаете, че правите грешка, която се опитахме да избегнем в точки #1 и #2 по-рано. Ето ги:
- IIF
- СЛИВАНЕ
- ИЗБЕРЕТЕ
Нека ги разгледаме един по един.
IIF
Използвах Immediate IF или IIF във Visual Basic и Visual Basic за приложения. Това също е еквивалентно на тернарния оператор на C#:<условие> ?
Тази функция при условие ще върне 1 от 2-та аргумента въз основа на резултата от условието. И тази функция е налична и в T-SQL. Инструкцията CASE в клаузата WHERE може да се използва в израза SELECT
Но това е просто захарно покритие с по-дълъг CASE израз. откъде знаем? Нека разгледаме пример.
SELECT IIF((SELECT Model FROM SportsCars WHERE SportsCarID = 1276) = 'McLaren Senna', 'Yes', 'No');
Резултатът от тази заявка е „Не“. Вижте обаче плана за изпълнение заедно със свойствата на Compute Scalar.
Тъй като IIF е СЛУЧАЙ КОГА какво мислите, че ще се случи, ако изпълните нещо подобно?
DECLARE @averageCost MONEY = 1000000.00;
DECLARE @noOfPayments TINYINT = 0; -- intentional to force the error
SELECT IIF((SELECT Model FROM SportsCars WHERE SportsCarID = 1276) = 'SF90 Spider', 83333.33,MIN(@averageCost / @noOfPayments));
Това ще доведе до грешка Деление на нула, ако @noOfPayments е 0. Същото се случи в точка №1 по-рано.
Може да се чудите какво причинява тази грешка, защото горната заявка ще доведе до TRUE и трябва да върне 83333.33. Проверете отново точка №1.
По този начин, ако сте останали с грешка като тази, когато използвате IIF, виновникът е SQL CASE.
СЛИВАНЕ
COALESCE също е пряк път към SQL CASE израз. Той оценява списъка със стойности и връща първата стойност, различна от нула. В предишната статия за COALESCE представих пример, който оценява подзаявка два пъти. Но използвах друг метод, за да разкрия SQL CASE в плана за изпълнение. Ето още един пример, който ще използва същите техники.
SELECT
COALESCE(m.Manufacturer + ' ','') + sc.Model AS Car
FROM SportsCars sc
LEFT JOIN Manufacturers m ON sc.ManufacturerID = m.ManufacturerID
Нека видим плана за изпълнение и изчисляването на скаларно дефинираните стойности.
SQL CASE е наред. Ключовата дума COALESCE не е никъде в прозореца Дефинирани стойности. Това доказва тайната зад тази функция.
Но това не е всичко. Колко пъти видяхте [Превозни средства].[dbo].[Стилове].[Стил] в прозореца Дефинирани стойности? ДВА ПЪТИ! Това е в съответствие с официалната документация на Microsoft. Представете си, ако един от аргументите в COALESCE е подзаявка. След това удвоете логическите четения и получете по-бавното изпълнение.
ИЗБЕРЕТЕ
И накрая, ИЗБЕРЕТЕ. Това е подобно на функцията ИЗБИРАНЕ на MS Access. Връща 1 стойност от списък със стойности въз основа на позиция на индекс. Той също така действа като индекс в масив.
Нека видим дали можем да изкопаем трансформацията в SQL CASE с пример. Вижте кода по-долу:
;WITH McLarenCars AS
(
SELECT
CASE
WHEN sc.Model IN ('Artura','Speedtail','P1/ P1 GTR','P1 LM') THEN '1'
ELSE '2'
END AS [type]
,sc.Model
,s.Style
FROM SportsCars sc
INNER JOIN Styles s ON sc.StyleID = s.StyleID
WHERE sc.ManufacturerID = 108
)
SELECT
Model
,Style
,CHOOSE([Type],'Hybrid','Gasoline') AS [type]
FROM McLarenCars
Ето нашия пример ИЗБИРАНЕ. Сега нека проверим плана за изпълнение и изчисляването на скаларно дефинирани стойности:
Виждате ли ключовата дума ИЗБИРАНЕ в прозореца Дефинирани стойности на фигура 10? Какво ще кажете за CASE КОГА?
Подобно на предишните примери, тази функция ИЗБИРАНЕ е просто захарна покривка към по-дълъг CASE израз. И тъй като заявката има 2 елемента за ИЗБИРАНЕ, ключовите думи CASE WHEN се появяват два пъти. Вижте прозореца Дефинирани стойности, затворен в червено поле.
Тук обаче имаме няколко CASE WHEN в SQL. Това се дължи на израза CASE във вътрешната заявка на CTE. Ако погледнете внимателно, тази част от вътрешната заявка също се появява два пъти.
Вземане за вкъщи
Сега, когато тайните са разкрити, какво научихме?
- SQL CASE се държи различно, когато се използват агрегатни функции. Бъдете внимателни, когато предавате аргументи на агрегатни функции като MIN, MAX или COUNT.
- Прост CASE израз ще се оценява няколко пъти. Забележете го и избягвайте да предавате подзаявка. Въпреки че е синтактично правилен, ще работи лошо.
- IIF, CHOOSE и COALESCE имат мръсни тайни. Имайте го предвид, преди да предадете стойности на тези функции. Той ще се трансформира в SQL CASE. В зависимост от стойностите причинявате или грешка, или намаляване на производителността.
Надявам се, че този различен поглед върху SQL CASE е бил полезен за вас. Ако е така, вашите приятели разработчици също може да го харесат. Моля, споделете го в любимите си социални медийни платформи. И ни уведомете какво мислите за това в секцията за коментари.