Dieser Artikel ist der elfte Teil einer Serie über Tabellenausdrücke. Bisher habe ich abgeleitete Tabellen und CTEs behandelt und kürzlich mit der Abdeckung von Ansichten begonnen. In Teil 9 habe ich Ansichten mit abgeleiteten Tabellen und CTEs verglichen, und in Teil 10 habe ich DDL-Änderungen und die Auswirkungen der Verwendung von SELECT * in der inneren Abfrage der Ansicht erörtert. In diesem Artikel konzentriere ich mich auf Modifikationsüberlegungen.
Wie Sie wahrscheinlich wissen, dürfen Sie Daten in Basistabellen indirekt über benannte Tabellenausdrücke wie Ansichten ändern. Sie können Änderungsberechtigungen für Ansichten steuern. Tatsächlich können Sie Benutzern Berechtigungen zum Ändern von Daten durch Ansichten erteilen, ohne ihnen Berechtigungen zum direkten Ändern der zugrunde liegenden Tabellen zu erteilen.
Sie müssen sich jedoch bestimmter Komplexitäten und Einschränkungen bewusst sein, die für Änderungen durch Ansichten gelten. Interessanterweise können einige der unterstützten Änderungen zu überraschenden Ergebnissen führen, insbesondere wenn der Benutzer, der die Daten ändert, nicht weiß, dass er mit einer Ansicht interagiert. Sie können Änderungen durch Ansichten weiter einschränken, indem Sie eine Option namens CHECK OPTION verwenden, die ich in diesem Artikel behandeln werde. Als Teil der Berichterstattung beschreibe ich eine merkwürdige Inkonsistenz zwischen der Art und Weise, wie die CHECK OPTION in einer Ansicht und eine CHECK-Einschränkung in einer Tabelle mit Änderungen umgehen – insbesondere mit NULLen.
Beispieldaten
Als Beispieldaten für diesen Artikel verwende ich Tabellen namens Orders und OrderDetails. Verwenden Sie den folgenden Code, um diese Tabellen in tempdb zu erstellen und sie mit einigen anfänglichen Beispieldaten zu füllen:
USE tempdb;GO DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders;GO CREATE TABLE dbo.Orders( orderid INT NOT NULL CONSTRAINT PK_Orders PRIMARY KEY, orderdate DATE NOT NULL, shippingdate DATE NULL); INSERT INTO dbo.Orders(bestell-id, bestelldatum, versanddatum) VALUES(1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '20210804', '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, unitprice NUMERIC(12, 2) NOT NULL, discount NUMERIC(5, 4) NOT NULL, CONSTRAINT PK_OrderDetails PRIMARY SCHLÜSSEL (Bestell-ID, Produkt-ID)); 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);
Die Orders-Tabelle enthält Bestellkopfzeilen und die OrderDetails-Tabelle enthält Bestellzeilen. Nicht versandte Bestellungen haben eine NULL in der Spalte Versanddatum. Wenn Sie ein Design bevorzugen, das keine NULLen verwendet, können Sie ein bestimmtes zukünftiges Datum für nicht versandte Bestellungen verwenden, z. B. „99991231.“
OPTION PRÜFEN
Um die Umstände zu verstehen, unter denen Sie die CHECK OPTION als Teil der Definition einer Ansicht verwenden möchten, untersuchen wir zunächst, was passieren kann, wenn Sie sie nicht verwenden.
Der folgende Code erstellt eine Ansicht mit dem Namen FastOrders, die Bestellungen darstellt, die innerhalb von sieben Tagen nach ihrer Aufgabe versandt wurden:
CREATE OR ALTER VIEW dbo.FastOrdersAS SELECT orderid, orderdate, shippingdate FROM dbo.Orders WHERE DATEDIFF(Tag, Bestelldatum, Versanddatum) <=7;GO
Verwenden Sie den folgenden Code, um über die Ansicht eine Bestellung einzufügen, die zwei Tage nach der Aufgabe versandt wurde:
INSERT INTO dbo.FastOrders(bestell-id, bestelldatum, versanddatum) VALUES(6, '20210805', '20210807');
Ansicht abfragen:
SELECT * FROM dbo.FastOrders;
Sie erhalten die folgende Ausgabe, die die neue Bestellung enthält:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------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
Fragen Sie die zugrunde liegende Tabelle ab:
SELECT * FROM dbo.Orders;
Sie erhalten die folgende Ausgabe, die die neue Bestellung enthält:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------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-07Die Zeile wurde über die Ansicht in die zugrunde liegende Basistabelle eingefügt.
Fügen Sie als Nächstes durch die Ansicht eine Zeile ein, die 10 Tage nach der Platzierung versendet wurde, was dem inneren Abfragefilter der Ansicht widerspricht:
INSERT INTO dbo.FastOrders(bestell-id, bestelldatum, versanddatum) VALUES(7, '20210805', '20210815');Die Anweisung wird erfolgreich abgeschlossen und meldet eine betroffene Zeile.
Ansicht abfragen:
SELECT * FROM dbo.FastOrders;Sie erhalten die folgende Ausgabe, die die neue Reihenfolge ausschließt:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-066 2021-08-05 2021-08-07Wenn Sie wissen, dass FastOrders eine Ansicht ist, scheint dies alles sinnvoll zu sein. Schließlich wurde die Zeile in die zugrunde liegende Tabelle eingefügt und erfüllt nicht den inneren Abfragefilter der Ansicht. Aber wenn Sie nicht wissen, dass FastOrders eine Ansicht und keine Basistabelle ist, erscheint dieses Verhalten überraschend.
Fragen Sie die zugrunde liegende Orders-Tabelle ab:
SELECT * FROM dbo.Orders;Sie erhalten die folgende Ausgabe, die die neue Bestellung enthält:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------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 05.08.2021 077.08.2021 05.08. 15Ein ähnliches überraschendes Verhalten könnte auftreten, wenn Sie den Wert des Versanddatums in einer Zeile, die derzeit Teil der Ansicht ist, über die Ansicht auf ein Datum aktualisieren, das es nicht mehr als Teil der Ansicht qualifiziert. Eine solche Aktualisierung ist normalerweise erlaubt, findet aber wiederum in der zugrunde liegenden Basistabelle statt. Wenn Sie die Ansicht nach einer solchen Aktualisierung abfragen, scheint die geänderte Zeile verschwunden zu sein. In der Praxis ist es immer noch in der zugrunde liegenden Tabelle vorhanden, es wird nur nicht mehr als Teil der Ansicht betrachtet.
Führen Sie den folgenden Code aus, um die zuvor hinzugefügten Zeilen zu löschen:
DELETE FROM dbo.Orders WHERE orderid>=6;Wenn Sie Änderungen verhindern möchten, die mit dem inneren Abfragefilter der Ansicht in Konflikt stehen, fügen Sie WITH CHECK OPTION am Ende der inneren Abfrage als Teil der Ansichtsdefinition hinzu, etwa so:
CREATE OR ALTER VIEW dbo.FastOrdersAS SELECT orderid, orderdate, shippingdate FROM dbo.Orders WHERE DATEDIFF(Tag, Bestelldatum, Versanddatum) <=7 WITH CHECK OPTION;GOEinfügungen und Aktualisierungen durch die Ansicht sind zulässig, solange sie dem Filter der inneren Abfrage entsprechen. Andernfalls werden sie abgelehnt.
Verwenden Sie beispielsweise den folgenden Code, um eine Zeile in die Ansicht einzufügen, die nicht mit dem inneren Abfragefilter in Konflikt steht:
INSERT INTO dbo.FastOrders(bestell-id, bestelldatum, versanddatum) VALUES(6, '20210805', '20210807');Die Zeile wurde erfolgreich hinzugefügt.
Versuchen Sie, eine Zeile einzufügen, die mit dem Filter in Konflikt steht:
INSERT INTO dbo.FastOrders(bestell-id, bestelldatum, versanddatum) VALUES(7, '20210805', '20210815');Diesmal wird die Zeile mit folgendem Fehler zurückgewiesen:
Ebene 16, Status 1, Zeile 135
Die versuchte Einfügung oder Aktualisierung ist fehlgeschlagen, weil die Zielansicht entweder WITH CHECK OPTION angibt oder eine Ansicht umfasst, die WITH CHECK OPTION angibt, und eine oder mehrere Zeilen, die aus der Operation resultieren, nicht unter qualifiziert waren CHECK OPTION-Einschränkung.NULL-Inkonsistenzen
Wenn Sie schon seit einiger Zeit mit T-SQL arbeiten, sind Sie sich wahrscheinlich der oben genannten Komplexität von Änderungen bewusst, und die Funktion CHECK OPTION dient dazu. Selbst erfahrene Personen finden die NULL-Behandlung der CHECK OPTION oft überraschend. Jahrelang dachte ich, dass die CHECK OPTION in einer Ansicht die gleiche Funktion erfüllt wie eine CHECK-Einschränkung in der Definition einer Basistabelle. So habe ich diese Option früher auch beschrieben, wenn ich darüber geschrieben oder gelehrt habe. Solange das Filterprädikat keine NULLen enthält, ist es in der Tat praktisch, sich die beiden in ähnlichen Begriffen vorzustellen. Sie verhalten sich in einem solchen Fall konsistent – sie akzeptieren Zeilen, die mit dem Prädikat übereinstimmen, und lehnen diejenigen ab, die ihm widersprechen. Allerdings behandeln die beiden NULL-Werte uneinheitlich.
Bei Verwendung der CHECK OPTION ist eine Änderung durch die Ansicht zulässig, solange das Prädikat als wahr ausgewertet wird, andernfalls wird es abgelehnt. Dies bedeutet, dass es abgelehnt wird, wenn das Prädikat der Ansicht falsch oder unbekannt ist (wenn eine NULL beteiligt ist). Bei einer CHECK-Einschränkung wird die Änderung zugelassen, wenn das Prädikat der Einschränkung als wahr oder unbekannt ausgewertet wird, und abgelehnt, wenn das Prädikat als falsch ausgewertet wird. Das ist ein interessanter Unterschied! Sehen wir uns das zuerst in Aktion an und versuchen dann, die Logik hinter dieser Inkonsistenz herauszufinden.
Versuchen Sie, durch die Ansicht eine Zeile mit einem NULL-Versanddatum einzufügen:
INSERT INTO dbo.FastOrders(bestell-id, bestelldatum, versanddatum) VALUES(8, '20210828', NULL);Das Prädikat der Ansicht wird als unbekannt ausgewertet und die Zeile wird mit folgendem Fehler zurückgewiesen:
Msg 550, Level 16, State 1, Line 147
Die versuchte Einfügung oder Aktualisierung ist fehlgeschlagen, weil die Zielansicht entweder WITH CHECK OPTION angibt oder eine Ansicht umfasst, die WITH CHECK OPTION angibt, und eine oder mehrere Zeilen, die aus der Operation resultieren, nicht sich unter der Einschränkung CHECK OPTION qualifizieren.Versuchen wir eine ähnliche Einfügung in eine Basistabelle mit einer CHECK-Einschränkung. Verwenden Sie den folgenden Code, um eine solche Einschränkung zur Tabellendefinition unserer Bestellung hinzuzufügen:
ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(Tag, Bestelldatum, Versanddatum) <=7);Um sicherzustellen, dass die Einschränkung funktioniert, wenn keine NULLen beteiligt sind, versuchen Sie zunächst, die folgende Bestellung mit einem Versanddatum einzufügen, das 10 Tage vor dem Bestelldatum liegt:
INSERT INTO dbo.Orders(bestell-id, bestelldatum, versanddatum) VALUES(7, '20210805', '20210815');Diese versuchte Einfügung wird mit folgendem Fehler abgelehnt:
Nachricht 547, Ebene 16, Status 0, Zeile 159
Die INSERT-Anweisung stand in Konflikt mit der CHECK-Einschränkung „CHK_Orders_FastOrder“. Der Konflikt ist in der Datenbank „tempdb“, Tabelle „dbo.Orders“ aufgetreten.Verwenden Sie den folgenden Code, um eine Zeile mit einem NULL-Versanddatum einzufügen:
INSERT INTO dbo.Orders(bestell-id, bestelldatum, versanddatum) VALUES(8, '20210828', NULL);Eine CHECK-Einschränkung soll falsche Fälle zurückweisen, aber in unserem Fall ergibt das Prädikat unbekannt, sodass die Zeile erfolgreich hinzugefügt wird.
Fragen Sie die Orders-Tabelle ab:
SELECT * FROM dbo.Orders;Sie können die neue Reihenfolge in der Ausgabe sehen:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------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 2021-08-28 NULL>Was ist die Logik hinter dieser Inkonsistenz? Sie könnten argumentieren, dass eine CHECK-Einschränkung nur erzwungen werden sollte, wenn das Prädikat der Einschränkung eindeutig verletzt wird, d. h. wenn sie als falsch ausgewertet wird. Auf diese Weise werden Zeilen mit NULL-Werten in der Spalte zugelassen, wenn Sie sich dafür entscheiden, NULL-Werte in der betreffenden Spalte zuzulassen, obwohl das Prädikat der Einschränkung als unbekannt ausgewertet wird. In unserem Fall stellen wir nicht versandte Bestellungen mit NULL in der Spalte „Lieferdatum“ dar, und wir lassen nicht versandte Bestellungen in der Tabelle zu, während wir die „Schnellbestellung“-Regel nur für versandte Bestellungen durchsetzen.
Das Argument für die Verwendung einer anderen Logik mit einer Ansicht ist, dass eine Änderung durch die Ansicht nur zulässig sein sollte, wenn die Ergebniszeile ein gültiger Teil der Ansicht ist. Wenn das Prädikat der Ansicht als unbekannt ausgewertet wird, z. B. wenn das Versanddatum NULL ist, ist die Ergebniszeile kein gültiger Teil der Ansicht und wird daher abgelehnt. Nur Zeilen, für die das Prädikat wahr ist, sind ein gültiger Teil der Ansicht und daher zulässig.
NULLen fügen der Sprache viel Komplexität hinzu. Ob Sie sie mögen oder nicht, wenn Ihre Daten sie unterstützen, sollten Sie sicherstellen, dass Sie verstehen, wie T-SQL damit umgeht.
An diesem Punkt können Sie die CHECK-Einschränkung aus der Orders-Tabelle löschen und auch die FastOrders-Ansicht zur Bereinigung löschen:
ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder;DROP VIEW IF EXISTS dbo.FastOrders;TOP/OFFSET-FETCH-Beschränkung
Modifikationen durch Ansichten, die die TOP- und OFFSET-FETCH-Filter beinhalten, sind normalerweise erlaubt. Wie bei unserer früheren Diskussion über Ansichten, die ohne die CHECK OPTION definiert wurden, kann das Ergebnis einer solchen Änderung dem Benutzer jedoch seltsam erscheinen, wenn er nicht weiß, dass er mit einer Ansicht interagiert.
Betrachten Sie als Beispiel die folgende Ansicht, die die letzten Bestellungen darstellt:
CREATE OR ALTER VIEW dbo.RecentOrdersAS SELECT TOP (5) Bestell-ID, Bestelldatum, Lieferdatum FROM dbo.Orders ORDER BY Bestelldatum DESC, Bestell-ID DESC;GOVerwenden Sie den folgenden Code, um sechs Bestellungen in die Ansicht „RecentOrders“ einzufügen:
INSERT INTO dbo.RecentOrders(bestell-id, bestelldatum, versanddatum) VALUES(9, '20210801', '20210803'), (10, '20210802', '20210804'), (11, '20210829', '20210831' ), (12, '20210830', '20210902'), (13, '20210830', '20210903'), (14, '20210831', '20210903');Ansicht abfragen:
SELECT * FROM dbo.RecentOrders;Sie erhalten die folgende Ausgabe:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------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 NULLVon den sechs eingefügten Aufträgen sind nur vier Teil der Ansicht. Dies erscheint durchaus sinnvoll, wenn Sie wissen, dass Sie eine Ansicht abfragen, die auf einer Abfrage mit einem TOP-Filter basiert. Aber es mag seltsam erscheinen, wenn Sie denken, dass Sie eine Basistabelle abfragen.
Fragen Sie die zugrunde liegende Orders-Tabelle direkt ab:
SELECT * FROM dbo.Orders;Sie erhalten die folgende Ausgabe mit allen hinzugefügten Bestellungen:
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------1 2021-08-02 2021-08-042 2021 -02.08 -01 2021-08-0310 2021-08-02 2021-08-0411 2021-08-29 2021-08-3112 2021-08-30 2021-09-0213 2021-08-30 2021-09-0314 2021-08 -31 03.09.2021Wenn Sie die CHECK OPTION zur Ansichtsdefinition hinzufügen, werden INSERT- und UPDATE-Anweisungen für die Ansicht zurückgewiesen. Verwenden Sie den folgenden Code, um diese Änderung anzuwenden:
CREATE OR ALTER VIEW dbo.RecentOrdersAS SELECT TOP (5) orderid, orderdate, shippingdate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC WITH CHECK OPTION;GOVersuchen Sie, eine Bestellung über die Ansicht hinzuzufügen:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippingdate) VALUES(15, '20210801', '20210805');Sie erhalten die folgende Fehlermeldung:
Msg 4427, Level 16, State 1, Line 247
Die Ansicht „dbo.RecentOrders“ kann nicht aktualisiert werden, da sie oder eine Ansicht, auf die sie verweist, mit WITH CHECK OPTION erstellt wurde und ihre Definition eine TOP- oder OFFSET-Klausel enthält.SQL Server versucht hier nicht, zu schlau zu sein. Die Änderung wird abgelehnt, selbst wenn die Zeile, die Sie einzufügen versuchen, zu diesem Zeitpunkt ein gültiger Teil der Ansicht werden würde. Versuchen Sie beispielsweise, eine Bestellung mit einem neueren Datum hinzuzufügen, die an dieser Stelle unter die ersten 5 fallen würde:
INSERT INTO dbo.RecentOrders(bestell-id, bestelldatum, versanddatum) VALUES(15, '20210904', '20210906');Das versuchte Einfügen wird dennoch mit folgendem Fehler abgelehnt:
Msg 4427, Level 16, State 1, Line 254
Die Ansicht „dbo.RecentOrders“ kann nicht aktualisiert werden, da sie oder eine Ansicht, auf die sie verweist, mit WITH CHECK OPTION erstellt wurde und ihre Definition eine TOP- oder OFFSET-Klausel enthält.Versuchen Sie, eine Zeile über die Ansicht zu aktualisieren:
UPDATE dbo.RecentOrders SET Versanddatum =DATEADD(Tag, 2, Bestelldatum);Auch in diesem Fall wird der Änderungsversuch mit folgendem Fehler abgelehnt:
Msg 4427, Level 16, State 1, Line 260
Die Ansicht „dbo.RecentOrders“ kann nicht aktualisiert werden, da sie oder eine Ansicht, auf die sie verweist, mit WITH CHECK OPTION erstellt wurde und ihre Definition eine TOP- oder OFFSET-Klausel enthält.Beachten Sie, dass das Definieren einer Ansicht basierend auf einer Abfrage mit TOP oder OFFSET-FETCH und der CHECK OPTION dazu führt, dass die INSERT- und UPDATE-Anweisungen durch die Ansicht nicht unterstützt werden.
Löschungen durch eine solche Ansicht werden unterstützt. Führen Sie den folgenden Code aus, um alle fünf aktuellen Bestellungen zu löschen:
DELETE FROM dbo.RecentOrders;Der Befehl wird erfolgreich abgeschlossen.
Fragen Sie die Tabelle ab:
SELECT * FROM dbo.Orders;Nach dem Löschen der Aufträge mit den IDs 8, 11, 12, 13 und 14 erhalten Sie folgende Ausgabe.
Bestell-ID Bestelldatum Versanddatum----------- ---------- -----------1 2021-08-02 2021-08-042 2021 -02.08.2021-08.053 04.08.2021 064.08.2021 26.08.2021 NULL5 27.08.2021 NULL6 0310 2021-08-02 2021-08-04Führen Sie an dieser Stelle den folgenden Code zur Bereinigung aus, bevor Sie die Beispiele im nächsten Abschnitt ausführen:
DELETE FROM dbo.Orders WHERE orderid> 5; ANSICHT LÖSCHEN, WENN VORHANDEN dbo.RecentOrders;Beitreten
Das Aktualisieren einer Ansicht, die mehrere Tabellen verbindet, wird unterstützt, solange nur eine der zugrunde liegenden Basistabellen von der Änderung betroffen ist.
Betrachten Sie als Beispiel die folgende Ansicht, die Orders und OrderDetails verbindet:
CREATE OR ALTER VIEW dbo.OrdersOrderDetailsAS SELECT O.orderid, O.orderdate, O.shippeddate, OD.productid, OD.qty, OD.unitprice, OD.discount FROM dbo.Orders AS O INNER JOIN dbo.OrderDetails AS OD ON O.orderid =OD.orderid;GOVersuchen Sie, eine Zeile durch die Ansicht einzufügen, sodass beide zugrunde liegenden Basistabellen betroffen wären:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippingdate, productid, qty, unitprice, discount) VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);Sie erhalten die folgende Fehlermeldung:
Msg 4405, Level 16, State 1, Line 306
Ansicht oder Funktion „dbo.OrdersOrderDetails“ kann nicht aktualisiert werden, da die Änderung mehrere Basistabellen betrifft.Versuchen Sie, eine Zeile durch die Ansicht einzufügen, sodass nur die Tabelle „Bestellungen“ betroffen wäre:
INSERT INTO dbo.OrdersOrderDetails(bestell-id, bestelldatum, versanddatum) VALUES(6, '20210828', NULL);Dieser Befehl wird erfolgreich abgeschlossen und die Zeile wird in die zugrunde liegende Orders-Tabelle eingefügt.
Aber was ist, wenn Sie auch eine Zeile durch die Ansicht in die OrderDetails-Tabelle einfügen können möchten? Mit der aktuellen Ansichtsdefinition ist dies nicht möglich (anstelle von Triggern beiseite), da die Ansicht die orderid-Spalte aus der Orders-Tabelle und nicht aus der OrderDetails-Tabelle zurückgibt. Es reicht aus, dass eine Spalte aus der OrderDetails-Tabelle, die ihren Wert nicht irgendwie automatisch abrufen kann, nicht Teil der Ansicht ist, um Einfügungen in OrderDetails über die Ansicht zu verhindern. Natürlich können Sie jederzeit entscheiden, dass die Ansicht sowohl orderid aus Orders als auch orderid aus OrderDetails enthält. In einem solchen Fall müssen Sie den beiden Spalten unterschiedliche Aliase zuweisen, da die Überschrift der durch die Ansicht repräsentierten Tabelle eindeutige Spaltennamen haben muss.
Verwenden Sie den folgenden Code, um die Ansichtsdefinition so zu ändern, dass sie beide Spalten enthält, und aliasieren Sie die Spalte von Orders als O_orderid und die Spalte von OrderDetails als OD_orderid:
CREATE OR ALTER VIEW 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;GOJetzt können Sie Zeilen über die Ansicht entweder zu Bestellungen oder zu Bestelldetails einfügen, je nachdem, aus welcher Tabelle die Zielspaltenliste stammt. Hier ist ein Beispiel für das Einfügen einiger Bestellzeilen, die mit Bestellung 6 verknüpft sind, über die Ansicht in 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);Die Zeilen wurden erfolgreich hinzugefügt.
Ansicht abfragen:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Sie erhalten die folgende Ausgabe:
O_orderid bestelldatum versanddatum OD_orderid produkt-id menge stückpreis rabatt----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-08-28 NULL 6 1001 5 10,50 0,05006 2021-08-28 NULL 6 1002 5 20,00 0,0500Eine ähnliche Einschränkung gilt für UPDATE-Anweisungen über die Ansicht. Aktualisierungen sind zulässig, solange nur eine zugrunde liegende Basistabelle betroffen ist. Aber Sie dürfen in der Anweisung auf Spalten von beiden Seiten verweisen, solange nur eine Seite geändert wird.
Als Beispiel setzt die folgende UPDATE-Anweisung über die Ansicht das Bestelldatum der Zeile, in der die Bestell-ID der Bestellposition 6 und die Produkt-ID 1001 ist, auf „20210901:“
UPDATE dbo.OrdersOrderDetails SET orderdate ='20210901' WHERE OD_orderid =6 AND productid =1001;Wir nennen diese Anweisung Update-Anweisung 1.
Das Update wird erfolgreich mit der folgenden Meldung abgeschlossen:
(1 Zeile betroffen)Was hier wichtig zu beachten ist, ist, dass die Anweisung nach Elementen aus der Tabelle „OrderDetails“ filtert, die geänderte Spalte „orderdate“ jedoch aus der Tabelle „Orders“ stammt. In dem Plan, den SQL Server für diese Anweisung erstellt, muss es also herausfinden, welche Bestellungen in der Orders-Tabelle geändert werden müssen. Der Plan für diese Anweisung ist in Abbildung 1 dargestellt.
Abbildung 1:Plan für Update-Anweisung 1
Sie können sehen, wie der Plan beginnt, indem Sie die OrderDetails-Seite nach orderid =6 und productid =1001 und die Orders-Seite nach orderid =6 filtern und die beiden zusammenführen. Das Ergebnis ist nur eine Zeile. Der einzige relevante Teil, den Sie bei dieser Aktivität beachten sollten, ist, welche Bestell-IDs in der Tabelle „Bestellungen“ Zeilen darstellen, die aktualisiert werden müssen. In unserem Fall ist es die Order mit der Order-ID 6. Außerdem bereitet der Compute Scalar-Operator ein Element namens Expr1002 mit dem Wert vor, den die Anweisung der Orderdate-Spalte der Zielorder zuweisen wird. Der letzte Teil des Plans mit dem Operator „Clustered Index Update“ wendet die tatsächliche Aktualisierung auf die Zeile in „Orders“ mit der Order-ID 6 an und setzt den Orderdate-Wert auf „Expr1002“.
Der wichtigste Punkt, der hier hervorgehoben werden muss, ist, dass nur eine Zeile mit der Bestell-ID 6 in der Orders-Tabelle aktualisiert wurde. Diese Zeile hat jedoch zwei Übereinstimmungen im Ergebnis des Joins mit der OrderDetails-Tabelle – eine mit der Produkt-ID 1001 (die das ursprüngliche Update gefiltert hat) und eine andere mit der Produkt-ID 1002 (die das ursprüngliche Update nicht gefiltert hat). Fragen Sie die Ansicht an diesem Punkt ab und filtern Sie alle Zeilen mit der Auftrags-ID 6:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Sie erhalten die folgende Ausgabe:
O_orderid bestelldatum versanddatum OD_orderid produkt-id menge stückpreis rabatt----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-09-01 NULL 6 1001 5 10,50 0,05006 2021-09-01 NULL 6 1002 5 20,00 0,0500Beide Zeilen zeigen das neue Bestelldatum, obwohl das ursprüngliche Update nur die Zeile mit der Produkt-ID 1001 gefiltert hat. Auch dies sollte absolut sinnvoll erscheinen, wenn Sie wissen, dass Sie mit einer Ansicht interagieren, die zwei Basistabellen unter den Deckblättern verbindet, aber könnte sehr seltsam erscheinen, wenn Sie dies nicht erkennen.
Seltsamerweise unterstützt SQL Server sogar nichtdeterministische Aktualisierungen, bei denen mehrere Quellzeilen (in unserem Fall aus OrderDetails) mit einer einzelnen Zielzeile (in unserem Fall in Orders) übereinstimmen. Theoretisch wäre es eine Möglichkeit, mit einem solchen Fall umzugehen, ihn abzulehnen. Tatsächlich lehnt SQL Server bei einer MERGE-Anweisung, bei der mehrere Quellzeilen mit einer Zielzeile übereinstimmen, den Versuch ab. Aber nicht mit einem UPDATE, das auf einem Join basiert, sei es direkt oder indirekt über einen benannten Tabellenausdruck wie eine Ansicht. SQL Server behandelt es einfach als nicht deterministisches Update.
Betrachten Sie das folgende Beispiel, das wir als Anweisung 2 bezeichnen:
UPDATE dbo.OrdersOrderDetails SET orderdate =CASE WHEN unitprice>=20.00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid =6;Hoffentlich verzeihen Sie mir, dass es sich um ein erfundenes Beispiel handelt, aber es veranschaulicht den Punkt.
Es gibt zwei qualifizierende Zeilen in der Ansicht, die zwei qualifizierende Quellauftragspositionszeilen aus der zugrunde liegenden OrderDetails-Tabelle darstellen. Aber es gibt nur eine qualifizierende Zielzeile in der zugrunde liegenden Orders-Tabelle. Darüber hinaus gibt der zugewiesene CASE-Ausdruck in einer OrderDetails-Quellzeile einen Wert ('20210902') und in der anderen OrderDetails-Quellzeile einen anderen Wert ('20210903') zurück. Was soll SQL Server in diesem Fall tun? Wie bereits erwähnt, würde eine ähnliche Situation mit der MERGE-Anweisung zu einem Fehler führen und die versuchte Änderung zurückweisen. Doch mit einer UPDATE-Anweisung wirft SQL Server einfach eine Münze. Technisch geschieht dies über eine interne Aggregatfunktion namens ANY.
Unser Update wird also erfolgreich abgeschlossen und es wird eine betroffene Zeile gemeldet. Der Plan für diese Anweisung ist in Abbildung 2 dargestellt.
Abbildung 2:Plan für Update-Anweisung 2Das Ergebnis des Joins enthält zwei Zeilen. Diese beiden Zeilen werden die Quellzeilen für die Aktualisierung. Aber dann wählt ein Aggregatoperator, der die ANY-Funktion anwendet, einen (beliebigen) orderid-Wert und einen (beliebigen) unitprice-Wert aus diesen Quellzeilen aus. Beide Quellzeilen haben denselben orderid-Wert, sodass die richtige Reihenfolge geändert wird. Aber je nachdem, welchen der Quell-Einheitspreiswerte das ANY-Aggregat am Ende auswählt, bestimmt dies, welchen Wert der CASE-Ausdruck zurückgibt, um dann als aktualisierter Wert für das Bestelldatum in der Zielbestellung verwendet zu werden. Sie können sicherlich ein Argument gegen die Unterstützung eines solchen Updates sehen, aber es wird in SQL Server vollständig unterstützt.
Lassen Sie uns die Ansicht abfragen, um das Ergebnis dieser Änderung zu sehen (jetzt ist es an der Zeit, auf das Ergebnis zu wetten):
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Ich habe die folgende Ausgabe:
O_orderid bestelldatum versanddatum OD_orderid produkt-id menge stückpreis rabatt----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-09-03 NULL 6 1001 5 10,50 0,05006 2021-09-03 NULL 6 1002 5 20,00 0,0500Es wurde nur einer der beiden Quell-Einheitspreiswerte ausgewählt und verwendet, um das Bestelldatum der einzelnen Zielbestellung zu bestimmen, aber beim Abfragen der Ansicht wird der Bestelldatumswert für beide übereinstimmenden Bestellpositionen wiederholt. Wie Sie erkennen können, hätte das Ergebnis genauso gut ein anderes Datum (2021-09-02) sein können, da die Wahl des Einheitspreiswerts nicht deterministisch war. Verrücktes Zeug!
Daher sind unter bestimmten Bedingungen INSERT- und UPDATE-Anweisungen über Ansichten zulässig, die mehrere zugrunde liegende Tabellen verknüpfen. Löschungen gegen solche Ansichten sind jedoch nicht zulässig. Wie kann SQL Server feststellen, welche der Seiten das Ziel des Löschvorgangs sein soll?
Hier ist ein Versuch, eine solche Löschung über die Ansicht anzuwenden:
DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid =6;Dieser Versuch wird mit folgendem Fehler abgelehnt:
Msg 4405, Level 16, State 1, Line 377
Ansicht oder Funktion „dbo.OrdersOrderDetails“ kann nicht aktualisiert werden, da die Änderung mehrere Basistabellen betrifft.Führen Sie an dieser Stelle den folgenden Code zur Bereinigung aus:
DELETE FROM dbo.OrderDetails WHERE orderid =6;DELETE FROM dbo.Orders WHERE orderid =6;DROP VIEW IF EXISTS dbo.OrdersOrderDetails;Abgeleitete Spalten
Eine weitere Einschränkung für Änderungen durch Sichten hat mit abgeleiteten Spalten zu tun. Wenn eine Ansichtsspalte das Ergebnis einer Berechnung ist, versucht SQL Server nicht, ihre Formel zurückzuentwickeln, wenn Sie versuchen, Daten über die Ansicht einzufügen oder zu aktualisieren – vielmehr werden solche Änderungen abgelehnt.
Betrachten Sie die folgende Ansicht als Beispiel:
CREATE OR ALTER VIEW dbo.OrderDetailsNetPriceAS SELECT orderid, productid, qty, unitprice * (1.0 - discount) AS netunitprice, discount FROM dbo.OrderDetails;GODie Ansicht berechnet die netunitprice-Spalte basierend auf den zugrunde liegenden OrderDetails-Tabellenspalten unitprice und discount.
Ansicht abfragen:
SELECT * FROM dbo.OrderDetailsNetPrice;Sie erhalten die folgende Ausgabe:
Bestell-ID Produkt-ID Menge Nettostückpreis Rabatt ----------- ----------- ----------- --------- . 1006 2 11,316000 0,0800Versuchen Sie, eine Zeile durch die Ansicht einzufügen:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, discount) VALUES(1, 1005, 1, 28,595, 0,05);Theoretisch können Sie herausfinden, welche Zeile in die zugrunde liegende OrderDetails-Tabelle eingefügt werden muss, indem Sie den Einheitspreiswert der Basistabelle aus den Nettoeinheitspreis- und Rabattwerten der Ansicht zurückentwickeln. SQL Server versucht kein solches Reverse Engineering, weist aber die versuchte Einfügung mit dem folgenden Fehler zurück:
Msg 4406, Level 16, State 1, Line 412
Aktualisierung oder Einfügung der Ansicht oder Funktion 'dbo.OrderDetailsNetPrice' ist fehlgeschlagen, da sie ein abgeleitetes oder konstantes Feld enthält.Versuchen Sie, die berechnete Spalte aus der Einfügung wegzulassen:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, discount) VALUES(1, 1005, 1, 0.05);Jetzt sind wir wieder bei der Anforderung, dass alle Spalten aus der zugrunde liegenden Tabelle, die ihre Werte nicht irgendwie bekommen, automatisch Teil der Einfügung sein müssen, und hier fehlt uns die Einheitspreis-Spalte. Diese Einfügung schlägt mit folgendem Fehler fehl:
Msg 515, Level 16, State 2, Line 421
Cannot insert the value NULL into column 'unitprice', table 'tempdb.dbo.OrderDetails'; Spalte erlaubt keine Nullen. EINFÜGEN schlägt fehl.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;Zusammenfassung
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.