Тази статия е втората от поредица за NULL сложности. Миналия месец въведох NULL като маркер на SQL за всякакъв вид липсваща стойност. Обясних, че SQL не ви предоставя възможността да правите разлика между липсващи и приложими (A-стойности) и липсващи и неприложими (I-стойности) маркери. Обясних също как сравненията, включващи NULL, работят с константи, променливи, параметри и колони. Този месец продължавам дискусията, като покривам несъответствията на третирането с NULL в различни T-SQL елементи.
Ще продължа да използвам примерната база данни TSQLV5, както миналия месец в някои от моите примери. Тук можете да намерите скрипта, който създава и попълва тази база данни, както и нейната диаграма за ER тук.
Несъответствия в третирането с NULL
Както вече разбрахте, третирането с NULL не е тривиално. Част от объркването и сложността са свързани с факта, че третирането на NULL може да бъде несъвместимо между различните елементи на T-SQL за подобни операции. В следващите раздели описвам NULL обработката при линейни спрямо агрегатни изчисления, клаузи ON/WHERE/HAVING, ограничение CHECK срещу опция CHECK, елементи IF/WHILE/CASE, оператора MERGE, отличителност и групиране, както и подреждане и уникалност.
Линейни спрямо агрегатни изчисления
T-SQL, както и за стандартния SQL, използва различна логика за обработка на NULL, когато прилага действителна агрегатна функция като SUM, MIN и MAX между редове спрямо когато прилага същото изчисление като линейно в колоните. За да демонстрирам тази разлика, ще използвам две примерни таблици, наречени #T1 и #T2, които създавате и попълвате, като изпълнявате следния код:
ПРОСТАНЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА #T1, #T2; ИЗБЕРЕТЕ * В #T1 ОТ ( СТОЙНОСТИ(10, 5, NULL) ) КАТО D(col1, col2, col3); ИЗБЕРЕТЕ * В #T2 ОТ ( СТОЙНОСТИ(10),(5),(NULL) ) КАТО D(col1);
Таблицата #T1 има три колони, наречени col1, col2 и col3. В момента има един ред със стойности на колоните съответно 10, 5 и NULL:
ИЗБЕРЕТЕ * ОТ #T1;
col1 col2 col3----------- ----------- -----------10 5 NULL
Таблицата #T2 има една колона, наречена col1. Понастоящем има три реда със стойностите 10, 5 и NULL в col1:
ИЗБЕРЕТЕ * ОТ #T2;
col1----------105NULL
Когато се прилага това, което в крайна сметка е агрегатно изчисление, като събиране като линейно в колони, наличието на всеки вход NULL дава резултат NULL. Следната заявка демонстрира това поведение:
ИЗБЕРЕТЕ col1 + col2 + col3 AS totalFROM #T1;
Тази заявка генерира следния изход:
общо ------------NULL
Обратно, действителните агрегатни функции, които се прилагат между редове, са проектирани да игнорират NULL входове. Следната заявка демонстрира това поведение с помощта на функцията SUM:
ИЗБЕРЕТЕ СУМА(кол1) КАТО общо ОТ #T2;
Тази заявка генерира следния изход:
общо-----------15Предупреждение:Нулевата стойност се елиминира чрез агрегат или друга операция SET.
Обърнете внимание на предупреждението, изисквано от стандарта SQL, което показва наличието на NULL входове, които са били игнорирани. Можете да потиснете такива предупреждения, като изключите опцията за сесия ANSI_WARNINGS.
По същия начин, когато се прилага към входен израз, функцията COUNT отчита броя на редовете с входни стойности, различни от NULL (за разлика от COUNT(*), който просто отчита броя на редовете). Например, замяната на SUM(col1) с COUNT(col1) в горната заявка връща броя на 2.
Любопитно е, че ако приложите COUNT агрегат към колона, която е дефинирана като не позволяваща NULL, оптимизаторът преобразува израза COUNT(<име_на_колона>) в COUNT(*). Това позволява използването на всеки индекс за целите на броене, вместо да изисква използването на индекс, който съдържа въпросната колона. Това е още една причина отвъд гарантирането на последователността и целостта на вашите данни, която трябва да ви насърчи да налагате ограничения като NOT NULL и други. Такива ограничения позволяват на оптимизатора по-голяма гъвкавост при разглеждането на по-оптимални алтернативи и избягване на ненужна работа.
Въз основа на тази логика функцията AVG разделя сумата от стойности, различни от NULL, на броя на стойностите, различни от NULL. Разгледайте следната заявка като пример:
ИЗБЕРЕТЕ AVG(1.0 * col1) КАТО avgallFROM #T2;
Тук сумата от стойностите на col1, които не са NULL 15, се разделя на броя на стойностите, различни от NULL 2. Вие умножавате col1 по цифровия литерал 1.0, за да принудите имплицитно преобразуване на целочислените входни стойности в числови, за да получите числово деление, а не цяло число дивизия. Тази заявка генерира следния изход:
avgall--------7,500000
По подобен начин агрегатите MIN и MAX игнорират NULL входове. Помислете за следната заявка:
ИЗБЕРЕТЕ MIN(col1) AS mincol1, MAX(col1) AS maxcol1FROM #T2;
Тази заявка генерира следния изход:
mincol1 maxcol1----------- -----------5 10
Опитът за прилагане на линейни изчисления, но емулирането на семантиката на агрегатната функция (игнориране на NULL) не е красиво. Емулирането на SUM, COUNT и AVG не е твърде сложно, но изисква да проверявате всеки вход за NULL, така:
ИЗБЕРЕТЕ col1, col2, col3, СЛУЧАЙ, КОГАТО КОАЛЕСЦИЯ (col1, col2, col3) Е NULL, ТОГАВА NULL ELSE КОАЛЕСЦИЯ (col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0) END AS sumall, КОГАТО col1 НЕ Е NULL ТОГАВА 1 ELSE 0 END + CASE КОГАТО col2 НЕ Е NULL ТОГАВА 1 ELSE 0 END + CASE КОГАТО col3 НЕ Е NULL ТОГАВА 1 ELSE 0 END КАТО cntall, СЛУЧАЙ, КОГАТО СЪЕДИНЯВАТЕ СЕ (col2, col13) NULL ELSE 1.0 * (СЛИВАНЕ (col1, 0) + КОАЛЕСЦИЯ (col2, 0) + COALESCE(col3, 0)) / (СЛУЧАЙ, КОГАТО col1 НЕ Е NULL ТОГАВА 1 ELSE 0 END + СЛУЧАЙ, КОГАТО col2 НЕ Е NUEL 0 КРАЙ + СЛУЧАЙ, КОГАТО col3 НЕ Е NULL ТОГАВА 1 ДРУГА 0 КРАЙ) КРАЙ КАТО avgallFROM #T1;
Тази заявка генерира следния изход:
col1 col2 col3 sumall cntall avgall----------- ----------- ----------- -------- --- ----------- ----------------10 5 NULL 15 2 7.500000000000
Опитът за прилагане на минимум или максимум като линейно изчисление към повече от две входни колони е доста труден дори преди да добавите логиката за игнориране на NULL, тъй като включва влагане на множество CASE изрази пряко или косвено (когато използвате повторно псевдоними на колони). Например, ето заявка, която изчислява максимума между col1, col2 и col3 в #T1, без частта, която игнорира NULL:
ИЗБЕРЕТЕ col1, col2, col3, CASE, КОГАТО col1 Е NULL ИЛИ col2 Е NULL ИЛИ col3 Е NULL ТОГАВА NULL ELSE max2 КРАЙ КАТО maxallFROM #T1 КРЪСТ ПРИЛОЖИ (СТОЙНОСТИ (СЛУЧАЙ, КОГАТО col1>=col1) ТОГАВА col2 КАТО A1(max1) КРЪСТО ПРИЛОЖИ (СЛУЧАЙ (СЛУЧАЙ, КОГАТО max1>=col3 ТОГАВА max1 ИНАЧЕ col3 END)) КАТО A2(max2);
Тази заявка генерира следния изход:
col1 col2 col3 maxall----------- ----------- ----------- ---------- -10 5 NULL NULL
Ако разгледате плана на заявката, ще намерите следния разширен израз, изчисляващ крайния резултат:
[Expr1005] =Скаларен оператор(CASE WHEN CASE WHEN [#T1].[col1] НЕ Е NULL THEN [#T1].[col1] ELSE CASE WHEN [#T1].[col2] НЕ Е NULL ТОГАВА [#T1].[col1] #T1].[col2] ELSE [#T1].[col3] END END IS NULL THEN NULL ELSE CASE WHEN CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1] .[col1] ELSE [#T1].[col2] END>=[#T1].[col3] СЛЕД СЛУЧАЙ, КОГАТО [#T1].[col1]>=[#T1].[col2] СЛЕД [#T1] .[col1] ELSE [#T1].[col2] END ELSE [#T1].[col3] END END)
И това е, когато участват само три колони. Представете си, че имате замесени дузина колони!
Сега добавете към това логиката за игнориране на NULL:
ИЗБЕРЕТЕ col1, col2, col3, max2 КАТО maxallFROM #T1 КРЪСТО ПРИЛОЖИ (СТОЙНОСТИ(СЛУЧАЙ, КОГАТО col1>=col2 ИЛИ col2 Е NULL, ТОГАВА col1 ELSE col2 КРАЙ)) КАТО A1(max1) КРЪСТО ПРИЛОЖИ (СТОЙНОСТИ (МАКС. СЛУЧАЙ WH>=col3 ИЛИ col3 Е NULL ТОГАВА max1 ИНАЧЕ col3 END)) КАТО A2(max2);
Тази заявка генерира следния изход:
col1 col2 col3 maxall----------- ----------- ----------- ---------- -10 5 NULL 10
Oracle има двойка функции, наречени НАЙ-ГОЛЕМИ и НАЙ-МАЛКИ, които прилагат минимални и максимални изчисления, съответно, като линейни към входните стойности. Тези функции връщат NULL при всеки NULL вход, както правят повечето линейни изчисления. Имаше отворен елемент за обратна връзка с молба за получаване на подобни функции в T-SQL, но тази заявка не беше пренесена в последната им промяна на сайта за обратна връзка. Ако Microsoft добави такива функции към T-SQL, би било чудесно да има опция за контрол дали да се игнорират NULL или не.
Междувременно има много по-елегантна техника в сравнение с гореспоменатите, която изчислява всякакъв вид агрегат като линеен през колони, използвайки действителната семантика на агрегатната функция, игнорирайки NULL. Използвате комбинация от оператора CROSS APPLY и извлечена заявка за таблица срещу конструктор на стойност на таблица, който завърта колони към редове и прилага агрегата като действителна агрегатна функция. Ето пример, демонстриращ изчисленията MIN и MAX, но можете да използвате тази техника с всяка обобщена функция, която харесвате:
ИЗБЕРЕТЕ col1, col2, col3, maxall, minallFROM #T1 КРЪСТО ПРИЛОЖИ (ИЗБЕРЕТЕ MAX(mycol), MIN(mycol) ОТ (СТОЙНОСТИ(col1),(col2),(col3)) КАТО D1(mycol)) AS D2(maxall, minall);
Тази заявка генерира следния изход:
col1 col2 col3 maxall minall----------- ----------- ----------- --------- -- -----------10 5 NULL 10 5
Ами ако искаш обратното? Ами ако трябва да изчислите агрегат между редове, но да произведете NULL, ако има някакъв NULL вход? Например, да предположим, че трябва да сумирате всички стойности на col1 от #T1, но да върнете NULL, ако някой от входовете е NULL. Това може да се постигне със следната техника:
ИЗБЕРЕТЕ SUM(col1) * NULLIF(MIN(СЛУЧАЙ, КОГАТО col1 Е NULL, ТОГАВА 0 ДРУГИ 1 КРАЙ), 0) КАТО sumallFROM #T2;
Прилагате MIN агрегат към CASE израз, който връща нули за NULL входове и единици за входове, различни от NULL. Ако има някакъв NULL вход, резултатът от функцията MIN е 0, в противен случай е 1. След това с помощта на функцията NULLIF преобразувате резултат 0 в NULL. След това умножавате резултата от функцията NULLIF по първоначалната сума. Ако има някакъв NULL вход, умножавате оригиналната сума по NULL, което води до NULL. Ако няма въведено NULL, умножавате резултата от първоначалната сума по 1, като се получава оригиналната сума.
Обратно към линейните изчисления, даващи NULL за всеки вход NULL, същата логика се прилага за конкатенация на низове с помощта на оператора +, както демонстрира следната заявка:
ИЗПОЛЗВАЙТЕ TSQLV5; ИЗБЕРЕТЕ empid, държава, регион, град, държава + N',' + регион + N',' + град КАТО emplocationFROM HR.Employees;
Тази заявка генерира следния изход:
empid държава регион град работа ------------- --------------- --------------- - -------------- ----------------1 САЩ WA Сиатъл САЩ, WA, Сиатъл2 САЩ WA Такома САЩ, WA, Такома3 САЩ WA Kirkland USA,WA,Kirkland4 USA WA Редмънд САЩ,WA,Redmond5 UK NULL Лондон NULL6 UK NULL Лондон NULL7 UK NULL Лондон NULL8 САЩ WA Сиатъл САЩ,WA,Seattle9 UK NULL Лондон NULL
Искате да свържете частите за местоположение на служителите в един низ, като използвате запетая като разделител. Но вие искате да игнорирате NULL входове. Вместо това, когато някой от входовете е NULL, вие получавате NULL като резултат. Някои изключват опцията за сесия CONCAT_NULL_YIELDS_NULL, която кара вход NULL да се преобразува в празен низ за целите на конкатенацията, но тази опция не се препоръчва, тъй като прилага нестандартно поведение. Освен това ще останете с множество последователни разделители, когато има NULL входове, което обикновено не е желаното поведение. Друга възможност е изрично да замените NULL входовете с празен низ, като използвате функциите ISNULL или COALESCE, но това обикновено води до дълъг подробен код. Много по-елегантен вариант е да използвате функцията CONCAT_WS, която беше въведена в SQL Server 2017. Тази функция обединява входовете, игнорирайки NULL, като използва разделителя, предоставен като първи вход. Ето заявката за решение, използваща тази функция:
ИЗБЕРЕТЕ empid, държава, регион, град, CONCAT_WS(N',', държава, регион, град) КАТО emplocationFROM HR.Employees;
Тази заявка генерира следния изход:
empid държава регион град работа ------------- --------------- --------------- - -------------- ----------------1 САЩ WA Сиатъл САЩ, WA, Сиатъл2 САЩ WA Такома САЩ, WA, Такома3 САЩ WA Kirkland USA,WA,Kirkland4 USA WA Редмънд САЩ,WA,Redmond5 UK NULL Лондон UK,London6 UK NULL Лондон UK,London7 UK NULL Лондон UK,London8 САЩ WA Сиатъл САЩ,WA,Seattle9 UK NULL Лондон UK,Лондон
ВКЛЮЧЕНО/КЪДЕ/ИМАМ
Когато използвате клаузите на заявка WHERE, HAVING и ON за целите на филтриране/съпоставяне, важно е да запомните, че те използват тризначна предикатна логика. Когато имате включена логика с три стойности, вие искате да определите точно как клаузата обработва TRUE, FALSE и NEZNAWN случаи. Тези три клаузи са предназначени да приемат ИСТИНСКИ случаи и да отхвърлят ФАЛШИ и НЕИЗВЕСТНИ случаи.
За да демонстрирам това поведение, ще използвам таблица, наречена Контакти, която създавате и попълвате, като изпълнявате следния код:.
ПРОСТЪПНЕТЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА dbo.Contacts;GO CREATE TABLE dbo.Contacts( id INT NOT NULL ОГРАНИЧЕНИЕ PK_Contacts ПЪРВИЧЕН КЛЮЧ, име VARCHAR(10) NOT NULL, часова ставка NUMERIC(12, 2) CHECK КОНТРОЛИЗАЦИЯ CHURCh0. )); INSERT INTO dbo.Contacts(id, name, timelyrate) СТОЙНОСТИ (1, 'A', 100.00), (2, 'B', 200.00), (3, 'C', NULL);
Забележете, че контакти 1 и 2 имат приложими почасови ставки, а контакт 3 не, така че неговата почасова ставка е настроена на NULL. Помислете за следната заявка за търсене на контакти с положителна часова ставка:
ИЗБЕРЕТЕ идентификатор, име, часова ставкаFROM dbo.ContactsWHERE часова ставка> 0,00;
Този предикат се оценява на TRUE за контакти 1 и 2 и на НЕИЗВЕСТНО за контакт 3, следователно изходът съдържа само контакти 1 и 2:
id name часова ставка----------- ---------- -----------1 A 100.002 B 200.00
Мисленето тук е, че когато сте сигурни, че предикатът е верен, искате да върнете реда, в противен случай искате да го изхвърлите. Това в началото може да изглежда тривиално, докато не разберете, че някои езикови елементи, които също използват предикати, работят по различен начин.
Ограничение CHECK спрямо опция CHECK
Ограничението CHECK е инструмент, който използвате за налагане на целостта в таблица, базирана на предикат. Предикатът се оценява, когато се опитате да вмъкнете или актуализирате редове в таблицата. За разлика от клаузите за филтриране на заявки и съвпадение, които приемат TRUE случаи и отхвърлят FALSE и UNKNOWN случаи, ограничението CHECK е предназначено да приема TRUE и UNKNOWN случаи и да отхвърля FALSE случаи. Мисленето тук е, че когато сте сигурни, че предикатът е фалшив, искате да отхвърлите опита за промяна, в противен случай искате да го разрешите.
Ако разгледате дефиницията на нашата таблица с контакти, ще забележите, че тя има следното ограничение CHECK, отхвърлящо контакти с неположителни почасови ставки:
CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate> 0.00)
Забележете, че ограничението използва същия предикат като този, който сте използвали в предишния филтър на заявка.
Опитайте се да добавите контакт с положителна часова ставка:
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);
Този опит е успешен.
Опитайте да добавите контакт с НУЛА часова ставка:
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);
Този опит също е успешен, тъй като ограничението CHECK е предназначено да приема ИСТИНСКИ и НЕИЗВЕСТНИ случаи. Такъв е случаят, когато филтърът за заявка и ограничението CHECK са проектирани да работят по различен начин.
Опитайте да добавите контакт с неположителна часова ставка:
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);
Този опит е неуспешен със следната грешка:
Съобщение 547, ниво 16, състояние 0, ред 454Изразът INSERT е в конфликт с ограничението CHECK "CHK_Contacts_hourlyrate". Конфликтът е възникнал в база данни "TSQLV5", таблица "dbo.Contacts", колона "hourlyrate".
T-SQL също така ви позволява да налагате целостта на модификациите чрез изгледи, като използвате опция ПРОВЕРКА. Някои смятат, че тази опция служи за подобна цел на ограничението CHECK, стига да приложите модификацията през изгледа. Например, помислете за следния изглед, който използва филтър, базиран на предиката часова ставка> 0,00 и се дефинира с опцията ПРОВЕРКА:
СЪЗДАДЕТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.MyContactsASSELECT id, name, hourlyrateFROM dbo.ContactsWHERE часова ставка> 0,00 С ОПЦИЯ ЗА ПРОВЕРКА;
Както се оказва, за разлика от ограничението CHECK, опцията за преглед CHECK е проектирана да приема TRUE случаи и да отхвърля както FALSE, така и NEZNOWN случаи. Така че всъщност е проектиран да се държи по-скоро като филтъра за заявки обикновено с цел налагане на целостта.
Опитайте да вмъкнете ред с положителна часова ставка през изгледа:
ВЪВЕТЕ В dbo.MyContacts(id, име, часова ставка) СТОЙНОСТИ (7, 'G', 300.00);
Този опит е успешен.
Опитайте да вмъкнете ред с НУЛОВА часова ставка през изгледа:
ВЪВЕТЕ В dbo.MyContacts(id, име, часова ставка) СТОЙНОСТИ (8, 'H', NULL);
Този опит е неуспешен със следната грешка:
Съобщение 550, ниво 16, състояние 1, ред 473Опитът за вмъкване или актуализиране е неуспешен, защото целевият изглед или посочва WITH CHECK OPTION или обхваща изглед, който указва WITH CHECK OPTION и един или повече редове, получени в резултат на операцията, не са били отговарят на ограничението CHECK OPTION.
Мисленето тук е, че след като добавите опцията CHECK към изгледа, вие искате да разрешите само модификации, водещи до редове, които се връщат от изгледа. Това е малко по-различно от мисленето с ограничение CHECK – отхвърлете промените, за които сте сигурни, че предикатът е фалшив. Това може да бъде малко объркващо. Ако искате изгледът да позволява модификации, които задават почасовата ставка на NULL, имате нужда от филтъра за заявки, за да разреши и тези, като добави ИЛИ почасовата ставка IS NULL. Просто трябва да осъзнаете, че ограничението CHECK и опцията CHECK са проектирани да работят по различен начин по отношение на НЕИЗВЕСТНИЯ случай. Първият го приема, докато вторият го отхвърля.
Направете заявка в таблицата с контакти след всички горепосочени промени:
ИЗБЕРЕТЕ идентификатор, име, часова ставкаFROM dbo.Contacts;
В този момент трябва да получите следния изход:
id name часова ставка----------- ---------- -----------1 A 100.002 B 200.003 C NULL4 D 150.005 E NULL7 G 300,00
АКО/WHILE/CASE
Езиковите елементи IF, WHILE и CASE работят с предикати.
Инструкцията IF е проектирана по следния начин:
IF<изявление или BEGIN-END блок, когато TRUE>ELSE <изявление или BEGIN-END блок, когато FALSE или UNKNOWN>
Интуитивно е да очаквате да имате блок TRUE след клаузата IF и блок FALSE след клаузата ELSE, но трябва да осъзнаете, че клаузата ELSE всъщност се активира, когато предикатът е FALSE или UNKNOWN. Теоретично, тризначният логически език би могъл да има оператор IF с разделяне на трите случая. Нещо като това:
IFWHEN TRUE <изявление или BEGIN-END блок, когато TRUE> WHEN FALSE <изявление или BEGIN-END блок, когато FALSE> WHEN UNKNOWN <изявление или BEGIN-END блок, когато UNKNOWN>
И дори позволявайте комбинации от логически резултати, така че ако искате да комбинирате FALSE и UNKNOWN в един раздел, можете да използвате нещо подобно:
IFWHEN TRUE <изявление или BEGIN-END блок, когато TRUE> WHEN FALSE OR UNKNOWN <изявление или BEGIN-END блок, когато FALSE OR UNKNOWN>
Междувременно можете да емулирате такива конструкции, като влагате изрази IF-ELSE и изрично търсите NULL в операндите с оператора IS NULL.
Инструкцията WHILE има само блок TRUE. Проектиран е по следния начин:
WHILE <предикат> <инструкция или блок BEGIN-END, когато TRUE>
Инструкцията или блокът BEGIN-END, формиращ тялото на цикъла, се активира, докато предикатът е TURE. Веднага щом предикатът е FALSE или UNKNOWN, управлението преминава към оператора след цикъла WHILE.
За разлика от IF и WHILE, които са оператори, изпълняващи код, CASE е израз, връщащ стойност. Синтаксисът на търсен Изразът CASE е както следва:
<пред>СЛУЧАЙ КОГАТО <предикат 1> THEN <израз 1, когато TRUE> WHEN <предикат 2> THEN <израз 2, когато TRUE> ... WHEN <предикат n> THEN <израз n, когато TRUE> ELSE <друго израз, когато всички са FALSE или НЕИЗВЕСТНО>КРАЙИзразът CASE е проектиран да върне израза след клаузата THEN, който съответства на първия предикат WHEN, който се оценява на TRUE. Ако има клауза ELSE, тя се активира, ако нито един предикат WHEN не е TRUE (всички са FALSE или UNKNOWN). При липса на изрична клауза ELSE се използва неявна ELSE NULL. Ако искате да обработвате случай UNKNOWN отделно, можете изрично да търсите NULL в операндите на предиката, като използвате оператора IS NULL.
Прост Изразът CASE използва имплицитни сравнения, базирани на равенство между изходния израз и сравняваните изрази:
CASE <изходен израз> WHEN <комп. израз 1> THEN <резултатен израз 1, когато TRUE> WHEN <израз за комп. 2> THEN <резултатен израз 2, когато TRUE> ... WHEN <комп. израз n> THEN <резултатен израз n когато TRUE> ELSEEND
Простият израз CASE е проектиран подобно на търсения израз CASE по отношение на обработката на тризначната логика, но тъй като сравненията използват имплицитно сравнение, базирано на равенство, не можете да обработвате случая UNKNOWN отделно. Опитът да се използва NULL в един от сравняваните изрази в клаузите WHEN е безсмислен, тъй като сравнението няма да доведе до TRUE, дори когато изходният израз е NULL. Помислете за следния пример:
ДЕКЛАРИРАНЕ @input КАТО INT =NULL; SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;
Това се преобразува имплицитно в следното:
ДЕКЛАРИРАНЕ @input КАТО INT =NULL; SELECT CASE WHEN @input =NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;
Следователно резултатът е:
Въвеждането не е NULLЗа да откриете NULL вход, трябва да използвате търсения синтаксис на израза CASE и оператора IS NULL, както следва:
ДЕКЛАРИРАНЕ @input КАТО INT =NULL; ИЗБЕРЕТЕ СЛУЧАЙ, КОГАТО @input Е NULL, ТОГАВА 'Входът е NULL' ELSE 'Input is not NULL' END;
Този път резултатът е:
Въвеждането е NULLСЛИВАНЕ
Инструкцията MERGE се използва за обединяване на данни от източник в цел. Използвате предикат за сливане, за да идентифицирате следните случаи и да приложите действие срещу целта:
- Изходен ред се съпоставя с целеви ред (активиран, когато се намери съвпадение за изходния ред, където предикатът за сливане е TRUE):приложете UPDATE или DELETE срещу целта
- Изходен ред не е съпоставен с целеви ред (активиран, когато не са намерени съвпадения за изходния ред, където предикатът за сливане е TRUE, а за всички предикатът е FALSE или UNKNOWN):приложете INSERT срещу целта
- Целевият ред не е съпоставен с изходен ред (активиран, когато не са намерени съвпадения за целевия ред, където предикатът за сливане е TRUE, а за всички предикатът е FALSE или UNKNOWN):приложете UPDATE или DELETE срещу целта
И трите сценария отделят TRUE за една група и FALSE или НЕИЗВЕСТНО за друга. Не получавате отделни секции за обработка на TRUE, обработка на FALSE и обработка на НЕИЗВЕСТНИ случаи.
За да демонстрирам това, ще използвам таблица, наречена T3, която създавате и попълвате, като стартирате следния код:
ИЗПУСКАНЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА dbo.T3;СЪЗДАВАЙТЕ ТАБЛИЦА dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1)); ВМЕСТЕ В dbo.T3(col1) СТОИНИ(1),(2),(NULL);
Помислете за следния оператор MERGE:
СЛИВАЙТЕ В dbo.T3 КАТО TGTUSING (СТОЙНОСТИ(1, 100), (3, 300)) КАТО SRC(col1, col2) НА SRC.col1 =TGT.col1 КОГАТО СЕ СЪВПАДА, ПОСЛЕ АКТУАЛИЗАЦИЯ SET TGT.col2 =SRC.col2WHEN НЕ СЪВПАДА ТОГАВА ВМЕСТЕ (col1, col2) STOS(SRC.col1, SRC.col2) КОГАТО НЕ СЕ СЪВПАДА ОТ ИЗТОЧНИКА, ТОГАВА АКТУАЛИЗАЦИЯ SET col2 =-1; ИЗБЕРЕТЕ col1, col2 ОТ dbo.T3;
Изходният ред, където col1 е 1, съответства на целевия ред, където col1 е 1 (предикатът е TRUE) и следователно col2 на целевия ред е зададен на 100.
Изходният ред, където col1 е 3, не се съпоставя с нито един целеви ред (за всички предикатът е FALSE или UNKNOWN) и следователно нов ред се вмъква в T3 с 3 като стойност на col1 и 300 като стойност на col2.
Целевите редове, където col1 е 2 и където col1 е NULL, не се съпоставят с нито един изходен ред (за всички редове предикатът е FALSE или UNKNOWN) и следователно и в двата случая col2 в целевите редове е зададен на -1.
Заявката към T3 връща следния изход след изпълнение на горния оператор MERGE:
col1 col2----------- -----------1 1002 -1NULL -13 300
Дръжте маса Т3 наоколо; използва се по-късно.
Различие и групиране
За разлика от сравненията, които се правят с помощта на оператори за равенство и неравенство, сравненията, направени за целите на отличителността и групирането, групират NULL заедно. Счита се, че едно NULL не се различава от друго NULL, но NULL се счита за различно от стойност, различна от NULL. Следователно, прилагането на клауза DISTINCT премахва дублиращи се появявания на NULL. Следната заявка демонстрира това:
ИЗБЕРЕТЕ РАЗЛИЧНА държава, регион ОТ HR.Employees;
Тази заявка генерира следния изход:
регион на страната--------------- ---------------UK NULLUSA WA
Има множество служители с държавата САЩ и региона NULL и след премахването на дубликатите резултатът показва само едно срещане на комбинацията.
Подобно на отличителността, групирането също групира NULL заедно, както демонстрира следната заявка:
ИЗБЕРЕТЕ държава, регион, COUNT(*) КАТО numempsFROM HR.EmployeesGROUP ПО държава, регион;
Тази заявка генерира следния изход:
country region numemps-------------- --------------- -----------UK NULL 4USA WA 5
Отново всичките четирима служители с държавата UK и регион NULL бяха групирани заедно.
Поръчка
Подреждането третира множество NULL числа като имащи една и съща стойност на подреждане. SQL стандартът оставя на реализацията да избере дали да поръча NULL първи или последни в сравнение със стойности, различни от NULL. Microsoft избра да счита NULL като имащи по-ниски стойности на подреждане в сравнение с не-NULL в SQL Server, така че когато използва посоката на възходящ ред, T-SQL подрежда първо NULL. Следната заявка демонстрира това:
ИЗБЕРЕТЕ идентификатор, име, часова ставкаFROM dbo.ContactsORDER ПО часова ставка;
Тази заявка генерира следния изход:
id name часова ставка----------- ---------- -----------3 C NULL5 E NULL1 A 100.004 D 150.002 B 200.007 G 300,00
Следващия месец ще добавя още по тази тема, обсъждайки стандартните елементи, които ви дават контрол върху поведението на подреждане NULL и заобикалянето на тези елементи в T-SQL.
Уникалност
Когато налага уникалност на колона с NULL, използвайки или ограничение UNIQUE, или уникален индекс, T-SQL третира NULL точно като стойности, различни от NULL. Той отхвърля дублиращи се NULL, сякаш едно NULL не е уникално от друго NULL.
Припомнете си, че нашата таблица T3 има УНИКАЛНО ограничение, дефинирано на col1. Ето нейното определение:
CONSTRAINT UNQ_T3 UNIQUE(col1)
Заявете T3, за да видите текущото му съдържание:
ИЗБЕРЕТЕ * ОТ dbo.T3;
Ако сте изпълнили всички модификации срещу T3 от по-ранните примери в тази статия, трябва да получите следния изход:
col1 col2----------- -----------1 1002 -1NULL -13 300
Опитайте се да добавите втори ред с NULL в col1:
ВЪВЕДЕТЕ В dbo.T3(col1, col2) СТОЙНОСТИ(NULL, 400);
Получавате следната грешка:
Съобщение 2627, ниво 14, състояние 1, ред 558Нарушение на ограничението UNIQUE KEY 'UNQ_T3'. Не може да се вмъкне дублиран ключ в обект 'dbo.T3'. Стойността на дублирания ключ е (
Това поведение всъщност е нестандартно. Следващия месец ще опиша стандартната спецификация и как да я емулирам в T-SQL.
Заключение
В тази втора част от поредицата за сложностите с NULL се фокусирах върху несъответствията в третирането с NULL между различните T-SQL елементи. Обхванах линейни спрямо агрегатни изчисления, клаузи за филтриране и съвпадение, ограничението CHECK срещу опцията CHECK, елементите IF, WHILE и CASE, оператора MERGE, отличителността и групирането, подреждането и уникалността. Несъответствията, които обхванах, допълнително подчертават колко важно е правилното разбиране на третирането на NULL в платформата, която използвате, за да сте сигурни, че пишете правилен и стабилен код. Следващия месец ще продължа поредицата, като обхвана стандартните SQL опции за лечение NULL, които не са налични в T-SQL, и ще осигуря заобиколни решения, които се поддържат в T-SQL.