sql >> Base de Datos >  >> RDS >> Database

Fundamentos de las expresiones de tabla, parte 11:vistas, consideraciones de modificación

Este artículo es la undécima parte de una serie sobre expresiones de tablas. Hasta ahora, he cubierto tablas derivadas y CTE, y recientemente comencé la cobertura de vistas. En la Parte 9 comparé vistas con tablas derivadas y CTE, y en la Parte 10 analicé los cambios de DDL y las implicaciones de usar SELECT * en la consulta interna de la vista. En este artículo, me centro en las consideraciones de modificación.

Como probablemente sepa, puede modificar datos en tablas base indirectamente a través de expresiones de tablas con nombre como vistas. Puede controlar los permisos de modificación contra las vistas. De hecho, puede otorgar a los usuarios permisos para modificar datos a través de vistas sin otorgarles permisos para modificar las tablas subyacentes directamente.

Debe tener en cuenta ciertas complejidades y restricciones que se aplican a las modificaciones a través de vistas. Curiosamente, algunas de las modificaciones admitidas pueden tener resultados sorprendentes, especialmente si el usuario que modifica los datos no sabe que está interactuando con una vista. Puede imponer más restricciones a las modificaciones a través de las vistas utilizando una opción llamada OPCIÓN DE COMPROBACIÓN, que trataré en este artículo. Como parte de la cobertura, describiré una curiosa inconsistencia entre cómo CHECK OPTION en una vista y una restricción CHECK en una tabla manejan las modificaciones, específicamente las que involucran valores NULL.

Datos de muestra

Como datos de muestra para este artículo, usaré tablas llamadas Pedidos y Detalles del pedido. Use el siguiente código para crear estas tablas en tempdb y complételas con algunos datos de muestra iniciales:

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 tabla Pedidos contiene encabezados de pedidos y la tabla Detalles de pedidos contiene líneas de pedidos. Los pedidos no enviados tienen un NULL en la columna de fecha de envío. Si prefiere un diseño que no use NULL, puede usar una fecha futura específica para pedidos no enviados, como "99991231".

CONSULTAR OPCIÓN

Para comprender las circunstancias en las que le gustaría usar la OPCIÓN DE COMPROBACIÓN como parte de la definición de una vista, primero examinaremos qué puede suceder cuando no la usa.

El siguiente código crea una vista llamada FastOrders que representa los pedidos enviados dentro de los siete días desde que se colocaron:

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

Utilice el siguiente código para insertar a través de la vista un pedido enviado dos días después de haber sido realizado:

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

Consultar la vista:

SELECT * FROM dbo.FastOrders;

Obtiene el siguiente resultado, que incluye el nuevo 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

Consulta la tabla subyacente:

SELECT * FROM dbo.Orders;

Obtiene el siguiente resultado, que incluye el nuevo 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

La fila se insertó en la tabla base subyacente a través de la vista.

A continuación, inserte a través de la vista una fila enviada 10 días después de haber sido colocada, contradiciendo el filtro de consulta interno de la vista:

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

La declaración se completa con éxito, informando una fila afectada.

Consultar la vista:

SELECT * FROM dbo.FastOrders;

Obtiene el siguiente resultado, que excluye el nuevo 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

Si sabe que FastOrders es una vista, todo esto puede parecer sensato. Después de todo, la fila se insertó en la tabla subyacente y no satisface el filtro de consulta interno de la vista. Pero si no sabe que FastOrders es una vista y no una tabla base, este comportamiento le parecerá sorprendente.

Consulta la tabla de pedidos subyacente:

SELECT * FROM dbo.Orders;

Obtiene el siguiente resultado, que incluye el nuevo 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

Podría experimentar un comportamiento sorprendente similar si actualiza a través de la vista el valor de la fecha de envío en una fila que actualmente es parte de la vista a una fecha que hace que ya no califique como parte de la vista. Dicha actualización normalmente está permitida, pero nuevamente, se lleva a cabo en la tabla base subyacente. Si consulta la vista después de dicha actualización, la fila modificada parece haber desaparecido. En la práctica, todavía está allí en la tabla subyacente, solo que ya no se considera parte de la vista.

Ejecute el siguiente código para eliminar las filas que agregó anteriormente:

DELETE FROM dbo.Orders WHERE orderid >= 6;

Si desea evitar modificaciones que entren en conflicto con el filtro de consulta interno de la vista, agregue CON OPCIÓN DE VERIFICACIÓN al final de la consulta interna como parte de la definición de la vista, así:

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

Se permiten inserciones y actualizaciones a través de la vista siempre que cumplan con el filtro de la consulta interna. De lo contrario, se rechazan.

Por ejemplo, use el siguiente código para insertar a través de la vista una fila que no entre en conflicto con el filtro de consulta interno:

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

La fila se agregó con éxito.

Intente insertar una fila que entre en conflicto con el filtro:

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

Esta vez la fila se rechaza con el siguiente error:

Nivel 16, Estado 1, Línea 135
El intento de inserción o actualización falló porque la vista de destino especifica CON OPCIÓN DE COMPROBACIÓN o abarca una vista que especifica CON OPCIÓN DE COMPROBACIÓN y una o más filas resultantes de la operación no calificaron bajo la Restricción CHECK OPTION.

Inconsistencias NULL

Si ha estado trabajando con T-SQL durante algún tiempo, es probable que esté al tanto de las complejidades de modificación antes mencionadas y de la función CHECK OPTION. A menudo, incluso las personas experimentadas encuentran sorprendente el manejo NULL de CHECK OPTION. Durante años, solía pensar que la opción CHECK en una vista cumplía la misma función que una restricción CHECK en la definición de una tabla base. Así es también como solía describir esta opción cuando escribía o enseñaba sobre ella. De hecho, siempre que no haya NULL involucrados en el predicado de filtro, es conveniente pensar en los dos en términos similares. Se comportan consistentemente en tal caso, aceptando filas que concuerdan con el predicado y rechazando las que están en conflicto con él. Sin embargo, los dos manejan NULL de manera inconsistente.

Cuando se usa la OPCIÓN DE COMPROBACIÓN, se permite una modificación a través de la vista siempre que el predicado se evalúe como verdadero; de lo contrario, se rechaza. Esto significa que se rechaza cuando el predicado de la vista se evalúa como falso o desconocido (cuando se trata de un NULL). Con una restricción CHECK, la modificación se permite cuando el predicado de la restricción se evalúa como verdadero o desconocido, y se rechaza cuando el predicado se evalúa como falso. ¡Esa es una diferencia interesante! Primero, veamos esto en acción, luego intentaremos descubrir la lógica detrás de esta inconsistencia.

Intente insertar a través de la vista una fila con una fecha de envío NULL:

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

El predicado de la vista se evalúa como desconocido y la fila se rechaza con el siguiente error:

Mensaje 550, nivel 16, estado 1, línea 147
El intento de inserción o actualización falló porque la vista de destino especifica CON OPCIÓN DE COMPROBACIÓN o abarca una vista que especifica CON OPCIÓN DE COMPROBACIÓN y una o más filas resultantes de la operación no calificar bajo la restricción CHECK OPTION.

Probemos una inserción similar en una tabla base con una restricción CHECK. Use el siguiente código para agregar una restricción de este tipo a la definición de la tabla de nuestro Pedido:

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

Primero, para asegurarse de que la restricción funcione cuando no haya NULL involucrados, intente insertar el siguiente pedido con una fecha de envío de 10 días después de la fecha del pedido:

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

Este intento de inserción se rechaza con el siguiente error:

Mensaje 547, Nivel 16, Estado 0, Línea 159
La declaración INSERT entró en conflicto con la restricción CHECK "CHK_Orders_FastOrder". El conflicto ocurrió en la base de datos "tempdb", tabla "dbo.Orders".

Utilice el siguiente código para insertar una fila con una fecha de envío NULL:

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

Se supone que una restricción CHECK rechaza los casos falsos, pero en nuestro caso, el predicado se evalúa como desconocido, por lo que la fila se agrega correctamente.

Consulta la tabla de pedidos:

SELECT * FROM dbo.Orders;

Puede ver el nuevo orden en la salida:

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

¿Cuál es la lógica detrás de esta inconsistencia? Podría argumentar que una restricción CHECK solo debe aplicarse cuando el predicado de la restricción se viola claramente, es decir, cuando se evalúa como falso. De esta manera, si elige permitir valores NULL en la columna en cuestión, se permiten las filas con valores NULL en la columna aunque el predicado de la restricción se evalúe como desconocido. En nuestro caso, representamos los pedidos no enviados con NULL en la columna de fecha de envío, y permitimos pedidos no enviados en la tabla mientras aplicamos la regla de "pedidos rápidos" solo para pedidos enviados.

El argumento para usar una lógica diferente con una vista es que se debe permitir una modificación a través de la vista solo si la fila de resultados es una parte válida de la vista. Si el predicado de la vista se evalúa como desconocido, por ejemplo, cuando la fecha de envío es NULL, la fila de resultados no es una parte válida de la vista, por lo que se rechaza. Solo las filas para las que el predicado se evalúa como verdadero son una parte válida de la vista y, por lo tanto, están permitidas.

Los valores NULL agregan mucha complejidad al lenguaje. Le gusten o no, si sus datos los respaldan, querrá asegurarse de que comprende cómo los maneja T-SQL.

En este punto, puede eliminar la restricción CHECK de la tabla Pedidos y también eliminar la vista FastOrders para la limpieza:

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

Restricción TOP/OFFSET-FETCH

Normalmente se permiten modificaciones a través de vistas que involucran los filtros TOP y OFFSET-FETCH. Sin embargo, al igual que con nuestra discusión anterior sobre las vistas definidas sin la OPCIÓN DE COMPROBACIÓN, el resultado de dicha modificación puede parecer extraño para el usuario si no sabe que está interactuando con una vista.

Considere la siguiente vista que representa pedidos recientes como ejemplo:

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

Utilice el siguiente código para insertar seis pedidos a través de la vista 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');

Consultar la vista:

SELECT * FROM dbo.RecentOrders;

Obtienes el siguiente resultado:

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

De las seis órdenes insertadas, solo cuatro forman parte de la vista. Esto parece perfectamente sensato si sabe que está consultando una vista que se basa en una consulta con un filtro TOP. Pero puede parecer extraño si está pensando que está consultando una tabla base.

Consulta la tabla de pedidos subyacente directamente:

SELECT * FROM dbo.Orders;

Obtiene el siguiente resultado que muestra todos los pedidos agregados:

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

Si agrega CHECK OPTION a la definición de vista, se rechazarán las declaraciones INSERT y UPDATE contra la vista. Utilice el siguiente código para aplicar este cambio:

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

Intente agregar un pedido a través de la vista:

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

Obtiene el siguiente error:

Msj 4427, nivel 16, estado 1, línea 247
No se puede actualizar la vista "dbo.RecentOrders" porque esta o una vista a la que hace referencia se creó con WITH CHECK OPTION y su definición contiene una cláusula TOP o OFFSET.

SQL Server no intenta ser demasiado inteligente aquí. Rechazará el cambio incluso si la fila que intenta insertar se convierte en una parte válida de la vista en ese punto. Por ejemplo, intente agregar un pedido con una fecha más reciente que estaría entre los 5 primeros en este punto:

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

El intento de inserción aún se rechaza con el siguiente error:

Msj 4427, nivel 16, estado 1, línea 254
No se puede actualizar la vista "dbo.RecentOrders" porque esta o una vista a la que hace referencia se creó con WITH CHECK OPTION y su definición contiene una cláusula TOP o OFFSET.

Intenta actualizar una fila a través de la vista:

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

En este caso también se rechaza el intento de cambio con el siguiente error:

Mensaje 4427, Nivel 16, Estado 1, Línea 260
No se puede actualizar la vista "dbo.RecentOrders" porque esta o una vista a la que hace referencia se creó con CON OPCIÓN DE COMPROBACIÓN y su definición contiene una cláusula TOP o OFFSET.

Tenga en cuenta que definir una vista basada en una consulta con TOP o OFFSET-FETCH y CHECK OPTION dará como resultado la falta de soporte para las declaraciones INSERT y UPDATE a través de la vista.

Se admiten las eliminaciones a través de dicha vista. Ejecute el siguiente código para eliminar los cinco pedidos más recientes:

DELETE FROM dbo.RecentOrders;

El comando se completa con éxito.

Consulta la tabla:

SELECT * FROM dbo.Orders;

Obtiene el siguiente resultado después de eliminar los pedidos con ID 8, 11, 12, 13 y 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

En este punto, ejecute el siguiente código para la limpieza antes de ejecutar los ejemplos en la siguiente sección:

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

Únete

Se admite la actualización de una vista que une varias tablas, siempre que el cambio solo afecte a una de las tablas base subyacentes.

Considere la siguiente vista que une Pedidos y Detalles de pedido como ejemplo:

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

Intente insertar una fila a través de la vista, de modo que ambas tablas base subyacentes se vean afectadas:

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

Obtiene el siguiente error:

Mensaje 4405, nivel 16, estado 1, línea 306
La vista o función 'dbo.OrdersOrderDetails' no se puede actualizar porque la modificación afecta a varias tablas base.

Intente insertar una fila a través de la vista, de modo que solo se vea afectada la tabla Pedidos:

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

Este comando se completa con éxito y la fila se inserta en la tabla de pedidos subyacente.

Pero, ¿qué sucede si también desea poder insertar una fila a través de la vista en la tabla OrderDetails? Con la definición de vista actual, esto es imposible (en lugar de dejar de lado los activadores) ya que la vista devuelve la columna orderid de la tabla Orders y no de la tabla OrderDetails. Basta con que una columna de la tabla OrderDetails que de alguna manera no puede obtener su valor automáticamente no sea parte de la vista para evitar inserciones en OrderDetails a través de la vista. Por supuesto, siempre puede decidir que la vista incluirá tanto orderid de Orders como orderid de OrderDetails. En tal caso, deberá asignar a las dos columnas diferentes alias, ya que el encabezado de la tabla representada por la vista debe tener nombres de columna únicos.

Utilice el siguiente código para modificar la definición de la vista para incluir ambas columnas, asignando un alias a la de Pedidos como O_orderid y a la 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

Ahora puede insertar filas a través de la vista en Pedidos o en Detalles del pedido, según la tabla de la que provenga la lista de columnas de destino. Aquí hay un ejemplo para insertar un par de líneas de pedido asociadas con el pedido 6 a través de la vista en 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);

Las filas se agregaron con éxito.

Consultar la vista:

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Obtienes el siguiente resultado:

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

Se aplica una restricción similar a las instrucciones UPDATE a través de la vista. Las actualizaciones están permitidas siempre que solo se vea afectada una tabla base subyacente. Pero puede hacer referencia a columnas de ambos lados en la declaración siempre que solo se modifique un lado.

Como ejemplo, la siguiente declaración de ACTUALIZACIÓN a través de la vista establece la fecha de pedido de la fila donde el ID de pedido de la línea de pedido es 6 y el ID de producto es 1001 a "20210901:"

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

Llamaremos a esta declaración Actualizar declaración 1.

La actualización se completa con éxito con el siguiente mensaje:

(1 row affected)

Lo que es importante tener en cuenta aquí es que la declaración filtra por elementos de la tabla OrderDetails, pero la columna modificada orderdate es de la tabla Orders. Por lo tanto, en el plan que construye SQL Server para esta declaración, tiene que averiguar qué pedidos deben modificarse en la tabla Pedidos. El plan para esta declaración se muestra en la Figura 1.

Figura 1:Plan para la declaración de actualización 1

Puede ver cómo comienza el plan filtrando el lado OrderDetails por orderid =6 y productid =1001, y el lado Orders por orderid =6, uniendo los dos. El resultado es solo una fila. La única parte relevante que se debe mantener de esta actividad es qué ID de pedido en la tabla Pedidos representan filas que deben actualizarse. En nuestro caso, es el pedido con ID de pedido 6. Además, el operador Compute Scalar prepara un miembro llamado Expr1002 con el valor que la declaración asignará a la columna de fecha de pedido del pedido de destino. La última parte del plan con el operador Actualización de índice agrupado aplica la actualización real a la fila en Pedidos con ID de pedido 6, estableciendo su valor de fecha de pedido en Expr1002.

El punto clave a enfatizar aquí es que solo se actualizó una fila con ID de pedido 6 en la tabla Pedidos. Sin embargo, esta fila tiene dos coincidencias en el resultado de la combinación con la tabla OrderDetails:una con el ID de producto 1001 (que filtró la actualización original) y otra con el ID de producto 1002 (que no filtró la actualización original). Consulta la vista en este punto, filtrando todas las filas con ID de orden 6:

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Obtienes el siguiente resultado:

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 filas muestran la fecha del nuevo pedido, aunque la actualización original filtró solo la fila con el ID de producto 1001. Una vez más, esto debería parecer perfectamente sensato si sabe que está interactuando con una vista que une dos tablas base debajo de las cubiertas, pero podría parecer muy extraño si no te das cuenta de esto.

Curiosamente, SQL Server incluso admite actualizaciones no deterministas en las que varias filas de origen (desde OrderDetails en nuestro caso) coinciden con una única fila de destino (en Orders en nuestro caso). Teóricamente, una forma de manejar tal caso sería rechazarlo. De hecho, con una instrucción MERGE en la que varias filas de origen coinciden con una fila de destino, SQL Server rechaza el intento. Pero no con una ACTUALIZACIÓN basada en una unión, ya sea directa o indirectamente a través de una expresión de tabla con nombre como una vista. SQL Server simplemente lo maneja como una actualización no determinista.

Considere el siguiente ejemplo, al que nos referiremos como Declaración 2:

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

Con suerte, me perdonará que es un ejemplo artificial, pero ilustra el punto.

Hay dos filas de calificación en la vista, que representan dos filas de línea de orden de fuente de calificación de la tabla OrderDetails subyacente. Pero solo hay una fila objetivo calificada en la tabla de pedidos subyacente. Además, en una fila de detalles del pedido de origen, la expresión CASE asignada devuelve un valor ('20210902') y en la otra fila de detalles del pedido de origen devuelve otro valor ('20210903'). ¿Qué debe hacer SQL Server en este caso? Como se mencionó, una situación similar con la declaración MERGE daría como resultado un error, rechazando el intento de cambio. Sin embargo, con una instrucción UPDATE, SQL Server simplemente lanza una moneda. Técnicamente, esto se hace usando una función agregada interna llamada ANY.

Entonces, nuestra actualización se completa con éxito, informando 1 fila afectada. El plan para esta declaración se muestra en la Figura 2.


Figura 2:Plan para la declaración de actualización 2

Hay dos filas en el resultado de la unión. Estas dos filas se convierten en las filas de origen de la actualización. Pero luego, un operador agregado que aplica la función CUALQUIERA elige un valor de ID de pedido (cualquiera) y un valor de precio unitario (cualquiera) de estas filas de origen. Ambas filas de origen tienen el mismo valor de orderid, por lo que se modificará el orden correcto. Pero dependiendo de cuál de los valores de precio unitario de origen termine el agregado ANY, esto determinará qué valor devolverá la expresión CASE, para luego ser utilizado como el valor de fecha de pedido actualizado en el pedido de destino. Ciertamente puede ver un argumento en contra de admitir dicha actualización, pero es totalmente compatible con SQL Server.

Consultemos la vista para ver el resultado de este cambio (ahora es el momento de hacer su apuesta en cuanto al resultado):

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Obtuve el siguiente resultado:

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 se seleccionó uno de los dos valores de precio unitario de origen y se usó para determinar la fecha de pedido del único pedido de destino; sin embargo, al consultar la vista, el valor de la fecha de pedido se repite para ambas líneas de pedido coincidentes. Como puede darse cuenta, el resultado podría haber sido la otra fecha (2021-09-02) ya que la elección del valor del precio unitario no fue determinista. ¡Cosas raras!

Por lo tanto, bajo ciertas condiciones, las declaraciones INSERT y UPDATE están permitidas a través de vistas que unen varias tablas subyacentes. Sin embargo, no se permiten eliminaciones contra tales vistas. ¿Cómo puede saber SQL Server cuál de los lados se supone que es el objetivo de la eliminación?

Aquí hay un intento de aplicar dicha eliminación a través de la vista:

DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;

Este intento es rechazado con el siguiente error:

Mensaje 4405, Nivel 16, Estado 1, Línea 377
La vista o función 'dbo.OrdersOrderDetails' no se puede actualizar porque la modificación afecta a varias tablas base.

En este punto, ejecute el siguiente código para la limpieza:

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

Columnas derivadas

Otra restricción a las modificaciones a través de vistas tiene que ver con las columnas derivadas. Si una columna de vista es el resultado de un cálculo, SQL Server no intentará aplicar ingeniería inversa a su fórmula cuando intente insertar o actualizar datos a través de la vista, sino que rechazará dichas modificaciones.

Considere la siguiente vista como ejemplo:

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

La vista calcula la columna netunitprice en función de las columnas de la tabla OrderDetails subyacentes unitprice y discount.

Consultar la vista:

SELECT * FROM dbo.OrderDetailsNetPrice;

Obtienes el siguiente resultado:

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

Intente insertar una fila a través de la vista:

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

Teóricamente, puede averiguar qué fila debe insertarse en la tabla OrderDetails subyacente mediante la ingeniería inversa del valor de precio unitario de la tabla base a partir de los valores de descuento y precio unitario neto de la vista. SQL Server no intenta tal ingeniería inversa, pero rechaza el intento de inserción con el siguiente error:

Mensaje 4406, Nivel 16, Estado 1, Línea 412
La actualización o inserción de la vista o función 'dbo.OrderDetailsNetPrice' falló porque contiene un campo derivado o constante.

Intente omitir la columna calculada de la inserción:

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

Ahora volvemos al requisito de que todas las columnas de la tabla subyacente que de alguna manera no obtienen sus valores automáticamente deben ser parte de la inserción, y aquí nos falta la columna de precio unitario. Esta inserción falla con el siguiente error:

Mensaje 515, nivel 16, estado 2, línea 421
No se puede insertar el valor NULL en la columna 'precio unitario', tabla 'tempdb.dbo.Detalles de pedido'; columna no permite nulos. 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;

Resumen

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.