sql >> Database >  >> RDS >> Database

Grondbeginselen van tabeluitdrukkingen, deel 11 - Aanzichten, overwegingen bij wijzigingen

Dit artikel is het elfde deel in een serie over tabeluitdrukkingen. Tot dusverre heb ik afgeleide tabellen en CTE's behandeld en onlangs ben ik begonnen met de dekking van weergaven. In deel 9 vergeleek ik views met afgeleide tabellen en CTE's, en in deel 10 besprak ik DDL-wijzigingen en de implicaties van het gebruik van SELECT * in de innerlijke query van de view. In dit artikel richt ik me op wijzigingsoverwegingen.

Zoals u waarschijnlijk weet, mag u gegevens in basistabellen indirect wijzigen via benoemde tabeluitdrukkingen zoals views. U kunt wijzigingsmachtigingen voor weergaven beheren. U kunt gebruikers zelfs machtigingen verlenen om gegevens te wijzigen via weergaven zonder hen machtigingen te verlenen om de onderliggende tabellen rechtstreeks te wijzigen.

U moet wel op de hoogte zijn van bepaalde complexiteiten en beperkingen die van toepassing zijn op wijzigingen door middel van views. Interessant is dat sommige van de ondersteunde wijzigingen verrassende resultaten kunnen opleveren, vooral als de gebruiker die de gegevens aanpast, niet weet dat ze interactie hebben met een weergave. U kunt verdere beperkingen opleggen aan wijzigingen via weergaven door een optie met de naam CONTROLEER OPTIE te gebruiken, die ik in dit artikel zal behandelen. Als onderdeel van de dekking zal ik een merkwaardige inconsistentie beschrijven tussen hoe de CHECK OPTION in een weergave en een CHECK-beperking in een tabel wijzigingen verwerken, met name die met NULL's.

Voorbeeldgegevens

Als voorbeeldgegevens voor dit artikel gebruik ik tabellen met de naam Orders en OrderDetails. Gebruik de volgende code om deze tabellen in tempdb te maken en vul ze met enkele initiële voorbeeldgegevens:

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,
  shippeddate DATE NULL
);
 
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate)
  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 KEY(orderid, productid)
);
 
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);

De tabel Orders bevat orderkoppen en de tabel OrderDetails bevat orderregels. Niet-verzonden bestellingen hebben een NULL in de kolom met verzenddatum. Als u de voorkeur geeft aan een ontwerp dat geen NULL's gebruikt, kunt u een specifieke toekomstige datum gebruiken voor niet-verzonden bestellingen, zoals '99991231'.

CONTROLEER OPTIE

Om de omstandigheden te begrijpen waarin u de CONTROLE OPTIE zou willen gebruiken als onderdeel van de definitie van een weergave, zullen we eerst onderzoeken wat er kan gebeuren als u deze niet gebruikt.

De volgende code creëert een weergave met de naam FastOrders die bestellingen weergeeft die zijn verzonden binnen zeven dagen nadat ze zijn geplaatst:

CREATE OR ALTER VIEW dbo.FastOrders
AS
  SELECT orderid, orderdate, shippeddate
  FROM dbo.Orders
  WHERE DATEDIFF(day, orderdate, shippeddate) <= 7;
GO

Gebruik de volgende code om via de weergave een bestelling in te voegen die twee dagen na plaatsing is verzonden:

INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(6, '20210805', '20210807');

Vraag de weergave op:

SELECT * FROM dbo.FastOrders;

U krijgt de volgende uitvoer, inclusief de nieuwe bestelling:

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
6           2021-08-05 2021-08-07

Vraag de onderliggende tabel op:

SELECT * FROM dbo.Orders;

U krijgt de volgende uitvoer, inclusief de nieuwe bestelling:

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
4           2021-08-26 NULL
5           2021-08-27 NULL
6           2021-08-05 2021-08-07

De rij is via de weergave in de onderliggende basistabel ingevoegd.

Voeg vervolgens via de weergave een rij in die 10 dagen na plaatsing is verzonden, in tegenspraak met het innerlijke zoekfilter van de weergave:

INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(7, '20210805', '20210815');

De verklaring is succesvol voltooid, waarbij één regel wordt gerapporteerd.

Vraag de weergave op:

SELECT * FROM dbo.FastOrders;

U krijgt de volgende uitvoer, die de nieuwe bestelling uitsluit:

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
6           2021-08-05 2021-08-07

Als je weet dat FastOrders een weergave is, lijkt dit misschien allemaal verstandig. De rij is immers ingevoegd in de onderliggende tabel en voldoet niet aan het innerlijke queryfilter van de weergave. Maar als u niet weet dat FastOrders een weergave is en geen basistabel, lijkt dit gedrag verrassend.

Vraag de onderliggende Orders-tabel op:

SELECT * FROM dbo.Orders;

U krijgt de volgende uitvoer, inclusief de nieuwe bestelling:

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
4           2021-08-26 NULL
5           2021-08-27 NULL
6           2021-08-05 2021-08-07
7           2021-08-05 2021-08-15

U zou een soortgelijk verrassend gedrag kunnen ervaren als u via de weergave de waarde van de verzenddatum in een rij die momenteel deel uitmaakt van de weergave bijwerkt tot een datum waardoor deze niet meer in aanmerking komt als onderdeel van de weergave. Zo'n update is normaal gesproken toegestaan, maar nogmaals, deze vindt plaats in de onderliggende basistabel. Als u na een dergelijke update de weergave opvraagt, lijkt de gewijzigde rij verdwenen. In de praktijk is het er nog steeds in de onderliggende tabel, het wordt alleen niet meer beschouwd als onderdeel van de weergave.

Voer de volgende code uit om de rijen die u eerder hebt toegevoegd te verwijderen:

DELETE FROM dbo.Orders WHERE orderid >= 6;

Als u wijzigingen wilt voorkomen die in strijd zijn met het innerlijke queryfilter van de weergave, voegt u MET CONTROLE OPTIE toe aan het einde van de innerlijke query als onderdeel van de weergavedefinitie, zoals:

CREATE OR ALTER VIEW dbo.FastOrders
AS
  SELECT orderid, orderdate, shippeddate
  FROM dbo.Orders
  WHERE DATEDIFF(day, orderdate, shippeddate) <= 7
  WITH CHECK OPTION;
GO

Invoegingen en updates via de weergave zijn toegestaan ​​zolang ze voldoen aan het filter van de innerlijke query. Anders worden ze afgewezen.

Gebruik bijvoorbeeld de volgende code om via de weergave een rij in te voegen die niet conflicteert met het innerlijke queryfilter:

INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(6, '20210805', '20210807');

De rij is succesvol toegevoegd.

Poging om een ​​rij in te voegen die conflicteert met het filter:

INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(7, '20210805', '20210815');

Deze keer wordt de rij afgewezen met de volgende fout:

Niveau 16, Staat 1, Regel 135
De poging tot invoegen of bijwerken is mislukt omdat de doelweergave ofwel WITH CHECK OPTION specificeert ofwel een view omvat die WITH CHECK OPTION specificeert en een of meer rijen die het resultaat zijn van de bewerking niet in aanmerking kwamen onder de CONTROLEER OPTIE beperking.

NULL inconsistenties

Als je al een tijdje met T-SQL werkt, ben je waarschijnlijk goed op de hoogte van de bovengenoemde wijzigingscomplexiteiten en de functie CHECK OPTION dient. Vaak vinden zelfs ervaren mensen de NULL-behandeling van de CHECK OPTION verrassend. Jarenlang dacht ik aan de CHECK-OPTIE in een weergave die dezelfde functie vervult als een CHECK-beperking in de definitie van een basistabel. Dat is ook hoe ik deze optie beschreef toen ik erover schreef of les gaf. Inderdaad, zolang er geen NULL's betrokken zijn bij het filterpredikaat, is het handig om de twee in vergelijkbare termen te beschouwen. Ze gedragen zich in zo'n geval consequent:ze accepteren rijen die overeenkomen met het predikaat en verwerpen rijen die ermee in strijd zijn. De twee verwerken NULL's echter inconsistent.

Bij gebruik van de CONTROLEEROPTIE is een wijziging toegestaan ​​via de weergave zolang het predikaat waar is, anders wordt het afgewezen. Dit betekent dat het wordt afgewezen wanneer het predikaat van de weergave wordt geëvalueerd als onwaar of onbekend (wanneer er een NULL is). Met een CHECK-beperking is de wijziging toegestaan ​​wanneer het predikaat van de beperking waar of onbekend is, en afgewezen wanneer het predikaat onwaar is. Dat is een interessant verschil! Laten we dit eerst in actie zien, dan zullen we proberen de logica achter deze inconsistentie te achterhalen.

Poging om via de weergave een rij in te voegen met een NULL-verzenddatum:

INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(8, '20210828', NULL);

Het predikaat van de weergave evalueert tot onbekend en de rij wordt afgewezen met de volgende fout:

Msg 550, Level 16, State 1, Line 147
De poging tot invoegen of bijwerken is mislukt omdat de doelweergave ofwel WITH CHECK OPTION specificeert ofwel een view omvat die WITH CHECK OPTION specificeert en een of meer rijen die het resultaat zijn van de bewerking niet kwalificeren onder de beperking CHECK OPTION.

Laten we een soortgelijke invoeging proberen tegen een basistabel met een CHECK-beperking. Gebruik de volgende code om een ​​dergelijke beperking toe te voegen aan de tabeldefinitie van onze Order:

ALTER TABLE dbo.Orders
  ADD CONSTRAINT CHK_Orders_FastOrder
    CHECK(DATEDIFF(day, orderdate, shippeddate) <= 7);

Ten eerste, om ervoor te zorgen dat de beperking werkt als er geen NULL's bij betrokken zijn, probeert u de volgende bestelling in te voegen met een verzenddatum die 10 dagen verwijderd is van de besteldatum:

INSERT INTO dbo.Orders(orderid, orderdate, shippeddate)
  VALUES(7, '20210805', '20210815');

Deze invoegpoging wordt afgewezen met de volgende fout:

Msg 547, Level 16, State 0, Line 159
De INSERT-instructie was in strijd met de CHECK-beperking "CHK_Orders_FastOrder". Het conflict deed zich voor in database "tempdb", tabel "dbo.Orders".

Gebruik de volgende code om een ​​rij in te voegen met een NULL verzenddatum:

INSERT INTO dbo.Orders(orderid, orderdate, shippeddate)
  VALUES(8, '20210828', NULL);

Er wordt verondersteld dat een CHECK-beperking valse gevallen afwijst, maar in ons geval evalueert het predikaat naar onbekend, dus de rij is succesvol toegevoegd.

Vraag de tabel Bestellingen op:

SELECT * FROM dbo.Orders;

U kunt de nieuwe volgorde in de uitvoer zien:

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
4           2021-08-26 NULL
5           2021-08-27 NULL
6           2021-08-05 2021-08-07
8           2021-08-28 NULL

Wat is de logica achter deze inconsistentie? Je zou kunnen stellen dat een CHECK-beperking alleen moet worden afgedwongen wanneer het predikaat van de beperking duidelijk wordt geschonden, dat wil zeggen wanneer deze wordt geëvalueerd als onwaar. Op deze manier zijn, als u ervoor kiest om NULL's in de betreffende kolom toe te staan, rijen met NULL's in de kolom toegestaan, ook al evalueert het predikaat van de beperking als onbekend. In ons geval vertegenwoordigen we niet-verzonden bestellingen met een NULL in de kolom met verzenddatum, en we staan ​​niet-verzonden bestellingen toe in de tabel terwijl we de regel 'snelle bestellingen' alleen toepassen voor verzonden bestellingen.

Het argument om andere logica te gebruiken bij een weergave is dat een wijziging alleen door de weergave moet worden toegestaan ​​als de resultaatrij een geldig onderdeel van de weergave is. Als het predikaat van de weergave als onbekend evalueert, bijvoorbeeld wanneer de verzenddatum NULL is, is de resultaatrij geen geldig onderdeel van de weergave en wordt deze daarom afgewezen. Alleen rijen waarvoor het predikaat waar evalueert, zijn een geldig onderdeel van de weergave en zijn daarom toegestaan.

NULL's voegen veel complexiteit toe aan de taal. Of je ze nu leuk vindt of niet, als je gegevens ze ondersteunen, wil je er zeker van zijn dat je begrijpt hoe T-SQL ermee omgaat.

Op dit punt kunt u de beperking CHECK uit de tabel Bestellingen laten vallen en ook de weergave FastOrders laten vallen om op te schonen:

ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder;
DROP VIEW IF EXISTS dbo.FastOrders;

TOP/OFFSET-FETCH-beperking

Wijzigingen door middel van weergaven met de TOP- en OFFSET-FETCH-filters zijn normaal gesproken toegestaan. Echter, net als bij onze eerdere discussie over weergaven die zijn gedefinieerd zonder de CONTROLEEROPTIE, kan het resultaat van een dergelijke wijziging vreemd lijken voor de gebruiker als ze niet weten dat ze interactie hebben met een weergave.

Beschouw de volgende weergave als voorbeeld voor recente bestellingen:

CREATE OR ALTER VIEW dbo.RecentOrders
AS
  SELECT TOP (5) orderid, orderdate, shippeddate
  FROM dbo.Orders
  ORDER BY orderdate DESC, orderid DESC;
GO

Gebruik de volgende code om via de RecentOrders-weergave zes bestellingen in te voegen:

INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate)
  VALUES(9,  '20210801', '20210803'),
        (10, '20210802', '20210804'),
        (11, '20210829', '20210831'),
        (12, '20210830', '20210902'),
        (13, '20210830', '20210903'),
        (14, '20210831', '20210903');

Vraag de weergave op:

SELECT * FROM dbo.RecentOrders;

U krijgt de volgende uitvoer:

orderid     orderdate  shippeddate
----------- ---------- -----------
14          2021-08-31 2021-09-03
13          2021-08-30 2021-09-03
12          2021-08-30 2021-09-02
11          2021-08-29 2021-08-31
8           2021-08-28 NULL

Van de zes ingevoegde orders maken er slechts vier deel uit van de weergave. Dit lijkt volkomen logisch als u zich ervan bewust bent dat u een weergave zoekt die is gebaseerd op een zoekopdracht met een TOP-filter. Maar het lijkt misschien vreemd als je denkt dat je een basistabel opvraagt.

Vraag de onderliggende Orders-tabel direct op:

SELECT * FROM dbo.Orders;

U krijgt de volgende uitvoer met alle toegevoegde bestellingen:

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
4           2021-08-26 NULL
5           2021-08-27 NULL
6           2021-08-05 2021-08-07
8           2021-08-28 NULL
9           2021-08-01 2021-08-03
10          2021-08-02 2021-08-04
11          2021-08-29 2021-08-31
12          2021-08-30 2021-09-02
13          2021-08-30 2021-09-03
14          2021-08-31 2021-09-03

Als u de CHECK OPTION toevoegt aan de weergavedefinitie, worden de INSERT- en UPDATE-instructies voor de weergave afgewezen. Gebruik de volgende code om deze wijziging toe te passen:

CREATE OR ALTER VIEW dbo.RecentOrders
AS
  SELECT TOP (5) orderid, orderdate, shippeddate
  FROM dbo.Orders
  ORDER BY orderdate DESC, orderid DESC
  WITH CHECK OPTION;
GO

Probeer een bestelling toe te voegen via de weergave:

INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate)
  VALUES(15, '20210801', '20210805');

U krijgt de volgende foutmelding:

Msg 4427, niveau 16, staat 1, regel 247
Kan de weergave "dbo.RecentOrders" niet bijwerken omdat deze of een weergave waarnaar deze verwijst is gemaakt met WITH CHECK OPTION en de definitie ervan een TOP- of OFFSET-clausule bevat.

SQL Server probeert hier niet al te slim te zijn. Het zal de wijziging afwijzen, zelfs als de rij die u probeert in te voegen op dat moment een geldig onderdeel van de weergave zou worden. Probeer bijvoorbeeld een bestelling toe te voegen met een recentere datum die op dit moment in de top 5 zou vallen:

INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate)
  VALUES(15, '20210904', '20210906');

De poging tot invoeging wordt nog steeds afgewezen met de volgende fout:

Msg 4427, niveau 16, staat 1, regel 254
Kan de weergave "dbo.RecentOrders" niet bijwerken omdat deze of een weergave waarnaar deze verwijst is gemaakt met WITH CHECK OPTION en de definitie ervan een TOP- of OFFSET-clausule bevat.

Probeer een rij bij te werken via de weergave:

UPDATE dbo.RecentOrders
  SET shippeddate = DATEADD(day, 2, orderdate);

In dit geval wordt de wijzigingspoging ook afgewezen met de volgende foutmelding:

Msg 4427, niveau 16, staat 1, regel 260
Kan de weergave "dbo.RecentOrders" niet bijwerken omdat deze of een weergave waarnaar deze verwijst is gemaakt met WITH CHECK OPTION en de definitie ervan een TOP- of OFFSET-clausule bevat.

Houd er rekening mee dat het definiëren van een weergave op basis van een zoekopdracht met TOP of OFFSET-FETCH en de CHECK OPTION zal resulteren in het ontbreken van ondersteuning voor INSERT- en UPDATE-instructies via de weergave.

Verwijderingen via een dergelijke weergave worden ondersteund. Voer de volgende code uit om alle huidige vijf meest recente bestellingen te verwijderen:

DELETE FROM dbo.RecentOrders;

De opdracht is succesvol voltooid.

Zoek in de tabel:

SELECT * FROM dbo.Orders;

U krijgt de volgende uitvoer na het verwijderen van de bestellingen met ID's 8, 11, 12, 13 en 14.

orderid     orderdate  shippeddate
----------- ---------- -----------
1           2021-08-02 2021-08-04
2           2021-08-02 2021-08-05
3           2021-08-04 2021-08-06
4           2021-08-26 NULL
5           2021-08-27 NULL
6           2021-08-05 2021-08-07
9           2021-08-01 2021-08-03
10          2021-08-02 2021-08-04

Voer nu de volgende code uit om op te schonen voordat u de voorbeelden in de volgende sectie uitvoert:

DELETE FROM dbo.Orders WHERE orderid > 5;
 
DROP VIEW IF EXISTS dbo.RecentOrders;

Doe mee

Het bijwerken van een weergave die meerdere tabellen samenvoegt, wordt ondersteund, zolang slechts een van de onderliggende basistabellen door de wijziging wordt beïnvloed.

Beschouw de volgende weergave als voorbeeld voor Orders en OrderDetails:

CREATE OR ALTER VIEW dbo.OrdersOrderDetails
AS
  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;
GO

Probeer een rij door de weergave in te voegen, zodat beide onderliggende basistabellen worden beïnvloed:

INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate, productid, qty, unitprice, discount)
  VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);

U krijgt de volgende foutmelding:

Msg 4405, Level 16, State 1, Line 306
Weergave of functie 'dbo.OrdersOrderDetails' kan niet worden bijgewerkt omdat de wijziging van invloed is op meerdere basistabellen.

Probeer een rij door de weergave in te voegen, zodat alleen de tabel Orders wordt beïnvloed:

INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate)
  VALUES(6, '20210828', NULL);

Deze opdracht is succesvol voltooid en de rij wordt ingevoegd in de onderliggende Orders-tabel.

Maar wat als u ook een rij door de view wilt kunnen invoegen in de OrderDetails-tabel? Met de huidige weergavedefinitie is dit onmogelijk (in plaats van triggers opzij) omdat de weergave de kolom orderid retourneert uit de tabel Orders en niet uit de tabel OrderDetails. Het volstaat dat één kolom uit de OrderDetails-tabel die op de een of andere manier zijn waarde niet automatisch kan krijgen, geen deel uitmaakt van de weergave om invoegingen in OrderDetails via de weergave te voorkomen. U kunt natuurlijk altijd beslissen dat de weergave zowel orderid van Orders als orderid van OrderDetails zal bevatten. In een dergelijk geval moet u de twee kolommen verschillende aliassen toewijzen, aangezien de kop van de tabel die door de weergave wordt vertegenwoordigd, unieke kolomnamen moet hebben.

Gebruik de volgende code om de weergavedefinitie te wijzigen om beide kolommen op te nemen, alias die van Orders als O_orderid en die van OrderDetails als OD_orderid:

CREATE OR ALTER VIEW dbo.OrdersOrderDetails
AS
  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

U kunt nu rijen door de weergave invoegen in Orders of OrderDetails, afhankelijk van uit welke tabel de doelkolomlijst afkomstig is. Hier is een voorbeeld voor het invoegen van een aantal orderregels die zijn gekoppeld aan order 6 via de weergave 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);

De rijen zijn succesvol toegevoegd.

Vraag de weergave op:

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

U krijgt de volgende uitvoer:

O_orderid   orderdate  shippeddate OD_orderid  productid   qty  unitprice  discount
----------- ---------- ----------- ----------- ----------- ---- ---------- ---------
6           2021-08-28 NULL        6           1001        5    10.50      0.0500
6           2021-08-28 NULL        6           1002        5    20.00      0.0500

Een vergelijkbare beperking is van toepassing op UPDATE-instructies via de weergave. Updates zijn toegestaan ​​zolang er slechts één onderliggende basistabel wordt beïnvloed. Maar je mag in de verklaring naar kolommen van beide kanten verwijzen, zolang er maar één kant wordt gewijzigd.

Als voorbeeld stelt de volgende UPDATE-instructie via de weergave de besteldatum van de rij waarin de bestel-ID van de orderregel 6 is en de product-ID 1001 is in op "20210901:"

UPDATE dbo.OrdersOrderDetails
  SET orderdate = '20210901'
  WHERE OD_orderid = 6 AND productid = 1001;

We noemen dit statement Update statement 1.

De update is succesvol voltooid met het volgende bericht:

(1 row affected)

Wat hier belangrijk is om op te merken, zijn de instructiefilters op elementen uit de OrderDetails-tabel, maar de gewijzigde kolom orderdate komt uit de Orders-tabel. In het plan dat SQL Server voor dit statement maakt, moet het dus uitzoeken welke orders moeten worden gewijzigd in de tabel Orders. Het plan voor deze verklaring wordt getoond in figuur 1.

Figuur 1:Plan voor update-statement 1

U kunt zien hoe het plan begint door de OrderDetails-zijde te filteren op zowel orderid =6 als productid =1001, en de Orders-zijde op orderid =6, waardoor deze twee worden samengevoegd. Het resultaat is slechts één rij. Het enige relevante onderdeel dat u van deze activiteit moet onthouden, is welke order-ID's in de tabel Orders rijen vertegenwoordigen die moeten worden bijgewerkt. In ons geval is dit de order met order-ID 6. Bovendien bereidt de Compute Scalar-operator een lid voor met de naam Expr1002 met de waarde die de instructie toewijst aan de orderdatumkolom van de doelorder. Het laatste deel van het plan met de operator Clustered Index Update past de daadwerkelijke update toe op de rij in Orders met order-ID 6, waarbij de orderdatumwaarde wordt ingesteld op Expr1002.

Het belangrijkste punt dat hier moet worden benadrukt, is dat slechts één rij met orderid 6 in de tabel Orders is bijgewerkt. Toch heeft deze rij twee overeenkomsten in het resultaat van de join met de tabel OrderDetails:een met product-ID 1001 (die door de oorspronkelijke update is gefilterd) en een andere met product-ID 1002 (die niet door de oorspronkelijke update is gefilterd). Vraag op dit punt naar de weergave en filter alle rijen met order-ID 6:

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

U krijgt de volgende uitvoer:

O_orderid   orderdate  shippeddate OD_orderid  productid   qty  unitprice  discount
----------- ---------- ----------- ----------- ----------- ---- ---------- ---------
6           2021-09-01 NULL        6           1001        5    10.50      0.0500
6           2021-09-01 NULL        6           1002        5    20.00      0.0500

Beide rijen tonen de nieuwe besteldatum, ook al filterde de oorspronkelijke update alleen de rij met product-ID 1001. Nogmaals, dit zou volkomen logisch moeten lijken als je weet dat je interactie hebt met een weergave die twee basistabellen onder de omslagen verbindt, maar kan heel vreemd lijken als je dit niet beseft.

Vreemd genoeg ondersteunt SQL Server zelfs niet-deterministische updates waarbij meerdere bronrijen (van OrderDetails in ons geval) overeenkomen met een enkele doelrij (in Orders in ons geval). Theoretisch zou een manier om een ​​dergelijk geval aan te pakken zijn om het te verwerpen. Inderdaad, met een MERGE-instructie waarbij meerdere bronrijen overeenkomen met één doelrij, weigert SQL Server de poging. Maar niet met een UPDATE op basis van een join, direct of indirect via een benoemde tabelexpressie zoals een view. SQL Server behandelt het gewoon als een niet-deterministische update.

Beschouw het volgende voorbeeld, dat we stelling 2 zullen noemen:

UPDATE dbo.OrdersOrderDetails
    SET orderdate = CASE
                    WHEN unitprice >= 20.00 THEN '20210902'
                    ELSE '20210903'
                    END
  WHERE OD_orderid = 6;

Hopelijk vergeef je me dat het een gekunsteld voorbeeld is, maar het illustreert het punt.

Er zijn twee kwalificerende rijen in de weergave, die twee kwalificerende bronorderregelrijen vertegenwoordigen uit de onderliggende OrderDetails-tabel. Maar er is slechts één kwalificerende doelrij in de onderliggende Orders-tabel. Bovendien retourneert de toegewezen CASE-expressie in één bron OrderDetails-rij één waarde ('20210902') en in de andere bron OrderDetails-rij een andere waarde ('20210903'). Wat moet SQL Server in dit geval doen? Zoals vermeld, zou een vergelijkbare situatie met de MERGE-instructie resulteren in een fout, waardoor de poging tot wijziging wordt afgewezen. Maar met een UPDATE-instructie gooit SQL Server gewoon een munt op. Technisch gezien wordt dit gedaan met behulp van een interne aggregatiefunctie genaamd ANY.

Onze update is dus succesvol voltooid en meldt dat 1 rij is aangetast. Het plan voor deze verklaring wordt getoond in figuur 2.


Figuur 2:Plan voor update-statement 2

Er zijn twee rijen in het resultaat van de samenvoeging. Deze twee rijen worden de bronrijen voor de update. Maar dan kiest een aggregaatoperator die de ANY-functie toepast één (willekeurige) orderid-waarde en één (willekeurige) eenheidsprijswaarde uit deze bronrijen. Beide bronrijen hebben dezelfde orderid waarde, dus de juiste volgorde wordt aangepast. Maar afhankelijk van welke van de broneenheidsprijswaarden het ELKE aggregaat uiteindelijk kiest, zal dit bepalen welke waarde de CASE-expressie zal retourneren, om vervolgens te worden gebruikt als de bijgewerkte orderdatumwaarde in de doelvolgorde. Je kunt zeker een argument zien tegen het ondersteunen van een dergelijke update, maar het wordt volledig ondersteund in SQL Server.

Laten we de weergave opvragen om het resultaat van deze wijziging te zien (dit is het moment om uw weddenschap te plaatsen wat betreft de uitkomst):

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Ik kreeg de volgende uitvoer:

O_orderid   orderdate  shippeddate OD_orderid  productid   qty  unitprice  discount
----------- ---------- ----------- ----------- ----------- ---- ---------- ---------
6           2021-09-03 NULL        6           1001        5    10.50      0.0500
6           2021-09-03 NULL        6           1002        5    20.00      0.0500

Slechts één van de twee broneenheidsprijswaarden is gekozen en gebruikt om de besteldatum van de enkele doelorder te bepalen, maar bij het opvragen van de weergave wordt de besteldatumwaarde herhaald voor beide overeenkomende orderregels. Zoals u zich kunt realiseren, had de uitkomst net zo goed de andere datum (2021-09-02) kunnen zijn, aangezien de keuze van de eenheidsprijs niet-deterministisch was. Gekke dingen!

Dus onder bepaalde voorwaarden zijn INSERT- en UPDATE-statements toegestaan ​​via views die meerdere onderliggende tabellen samenvoegen. Verwijderingen zijn echter niet toegestaan ​​tegen dergelijke weergaven. Hoe kan SQL Server zien welke van de zijden het doel is voor het verwijderen?

Hier is een poging om een ​​dergelijke verwijdering toe te passen via de weergave:

DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Deze poging is afgewezen met de volgende fout:

Msg 4405, Level 16, State 1, Line 377
Weergave of functie 'dbo.OrdersOrderDetails' kan niet worden bijgewerkt omdat de wijziging van invloed is op meerdere basistabellen.

Voer nu de volgende code uit om op te schonen:

DELETE FROM dbo.OrderDetails WHERE orderid = 6;
DELETE FROM dbo.Orders WHERE orderid = 6;
DROP VIEW IF EXISTS dbo.OrdersOrderDetails;

Afgeleide kolommen

Een andere beperking voor wijzigingen via views heeft te maken met afgeleide kolommen. Als een weergavekolom het resultaat is van een berekening, zal SQL Server niet proberen de formule te reverse-engineeren wanneer u probeert gegevens in te voegen of bij te werken via de weergave, maar dergelijke wijzigingen afwijzen.

Beschouw de volgende weergave als voorbeeld:

CREATE OR ALTER VIEW dbo.OrderDetailsNetPrice
AS
  SELECT orderid, productid, qty, unitprice * (1.0 - discount) AS netunitprice, discount
  FROM dbo.OrderDetails;
GO

De weergave berekent de neteenheidsprijskolom op basis van de onderliggende OrderDetails-tabelkolommen eenheidsprijs en korting.

Vraag de weergave op:

SELECT * FROM dbo.OrderDetailsNetPrice;

U krijgt de volgende uitvoer:

orderid     productid   qty         netunitprice  discount
----------- ----------- ----------- ------------- ---------
1           1001        5           9.975000      0.0500
1           1004        2           20.000000     0.0000
2           1003        1           47.691000     0.1000
3           1001        1           9.975000      0.0500
3           1003        2           49.491000     0.1000
4           1001        2           9.975000      0.0500
4           1004        1           20.300000     0.0000
4           1005        1           28.595000     0.0500
5           1003        5           54.990000     0.0000
5           1006        2           11.316000     0.0800

Probeer een rij door de weergave in te voegen:

INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, discount)
  VALUES(1, 1005, 1, 28.595, 0.05);

Theoretisch kunt u erachter komen welke rij in de onderliggende OrderDetails-tabel moet worden ingevoegd door de waarde van de eenheidsprijs van de basistabel te reverse-engineeren op basis van de neteenheidsprijs en kortingswaarden van de view. SQL Server probeert dergelijke reverse-engineering niet, maar verwerpt de poging tot invoeging met de volgende fout:

Msg 4406, Level 16, State 1, Line 412
Bijwerken of invoegen van weergave of functie 'dbo.OrderDetailsNetPrice' is mislukt omdat deze een afgeleid of constant veld bevat.

Probeer de berekende kolom uit de invoeging weg te laten:

INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, discount)
  VALUES(1, 1005, 1, 0.05);

Nu zijn we terug bij de vereiste dat alle kolommen uit de onderliggende tabel die op de een of andere manier hun waarden niet automatisch krijgen, deel moeten uitmaken van de invoeging, en hier missen we de kolom eenheidsprijs. Deze invoeging mislukt met de volgende fout:

Msg 515, Level 16, State 2, Line 421
Cannot insert the value NULL into column 'unitprice', table 'tempdb.dbo.OrderDetails'; column does not allow nulls. INSERT fails.

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.Orders
AS
  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;

Samenvatting

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.


  1. Wat zijn databanken?

  2. ODBC gebruiken met Salesforce en Azure Active Directory (AD) Single Sign On (SSO)

  3. Langzame volgorde van zoekopdrachten door een kolom in een samengevoegde tabel

  4. Wat is sneller, SELECT DISTINCT of GROUP BY in MySQL?