Database
 sql >> Base de Dados >  >> RDS >> Database

Fundamentos de Expressões de Tabela, Parte 11 - Visualizações, Considerações de Modificação


Este artigo é a décima primeira parte de uma série sobre expressões de tabela. Até agora, cobri tabelas derivadas e CTEs e, recentemente, comecei a cobertura de visualizações. Na Parte 9, comparei visualizações com tabelas derivadas e CTEs e, na Parte 10, discuti as alterações de DDL e as implicações do uso de SELECT * na consulta interna da visualização. Neste artigo, concentro-me nas considerações de modificação.

Como você provavelmente sabe, você tem permissão para modificar dados em tabelas base indiretamente por meio de expressões de tabela nomeadas, como visualizações. Você pode controlar as permissões de modificação em visualizações. Na verdade, você pode conceder aos usuários permissões para modificar dados por meio de exibições sem conceder a eles permissões para modificar as tabelas subjacentes diretamente.

Você precisa estar ciente de certas complexidades e restrições que se aplicam a modificações por meio de visualizações. Curiosamente, algumas das modificações suportadas podem ter resultados surpreendentes, especialmente se o usuário que modifica os dados não estiver ciente de que está interagindo com uma visualização. Você pode impor mais restrições a modificações por meio de visualizações usando uma opção chamada CHECK OPTION, que abordarei neste artigo. Como parte da cobertura, descreverei uma curiosa inconsistência entre como a CHECK OPTION em uma view e uma restrição CHECK em uma tabela tratam de modificações — especificamente aquelas envolvendo NULLs.

Dados de amostra


Como dados de exemplo para este artigo, usarei tabelas chamadas Orders e OrderDetails. Use o código a seguir para criar essas tabelas no tempdb e preenchê-las com alguns dados de amostra iniciais:
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);

A tabela Orders contém cabeçalhos de pedidos e a tabela OrderDetails contém linhas de pedidos. Pedidos não enviados têm um NULL na coluna data de envio. Se você preferir um design que não use NULLs, poderá usar uma data futura específica para pedidos não enviados, como "99991231".

VERIFICAR OPÇÃO


Para entender as circunstâncias em que você deseja usar a OPÇÃO CHECK como parte da definição de uma visualização, primeiro examinaremos o que pode acontecer quando você não a usa.

O código a seguir cria uma visualização chamada FastOrders que representa os pedidos enviados dentro de sete dias desde que foram feitos:
CREATE OR ALTER VIEW dbo.FastOrders
AS
  SELECT orderid, orderdate, shippeddate
  FROM dbo.Orders
  WHERE DATEDIFF(day, orderdate, shippeddate) <= 7;
GO

Use o código a seguir para inserir na visualização um pedido enviado dois dias após ser feito:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(6, '20210805', '20210807');

Consulte a visualização:
SELECT * FROM dbo.FastOrders;

Você obtém a seguinte saída, que inclui o novo pedido:
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

Consulte a tabela subjacente:
SELECT * FROM dbo.Orders;

Você obtém a seguinte saída, que inclui o novo pedido:
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

A linha foi inserida na tabela base subjacente por meio da exibição.

Em seguida, insira pela visualização uma linha enviada 10 dias após ser colocada, contrariando o filtro de consulta interno da visualização:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(7, '20210805', '20210815');

A instrução é concluída com êxito, relatando uma linha afetada.

Consulte a visualização:
SELECT * FROM dbo.FastOrders;

Você obtém a seguinte saída, que exclui o novo pedido:
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 você sabe que FastOrders é uma visualização, tudo isso pode parecer sensato. Afinal, a linha foi inserida na tabela subjacente e não atende ao filtro de consulta interna da exibição. Mas se você não sabe que FastOrders é uma visão e não uma tabela base, esse comportamento parece surpreendente.

Consulte a tabela de pedidos subjacente:
SELECT * FROM dbo.Orders;

Você obtém a seguinte saída, que inclui o novo pedido:
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

Você pode experimentar um comportamento surpreendente semelhante se atualizar por meio da exibição o valor de data de envio em uma linha que atualmente faz parte da exibição para uma data que a torne não mais qualificada como parte da exibição. Essa atualização normalmente é permitida, mas, novamente, ocorre na tabela base subjacente. Se você consultar a exibição após essa atualização, a linha modificada parece ter desaparecido. Na prática, ele ainda está na tabela subjacente, apenas não é mais considerado parte da visão.

Execute o seguinte código para excluir as linhas que você adicionou anteriormente:
DELETE FROM dbo.Orders WHERE orderid >= 6;

Se você quiser evitar modificações que entrem em conflito com o filtro de consulta interna da visualização, adicione WITH CHECK OPTION no final da consulta interna como parte da definição da visualização, assim:
CREATE OR ALTER VIEW dbo.FastOrders
AS
  SELECT orderid, orderdate, shippeddate
  FROM dbo.Orders
  WHERE DATEDIFF(day, orderdate, shippeddate) <= 7
  WITH CHECK OPTION;
GO

Inserções e atualizações através da visualização são permitidas desde que estejam em conformidade com o filtro da consulta interna. Caso contrário, são rejeitados.

Por exemplo, use o código a seguir para inserir na exibição uma linha que não entre em conflito com o filtro de consulta interna:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(6, '20210805', '20210807');

A linha foi adicionada com sucesso.

Tente inserir uma linha que entre em conflito com o filtro:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(7, '20210805', '20210815');

Desta vez, a linha é rejeitada com o seguinte erro:
Nível 16, Estado 1, Linha 135
A tentativa de inserção ou atualização falhou porque a visualização de destino especifica WITH CHECK OPTION ou abrange uma visualização que especifica WITH CHECK OPTION e uma ou mais linhas resultantes da operação não se qualificaram sob o A restrição CHECK OPTION.

Inconsistências nulas


Se você trabalha com T-SQL há algum tempo, provavelmente está ciente das complexidades de modificação mencionadas acima e a função CHECK OPTION serve. Muitas vezes, mesmo pessoas experientes acham surpreendente o tratamento NULL da OPÇÃO CHECK. Por anos eu costumava pensar na OPÇÃO CHECK em uma visão como servindo a mesma função que uma restrição CHECK na definição de uma tabela base. Também é assim que eu costumava descrever essa opção ao escrever ou ensinar sobre ela. De fato, desde que não haja NULLs envolvidos no predicado do filtro, é conveniente pensar nos dois em termos semelhantes. Eles se comportam de forma consistente nesse caso – aceitando linhas que concordam com o predicado e rejeitando aquelas que entram em conflito com ele. No entanto, os dois manipulam NULLs de forma inconsistente.

Ao usar a OPÇÃO CHECK, uma modificação é permitida através da visão desde que o predicado seja avaliado como verdadeiro, caso contrário é rejeitado. Isso significa que é rejeitado quando o predicado da visão é avaliado como falso ou desconhecido (quando um NULL está envolvido). Com uma restrição CHECK, a modificação é permitida quando o predicado da restrição é avaliado como verdadeiro ou desconhecido e rejeitado quando o predicado é avaliado como falso. Essa é uma diferença interessante! Primeiro, vamos ver isso em ação, depois tentaremos descobrir a lógica por trás dessa inconsistência.

Tente inserir através da visualização uma linha com uma data de envio NULL:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate)
  VALUES(8, '20210828', NULL);

O predicado da visualização é avaliado como desconhecido e a linha é rejeitada com o seguinte erro:
Msg 550, Level 16, State 1, Line 147
A tentativa de inserção ou atualização falhou porque a visualização de destino especifica WITH CHECK OPTION ou abrange uma visualização que especifica WITH CHECK OPTION e uma ou mais linhas resultantes da operação não se qualificar sob a restrição CHECK OPTION.
Vamos tentar uma inserção semelhante em uma tabela base com uma restrição CHECK. Use o código a seguir para adicionar essa restrição à definição da tabela do nosso pedido:
ALTER TABLE dbo.Orders
  ADD CONSTRAINT CHK_Orders_FastOrder
    CHECK(DATEDIFF(day, orderdate, shippeddate) <= 7);

Primeiro, para garantir que a restrição funcione quando não houver NULLs envolvidos, tente inserir o seguinte pedido com uma data de envio 10 dias antes da data do pedido:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate)
  VALUES(7, '20210805', '20210815');

Esta tentativa de inserção é rejeitada com o seguinte erro:
Msg 547, Level 16, State 0, Line 159
A instrução INSERT entrou em conflito com a restrição CHECK "CHK_Orders_FastOrder". O conflito ocorreu no banco de dados "tempdb", tabela "dbo.Orders".
Use o código a seguir para inserir uma linha com uma data de envio NULL:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate)
  VALUES(8, '20210828', NULL);

Uma restrição CHECK deve rejeitar casos falsos, mas em nosso caso o predicado é avaliado como desconhecido, então a linha é adicionada com sucesso.

Consulte a tabela Pedidos:
SELECT * FROM dbo.Orders;

Você pode ver a nova ordem na saída:
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 é a lógica por trás dessa inconsistência? Você pode argumentar que uma restrição CHECK só deve ser aplicada quando o predicado da restrição for claramente violado, ou seja, quando for avaliado como falso. Dessa forma, se você optar por permitir NULLs na coluna em questão, as linhas com NULLs na coluna serão permitidas mesmo que o predicado da restrição seja avaliado como desconhecido. Em nosso caso, representamos pedidos não enviados com um NULL na coluna data de envio e permitimos pedidos não enviados na tabela enquanto aplicamos a regra de “pedidos rápidos” apenas para pedidos enviados.

O argumento para usar uma lógica diferente com uma visão é que uma modificação deve ser permitida por meio da visão somente se a linha do resultado for uma parte válida da visão. Se o predicado da visualização for avaliado como desconhecido, por exemplo, quando a data de envio for NULL, a linha do resultado não será uma parte válida da visualização, portanto, será rejeitada. Somente as linhas para as quais o predicado é avaliado como true são uma parte válida da exibição e, portanto, permitidas.

NULLs adicionam muita complexidade à linguagem. Goste deles ou não, se seus dados os suportarem, você quer ter certeza de que entende como o T-SQL os trata.

Neste ponto, você pode descartar a restrição CHECK da tabela Orders e também descartar a visualização FastOrders para limpeza:
ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder;
DROP VIEW IF EXISTS dbo.FastOrders;

Restrição TOP/OFFSET-FETCH


Normalmente são permitidas modificações através de visualizações envolvendo os filtros TOP e OFFSET-FETCH. No entanto, como em nossa discussão anterior sobre visualizações definidas sem a OPÇÃO DE VERIFICAÇÃO, o resultado de tal modificação pode parecer estranho para o usuário se ele não souber que está interagindo com uma visualização.

Considere a seguinte visualização representando pedidos recentes como exemplo:
CREATE OR ALTER VIEW dbo.RecentOrders
AS
  SELECT TOP (5) orderid, orderdate, shippeddate
  FROM dbo.Orders
  ORDER BY orderdate DESC, orderid DESC;
GO

Use o código a seguir para inserir seis pedidos na visualização RecentOrders:
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');

Consulte a visualização:
SELECT * FROM dbo.RecentOrders;

Você obtém a seguinte saída:
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

Dos seis pedidos inseridos, apenas quatro fazem parte da visualização. Isso parece perfeitamente sensato se você estiver ciente de que está consultando uma visualização baseada em uma consulta com um filtro TOP. Mas pode parecer estranho se você estiver pensando que está consultando uma tabela base.

Consulte diretamente a tabela Orders subjacente:
SELECT * FROM dbo.Orders;

Você obtém a seguinte saída mostrando todos os pedidos adicionados:
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 você adicionar CHECK OPTION à definição de exibição, as instruções INSERT e UPDATE contra a exibição serão rejeitadas. Use o código a seguir para aplicar essa alteração:
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

Tente adicionar um pedido através da visualização:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate)
  VALUES(15, '20210801', '20210805');

Você recebe o seguinte erro:
Msg 4427, Level 16, State 1, Line 247
Não é possível atualizar a view "dbo.RecentOrders" porque ela ou uma view que ela referencia foi criada com WITH CHECK OPTION e sua definição contém uma cláusula TOP ou OFFSET.
O SQL Server não tenta ser muito inteligente aqui. Ele rejeitará a alteração mesmo que a linha que você tentar inserir se torne uma parte válida da exibição nesse ponto. Por exemplo, tente adicionar um pedido com uma data mais recente que cairia no top 5 neste momento:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate)
  VALUES(15, '20210904', '20210906');

A tentativa de inserção ainda é rejeitada com o seguinte erro:
Msg 4427, Level 16, State 1, Line 254
Não é possível atualizar a view "dbo.RecentOrders" porque ela ou uma view que ela referencia foi criada com WITH CHECK OPTION e sua definição contém uma cláusula TOP ou OFFSET.
Tente atualizar uma linha pela view:
UPDATE dbo.RecentOrders
  SET shippeddate = DATEADD(day, 2, orderdate);

Nesse caso, a tentativa de alteração também é rejeitada com o seguinte erro:
Msg 4427, Level 16, State 1, Line 260
Não é possível atualizar a view "dbo.RecentOrders" porque ela ou uma view que ela referencia foi criada com WITH CHECK OPTION e sua definição contém uma cláusula TOP ou OFFSET.
Esteja ciente de que definir uma exibição com base em uma consulta com TOP ou OFFSET-FETCH e CHECK OPTION resultará na falta de suporte para instruções INSERT e UPDATE por meio da exibição.

Exclusões por meio de tal exibição são suportadas. Execute o seguinte código para excluir todos os cinco pedidos mais recentes atuais:
DELETE FROM dbo.RecentOrders;

O comando é concluído com sucesso.

Consulte a tabela:
SELECT * FROM dbo.Orders;

Você obtém a seguinte saída após a exclusão dos pedidos com IDs 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

Neste ponto, execute o seguinte código para limpeza antes de executar os exemplos na próxima seção:
DELETE FROM dbo.Orders WHERE orderid > 5;
 
DROP VIEW IF EXISTS dbo.RecentOrders;

Participações


A atualização de uma exibição que une várias tabelas é suportada, desde que apenas uma das tabelas base subjacentes seja afetada pela alteração.

Considere a seguinte visualização juntando Orders e OrderDetails como um exemplo:
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

Tente inserir uma linha por meio da exibição, para que ambas as tabelas base subjacentes sejam afetadas:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate, productid, qty, unitprice, discount)
  VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);

Você recebe o seguinte erro:
Msg 4405, Level 16, State 1, Line 306
Exibição ou função 'dbo.OrdersOrderDetails' não é atualizável porque a modificação afeta várias tabelas base.
Tente inserir uma linha pela view, para que apenas a tabela Orders seja afetada:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate)
  VALUES(6, '20210828', NULL);

Esse comando é concluído com êxito e a linha é inserida na tabela de Pedidos subjacente.

Mas e se você também quiser inserir uma linha através da visualização na tabela OrderDetails? Com a definição de exibição atual, isso é impossível (em vez de gatilhos à parte), pois a exibição retorna a coluna orderid da tabela Orders e não da tabela OrderDetails. Basta que uma coluna da tabela OrderDetails que não pode de alguma forma obter seu valor automaticamente não faça parte da exibição para evitar inserções em OrderDetails por meio da exibição. Claro, você sempre pode decidir que a exibição incluirá orderid de Orders e orderid de OrderDetails. Nesse caso, você terá que atribuir as duas colunas com aliases diferentes, pois o cabeçalho da tabela representada pela exibição deve ter nomes de coluna exclusivos.

Use o código a seguir para alterar a definição de exibição para incluir ambas as colunas, alias a de Orders como O_orderid e a de OrderDetails como 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

Agora você pode inserir linhas através da exibição para Orders ou OrderDetails, dependendo de qual tabela a lista de colunas de destino está vindo. Aqui está um exemplo para inserir algumas linhas de pedido associadas ao pedido 6 por meio da exibição em 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);

As linhas são adicionadas com sucesso.

Consulte a visualização:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Você obtém a seguinte saída:
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

Uma restrição semelhante é aplicável a instruções UPDATE por meio da exibição. As atualizações são permitidas desde que apenas uma tabela base subjacente seja afetada. Mas você tem permissão para fazer referência a colunas de ambos os lados da instrução, desde que apenas um lado seja modificado.

Como exemplo, a seguinte instrução UPDATE por meio da exibição define a data do pedido da linha em que o ID do pedido da linha do pedido é 6 e o ​​ID do produto é 1001 a “20210901:”
UPDATE dbo.OrdersOrderDetails
  SET orderdate = '20210901'
  WHERE OD_orderid = 6 AND productid = 1001;

Chamaremos essa instrução de instrução de atualização 1.

A atualização é concluída com sucesso com a seguinte mensagem:
(1 row affected)

O que é importante notar aqui é que a instrução filtra por elementos da tabela OrderDetails, mas a coluna modificada orderdate é da tabela Orders. Portanto, no plano que o SQL Server cria para essa instrução, ele precisa descobrir quais pedidos precisam ser modificados na tabela Pedidos. O plano para esta declaração é mostrado na Figura 1.

Figura 1:planejar a instrução de atualização 1

Você pode ver como o plano começa filtrando o lado OrderDetails por orderid =6 e productid =1001, e o lado Orders por orderid =6, juntando os dois. O resultado é apenas uma linha. A única parte relevante a evitar dessa atividade é quais IDs de pedidos na tabela Pedidos representam as linhas que precisam ser atualizadas. No nosso caso, é o pedido com o ID do pedido 6. Além disso, o operador Compute Scalar prepara um membro chamado Expr1002 com o valor que a instrução atribuirá à coluna orderdate do pedido de destino. A última parte do plano com o operador Clustered Index Update aplica a atualização real à linha em Orders with order ID 6, definindo seu valor orderdate como Expr1002.

O ponto chave para enfatizar aqui é que apenas uma linha com orderid 6 na tabela Orders foi atualizada. No entanto, essa linha tem duas correspondências no resultado da junção com a tabela OrderDetails — uma com a ID do produto 1001 (que a atualização original filtrou) e outra com a ID do produto 1002 (que a atualização original não filtrou). Consulte a visualização neste momento, filtrando todas as linhas com o ID do pedido 6:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Você obtém a seguinte saída:
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

Ambas as linhas mostram a nova data do pedido, mesmo que a atualização original tenha filtrado apenas a linha com o ID do produto 1001. Mais uma vez, isso deve parecer perfeitamente sensato se você souber que está interagindo com uma exibição que une duas tabelas base sob as capas, mas pode parecer muito estranho se você não perceber isso.

Curiosamente, o SQL Server ainda oferece suporte a atualizações não determinísticas em que várias linhas de origem (de OrderDetails em nosso caso) correspondem a uma única linha de destino (em Orders em nosso caso). Teoricamente, uma maneira de lidar com esse caso seria rejeitá-lo. De fato, com uma instrução MERGE em que várias linhas de origem correspondem a uma linha de destino, o SQL Server rejeita a tentativa. Mas não com um UPDATE baseado em uma junção, seja direta ou indiretamente por meio de uma expressão de tabela nomeada como uma visão. O SQL Server simplesmente o trata como uma atualização não determinística.

Considere o exemplo a seguir, que chamaremos de Declaração 2:
UPDATE dbo.OrdersOrderDetails
    SET orderdate = CASE
                    WHEN unitprice >= 20.00 THEN '20210902'
                    ELSE '20210903'
                    END
  WHERE OD_orderid = 6;

Espero que você me perdoe por ser um exemplo artificial, mas ilustra o ponto.

Há duas linhas qualificadas na exibição, representando duas linhas de linha de ordem de origem qualificadas da tabela OrderDetails subjacente. Mas há apenas uma linha de destino qualificada na tabela de Pedidos subjacente. Além disso, em uma linha OrderDetails de origem, a expressão CASE atribuída retorna um valor ('20210902') e na outra linha OrderDetails de origem ela retorna outro valor ('20210903'). O que o SQL Server deve fazer neste caso? Conforme mencionado, uma situação semelhante com a instrução MERGE resultaria em um erro, rejeitando a tentativa de alteração. No entanto, com uma instrução UPDATE, o SQL Server simplesmente joga uma moeda. Tecnicamente, isso é feito usando uma função agregada interna chamada ANY.

Portanto, nossa atualização foi concluída com sucesso, relatando 1 linha afetada. O plano para esta declaração é mostrado na Figura 2.


Figura 2:planejar a instrução de atualização 2

Há duas linhas no resultado da junção. Essas duas linhas se tornam as linhas de origem da atualização. Mas então um operador agregado que aplica a função ANY escolhe um (qualquer) valor orderid e um (qualquer) valor unitprice dessas linhas de origem. Ambas as linhas de origem têm o mesmo valor de orderid, portanto, a ordem correta será modificada. Mas dependendo de qual dos valores de preço unitário de origem o agregado ANY acaba escolhendo, isso determinará qual valor a expressão CASE retornará, para então ser usado como o valor de orderdate atualizado no pedido de destino. Você certamente pode ver um argumento contra o suporte a essa atualização, mas é totalmente compatível com o SQL Server.

Vamos consultar a visualização para ver o resultado dessa mudança (agora é a hora de fazer sua aposta quanto ao resultado):
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Obtive a seguinte saída:
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

Apenas um dos dois valores de preço unitário de origem foi escolhido e usado para determinar a data do pedido do único pedido de destino, mas ao consultar a exibição, o valor de datedate é repetido para ambas as linhas do pedido correspondentes. Como você pode perceber, o resultado poderia ter sido a outra data (2021-09-02), já que a escolha do valor do preço unitário não foi determinística. Coisas malucas!

Portanto, sob certas condições, as instruções INSERT e UPDATE são permitidas por meio de exibições que unem várias tabelas subjacentes. Exclusões, no entanto, não são permitidas em tais visualizações. Como o SQL Server pode dizer qual dos lados deve ser o destino da exclusão?

Aqui está uma tentativa de aplicar tal exclusão por meio da exibição:
DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Esta tentativa é rejeitada com o seguinte erro:
Msg 4405, Level 16, State 1, Line 377
A exibição ou função 'dbo.OrdersOrderDetails' não é atualizável porque a modificação afeta várias tabelas base.
Neste ponto, execute o seguinte código para limpeza:
DELETE FROM dbo.OrderDetails WHERE orderid = 6;
DELETE FROM dbo.Orders WHERE orderid = 6;
DROP VIEW IF EXISTS dbo.OrdersOrderDetails;

Colunas derivadas


Outra restrição às modificações por meio de visualizações tem a ver com colunas derivadas. Se uma coluna de exibição for resultado de um cálculo, o SQL Server não tentará fazer engenharia reversa de sua fórmula quando você tentar inserir ou atualizar dados por meio da exibição - em vez disso, ele rejeitará essas modificações.

Considere a seguinte visão como um exemplo:
CREATE OR ALTER VIEW dbo.OrderDetailsNetPrice
AS
  SELECT orderid, productid, qty, unitprice * (1.0 - discount) AS netunitprice, discount
  FROM dbo.OrderDetails;
GO

A exibição calcula a coluna netunitprice com base nas colunas subjacentes da tabela OrderDetails unitprice e discount.

Consulte a visualização:
SELECT * FROM dbo.OrderDetailsNetPrice;

Você obtém a seguinte saída:
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

Tente inserir uma linha pela view:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, discount)
  VALUES(1, 1005, 1, 28.595, 0.05);

Teoricamente, você pode descobrir qual linha precisa ser inserida na tabela OrderDetails subjacente por engenharia reversa do valor unitprice da tabela base a partir dos valores netunitprice e de desconto da exibição. O SQL Server não tenta essa engenharia reversa, mas rejeita a tentativa de inserção com o seguinte erro:
Msg 4406, Level 16, State 1, Line 412
Falha na atualização ou inserção da exibição ou função 'dbo.OrderDetailsNetPrice' porque contém um campo derivado ou constante.
Tente omitir a coluna computada da inserção:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, discount)
  VALUES(1, 1005, 1, 0.05);

Agora estamos de volta ao requisito de que todas as colunas da tabela subjacente que de alguma forma não obtêm seus valores automaticamente precisam fazer parte da inserção, e aqui estamos perdendo a coluna de preço unitário. Esta inserção falha com o seguinte erro:
Msg 515, Level 16, State 2, Line 421
Não é possível inserir o valor NULL na coluna 'unitprice', tabela 'tempdb.dbo.OrderDetails'; coluna não permite nulos. INSERIR falha.
Se você quiser dar suporte a inserções através da visualização, você basicamente tem duas opções. Uma é incluir a coluna preço unitário na definição da visão. Outra é criar um gatilho em vez de na exibição onde você mesmo manipula a lógica de engenharia reversa.

Neste ponto, execute o seguinte código para limpeza:
DROP VIEW IF EXISTS dbo.OrderDetailsNetPrice;

Operadores de conjunto


Conforme mencionado na última seção, você não tem permissão para modificar uma coluna em uma exibição se a coluna for resultado de um cálculo. 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;

Resumo


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.