Questo articolo è l'undicesima parte di una serie sulle espressioni di tabella. Finora ho trattato tabelle derivate e CTE e recentemente ho iniziato la copertura delle visualizzazioni. Nella parte 9 ho confrontato le viste con tabelle derivate e CTE e nella parte 10 ho discusso le modifiche DDL e le implicazioni dell'utilizzo di SELECT * nella query interna della vista. In questo articolo, mi concentro sulle considerazioni sulla modifica.
Come probabilmente saprai, puoi modificare i dati nelle tabelle di base indirettamente tramite espressioni di tabelle con nome come le viste. Puoi controllare le autorizzazioni di modifica rispetto alle viste. In effetti, puoi concedere agli utenti le autorizzazioni per modificare i dati tramite le viste senza concedere loro le autorizzazioni per modificare direttamente le tabelle sottostanti.
È necessario essere consapevoli di alcune complessità e restrizioni che si applicano alle modifiche tramite viste. È interessante notare che alcune delle modifiche supportate possono portare a risultati sorprendenti, soprattutto se l'utente che modifica i dati non è consapevole che sta interagendo con una vista. Puoi imporre ulteriori restrizioni alle modifiche tramite le visualizzazioni utilizzando un'opzione chiamata VERIFICA OPZIONE, che tratterò in questo articolo. Come parte della copertura, descriverò una curiosa incoerenza tra il modo in cui l'OPZIONE CHECK in una vista e un vincolo CHECK in una tabella gestiscono le modifiche, in particolare quelle che coinvolgono NULL.
Dati di esempio
Come dati di esempio per questo articolo, utilizzerò tabelle denominate Orders e OrderDetails. Utilizzare il codice seguente per creare queste tabelle in tempdb e popolarle con alcuni dati di esempio iniziali:
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);
La tabella Orders contiene le intestazioni degli ordini e la tabella OrderDetails contiene le righe ordine. Gli ordini non spediti hanno un NULL nella colonna data di spedizione. Se preferisci un design che non utilizza NULL, puoi utilizzare una data futura specifica per gli ordini non spediti, ad esempio "99991231".
CONTROLLA OPZIONE
Per comprendere le circostanze in cui vorresti utilizzare l'OPZIONE VERIFICA come parte della definizione di una vista, esamineremo prima cosa può succedere quando non la usi.
Il codice seguente crea una vista denominata FastOrders che rappresenta gli ordini spediti entro sette giorni da quando sono stati effettuati:
CREATE OR ALTER VIEW dbo.FastOrders AS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <= 7; GO
Utilizza il seguente codice per inserire nella visualizzazione un ordine spedito due giorni dopo essere stato effettuato:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
Interroga la vista:
SELECT * FROM dbo.FastOrders;
Ottieni il seguente output, che include il nuovo ordine:
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
Interroga la tabella sottostante:
SELECT * FROM dbo.Orders;
Ottieni il seguente output, che include il nuovo ordine:
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
La riga è stata inserita nella tabella di base sottostante tramite la vista.
Successivamente, inserisci nella vista una riga spedita 10 giorni dopo essere stata inserita, contraddicendo il filtro di query interno della vista:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
L'istruzione si completa correttamente, riportando una riga interessata.
Interroga la vista:
SELECT * FROM dbo.FastOrders;
Ottieni il seguente output, che esclude il nuovo ordine:
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
Se sai che FastOrders è una vista, tutto ciò potrebbe sembrare sensato. Dopotutto, la riga è stata inserita nella tabella sottostante e non soddisfa il filtro di query interno della vista. Ma se non sai che FastOrders è una vista e non una tabella di base, questo comportamento sembrerebbe sorprendente.
Interroga la tabella Ordini sottostante:
SELECT * FROM dbo.Orders;
Ottieni il seguente output, che include il nuovo ordine:
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
Potresti riscontrare un comportamento sorprendente simile se aggiorni tramite la vista il valore di data di spedizione in una riga che fa attualmente parte della vista a una data che lo rende non più idoneo come parte della vista. Tale aggiornamento è normalmente consentito, ma ancora una volta avviene nella tabella di base sottostante. Se si interroga la vista dopo un tale aggiornamento, la riga modificata sembra essere sparita. In pratica è ancora lì nella tabella sottostante, semplicemente non è più considerato parte della vista.
Esegui il codice seguente per eliminare le righe aggiunte in precedenza:
DELETE FROM dbo.Orders WHERE orderid >= 6;
Se vuoi evitare modifiche che entrano in conflitto con il filtro della query interna della vista, aggiungi WITH CHECK OPTION alla fine della query interna come parte della definizione della vista, in questo modo:
CREATE OR ALTER VIEW dbo.FastOrders AS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <= 7 WITH CHECK OPTION; GO
Gli inserimenti e gli aggiornamenti attraverso la vista sono consentiti purché rispettino il filtro della query interna. In caso contrario, vengono rifiutati.
Ad esempio, utilizza il codice seguente per inserire nella visualizzazione una riga che non sia in conflitto con il filtro di query interno:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
La riga è stata aggiunta correttamente.
Tentativo di inserire una riga in conflitto con il filtro:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Questa volta la riga viene rifiutata con il seguente errore:
Livello 16, stato 1, riga 135Il tentativo di inserimento o aggiornamento non è riuscito perché la vista di destinazione specifica WITH CHECK OPTION o si estende su una vista che specifica WITH CHECK OPTION e una o più righe risultanti dall'operazione non sono state qualificate per CONTROLLO OPZIONE vincolo.
NULL Incoerenze
Se hai lavorato con T-SQL per un po' di tempo, probabilmente sei ben consapevole delle suddette complessità di modifica e serve la funzione CHECK OPTION. Spesso, anche le persone esperte trovano sorprendente la gestione NULL dell'OPZIONE CHECK. Per anni pensavo che l'OPZIONE CHECK serva alla stessa funzione di un vincolo CHECK nella definizione di una tabella di base. Questo è anche il modo in cui descrivevo questa opzione quando scrivevo o insegnavo a riguardo. In effetti, finché non ci sono NULL coinvolti nel predicato del filtro, è conveniente pensare ai due in termini simili. In questo caso si comportano in modo coerente, accettando righe che concordano con il predicato e rifiutando quelle che sono in conflitto con esso. Tuttavia, i due gestiscono i NULL in modo incoerente.
Quando si utilizza l'OPZIONE CHECK, è consentita una modifica attraverso la vista purché il predicato restituisca true, altrimenti viene rifiutato. Ciò significa che viene rifiutato quando il predicato della vista restituisce falso o sconosciuto (quando è coinvolto un NULL). Con un vincolo CHECK, la modifica è consentita quando il predicato del vincolo restituisce true o unknown e rifiutata quando il predicato restituisce false. Questa è una differenza interessante! Per prima cosa, vediamolo in azione, poi cercheremo di capire la logica dietro questa incoerenza.
Tentativo di inserire nella visualizzazione una riga con data di spedizione NULL:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
Il predicato della vista restituisce sconosciuto e la riga viene rifiutata con il seguente errore:
Msg 550, livello 16, stato 1, riga 147Il tentativo di inserimento o aggiornamento non è riuscito perché la vista di destinazione specifica WITH CHECK OPTION o si estende su una vista che specifica WITH CHECK OPTION e una o più righe risultanti dall'operazione non qualificarsi sotto il vincolo CHECK OPTION.
Proviamo un inserimento simile su una tabella di base con un vincolo CHECK. Utilizza il codice seguente per aggiungere un tale vincolo alla definizione della tabella dell'ordine:
ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(day, orderdate, shippeddate) <= 7);
Innanzitutto, per assicurarti che il vincolo funzioni quando non ci sono NULL coinvolti, prova a inserire il seguente ordine con una data di spedizione a 10 giorni dalla data dell'ordine:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Questo tentativo di inserimento viene rifiutato con il seguente errore:
Msg 547, livello 16, stato 0, riga 159L'istruzione INSERT era in conflitto con il vincolo CHECK "CHK_Orders_FastOrder". Il conflitto si è verificato nel database "tempdb", tabella "dbo.Orders".
Utilizza il codice seguente per inserire una riga con una data di spedizione NULL:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
Un vincolo CHECK dovrebbe rifiutare casi falsi, ma nel nostro caso il predicato restituisce sconosciuto, quindi la riga viene aggiunta correttamente.
Interroga la tabella Ordini:
SELECT * FROM dbo.Orders;
Puoi vedere il nuovo ordine nell'output:
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
Qual è la logica dietro questa incoerenza? Si potrebbe obiettare che un vincolo CHECK dovrebbe essere applicato solo quando il predicato del vincolo è chiaramente violato, ovvero quando risulta falso. In questo modo, se si sceglie di consentire NULL nella colonna in questione, le righe con NULL nella colonna sono consentite anche se il predicato del vincolo restituisce sconosciuto. Nel nostro caso, rappresentiamo gli ordini non spediti con un NULL nella colonna data di spedizione e consentiamo gli ordini non spediti nella tabella applicando la regola "ordini veloci" solo per gli ordini spediti.
L'argomento per utilizzare una logica diversa con una vista è che una modifica dovrebbe essere consentita attraverso la vista solo se la riga del risultato è una parte valida della vista. Se il predicato della vista restituisce sconosciuto, ad esempio quando la data di spedizione è NULL, la riga del risultato non è una parte valida della vista, quindi viene rifiutata. Solo le righe per le quali il predicato restituisce true sono una parte valida della vista e quindi consentite.
I NULL aggiungono molta complessità al linguaggio. Che ti piacciano o meno, se i tuoi dati li supportano, vuoi assicurarti di capire come T-SQL li gestisce.
A questo punto puoi eliminare il vincolo CHECK dalla tabella Ordini e anche eliminare la vista FastOrders per la pulizia:
ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder; DROP VIEW IF EXISTS dbo.FastOrders;
Limitazione TOP/OFFSET-FETCH
Sono normalmente consentite modifiche tramite viste che coinvolgono i filtri TOP e OFFSET-FETCH. Tuttavia, come con la nostra precedente discussione sulle viste definite senza l'OPZIONE CHECK, il risultato di tale modifica potrebbe sembrare strano all'utente se non sa che sta interagendo con una vista.
Considera la seguente vista che rappresenta gli ordini recenti come esempio:
CREATE OR ALTER VIEW dbo.RecentOrders AS SELECT TOP (5) orderid, orderdate, shippeddate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC; GO
Utilizza il codice seguente per inserire nella vista RecentOrders sei ordini:
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');
Interroga la vista:
SELECT * FROM dbo.RecentOrders;
Ottieni il seguente output:
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
Dei sei ordini inseriti, solo quattro fanno parte della vista. Questo sembra perfettamente sensato se sei consapevole che stai interrogando una vista basata su una query con un filtro TOP. Ma potrebbe sembrare strano se stai pensando di interrogare una tabella di base.
Interroga direttamente la tabella Ordini sottostante:
SELECT * FROM dbo.Orders;
Ottieni il seguente output che mostra tutti gli ordini aggiunti:
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
Se aggiungi CHECK OPTION alla definizione della vista, le istruzioni INSERT e UPDATE rispetto alla vista verranno rifiutate. Utilizza il codice seguente per applicare questa modifica:
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
Prova ad aggiungere un ordine tramite la vista:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210801', '20210805');
Viene visualizzato il seguente errore:
Msg 4427, livello 16, stato 1, riga 247Impossibile aggiornare la vista "dbo.RecentOrders" perché essa o una vista a cui fa riferimento è stata creata con WITH CHECK OPTION e la relativa definizione contiene una clausola TOP o OFFSET.
SQL Server non cerca di essere troppo intelligente qui. Rifiuterà la modifica anche se la riga che tenti di inserire diventerebbe una parte valida della vista a quel punto. Ad esempio, prova ad aggiungere un ordine con una data più recente che rientri tra i primi 5 a questo punto:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210904', '20210906');
Il tentativo di inserimento viene comunque rifiutato con il seguente errore:
Msg 4427, livello 16, stato 1, riga 254Impossibile aggiornare la vista "dbo.RecentOrders" perché essa o una vista a cui fa riferimento è stata creata con WITH CHECK OPTION e la relativa definizione contiene una clausola TOP o OFFSET.
Prova ad aggiornare una riga tramite la vista:
UPDATE dbo.RecentOrders SET shippeddate = DATEADD(day, 2, orderdate);
In questo caso anche il tentativo di modifica viene rifiutato con il seguente errore:
Msg 4427, livello 16, stato 1, riga 260Impossibile aggiornare la vista "dbo.RecentOrders" perché essa o una vista a cui fa riferimento è stata creata con WITH CHECK OPTION e la relativa definizione contiene una clausola TOP o OFFSET.
Tieni presente che la definizione di una vista basata su una query con TOP o OFFSET-FETCH e CHECK OPTION comporterà la mancanza di supporto per le istruzioni INSERT e UPDATE attraverso la vista.
Le eliminazioni tramite tale visualizzazione sono supportate. Esegui il codice seguente per eliminare tutti e cinque gli ordini più recenti correnti:
DELETE FROM dbo.RecentOrders;
Il comando viene completato correttamente.
Interroga la tabella:
SELECT * FROM dbo.Orders;
Ottieni il seguente output dopo l'eliminazione degli ordini con ID 8, 11, 12, 13 e 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
A questo punto, esegui il codice seguente per la pulizia prima di eseguire gli esempi nella sezione successiva:
DELETE FROM dbo.Orders WHERE orderid > 5; DROP VIEW IF EXISTS dbo.RecentOrders;
Unisciti
È supportato l'aggiornamento di una vista che unisce più tabelle, purché solo una delle tabelle di base sottostanti sia interessata dalla modifica.
Si consideri la seguente vista che unisce Orders e OrderDetails come esempio:
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
Prova a inserire una riga nella vista, in modo che siano interessate entrambe le tabelle di base sottostanti:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate, productid, qty, unitprice, discount) VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);
Viene visualizzato il seguente errore:
Msg 4405, livello 16, stato 1, riga 306La visualizzazione o la funzione 'dbo.OrdersOrderDetails' non è aggiornabile perché la modifica interessa più tabelle di base.
Prova a inserire una riga nella vista, così solo la tabella Ordini sarebbe interessata:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate) VALUES(6, '20210828', NULL);
Questo comando viene completato correttamente e la riga viene inserita nella tabella Ordini sottostante.
Ma cosa succede se si desidera anche essere in grado di inserire una riga nella vista nella tabella OrderDetails? Con la definizione della vista corrente, ciò è impossibile (invece dei trigger a parte) poiché la vista restituisce la colonna orderid dalla tabella Orders e non dalla tabella OrderDetails. È sufficiente che una colonna della tabella OrderDetails che non può in qualche modo ottenere il suo valore automaticamente non faccia parte della vista per impedire gli inserimenti in OrderDetails attraverso la vista. Naturalmente, puoi sempre decidere che la visualizzazione includerà sia orderid da Orders che orderid da OrderDetails. In tal caso, dovrai assegnare alle due colonne diversi alias poiché l'intestazione della tabella rappresentata dalla vista deve avere nomi di colonna univoci.
Utilizzare il codice seguente per modificare la definizione della vista in modo da includere entrambe le colonne, alias quella di Orders come O_orderid e quella di OrderDetails come 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
Ora puoi inserire righe nella vista sia in Orders che in OrderDetails, a seconda della tabella da cui proviene l'elenco delle colonne di destinazione. Ecco un esempio per inserire un paio di righe d'ordine associate all'ordine 6 attraverso la vista 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);
Le righe sono state aggiunte correttamente.
Interroga la vista:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Ottieni il seguente output:
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
Una restrizione simile è applicabile alle istruzioni UPDATE tramite la vista. Gli aggiornamenti sono consentiti purché sia interessata solo una tabella di base sottostante. Ma puoi fare riferimento alle colonne di entrambi i lati nell'istruzione purché solo un lato venga modificato.
Ad esempio, la seguente istruzione UPDATE nella visualizzazione imposta la data dell'ordine della riga in cui l'ID ordine della riga ordine è 6 e l'ID prodotto è compreso tra 1001 e "20210901:"
UPDATE dbo.OrdersOrderDetails SET orderdate = '20210901' WHERE OD_orderid = 6 AND productid = 1001;
Chiameremo questa affermazione Istruzione di aggiornamento 1.
L'aggiornamento viene completato correttamente con il seguente messaggio:
(1 row affected)
Ciò che è importante notare qui è l'istruzione che filtra in base agli elementi della tabella OrderDetails, ma la colonna orderdate modificata proviene dalla tabella Orders. Quindi, nel piano compilato da SQL Server per questa istruzione, deve capire quali ordini devono essere modificati nella tabella Ordini. Il piano per questa affermazione è mostrato nella Figura 1.
Figura 1:Piano per l'istruzione di aggiornamento 1
Puoi vedere come inizia il piano filtrando il lato OrderDetails sia per orderid =6 che productid =1001, e il lato Orders per orderid =6, unendo i due. Il risultato è solo una riga. L'unica parte rilevante da tenere lontano da questa attività è quali ID ordine nella tabella Ordini rappresentano le righe che devono essere aggiornate. Nel nostro caso, è l'ordine con ID ordine 6. Inoltre, l'operatore Compute Scalar prepara un membro chiamato Expr1002 con il valore che l'istruzione assegnerà alla colonna orderdate dell'ordine target. L'ultima parte del piano con l'operatore Clustered Index Update applica l'aggiornamento effettivo alla riga in Orders with order ID 6, impostando il valore orderdate su Espr1002.
Il punto chiave da sottolineare qui è che solo una riga con orderid 6 nella tabella Orders è stata aggiornata. Tuttavia, questa riga ha due corrispondenze nel risultato dell'unione con la tabella OrderDetails:una con ID prodotto 1001 (che l'aggiornamento originale ha filtrato) e un'altra con ID prodotto 1002 (che l'aggiornamento originale non ha filtrato). Interroga la vista a questo punto, filtrando tutte le righe con ID ordine 6:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Ottieni il seguente output:
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
Entrambe le righe mostrano la data del nuovo ordine, anche se l'aggiornamento originale ha filtrato solo la riga con ID prodotto 1001. Ancora una volta, questo dovrebbe sembrare perfettamente sensato se sai che stai interagendo con una vista che unisce due tabelle di base sotto le copertine, ma potrebbe sembrare molto strano se non te ne rendi conto.
Curiosamente, SQL Server supporta anche aggiornamenti non deterministici in cui più righe di origine (da OrderDetails nel nostro caso) corrispondono a una singola riga di destinazione (in Orders nel nostro caso). In teoria, un modo per gestire un caso del genere sarebbe rifiutarlo. Infatti, con un'istruzione MERGE in cui più righe di origine corrispondono a una riga di destinazione, SQL Server rifiuta il tentativo. Ma non con un UPDATE basato su un join, direttamente o indirettamente tramite un'espressione di tabella denominata come una vista. SQL Server lo gestisce semplicemente come un aggiornamento non deterministico.
Considera il seguente esempio, che chiameremo Dichiarazione 2:
UPDATE dbo.OrdersOrderDetails SET orderdate = CASE WHEN unitprice >= 20.00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid = 6;
Spero che mi perdonerai se è un esempio artificioso, ma illustra il punto.
Nella vista sono presenti due righe qualificanti, che rappresentano due righe di riga ordine di origine qualificanti dalla tabella OrderDetails sottostante. Ma c'è solo una riga target qualificante nella tabella Orders sottostante. Inoltre, in una riga OrderDetails di origine, l'espressione CASE assegnata restituisce un valore ('20210902') e nell'altra riga OrderDetails di origine restituisce un altro valore ('20210903'). Cosa dovrebbe fare SQL Server in questo caso? Come accennato, una situazione simile con l'istruzione MERGE comporterebbe un errore, rifiutando il tentativo di modifica. Tuttavia, con un'istruzione UPDATE, SQL Server lancia semplicemente una moneta. Tecnicamente, questo viene fatto utilizzando una funzione di aggregazione interna chiamata ANY.
Quindi, il nostro aggiornamento viene completato correttamente, segnalando 1 riga interessata. Il piano per questa affermazione è mostrato nella Figura 2.
Figura 2:Piano per l'istruzione di aggiornamento 2
Ci sono due righe nel risultato del join. Queste due righe diventano le righe di origine per l'aggiornamento. Ma poi un operatore aggregato che applica la funzione ANY seleziona un valore orderid (qualsiasi) e un valore unitprice (qualsiasi) da queste righe di origine. Entrambe le righe di origine hanno lo stesso valore orderid, quindi l'ordine corretto verrà modificato. Ma a seconda di quale dei valori di prezzo unitario di origine viene scelto dall'aggregato ANY, ciò determinerà quale valore restituirà l'espressione CASE, per essere quindi utilizzato come valore aggiornato della data dell'ordine nell'ordine di destinazione. Puoi sicuramente vedere un argomento contro il supporto di un tale aggiornamento, ma è completamente supportato in SQL Server.
Interroghiamo la vista per vedere il risultato di questa modifica (ora è il momento di fare la tua scommessa per quanto riguarda il risultato):
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Ho ottenuto il seguente output:
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
Solo uno dei due valori di prezzo unitario di origine è stato selezionato e utilizzato per determinare la data dell'ordine dell'ordine di destinazione singolo, tuttavia quando si esegue una query sulla vista, il valore della data dell'ordine viene ripetuto per entrambe le righe dell'ordine corrispondenti. Come puoi capire, il risultato avrebbe potuto benissimo essere l'altra data (2021-09-02) poiché la scelta del valore del prezzo unitario non era deterministica. Roba stravagante!
Pertanto, in determinate condizioni, le istruzioni INSERT e UPDATE sono consentite tramite viste che uniscono più tabelle sottostanti. Le eliminazioni, tuttavia, non sono consentite contro tali visualizzazioni. In che modo SQL Server può dire quale delle parti dovrebbe essere la destinazione per l'eliminazione?
Ecco un tentativo di applicare tale eliminazione tramite la vista:
DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Questo tentativo viene rifiutato con il seguente errore:
Msg 4405, livello 16, stato 1, riga 377La visualizzazione o la funzione 'dbo.OrdersOrderDetails' non è aggiornabile perché la modifica interessa più tabelle di base.
A questo punto, esegui il codice seguente per la pulizia:
DELETE FROM dbo.OrderDetails WHERE orderid = 6; DELETE FROM dbo.Orders WHERE orderid = 6; DROP VIEW IF EXISTS dbo.OrdersOrderDetails;
Colonne derivate
Un'altra restrizione alle modifiche tramite viste riguarda le colonne derivate. Se una colonna della vista è il risultato di un calcolo, SQL Server non tenterà di decodificare la sua formula quando tenti di inserire o aggiornare i dati tramite la vista, ma rifiuterà tali modifiche.
Considera la vista seguente come esempio:
CREATE OR ALTER VIEW dbo.OrderDetailsNetPrice AS SELECT orderid, productid, qty, unitprice * (1.0 - discount) AS netunitprice, discount FROM dbo.OrderDetails; GO
La vista calcola la colonna netunitprice in base alle colonne sottostanti della tabella OrderDetails prezzo unitario e sconto.
Interroga la vista:
SELECT * FROM dbo.OrderDetailsNetPrice;
Ottieni il seguente output:
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
Prova a inserire una riga nella vista:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, discount) VALUES(1, 1005, 1, 28.595, 0.05);
In teoria, puoi capire quale riga deve essere inserita nella tabella OrderDetails sottostante eseguendo il reverse engineering del valore unitprice della tabella di base dal netunitprice e dai valori di sconto della vista. SQL Server non tenta tale reverse engineering, ma rifiuta il tentativo di inserimento con il seguente errore:
Msg 4406, livello 16, stato 1, riga 412Aggiornamento o inserimento di vista o funzione 'dbo.OrderDetailsNetPrice' non riuscito perché contiene un campo derivato o costante.
Prova a omettere la colonna calcolata dall'inserimento:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, discount) VALUES(1, 1005, 1, 0.05);
Ora torniamo al requisito che tutte le colonne della tabella sottostante che in qualche modo non ottengono automaticamente i loro valori debbano far parte dell'inserimento e qui manca la colonna del prezzo unitario. Questo inserimento non riesce con il seguente errore:
Msg 515, Level 16, State 2, Line 421Cannot 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;
Riepilogo
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.