Ten artykuł jest jedenastą częścią serii dotyczącej wyrażeń tabelowych. Do tej pory omówiłem tabele pochodne i CTE, a ostatnio zacząłem opisywać widoki. W części 9 porównałem widoki z tabelami pochodnymi i CTE, a w części 10 omówiłem zmiany DDL i implikacje użycia SELECT * w wewnętrznym zapytaniu widoku. W tym artykule skupiam się na rozważaniach dotyczących modyfikacji.
Jak zapewne wiesz, możesz modyfikować dane w tabelach podstawowych pośrednio za pomocą nazwanych wyrażeń tabel, takich jak widoki. Możesz kontrolować uprawnienia do modyfikacji widoków. W rzeczywistości możesz przyznać użytkownikom uprawnienia do modyfikowania danych za pomocą widoków bez przyznawania im uprawnień do bezpośredniego modyfikowania bazowych tabel.
Musisz zdawać sobie sprawę z pewnych zawiłości i ograniczeń, które dotyczą modyfikacji za pomocą widoków. Co ciekawe, niektóre z obsługiwanych modyfikacji mogą zakończyć się zaskakującymi wynikami, zwłaszcza jeśli użytkownik modyfikujący dane nie jest świadomy, że wchodzi w interakcję z widokiem. Możesz nałożyć dalsze ograniczenia na modyfikacje za pomocą widoków, korzystając z opcji o nazwie SPRAWDŹ OPCJE, którą omówię w tym artykule. W ramach tego omówienia opiszę ciekawą niespójność między sposobem, w jaki opcja CHECK OPTION w widoku i ograniczenie CHECK w tabeli obsługują modyfikacje — w szczególności te dotyczące wartości NULL.
Przykładowe dane
Jako przykładowe dane do tego artykułu użyję tabel o nazwie Orders i OrderDetails. Użyj następującego kodu, aby utworzyć te tabele w tempdb i wypełnić je początkowymi przykładowymi danymi:
USE tempdb;GO DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders;GO CREATE TABLE dbo.Orders( id zamówienia INT NOT NULL CONSTRAINT PK_Orders PRIMARY KEY, data zamówienia DATE NOT NULL, data wysyłki DATE NULL); INSERT INTO dbo.Orders(orderid, orderdate, shippingdate) VALUES (1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '20210804', '20210806'), ( 4, „20210826”, NULL), (5, „20210827”, NULL); CREATE TABLE dbo.OrderDetails( id zamówienia INT NOT NULL CONSTRAINT FK_OrderDetails_Orders REFERENCES dbo.Orders, id produktu INT NOT NULL, ilość INT NOT NULL, cena jednostkowa NUMERIC(12, 2) NOT NULL, rabat NUMERIC(5, 4) KLUCZ(id_zamówienia; id_produktu)); INSERT INTO dbo.OrderDetails(orderid, productid, qty, unitprice, Discount) VALUES(1, 1001, 5, 10.50, 0,05), (1, 1004, 2, 20,00, 0,00), (2, 1003, 1, 52,99, 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, 1, 20,30, 0,00) , (4, 1005, 1, 30,10, 0,05), (5, 1003, 5, 54,99, 0,00), (5, 1006, 2, 12,30, 0,08);
Tabela Orders zawiera nagłówki zamówień, a tabela OrderDetails zawiera wiersze zamówień. Niewysłane zamówienia mają wartość NULL w kolumnie data wysyłki. Jeśli wolisz projekt, który nie używa wartości NULL, możesz użyć konkretnej przyszłej daty dla niewysłanych zamówień, na przykład „99991231”.
SPRAWDŹ OPCJE
Aby zrozumieć okoliczności, w których chcesz użyć OPCJI SPRAWDŹ jako części definicji widoku, najpierw zbadamy, co może się stać, gdy jej nie użyjesz.
Poniższy kod tworzy widok o nazwie FastOrders reprezentujący zamówienia wysłane w ciągu siedmiu dni od ich złożenia:
UTWÓRZ LUB ZMIEŃ WIDOK dbo. FastOrdersAS SELECT identyfikator zamówienia, data zamówienia, data wysyłki FROM dbo. Zamówienia WHERE DATEDIFF(dzień, data zamówienia, data wysyłki) <=7;GO
Użyj poniższego kodu, aby wstawić w widoku zamówienie wysłane dwa dni po złożeniu:
WSTAW DO dbo.FastOrders(id zamówienia, data zamówienia, data wysyłki) WARTOŚCI(6, '20210805', '20210807');
Zapytanie o widok:
WYBIERZ * Z dbo.FastOrders;
Otrzymasz następujące dane wyjściowe, które obejmują nowe zamówienie:
Zapytaj tabelę bazową:
WYBIERZ * Z dbo.Orders;
Otrzymasz następujące dane wyjściowe, które obejmują nowe zamówienie:
Wiersz został wstawiony do tabeli bazowej poprzez widok.
Następnie wstaw do widoku wiersz wysłany 10 dni po umieszczeniu, który jest sprzeczny z wewnętrznym filtrem zapytań widoku:
WSTAW DO dbo.FastOrders(identyfikator zamówienia, data zamówienia, data wysyłki) WARTOŚCI(7, '20210805', '20210815');
Oświadczenie zostało zakończone pomyślnie, zgłaszając jeden wiersz, którego dotyczy problem.
Zapytanie o widok:
WYBIERZ * Z dbo.FastOrders;
Otrzymasz następujące dane wyjściowe, które wykluczają nowe zamówienie:
Jeśli wiesz, że FastOrders to pogląd, to wszystko może wydawać się rozsądne. W końcu wiersz został wstawiony do tabeli bazowej i nie spełnia wewnętrznego filtra zapytań widoku. Ale jeśli nie wiesz, że FastOrders to widok, a nie tabela bazowa, takie zachowanie może wydawać się zaskakujące.
Zapytaj podstawową tabelę Orders:
WYBIERZ * Z dbo.Orders;
Otrzymasz następujące dane wyjściowe, które obejmują nowe zamówienie:
Możesz doświadczyć podobnego zaskakującego zachowania, jeśli zaktualizujesz za pomocą widoku wartość senddate w wierszu, który jest obecnie częścią widoku, do daty, która sprawia, że nie kwalifikuje się ona już jako część widoku. Taka aktualizacja jest zwykle dozwolona, ale ponownie odbywa się w podstawowej tabeli bazowej. Jeśli wyślesz zapytanie do widoku po takiej aktualizacji, zmodyfikowany wiersz wydaje się zniknąć. W praktyce nadal znajduje się w tabeli poniżej, po prostu nie jest już uważany za część widoku.
Uruchom następujący kod, aby usunąć wiersze dodane wcześniej:
DELETE FROM dbo.Orders WHERE identyfikator zamówienia>=6;
Jeśli chcesz zapobiec modyfikacjom, które powodują konflikt z wewnętrznym filtrem zapytania widoku, dodaj opcję WITH CHECK OPTION na końcu wewnętrznego zapytania jako część definicji widoku, na przykład:
UTWÓRZ LUB ZMIEŃ WIDOK dbo.FastOrdersAS SELECT identyfikator zamówienia, data zamówienia, data wysyłki FROM dbo. Zamówienia WHERE DATEDIFF(dzień, data zamówienia, data wysyłki) <=7 WITH CHECK OPTION;GO
Wstawki i aktualizacje w widoku są dozwolone, o ile są zgodne z filtrem wewnętrznego zapytania. W przeciwnym razie zostaną odrzucone.
Na przykład użyj następującego kodu, aby wstawić do widoku wiersz, który nie powoduje konfliktu z wewnętrznym filtrem zapytania:
WSTAW DO dbo.FastOrders(id zamówienia, data zamówienia, data wysyłki) WARTOŚCI(6, '20210805', '20210807');
Wiersz został pomyślnie dodany.
Spróbuj wstawić wiersz, który powoduje konflikt z filtrem:
WSTAW DO dbo.FastOrders(identyfikator zamówienia, data zamówienia, data wysyłki) WARTOŚCI(7, '20210805', '20210815');
Tym razem wiersz jest odrzucany z następującym błędem:
Poziom 16, Stan 1, Wiersz 135Próba wstawienia lub aktualizacji nie powiodła się, ponieważ widok docelowy określa Z OPCJĄ SPRAWDZANIA lub obejmuje widok, który określa Z OPCJĄ SPRAWDZANIA, a jeden lub więcej wierszy wynikających z operacji nie spełnia kryteriów Sprawdź ograniczenie opcji.
NULL niespójności
Jeśli od jakiegoś czasu pracujesz z T-SQL, prawdopodobnie doskonale zdajesz sobie sprawę ze wspomnianych wcześniej zawiłości modyfikacji i funkcji, jaką służy CHECK OPTION. Często nawet doświadczeni ludzie uważają, że NULL obsługa opcji CHECK Option jest zaskakująca. Przez lata myślałem o opcji CHECK OPTION jako pełniącej tę samą funkcję, co ograniczenie CHECK w definicji tabeli bazowej. Tak też opisałem tę opcję, pisząc lub ucząc o tym. Rzeczywiście, tak długo, jak w predykacie filtrującym nie występują żadne wartości NULL, wygodnie jest myśleć o tych dwóch w podobny sposób. Zachowują się w takim przypadku konsekwentnie – akceptując wiersze zgodne z predykatem i odrzucając te, które są z nim sprzeczne. Jednak te dwa niekonsekwentnie obsługują wartości NULL.
Podczas korzystania z OPCJI SPRAWDŹ, modyfikacja jest dozwolona w widoku tak długo, jak predykat ma wartość prawda, w przeciwnym razie jest odrzucana. Oznacza to, że jest odrzucany, gdy predykat widoku ma wartość fałszywą lub nieznaną (gdy w grę wchodzi NULL). W przypadku ograniczenia CHECK, modyfikacja jest dozwolona, gdy predykat ograniczenia ma wartość prawda lub nieznany, i odrzucana, gdy predykat ma wartość fałszu. To interesująca różnica! Najpierw zobaczmy, jak to działa, a następnie spróbujemy zrozumieć logikę tej niespójności.
Spróbuj wstawić do widoku wiersz z datą wysłania NULL:
WSTAW DO dbo.FastOrders(identyfikator zamówienia, data zamówienia, data wysyłki) VALUES(8, '20210828', NULL);
Predykat widoku jest oceniany jako nieznany, a wiersz jest odrzucany z następującym błędem:
Msg 550, Level 16, State 1, Line 147Próba wstawienia lub aktualizacji nie powiodła się, ponieważ widok docelowy albo określa Z OPCJĄ SPRAWDZANIA, albo obejmuje widok, który określa Z OPCJĄ SPRAWDZANIA, a jeden lub więcej wierszy wynikających z operacji nie kwalifikują się w ramach ograniczenia SPRAWDŹ OPCJE.
Spróbujmy podobnego wstawienia w tabeli bazowej z ograniczeniem CHECK. Użyj poniższego kodu, aby dodać takie ograniczenie do definicji tabeli naszego zamówienia:
ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(dzień, data zamówienia, data wysyłki) <=7);
Po pierwsze, aby upewnić się, że ograniczenie działa, gdy nie występują żadne wartości NULL, spróbuj wstawić następujące zamówienie z datą wysłania 10 dni od daty zamówienia:
INSERT INTO dbo.Orders(orderid, orderdate, shippingdate) VALUES(7, '20210805', '20210815');
Ta próba wstawienia jest odrzucana z następującym błędem:
Msg 547, poziom 16, stan 0, wiersz 159Instrukcja INSERT była w konflikcie z ograniczeniem CHECK "CHK_Orders_FastOrder". Konflikt wystąpił w bazie danych „tempdb”, tabeli „dbo.Orders”.
Użyj następującego kodu, aby wstawić wiersz z datą wysłania NULL:
INSERT INTO dbo.Orders(orderid, orderdate, shippingdate) VALUES(8, '20210828', NULL);
Ograniczenie CHECK ma odrzucać fałszywe przypadki, ale w naszym przypadku predykat ma wartość nieznany, więc wiersz jest dodawany pomyślnie.
Zapytaj tabelę Zamówienia:
WYBIERZ * Z dbo.Orders;
Możesz zobaczyć nowe zamówienie w danych wyjściowych:
Jaka jest logika tej niespójności? Można argumentować, że ograniczenie CHECK powinno być wymuszane tylko wtedy, gdy predykat ograniczenia jest wyraźnie naruszony, co oznacza, że jego ocena jest fałszywa. W ten sposób, jeśli zdecydujesz się zezwolić na wartości NULL w danej kolumnie, wiersze z wartościami NULL w kolumnie są dozwolone, nawet jeśli predykat ograniczenia ma wartość nieznany. W naszym przypadku niewysłane zamówienia oznaczamy wartością NULL w kolumnie data wysyłki i zezwalamy na niewysłane zamówienia w tabeli, jednocześnie wymuszając regułę „szybkich zamówień” tylko dla zamówień wysłanych.
Argumentem za użyciem innej logiki z widokiem jest to, że modyfikacja powinna być dozwolona w widoku tylko wtedy, gdy wiersz wyników jest prawidłową częścią widoku. Jeśli ocena predykatu widoku jest nieznana, np. gdy data wysłania wynosi NULL, wiersz wynikowy nie jest poprawną częścią widoku, dlatego jest odrzucany. Tylko wiersze, dla których predykat ma wartość prawda, są prawidłową częścią widoku i dlatego są dozwolone.
NULL znacznie komplikują język. Lubisz je lub nie, jeśli Twoje dane je obsługują, chcesz mieć pewność, że rozumiesz, jak obsługuje je T-SQL.
W tym momencie możesz usunąć ograniczenie CHECK z tabeli Zamówienia, a także usunąć widok FastOrders w celu oczyszczenia:
ALTER TABELA dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder;DROP VIEW, JEŚLI ISTNIEJE dbo.FastOrders;
Ograniczenie POBIERANIA W GÓRĘ/Z PRZESUNIĘCIEM
Modyfikacje za pomocą widoków obejmujących filtry TOP i OFFSET-FETCH są zwykle dozwolone. Jednak, podobnie jak w przypadku naszej wcześniejszej dyskusji na temat widoków zdefiniowanych bez OPCJI SPRAWDŹ, wynik takiej modyfikacji może wydawać się dziwny dla użytkownika, jeśli nie jest on świadomy, że wchodzi w interakcję z widokiem.
Rozważ następujący widok przedstawiający ostatnie zamówienia jako przykład:
UTWÓRZ LUB ZMIEŃ WIDOK dbo.RecentOrdersAS SELECT TOP (5) identyfikator zamówienia, data zamówienia, data wysyłki FROM dbo. Zamówienia ORDER WEDŁUG DATY zamówienia DESC, identyfikator zamówienia DESC;GO
Użyj następującego kodu, aby wstawić sześć zamówień w widoku Ostatnie zamówienia:
INSERT INTO dbo.RecentOrders(identyfikator zamówienia, data zamówienia, data wysyłki) WARTOŚCI (9, '20210801', '20210803'), (10, '20210802', '20210804'), (11, '20210829', '20210831' ), (12, „20210830”, „20210902”), (13, „20210830”, „20210903”), (14, „20210831”, „20210903”);
Zapytanie o widok:
WYBIERZ * Z dbo.RecentOrders;
Otrzymasz następujące dane wyjściowe:
Z sześciu wstawionych zamówień tylko cztery są częścią widoku. Wydaje się to całkiem rozsądne, jeśli masz świadomość, że wysyłasz zapytanie do widoku opartego na zapytaniu z filtrem TOP. Ale może się to wydawać dziwne, jeśli myślisz, że wysyłasz zapytanie do tabeli bazowej.
Zapytanie bezpośrednio do podstawowej tabeli Orders:
WYBIERZ * Z dbo.Orders;
Otrzymasz następujące dane wyjściowe pokazujące wszystkie dodane zamówienia:
Jeśli dodasz opcję CHECK OPTION do definicji widoku, instrukcje INSERT i UPDATE względem widoku zostaną odrzucone. Użyj następującego kodu, aby zastosować tę zmianę:
UTWÓRZ LUB ZMIEŃ WIDOK dbo.RecentOrdersAS WYBIERZ TOP (5) identyfikator zamówienia, data zamówienia, data wysyłki FROM dbo. Zamówienia ORDER WEDŁUG DATY zamówienia DESC, identyfikator zamówienia DESC WITH CHECK OPTION;GO
Spróbuj dodać zamówienie przez widok:
WSTAW DO dbo.RecentOrders(id zamówienia, data zamówienia, data wysyłki) WARTOŚCI (15, '20210801', '20210805');
Pojawia się następujący błąd:
Msg 4427, poziom 16, stan 1, wiersz 247Nie można zaktualizować widoku „dbo.RecentOrders”, ponieważ ten widok lub widok, do którego się odwołuje, został utworzony za pomocą opcji WITH CHECK OPTION i jego definicja zawiera klauzulę TOP lub OFFSET.
SQL Server nie stara się być tutaj zbyt sprytny. Odrzuci zmianę, nawet jeśli wiersz, który próbujesz wstawić, stanie się w tym momencie prawidłową częścią widoku. Na przykład, spróbuj dodać zamówienie z nowszą datą, które w tym momencie znalazłoby się w pierwszej piątce:
WSTAW DO dbo.RecentOrders(id zamówienia, data zamówienia, data wysyłki) WARTOŚCI (15, „20210904”, „20210906”);
Próba wstawienia jest nadal odrzucana z następującym błędem:
Msg 4427, poziom 16, stan 1, wiersz 254Nie można zaktualizować widoku „dbo.RecentOrders”, ponieważ ten widok lub widok, do którego się odwołuje, został utworzony za pomocą opcji WITH CHECK OPTION i jego definicja zawiera klauzulę TOP lub OFFSET.
Spróbuj zaktualizować wiersz w widoku:
UPDATE dbo.RecentOrders SET data wysyłki =DATEADD(dzień, 2, data zamówienia);
W takim przypadku próba zmiany jest również odrzucana z następującym błędem:
Msg 4427, poziom 16, stan 1, wiersz 260Nie można zaktualizować widoku „dbo.RecentOrders”, ponieważ ten widok lub widok, do którego się odwołuje, został utworzony za pomocą opcji WITH CHECK OPTION i jego definicja zawiera klauzulę TOP lub OFFSET.
Należy pamiętać, że zdefiniowanie widoku opartego na zapytaniu z opcją TOP lub OFFSET-FETCH i opcją CHECK OPTION spowoduje brak obsługi instrukcji INSERT i UPDATE w widoku.
Obsługiwane są usunięcia przez taki widok. Uruchom następujący kod, aby usunąć wszystkie bieżące pięć ostatnich zamówień:
USUŃ Z dbo.RecentOrders;
Polecenie zakończyło się pomyślnie.
Zapytanie do tabeli:
WYBIERZ * Z dbo.Orders;
Po usunięciu zamówień o identyfikatorach 8, 11, 12, 13 i 14. otrzymasz następujące dane wyjściowe.
W tym momencie uruchom następujący kod w celu oczyszczenia przed uruchomieniem przykładów w następnej sekcji:
USUŃ Z dbo.Zamówienia GDZIE orderid> 5; DROP VIEW IF EXISTS dbo.RecentOrders;
Dołączenia
Aktualizowanie widoku łączącego wiele tabel jest obsługiwane, o ile zmiana dotyczy tylko jednej z podstawowych tabel podstawowych.
Rozważ następujący widok łączący Zamówienia i Szczegóły zamówienia jako przykład:
UTWÓRZ LUB ZMIEŃ WIDOK dbo.OrdersOrderDetailsAS SELECT O.orderid, O.orderdate, O.shippeddate, OD.productid, OD.qty, OD.unitprice, OD.discount FROM dbo.Zamówienia AS O INNER JOIN dbo.OrderDetails AS OD ON O.orderid =OD.orderid;GO
Spróbuj wstawić wiersz w widoku, aby wpłynąć na obie tabele bazowe:
INSERT INTO dbo.OrdersOrderDetails(id zamówienia, data zamówienia, data wysyłki, id produktu, ilość, cena jednostkowa, rabat) VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);
Pojawia się następujący błąd:
Msg 4405, Poziom 16, Stan 1, Wiersz 306Nie można zaktualizować widoku lub funkcji „dbo.OrdersOrderDetails”, ponieważ modyfikacja wpływa na wiele tabel podstawowych.
Spróbuj wstawić wiersz w widoku, aby wpłynąć tylko na tabelę Zamówienia:
INSERT INTO dbo.OrdersOrderDetails(id zamówienia, data zamówienia, data wysyłki) VALUES(6, '20210828', NULL);
To polecenie kończy się pomyślnie, a wiersz jest wstawiany do podstawowej tabeli Orders.
Ale co, jeśli chcesz również móc wstawić wiersz przez widok do tabeli OrderDetails? Przy obecnej definicji widoku jest to niemożliwe (zamiast wyzwalaczy na bok), ponieważ widok zwraca kolumnę Orderid z tabeli Orders, a nie z tabeli OrderDetails. Wystarczy, że jedna kolumna z tabeli OrderDetails, która nie może w jakiś sposób automatycznie uzyskać swojej wartości, nie jest częścią widoku, aby zapobiec wstawianiu do OrderDetails przez widok. Oczywiście zawsze możesz zdecydować, że widok będzie zawierał zarówno orderid z Orders, jak i orderid z OrderDetails. W takim przypadku będziesz musiał przypisać dwie kolumny z różnymi aliasami, ponieważ nagłówek tabeli reprezentowanej przez widok musi mieć unikalne nazwy kolumn.
Użyj poniższego kodu, aby zmienić definicję widoku tak, aby zawierała obie kolumny, aliasując jedną z Orders jako O_orderid i jedną z OrderDetails jako OD_orderid:
UTWÓRZ LUB ZMIEŃ WIDOK dbo.OrdersOrderDetailsAS SELECT O.orderid AS O_orderid, O.orderdate, O.shippeddate, OD.orderid AS OD_orderid,OD.productid, OD.qty, OD.unitprice, OD.discount FROM dbo.Orders AS O INNER JOIN dbo.OrderDetails AS OD ON O.orderid =OD.orderid;GO
Teraz możesz wstawiać wiersze w widoku do Orders lub OrderDetails, w zależności od tego, z której tabeli pochodzi docelowa lista kolumn. Oto przykład wstawiania kilku wierszy zamówienia powiązanych z zamówieniem 6 przez widok do OrderDetails:
WSTAW DO dbo.OrdersOrderDetails(OD_orderid, productid, qty, unitprice, Discount) VALUES(6, 1001, 5, 10,50, 0,05), (6, 1002, 5, 20,00, 0,05);
Wiersze zostały pomyślnie dodane.
Zapytanie o widok:
SELECT * FROM dbo.OrdersOrderDetails GDZIE O_orderid =6;
Otrzymasz następujące dane wyjściowe:
O_id_zamówienia data_wysyłki OD_id_zamówienia id_produktu ilość zniżka cena jednostkowa----------- ---------- ----------- ------- ---- ------------ ---- ---------- ---------6 2021-08-28 NULL 6 1001 5 10,50 0,05006 2021-08-28 NULL 6 1002 5 20,00 0,0500
Podobne ograniczenie dotyczy instrukcji UPDATE za pośrednictwem widoku. Aktualizacje są dozwolone, o ile dotyczy to tylko jednej podstawowej tabeli bazowej. Ale możesz odwoływać się do kolumn z obu stron w oświadczeniu, o ile tylko jedna strona zostanie zmodyfikowana.
Na przykład następująca instrukcja UPDATE w widoku ustawia datę zamówienia w wierszu, w którym identyfikator zamówienia linii zamówienia to 6, a identyfikator produktu to 1001 na „20210901:”
UPDATE dbo.OrdersOrderDetails SET orderdate ='20210901' GDZIE OD_orderid =6 AND productid =1001;
Nazwiemy to oświadczenie Aktualizuj oświadczenie 1.
Aktualizacja zakończyła się pomyślnie, wyświetlając następujący komunikat:
(1 wiersz dotyczy)
Należy tutaj zwrócić uwagę na filtrowanie wyciągów według elementów z tabeli OrderDetails, ale zmodyfikowana data orderdate kolumny pochodzi z tabeli Orders. Tak więc w planie, który SQL Server buduje dla tej instrukcji, musi ustalić, które zamówienia wymagają modyfikacji w tabeli Orders. Plan dla tego stwierdzenia pokazano na rysunku 1.
Rysunek 1:Oświadczenie Plan for Update 1
Możesz zobaczyć, jak zaczyna się plan, filtrując stronę OrderDetails według identyfikatora zamówienia =6 i identyfikatora produktu =1001, a stronę Zamówienia według identyfikatora zamówienia =6, łącząc te dwa elementy. Wynik to tylko jeden wiersz. Jedyną istotną częścią tego działania jest to, które identyfikatory zamówień w tabeli Zamówienia reprezentują wiersze, które należy zaktualizować. W naszym przypadku jest to zamówienie o ID zamówienia 6. Dodatkowo operator Compute Scalar przygotowuje członka o nazwie Expr1002 z wartością, którą instrukcja przypisze do kolumny orderdate zamówienia docelowego. Ostatnia część planu z operatorem Clustered Index Update stosuje rzeczywistą aktualizację do wiersza w Orders with order ID 6, ustawiając jego wartość orderdate na Expr1002.
Kluczową kwestią, którą należy tutaj podkreślić, jest tylko jeden wiersz z numerem zamówienia 6 w tabeli Zamówienia, który został zaktualizowany. Jednak ten wiersz zawiera dwa dopasowania w wyniku sprzężenia z tabelą OrderDetails — jedno z identyfikatorem produktu 1001 (którego oryginalna aktualizacja odfiltrowała) i drugie z identyfikatorem produktu 1002 (którego oryginalna aktualizacja nie filtrowała). Zapytaj widok w tym momencie, filtrując wszystkie wiersze o identyfikatorze zamówienia 6:
SELECT * FROM dbo.OrdersOrderDetails GDZIE O_orderid =6;
Otrzymasz następujące dane wyjściowe:
O_id_zamówienia data_wysyłki OD_id_zamówienia id_produktu ilość zniżka cena jednostkowa----------- ---------- ----------- ------- ---- ------------ ---- ---------- ---------6 2021-09-01 NULL 6 1001 5 10,50 0,05006 2021-09-01 NULL 6 1002 5 20,00 0,0500
Oba wiersze pokazują nową datę zamówienia, mimo że pierwotna aktualizacja filtrowała tylko wiersz o identyfikatorze produktu 1001. Po raz kolejny powinno to wydawać się całkiem rozsądne, jeśli wiesz, że korzystasz z widoku, który łączy dwie tabele podstawowe pod okładkami, ale może wydawać się bardzo dziwne, jeśli nie zdajesz sobie z tego sprawy.
Co ciekawe, SQL Server obsługuje nawet niedeterministyczne aktualizacje, w których wiele wierszy źródłowych (w naszym przypadku OrderDetails) pasuje do jednego wiersza docelowego (w naszym przypadku Orders). Teoretycznie jednym ze sposobów załatwienia takiego przypadku byłoby odrzucenie go. Rzeczywiście, w przypadku instrukcji MERGE, w której wiele wierszy źródłowych pasuje do jednego wiersza docelowego, SQL Server odrzuca próbę. Ale nie za pomocą UPDATE opartej na sprzężeniu, bezpośrednio lub pośrednio przez nazwane wyrażenie tabeli, takie jak widok. SQL Server po prostu obsługuje to jako aktualizację niedeterministyczną.
Rozważmy następujący przykład, który będziemy nazywać Stwierdzeniem 2:
UPDATE dbo.OrdersOrderDetails SET data zamówienia =CASE WHEN cena jednostkowa>=20,00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid =6;
Mam nadzieję, że wybaczysz mi, że to wymyślony przykład, ale ilustruje to sedno.
W widoku znajdują się dwa kwalifikujące się wiersze, reprezentujące dwa kwalifikujące się wiersze zamówienia źródłowego z bazowej tabeli OrderDetails. Ale w podstawowej tabeli Orders jest tylko jeden kwalifikujący się wiersz docelowy. Ponadto w jednym źródłowym wierszu OrderDetails przypisane wyrażenie CASE zwraca jedną wartość („20210902”), a w drugim źródłowym wierszu OrderDetails zwraca inną wartość („20210903”). Co powinien zrobić SQL Server w takim przypadku? Jak wspomniano, podobna sytuacja z instrukcją MERGE skutkowałaby błędem, odrzuceniem próby zmiany. Jednak dzięki instrukcji UPDATE SQL Server po prostu rzuca monetą. Z technicznego punktu widzenia odbywa się to za pomocą wewnętrznej funkcji agregującej o nazwie ANY.
Tak więc nasza aktualizacja zakończyła się pomyślnie, zgłaszając wpływ na 1 wiersz. Plan dla tego stwierdzenia pokazano na rysunku 2.
Rysunek 2:Oświadczenie Plan for Update 2
W wyniku złączenia znajdują się dwa wiersze. Te dwa wiersze stają się wierszami źródłowymi aktualizacji. Ale wtedy operator agregujący stosujący funkcję ANY wybiera jedną (dowolną) wartość identyfikatora zamówienia i jedną (dowolną) wartość ceny jednostkowej z tych wierszy źródłowych. Oba wiersze źródłowe mają tę samą wartość identyfikatora zamówienia, więc właściwa kolejność zostanie zmodyfikowana. Jednak w zależności od tego, która z wartości źródłowych ceny jednostkowej zostanie wybrana przez DOWOLNY agregat, określi to, którą wartość zwróci wyrażenie CASE, która zostanie następnie użyta jako zaktualizowana wartość daty zamówienia w kolejności docelowej. Z pewnością możesz zobaczyć argument przeciwko wspieraniu takiej aktualizacji, ale jest ona w pełni obsługiwana w SQL Server.
Przeanalizujmy widok, aby zobaczyć wynik tej zmiany (teraz nadszedł czas, aby postawić zakład jak na wynik):
SELECT * FROM dbo.OrdersOrderDetails GDZIE O_orderid =6;
Otrzymałem następujący wynik:
O_id_zamówienia data_wysyłki OD_id_zamówienia id_produktu ilość zniżka cena jednostkowa----------- ---------- ----------- ------- ---- ------------ ---- ---------- ---------6 2021-09-03 NULL 6 1001 5 10,50 0,05006 2021-09-03 NULL 6 1002 5 20,00 0,0500
Tylko jedna z dwóch źródłowych wartości ceny jednostkowej została wybrana i użyta do określenia daty zamówienia pojedynczego zamówienia docelowego, jednak podczas wykonywania zapytania w widoku wartość daty zamówienia jest powtarzana dla obu pasujących wierszy zamówienia. Jak możesz sobie uświadomić, rezultatem równie dobrze mogła być inna data (2021.09.02), ponieważ wybór wartości ceny jednostkowej był niedeterministyczny. Zwariowane rzeczy!
Tak więc, pod pewnymi warunkami, instrukcje INSERT i UPDATE są dozwolone za pośrednictwem widoków, które łączą wiele bazowych tabel. Usunięcie jednak nie jest dozwolone w przypadku takich widoków. Jak SQL Server może określić, która ze stron ma być celem usunięcia?
Oto próba zastosowania takiego usunięcia w widoku:
USUŃ Z dbo.OrdersOrderDetails GDZIE O_orderid =6;
Ta próba jest odrzucana z następującym błędem:
Msg 4405, Poziom 16, Stan 1, Wiersz 377Nie można zaktualizować widoku lub funkcji „dbo.OrdersOrderDetails”, ponieważ modyfikacja wpływa na wiele tabel podstawowych.
W tym momencie uruchom następujący kod do czyszczenia:
DELETE FROM dbo.OrderDetails WHERE orderid =6;DELETE FROM dbo.Orders WHERE orderid =6;DROP VIEW IF ISISTS dbo.OrdersOrderDetails;
Kolumny pochodne
Kolejne ograniczenie modyfikacji za pomocą widoków dotyczy kolumn pochodnych. Jeśli kolumna widoku jest wynikiem obliczeń, SQL Server nie będzie próbował odtwarzać swojej formuły podczas próby wstawienia lub aktualizacji danych za pośrednictwem widoku — raczej odrzuci takie modyfikacje.
Rozważ następujący widok jako przykład:
UTWÓRZ LUB ZMIEŃ WIDOK dbo.OrderDetailsNetPriceAS SELECT identyfikator zamówienia, identyfikator produktu, ilość, cena jednostkowa * (1,0 - rabat) AS cenajednostkowa, rabat FROM dbo.OrderDetails;GO
Widok oblicza kolumnę netunitprice na podstawie podstawowych kolumn tabeli OrderDetails cena jednostkowa i rabat.
Zapytanie o widok:
WYBIERZ * Z dbo.OrderDetailsNetPrice;
Otrzymasz następujące dane wyjściowe:
id zamówienia id produktu ilość rabatu jednostkowego netto----------- ----------- ----------- --------- ---- ---------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
Spróbuj wstawić wiersz przez widok:
WSTAW DO dbo.OrderDetailsNetPrice(id zamówienia, identyfikator produktu, ilość, cenajednostkowa, rabat) VALUES (1, 1005, 1, 28.595, 0,05);
Teoretycznie można określić, który wiersz należy wstawić do podstawowej tabeli OrderDetails, analizując wstecznie wartość ceny jednostkowej tabeli podstawowej na podstawie wartości ceny jednostkowej netto i rabatu w widoku. SQL Server nie próbuje takiej inżynierii wstecznej, ale odrzuca próbę wstawienia z następującym błędem:
Komunikat 4406, poziom 16, stan 1, wiersz 412Aktualizacja lub wstawienie widoku lub funkcji „dbo.OrderDetailsNetPrice” nie powiodło się, ponieważ zawiera pole pochodne lub stałe.
Spróbuj pominąć obliczoną kolumnę we wstawieniu:
WSTAW DO dbo.OrderDetailsNetPrice(id zamówienia, identyfikator produktu, ilość, rabat) WARTOŚCI (1, 1005, 1, 0,05);
Teraz wróciliśmy do wymogu, że wszystkie kolumny z tabeli bazowej, które w jakiś sposób nie otrzymują swoich wartości automatycznie, muszą być częścią wstawiania, a tutaj brakuje kolumny z ceną jednostkową. To wstawienie kończy się niepowodzeniem z następującym błędem:
Msg 515, poziom 16, stan 2, wiersz 421Nie można wstawić wartości NULL do kolumny „cena jednostkowa”, tabeli „tempdb.dbo.OrderDetails”; kolumna nie zezwala na wartości null. WSTAWIANIE nie powiodło się.
Jeśli chcesz obsługiwać wstawienia w widoku, zasadniczo masz dwie opcje. Jednym z nich jest uwzględnienie kolumny ceny jednostkowej w definicji widoku. Another is to create an instead of trigger on the view where you handle the reverse engineering logic yourself.
W tym momencie uruchom następujący kod do czyszczenia:
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;GO
Since 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;
Podsumowanie
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.