Тази статия е единадесетата част от поредица за изрази на таблици. Досега обхванах производни таблици и CTE и наскоро започнах отразяването на изгледи. В част 9 сравних изгледи с извлечени таблици и CTE, а в част 10 обсъдих промените в DDL и последиците от използването на SELECT * във вътрешната заявка на изгледа. В тази статия се фокусирам върху съображенията за модификация.
Както вероятно знаете, ви е позволено да променяте данни в базовите таблици индиректно чрез изрази на именувани таблици, като изгледи. Можете да контролирате разрешенията за модификация спрямо изгледите. Всъщност можете да предоставите на потребителите разрешения да променят данни чрез изгледи, без да им давате разрешения да променят директно основните таблици.
Трябва да сте наясно с определени сложности и ограничения, които се прилагат за модификации чрез изгледи. Интересното е, че някои от поддържаните модификации могат да доведат до изненадващи резултати, особено ако потребителят, който променя данните, не знае, че взаимодейства с изглед. Можете да наложите допълнителни ограничения за модификации чрез изгледи, като използвате опция, наречена ПРОВЕРКА ОПЦИЯ, която ще разгледам в тази статия. Като част от покритието ще опиша едно любопитно несъответствие между това как ОПЦИЯТА CHECK в изглед и ограничението CHECK в таблица обработват модификации – по-специално такива, включващи NULL.
Примерни данни
Като примерни данни за тази статия ще използвам таблици, наречени Orders и OrderDetails. Използвайте следния код, за да създадете тези таблици в tempdb и да ги попълните с някои първоначални примерни данни:
ИЗПОЛЗВАЙТЕ tempdb;GO DROP TABLE АКО СЪЩЕСТВУВА dbo.OrderDetails, dbo.Orders;GO CREATE TABLE dbo.Orders( orderid INT NOT NULL CONSTRAINT PK_Orders PRIMARY KEY, orderdate DATE NOT NULL, shippeddate); INSERT INTO dbo.Orders(ordeid, orderdate, shippeddate) VALUES(1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '20210804'), (3, '20210806' 4, '20210826', NULL), (5, '20210827', NULL); CREATE TABLE dbo.OrderDetails( orderid INT NOT NULL CONSTRAINT FK_OrderDetails_Orders REFERENCES dbo.Orders, productid INT NOT NULL, qty INT NOT NULL, единична цена NUMERIC(12, 2) NOT NULL_DETAIL, отстъпка NULL, NOT NULL, отстъпка NOT NULL, отстъпка КЛЮЧ (поръчка, идентификатор на продукта)); INSERT INTO dbo.OrderDetails(orderid, productid, qty, единична цена, отстъпка) VALUES(1, 1001, 5, 10.50, 0.05), (1, 1004, 2, 20.00, 0.00), (2, 1052, 19, 10 0,10), (3, 1001, 1, 10,50, 0,05), (3, 1003, 2, 54,99, 0,10), (4, 1001, 2, 10,50, 0,05), (4, 1004, 30, 20. , (4, 1005, 1, 30.10, 0.05), (5, 1003, 5, 54.99, 0.00), (5, 1006, 2, 12.30, 0.08);
Таблицата Orders съдържа заглавки на поръчки, а таблицата OrderDetails съдържа редове за поръчки. Неизпратените поръчки имат NULL в колоната за дата на доставка. Ако предпочитате дизайн, който не използва NULL, можете да използвате конкретна бъдеща дата за неизпратени поръчки, като например „99991231.“
ПРОВЕТЕ ОПЦИЯТА
За да разберем обстоятелствата, при които бихте искали да използвате ОПЦИЯТА ПРОВЕРКА като част от дефиницията на изглед, първо ще разгледаме какво може да се случи, когато не я използвате.
Следният код създава изглед, наречен FastOrders, представляващ поръчки, изпратени в рамките на седем дни след поставянето им:
СЪЗДАДЕТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.FastOrdersAS ИЗБЕРЕТЕ orderid, orderdate, shippeddate ОТ dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <=7;GO
Използвайте следния код, за да вмъкнете в изгледа поръчка, изпратена два дни след поставянето:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
Запитване за изгледа:
ИЗБЕРЕТЕ * ОТ dbo.FastOrders;
Получавате следния изход, който включва новата поръчка:
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-066 2021-08-05 2021-08-07
Направете заявка към основната таблица:
ИЗБЕРЕТЕ * ОТ dbo.Orders;
Получавате следния изход, който включва новата поръчка:
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-07
Редът беше вмъкнат в основната таблица през изгледа.
След това вмъкнете в изгледа ред, изпратен 10 дни след поставянето, което противоречи на вътрешния филтър за заявки на изгледа:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Изявлението завършва успешно, като отчита един засегнат ред.
Запитване за изгледа:
ИЗБЕРЕТЕ * ОТ dbo.FastOrders;
Получавате следния изход, който изключва новата поръчка:
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-066 2021-08-05 2021-08-07
Ако знаете, че FastOrds е изглед, всичко това може да изглежда разумно. В крайна сметка редът е вмъкнат в основната таблица и не отговаря на вътрешния филтър за заявки на изгледа. Но ако не сте наясно, че FastOrders е изглед, а не базова таблица, това поведение ще изглежда изненадващо.
Направете заявка към основната таблица с поръчки:
ИЗБЕРЕТЕ * ОТ dbo.Orders;
Получавате следния изход, който включва новата поръчка:
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-08-07-08-20-20 15
Можете да изпитате подобно изненадващо поведение, ако актуализирате през изгледа стойността на shippeddate в ред, който в момента е част от изгледа, до дата, която го прави повече да не се квалифицира като част от изгледа. Такава актуализация обикновено е разрешена, но отново се извършва в основната базова таблица. Ако направите заявка за изгледа след такава актуализация, модифицираният ред изглежда изчезна. На практика все още е там в основната таблица, просто вече не се счита за част от изгледа.
Изпълнете следния код, за да изтриете редовете, които сте добавили по-рано:
ИЗТРИВАНЕ ОТ dbo.Orders WHERE orderid>=6;
Ако искате да предотвратите модификации, които са в конфликт с вътрешния филтър за заявка на изгледа, добавете WITH CHECK OPTION в края на вътрешната заявка като част от дефиницията на изгледа, както следва:
СЪЗДАДЕТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.FastOrdersAS SELECT orderid, orderdate, shippeddate ОТ dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <=7 С ПРОВЕРКА ОПЦИЯ;GO
Вмъкванията и актуализациите през изгледа са разрешени, стига да отговарят на филтъра на вътрешната заявка. В противен случай те се отхвърлят.
Например, използвайте следния код, за да вмъкнете в изгледа ред, който не е в конфликт с вътрешния филтър на заявката:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
Редът е добавен успешно.
Опитайте се да вмъкнете ред, който е в конфликт с филтъра:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Този път редът е отхвърлен със следната грешка:
Ниво 16, състояние 1, ред 135Опитът за вмъкване или актуализиране е неуспешен, тъй като целевият изглед или посочва WITH CHECK OPTION или обхваща изглед, който посочва WITH CHECK OPTION и един или повече редове, получени в резултат на операцията, не отговарят на изискванията по Ограничение CHECK OPTION.
NULL несъответствия
Ако работите с T-SQL от известно време, вероятно сте добре запознати с гореспоменатите сложности на модификацията и функцията CHECK OPTION, която служи. Често дори опитни хора намират обработката на NULL на CHECK OPTION за изненадваща. В продължение на години мислех за CHECK OPTION като изглед, който изпълнява същата функция като ограничение CHECK в дефиницията на основна таблица. Също така описвах тази опция, когато пишех или преподавах за нея. Всъщност, стига да няма NULL, включени във филтърния предикат, е удобно да мислим за двете в сходни термини. Те се държат последователно в такъв случай – приемат редове, които са съгласни с предиката, и отхвърлят тези, които са в конфликт с него. Двете обаче обработват NULL непоследователно.
Когато използвате ОПЦИЯТА ПРОВЕРКА, модификацията е разрешена през изгледа, докато предикатът се оценява на истина, в противен случай се отхвърля. Това означава, че се отхвърля, когато предикатът на изгледа се оценява на false или неизвестен (когато е включено NULL). С ограничение CHECK модификацията е разрешена, когато предикатът на ограничението се оценява на вярно или неизвестно, и се отхвърля, когато предикатът се оценява на false. Това е интересна разлика! Първо, нека видим това в действие, след това ще се опитаме да разберем логиката зад това непоследователност.
Опит за вмъкване в изгледа на ред с NULL дата на доставка:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
Предикатът на изгледа се оценява като неизвестен и редът се отхвърля със следната грешка:
Съобщение 550, ниво 16, състояние 1, ред 147Опитът за вмъкване или актуализиране е неуспешен, тъй като целевият изглед или посочва WITH CHECK OPTION или обхваща изглед, който посочва WITH CHECK OPTION и един или повече редове, получени в резултат на операцията, не са били отговарят на ограничението CHECK OPTION.
Нека опитаме подобно вмъкване срещу основна таблица с ограничение CHECK. Използвайте следния код, за да добавите такова ограничение към дефиницията на таблицата на нашата поръчка:
ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(ден, дата на поръчка, дата на доставка) <=7);
Първо, за да сте сигурни, че ограничението работи, когато няма включени NULL, опитайте да вмъкнете следната поръчка с дата на доставка 10 дни след датата на поръчката:
INSERT INTO dbo.Orders(ordeid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Този опит за вмъкване е отхвърлен със следната грешка:
Съобщение 547, ниво 16, състояние 0, ред 159Изразът INSERT е в конфликт с ограничението CHECK "CHK_Orders_FastOrder". Конфликтът е възникнал в база данни "tempdb", таблица "dbo.Orders".
Използвайте следния код, за да вмъкнете ред с NULL дата на доставка:
INSERT INTO dbo.Orders(ordeid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
Предполага се, че ограничението CHECK отхвърля фалшиви случаи, но в нашия случай предикатът се оценява на неизвестно, така че редът се добавя успешно.
Направете заявка в таблицата за поръчки:
ИЗБЕРЕТЕ * ОТ dbo.Orders;
Можете да видите новата поръчка в изхода:
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-078 NULL-2021-08-01-080>Каква е логиката зад това несъответствие? Можете да спорите, че ограничение CHECK трябва да се прилага само когато предикатът на ограничението е явно нарушен, което означава, когато се оценява като false. По този начин, ако изберете да разрешите NULL във въпросната колона, редове с NULL в колоната са разрешени, въпреки че предикатът на ограничението се оценява на неизвестно. В нашия случай ние представяме неизпратени поръчки с NULL в колоната shippeddate и разрешаваме неизпратени поръчки в таблицата, като същевременно налагаме правилото за „бързи поръчки“ само за изпратени поръчки.
Аргументът за използване на различна логика с изглед е, че модификацията трябва да бъде разрешена през изгледа само ако редът с резултат е валидна част от изгледа. Ако предикатът на изгледа се оцени като неизвестен, например, когато датата на доставка е NULL, редът с резултат не е валидна част от изгледа, следователно се отхвърля. Само редове, за които предикатът се оценява на true, са валидна част от изгледа и следователно са разрешени.
NULL добавят много сложност към езика. Харесвате или не, ако вашите данни ги поддържат, искате да сте сигурни, че разбирате как T-SQL ги обработва.
В този момент можете да премахнете ограничението CHECK от таблицата Orders и също така да премахнете изгледа FastOrders за почистване:
ПРОМЕНЯ ТАБЛИЦА dbo.Orders ОТПУСКАНЕ ОГРАНИЧЕНИЕ CHK_Orders_FastOrder;ПРОСТЪПНЕТЕ ИЗГЛЕД, АКО СЪЩЕСТВУВА dbo.FastOrders;Ограничение НАГОРЕ/ОТМЕСТВАНЕ-ИЗВЪРХВАНЕ
Обикновено се допускат модификации чрез изгледи, включващи филтрите TOP и OFFSET-FETCH. Въпреки това, подобно на нашата по-ранна дискусия за изгледите, дефинирани без ОПЦИЯТА ПРОВЕРКА, резултатът от такава модификация може да изглежда странен за потребителя, ако не знае, че взаимодейства с изглед.
Помислете за следния изглед, представящ скорошни поръчки като пример:
СЪЗДАВАЙТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.RecentOrdersAS ИЗБЕРЕТЕ ТОП (5) orderid, orderdate, shippeddate ОТ dbo.Orders ORDER BY orderdate DESC, orderid DESC;GOИзползвайте следния код, за да вмъкнете през изгледа RecentOrders шест поръчки:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(9, '20210801', '20210803'), (10, '20210802', '20210804'), (11, '2020'201) ), (12, '20210830', '20210902'), (13, '20210830', '20210903'), (14, '20210831', '20210903');Запитване за изгледа:
ИЗБЕРЕТЕ * ОТ dbo.RecentOrders;Получавате следния изход:
поръчка дата дата на доставка----- ---------- -----------14 2021-08-31 2021-09-0313 2021 -08-30 2021-09-0312 2021-08-30 2021-09-0211 2021-08-29 2021-08-318 2021-08-28 NULLОт шестте вмъкнати поръчки само четири са част от изгледа. Това изглежда напълно разумно, ако сте наясно, че отправяте заявка към изглед, който се основава на заявка с ТОП филтър. Но може да изглежда странно, ако си мислите, че правите заявка към основна таблица.
Направете заявка директно към основната таблица с поръчки:
ИЗБЕРЕТЕ * ОТ dbo.Orders;Получавате следния изход, показващ всички добавени поръчки:
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-01-078 NULL-02-2021 -01 2021-08-0310 2021-08-02 2021-08-0411 2021-08-29 2021-08-3112 2021-08-30 2021-09-0213 2021-2013-01-04-02-04 -31 2021-09-03Ако добавите CHECK OPTION към дефиницията на изгледа, операторите INSERT и UPDATE срещу изгледа ще бъдат отхвърлени. Използвайте следния код, за да приложите тази промяна:
СЪЗДАВАЙТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.RecentOrdersAS ИЗБЕРЕТЕ ТОП (5) orderid, orderdate, shippeddate ОТ dbo.Orders ORDER BY orderdate DESC, orderid DESC С ОПЦИЯ ПРОВЕРКА;GOОпитайте да добавите поръчка през изгледа:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210801', '20210805');Получавате следната грешка:
Съобщение 4427, ниво 16, състояние 1, ред 247
Не може да се актуализира изгледът "dbo.RecentOrders", защото той или изглед, който препраща, е създаден с WITH CHECK OPTION и дефиницията му съдържа клауза TOP или OFFSET.SQL Server не се опитва да бъде твърде умен тук. Той ще отхвърли промяната, дори ако редът, който се опитвате да вмъкнете, стане валидна част от изгледа в този момент. Например, опитайте се да добавите поръчка с по-нова дата, която ще попадне в първите 5 в този момент:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210904', '20210906');Опитът за вмъкване все още се отхвърля със следната грешка:
Съобщение 4427, ниво 16, състояние 1, ред 254
Не може да се актуализира изгледът "dbo.RecentOrders", защото той или изглед, който препраща, е създаден с WITH CHECK OPTION и дефиницията му съдържа клауза TOP или OFFSET.Опитайте да актуализирате ред през изгледа:
АКТУАЛИЗИРАНЕ dbo.RecentOrders SET shippeddate =DATEADD(ден, 2, дата на поръчка);В този случай опитът за промяна също се отхвърля със следната грешка:
Съобщение 4427, ниво 16, състояние 1, ред 260
Не може да се актуализира изгледът "dbo.RecentOrders", защото той или изглед, който препраща, е създаден с WITH CHECK OPTION и дефиницията му съдържа клауза TOP или OFFSET.Имайте предвид, че дефинирането на изглед въз основа на заявка с TOP или OFFSET-FETCH и CHECK OPTION ще доведе до липса на поддръжка за изрази INSERT и UPDATE през изгледа.
Поддържат се изтривания чрез такъв изглед. Изпълнете следния код, за да изтриете всички текущи пет най-нови поръчки:
ИЗТРИВАНЕ ОТ dbo.RecentOrders;Командата завършва успешно.
Запитване в таблицата:
ИЗБЕРЕТЕ * ОТ dbo.Orders;Получавате следния изход след изтриване на поръчките с идентификационни номера 8, 11, 12, 13 и 14.
поръчка дата дата на доставка----- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-08-01-20-08-20 0310 2021-08-02 2021-08-04В този момент изпълнете следния код за почистване, преди да стартирате примерите в следващия раздел:
ИЗТРИВАНЕ ОТ dbo.Orders WHERE orderid> 5; ИЗПУСКАНЕ НА ИЗГЛЕД, АКО СЪЩЕСТВУВА dbo.RecentOrders;Присъединява се
Поддържа се актуализиране на изглед, който обединява множество таблици, стига само една от основните базови таблици да е засегната от промяната.
Помислете за пример за следния изглед, обединяващ Поръчки и Подробности за поръчка:
СЪЗДАДЕТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.OrdersOrderDetailsAS SELECT O.orderid, O.orderdate, O.shippeddate, OD.productid, OD.qty, OD.unitprice, OD.discount ОТ dbo.Orders КАТО O INNER JOIN AS Dbo.OrderDetails OD ON O.orderid =OD.orderid;GOОпитайте се да вмъкнете ред през изгледа, така че и двете основни базови таблици ще бъдат засегнати:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate, productid, qty, unitprice, discount) VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);Получавате следната грешка:
Съобщение 4405, ниво 16, състояние 1, ред 306
Изглед или функция 'dbo.OrdersOrderDetails' не може да се актуализира, тъй като модификацията засяга множество базови таблици.Опитайте се да вмъкнете ред през изгледа, така че ще бъде засегната само таблицата Поръчки:
INSERT INTO dbo.OrdersOrderDetails(ordeid, orderdate, shippeddate) VALUES(6, '20210828', NULL);Тази команда завършва успешно и редът се вмъква в основната таблица с поръчки.
Но какво ще стане, ако искате също да можете да вмъкнете ред през изгледа в таблицата OrderDetails? С текущата дефиниция на изглед, това е невъзможно (вместо тригерите настрана), тъй като изгледът връща колоната orderid от таблицата Orders, а не от таблицата OrderDetails. Достатъчно е, че една колона от таблицата OrderDetails, която по някакъв начин не може да получи стойността си автоматично, не е част от изгледа, за да предотврати вмъкванията в OrderDetails през изгледа. Разбира се, винаги можете да решите, че изгледът ще включва както orderid от Orders, така и orderid от OrderDetails. В такъв случай ще трябва да присвоите двете колони с различни псевдоними, тъй като заглавието на таблицата, представена от изгледа, трябва да има уникални имена на колони.
Използвайте следния код, за да промените дефиницията на изгледа, за да включите и двете колони, като поставите псевдоним на тази от Orders като O_orderid и тази от OrderDetails като OD_orderid:
СЪЗДАВАЙТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕД AS O INNER JOIN dbo.OrderDetails КАТО OD ON O.orderid =OD.orderid;GOСега можете да вмъквате редове през изгледа или в Orders, или в OrderDetails, в зависимост от това от коя таблица идва списъкът с целеви колони. Ето пример за вмъкване на няколко реда за поръчка, свързани с поръчка 6 през изгледа в OrderDetails:
INSERT INTO dbo.OrdersOrderDetails(OD_orderid, productid, qty, unitprice, discount) VALUES(6, 1001, 5, 10.50, 0.05), (6, 1002, 5, 20.00, 0.05);Редовете са добавени успешно.
Запитване за изгледа:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Получавате следния изход:
O_orderid orderdate shippeddate OD_orderid productid qty единична цена отстъпка----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-08-28 NULL 6 1001 5 10,50 0,05006 28.08.2021 NULL 6 1002 5 20,00 0,0500Подобно ограничение е приложимо за изрази UPDATE през изгледа. Актуализациите са разрешени, стига да е засегната само една базова таблица. Но ви е позволено да препращате към колони от двете страни в изявлението, стига само едната страна да бъде променена.
Като пример, следният оператор UPDATE през изгледа задава датата на поръчката на реда, където идентификационният номер на поръчката на реда за поръчка е 6 и идентификационният номер на продукта е 1001 до „20210901:“
АКТУАЛИЗИРАНЕ dbo.OrdersOrderDetails SET orderdate ='20210901' КЪДЕТО OD_orderid =6 И productid =1001;Ще наречем това изявление Актуализирано изявление 1.
Актуализацията завършва успешно със следното съобщение:
(1 ред засегнат)Това, което е важно да се отбележи тук, е филтрите на изразите по елементи от таблицата OrderDetails, но модифицираната дата на поръчка на колона е от таблицата Orders. И така, в плана, който SQL Server изгражда за този израз, той трябва да разбере кои поръчки трябва да бъдат променени в таблицата Orders. Планът за това изявление е показан на Фигура 1.
Фигура 1:План за изявление за актуализиране 1
Можете да видите как започва планът, като филтрирате страната на OrderDetails по orderid =6 и productid =1001, както и на OrderDetails от страната на orderid =6, съединявайки двете. Резултатът е само един ред. Единствената уместна част, която трябва да се запази от тази дейност, е кои идентификатори на поръчки в таблицата Поръчки представляват редове, които трябва да бъдат актуализирани. В нашия случай това е поръчката с идентификатор на поръчка 6. Освен това операторът Compute Scalar подготвя член, наречен Expr1002 със стойността, която операторът ще присвои на колоната за дата на поръчка на целевата поръчка. Последната част от плана с оператора Clustered Index Update прилага действителната актуализация към реда в Поръчки с идентификатор на поръчка 6, като задава стойността му за дата на поръчка на Expr1002.
Ключовият момент, който трябва да се подчертае тук, е актуализиран само един ред с идентификатор на поръчка 6 в таблицата с поръчките. И все пак този ред има две съвпадения в резултата от обединяването с таблицата OrderDetails – едно с идентификатор на продукт 1001 (който оригиналната актуализация филтрира) и друго с идентификатор на продукт 1002 (който оригиналната актуализация не филтрира). Заявете изгледа в този момент, като филтрирате всички редове с идентификатор на поръчка 6:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Получавате следния изход:
O_orderid orderdate shippeddate OD_orderid productid qty единична цена отстъпка----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-09-01 NULL 6 1001 5 10,50 0,05006 01.09.2021 NULL 6 1002 5 20,00 0,0500И двата реда показват новата дата на поръчка, въпреки че оригиналната актуализация филтрира само реда с идентификатор на продукт 1001. Отново, това трябва да изглежда напълно разумно, ако знаете, че взаимодействате с изглед, който съединява две основни таблици под кориците, но може да изглежда много странно, ако не осъзнавате това.
Любопитното е, че SQL Server дори поддържа недетерминистични актуализации, при които множество изходни редове (от OrderDetails в нашия случай) съвпадат с един целеви ред (в Orders в нашия случай). Теоретично един от начините за справяне с такъв случай би бил да се отхвърли. Всъщност, с оператор MERGE, където множество изходни редове съвпадат с един целеви ред, SQL Server отхвърля опита. Но не и с UPDATE, базирано на присъединяване, независимо дали пряко или косвено чрез израз на именувана таблица като изглед. SQL Server просто го обработва като недетерминирана актуализация.
Помислете за следния пример, който ще наричаме изявление 2:
АКТУАЛИЗИРАНЕ dbo.OrdersOrderDetails SET orderdate =CASE WHEN unitprice>=20,00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid =6;Надявам се, че ще ми простите, че това е измислен пример, но илюстрира смисъла.
В изгледа има два квалифицирани реда, представляващи два квалифицирани реда за изходна поръчка от основната таблица OrderDetails. Но има само един квалифициран целеви ред в основната таблица с поръчки. Освен това, в един изходен ред OrderDetails, присвоеният израз CASE връща една стойност ('20210902'), а в другия изходен ред OrderDetails връща друга стойност ('20210903'). Какво трябва да направи SQL Server в този случай? Както споменахме, подобна ситуация с оператора MERGE би довела до грешка, отхвърляща опитаната промяна. И все пак с изявление UPDATE SQL Server просто хвърля монета. Технически това се прави с помощта на вътрешна агрегатна функция, наречена ANY.
И така, нашата актуализация завършва успешно, съобщавайки, че е засегнат 1 ред. Планът за това изявление е показан на Фигура 2.
Фигура 2:План за изявление за актуализиране 2В резултата от обединяването има два реда. Тези два реда стават изходните редове за актуализацията. Но след това агрегатен оператор, прилагащ функцията ANY, избира една (всякаква) стойност на поръчката и една (всякаква) стойност на единична цена от тези изходни редове. И двата изходни реда имат една и съща стойност на orderid, така че правилният ред ще бъде променен. Но в зависимост от това коя от изходните стойности на единична цена ВСЯКАКъв агрегат в крайна сметка ще избере, това ще определи коя стойност ще върне изразът CASE, за да се използва като актуализирана стойност на датата на поръчка в целевата поръчка. Със сигурност можете да видите аргумент срещу поддържането на такава актуализация, но тя се поддържа напълно в SQL Server.
Нека да запитаме изгледа, за да видим резултата от тази промяна (сега е моментът да направите своя залог относно резултата):
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Получих следния изход:
O_orderid orderdate shippeddate OD_orderid productid qty единична цена отстъпка----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-09-03 NULL 6 1001 5 10,50 0,05006 03.09.2021 NULL 6 1002 5 20,00 0,0500Само една от двете изходни стойности на единична цена беше избрана и използвана за определяне на датата на поръчка на единичната целева поръчка, но при запитване на изгледа стойността на датата на поръчката се повтаря и за двата съответстващи реда на поръчка. Както можете да разберете, резултатът можеше също да бъде другата дата (2021-09-02), тъй като изборът на стойността на единичната цена беше недетерминиран. Страхотни неща!
Така че, при определени условия, операторите INSERT и UPDATE са разрешени чрез изгледи, които се свързват с множество основни таблици. Изтривания обаче не са разрешени срещу такива изгледи. Как може SQL Server да разбере коя от страните трябва да бъде целта за изтриването?
Ето опит за прилагане на такова изтриване чрез изгледа:
ИЗТРИВАНЕ ОТ dbo.OrdersOrderDetails WHERE O_orderid =6;Този опит е отхвърлен със следната грешка:
Съобщение 4405, ниво 16, състояние 1, ред 377
Изглед или функция 'dbo.OrdersOrderDetails' не може да се актуализира, тъй като модификацията засяга множество базови таблици.В този момент изпълнете следния код за почистване:
ИЗТРИВАНЕ ОТ dbo.OrderDetails WHERE orderid =6;DELETE FROM dbo.Orders WHERE orderid =6;ПРОСТЪПНЕТЕ ИЗГЛЕД, АКО СЪЩЕСТВУВА dbo.OrdersOrderDetails;Извлечени колони
Друго ограничение за модификации чрез изгледи е свързано с извлечените колони. Ако колона за изглед е резултат от изчисление, SQL Server няма да се опитва да промени формулата си, когато се опитате да вмъкнете или актуализирате данни през изгледа — по-скоро ще отхвърли такива модификации.
Разгледайте следния изглед като пример:
СЪЗДАДЕТЕ ИЛИ ПРОМЕНЯТЕ ИЗГЛЕЖДАНЕ dbo.OrderDetailsNetPriceAS ИЗБЕРЕТЕ orderid, productid, qty, unitprice * (1.0 - отстъпка) КАТО netunitprice, отстъпка ОТ dbo.OrderDetails;GOИзгледът изчислява колоната netunitprice въз основа на основните колони в таблицата OrderDetails единична цена и отстъпка.
Запитване за изгледа:
ИЗБЕРЕТЕ * ОТ dbo.OrderDetailsNetPrice;Получавате следния изход:
поръчате идентификатор на продукта qty netunitцена отстъпка----------- ----------- ----------- --------- ---- ---------1 1001 5 9.975000 0.05001 1004 2 20.000000 0.00002 1003 1 47.691000 0.10003 1001 1 9.975000 0.05003 1003 2 49.491000 0.10004 1001 2 9.975000 0.05004 1004 1 20.300000 0.00004 1005 1 28.595000 0.05005 1003 5 54.990000 0.00005 1006 2 11,316000 0,0800Опитайте се да вмъкнете ред през изгледа:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, отстъпка) VALUES(1, 1005, 1, 28.595, 0.05);Теоретично можете да разберете кой ред трябва да бъде вмъкнат в основната таблица OrderDetails чрез обратен инженеринг на стойността на единичната цена на базовата таблица от стойностите na netunitprice и отстъпката на изгледа. SQL Server не прави опит за подобно обратно инженерство, но отхвърля опита за вмъкване със следната грешка:
Съобщение 4406, ниво 16, състояние 1, ред 412
Актуализиране или вмъкване на изглед или функция „dbo.OrderDetailsNetPrice“ не бе успешно, защото съдържа извлечено или константно поле.Опитайте се да пропуснете изчислената колона от вмъкването:
INSERT INTO dbo.OrderDetailsNetPrice(ordeid, productid, qty, discount) VALUES(1, 1005, 1, 0.05);Сега се връщаме към изискването всички колони от основната таблица, които по някакъв начин не получават своите стойности автоматично, трябва да бъдат част от вмъкването и тук липсва колоната с единична цена. Това вмъкване е неуспешно със следната грешка:
Съобщение 515, ниво 16, състояние 2, ред 421
Не може да се вмъкне стойността NULL в колона 'unitprice', таблица 'tempdb.dbo.OrderDetails'; колоната не позволява нулеви стойности. INSERT е неуспешен.If you want to support insertions through the view, you basically have two options. One is to include the unitprice column in the view definition. Another is to create an instead of trigger on the view where you handle the reverse engineering logic yourself.
At this point, run the following code for cleanup:
DROP VIEW IF EXISTS dbo.OrderDetailsNetPrice;Set Operators
As mentioned in the last section, you’re not allowed to modify a column in a view if the column is a result of a computation. The columns modified in the view using INSERT and UPDATE statements have to map directly to the underlying base table’s columns with no manipulation. In the list of restrictions to modifications through views, T-SQL’s documentation specifies that columns formed by using the set operators UNION, UNION ALL, EXCEPT, and INTERSECT amount to a computation and therefore are also not updatable.
One exception to this restriction is when using the UNION ALL operator to combine rows from different tables to form an updatable partitioned view. That’s a big topic in its own right. I’ll cover it briefly here to give you a sense, and you can investigate it further if you like in the product’s documentation.
Partitioned views predates table and index partitioning in SQL Server. The basic idea is that you can store disjoint subsets of rows in different base tables and have a view that unifies the rows from the different tables using a UNION ALL operator. If certain requirements are met, you can not only read the data through the view but also modify it through the view. SQL Server will figure out how to direct the modifications through the view to the right underlying tables.
The requirements for supporting modifications through such a view include having a partitioning column. Each of the underlying tables needs to have a CHECK constraint based on the partitioning column that defines a disjoint subset of rows. Also, the partitioning column needs to be part of the table’s primary key, meaning it cannot allow NULLs.
Consider the Orders table you used earlier in this article. Suppose that instead of holding all orders in one table, you want to store unshipped orders in one table (called UnshippedOrders) and shipped orders in another table (called ShippedOrders). You also want to create a view called Orders combining the rows from both tables. You want the view to be updatable.
Let’s start by removing any existing objects before creating the new ones:
DROP VIEW IF EXISTS dbo.Orders;DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders;DROP TABLE IF EXISTS dbo.ShippedOrders, dbo.UnshippedOrders;The partitioning column in our example is the shippeddate column. Our first obstacle is that we want to represent unshipped orders with a NULL shippeddate, but the partitioning column cannot allow NULLs. One possible workaround is to decide on some specific future date to represent unshipped orders. For example, the maximum supported date December 31st, 9999. Then you could have a CHECK constraint in the UnshippedOrders table checking that the shipped date is this specific one, and a CHECK constraint in the ShippedOrders table checking that the shipped date is before this one. This will meet the requirement for disjoint sets of rows.
Another obstacle is that the partitioning column needs to be part of the primary key. Originally the primary key was based on the orderid column alone. Now it will need to be extended to be based on (orderid, shippeddate). You will probably still want to enforce uniqueness based on orderid alone. To achieve this, you’ll need to add a unique constraint based on orderid.
With all this in mind, here are the definitions of the ShippedOrders and UnshippedOrders tables:
CREATE TABLE dbo.ShippedOrders( orderid INT NOT NULL, orderdate DATE NOT NULL, shippeddate DATE NOT NULL, CONSTRAINT PK_ShippedOrders PRIMARY KEY(orderid, shippeddate), CONSTRAINT UNQ_ShippedOrders_orderid UNIQUE(orderid), CONSTRAINT CHK_ShippedOrders_shippeddate CHECK(shippeddate <'99991231')); CREATE TABLE dbo.UnshippedOrders( orderid INT NOT NULL, orderdate DATE NOT NULL, shippeddate DATE NOT NULL DEFAULT('99991231'), CONSTRAINT PK_UnshippedOrders PRIMARY KEY(orderid, shippeddate), CONSTRAINT UNQ_UnshippedOrders_orderid UNIQUE(orderid), CONSTRAINT CHK_UnshippedOrders_shippeddate CHECK(shippeddate ='99991231'));You then create the Orders view, unifying the rows from the two tables using the UNION ALL operator, like so:
CREATE OR ALTER VIEW dbo.OrdersAS SELECT orderid, orderdate, shippeddate FROM dbo.ShippedOrders UNION ALL SELECT orderid, orderdate, shippeddate FROM dbo.UnshippedOrders;GOSince this view meets all requirements for updatability, you can insert, update, and delete rows through the view. SQL Server will direct the changes to the right underlying tables. As an example, the following statement inserts a few rows, including both shipped and unshipped orders:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '20210804', '20210806'), (4, '20210826', '99991231'), (5, '20210827', '99991231');The plan for this code is shown in Figure 3.
Figure 3:Plan for INSERT statement against partitioned view
As you can see, a Compute Scalar operator computes for each source row a member called Ptn1018. This member is set to 0 for shipped orders (shippeddate <'9999-12-31') and 1 for unshipped orders (shippeddate ='9999-12-31'). The rows are spooled along with the member Ptn1018, and then the spool is read twice. Once filtering the rows where Ptn1018 =0, inserting those into the underlying ShippedOrders table, and another time filtering the rows where Ptn1018 =1, inserting those into the underlying UnshippedOrders table.If this seems like an attractive option, consider it very carefully. Remember this is an old feature, predating table and index partitioning. There are many requirements, restrictions, and complications, including optimization complications, integrity enforcement complications, and others. As mentioned, here I just wanted to cover it briefly to describe the exception to the modification restriction involving set operators.When you’re done, run the following code for cleanup:
DROP VIEW IF EXISTS dbo.Orders;DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders;DROP TABLE IF EXISTS dbo.ShippedOrders, dbo.UnshippedOrders;Резюме
When I started the coverage of views, one of the first things I explained was that a view is a table. You can read data from a view and you can modify data through a view. But you need to understand that modifications through the view are restricted in a few ways, and the outcome of such modifications could be surprising in some cases.
Using the CHECK OPTION, you’re only allowed to update and insert rows through the view as long as the result rows are considered a valid part of the view. This means unlike a CHECK constraint in a table, the CHECK OPTION rejects changes where the inner query’s filter evaluates to unknown (when a NULL is involved). You’re not allowed to insert or update rows through a view if it’s defined with the CHECK OPTION and uses the TOP or OFFSET-FETCH filters. But you’re allowed to delete rows through such a view.
If a view joins multiple base tables, inserts and updates through the view are allowed provided that only one underlying base table is affected. Oddly, if a modification of a single target row involves multiple related source rows, the modification is allowed but is processed as a nondeterministic one. In such a case, SQL Server uses the internal ANY aggregate the pick a single value from the source rows.
You cannot update or insert rows through a view where at least one of the updated columns is a derived one resulting from a computation. The same applies when using a set operator, with an exception when using the UNION ALL operator to create an updatable partitioned view.