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

Fundamentos de las expresiones de tabla, Parte 10:Vistas, SELECT * y cambios DDL

Como parte de la serie sobre expresiones de mesa, el mes pasado inicié la cobertura de vistas. Específicamente, comencé la cobertura de los aspectos lógicos de las vistas y comparé su diseño con el de tablas derivadas y CTE. Este mes continuaré con la cobertura de los aspectos lógicos de las vistas, centrando mi atención en los cambios SELECT * y DDL.

El código que usaré en este artículo se puede ejecutar en cualquier base de datos, pero en mis demostraciones usaré TSQLV5, la misma base de datos de muestra que usé en artículos anteriores. Puede encontrar el script que crea y completa TSQLV5 aquí, y su diagrama ER aquí.

Usar SELECT * en la consulta interna de la vista es una mala idea

En la sección de conclusión del artículo del mes pasado, planteé una pregunta para reflexionar. Expliqué que anteriormente en la serie defendí el uso de SELECT * en las expresiones de tablas internas que se usan con tablas derivadas y CTE. Consulte la Parte 3 de la serie para obtener detalles si necesita refrescar su memoria. Luego le pedí que pensara si la misma recomendación seguiría siendo válida para la expresión de la tabla interna utilizada para definir la vista. Quizás el título de esta sección ya era un spoiler, pero diré de inmediato que con las vistas en realidad es una muy mala idea.

Comenzaré con vistas que no están definidas con el atributo SCHEMABINDING, que evita cambios DDL relevantes en objetos dependientes, y luego explicaré cómo cambian las cosas cuando usa este atributo.

Saltaré directamente a un ejemplo ya que esta será la forma más fácil de presentar mi argumento.

Use el siguiente código para crear una tabla llamada dbo.T1 y una vista llamada dbo.V1 basada en una consulta con SELECT * contra la tabla:

USE TSQLV5;
 
DROP VIEW IF EXISTS dbo.V1;
DROP TABLE IF EXISTS dbo.T1;
GO
 
CREATE TABLE dbo.T1
(
  keycol INT NOT NULL IDENTITY
    CONSTRAINT PK_T1 PRIMARY KEY,
  intcol INT NOT NULL,
  charcol VARCHAR(10) NOT NULL
);
 
INSERT INTO dbo.T1(intcol, charcol) VALUES
  (10, 'A'),
  (20, 'B');
GO
 
CREATE OR ALTER VIEW dbo.V1
AS
  SELECT *
  FROM dbo.T1;
GO

Observe que la tabla actualmente tiene las columnas keycol, intcol y charcol.

Use el siguiente código para consultar la vista:

SELECT * FROM dbo.V1;

Obtienes el siguiente resultado:

keycol      intcol      charcol
----------- ----------- ----------
1           10          A
2           20          B

Nada demasiado especial aquí.

Cuando crea una vista, SQL Server registra información de metadatos en varios objetos de catálogo. Registra cierta información general, que puede consultar a través de sys.views, la definición de vista que puede consultar a través de sys.sql_modules, información de columna que puede consultar a través de sys.columns, y hay más información disponible a través de otros objetos. Lo que también es relevante para nuestra discusión es que SQL Server le permite controlar los permisos de acceso frente a las vistas. Lo que quiero advertirle cuando use SELECT * en la expresión de la tabla interna de la vista es lo que puede suceder cuando los cambios DDL se aplican a los objetos dependientes subyacentes.

Use el siguiente código para crear un usuario llamado usuario1 y otorgarle permisos para seleccionar las columnas keycol e intcol de la vista, pero no charcol:

DROP USER IF EXISTS user1;
 
CREATE USER user1 WITHOUT LOGIN; 
 
GRANT SELECT ON dbo.V1(keycol, intcol) TO user1;

En este punto, inspeccionemos algunos de los metadatos registrados relacionados con nuestra vista. Utilice el siguiente código para devolver la entrada que representa la vista de sys.views:

SELECT SCHEMA_NAME(schema_id) AS schemaname, name, object_id, type_desc
FROM sys.views
WHERE object_id = OBJECT_ID(N'dbo.V1');

Este código genera el siguiente resultado:

schemaname  name  object_id   type_desc
----------- ----- ----------- ----------
dbo         V1    130099504   VIEW

Use el siguiente código para obtener la definición de vista de sys.modules:

SELECT definition 
FROM sys.sql_modules
WHERE object_id = OBJECT_ID(N'dbo.V1');

Otra opción es usar la función OBJECT_DEFINITION así:

SELECT OBJECT_DEFINITION(OBJECT_ID(N'dbo.V1'));

Obtienes el siguiente resultado:

CREATE   VIEW dbo.V1
AS
  SELECT *
  FROM dbo.T1;

Utilice el siguiente código para consultar las definiciones de columna de la vista desde sys.columns:

SELECT name AS column_name, column_id, TYPE_NAME(system_type_id) AS data_type
FROM sys.columns
WHERE object_id = OBJECT_ID(N'dbo.V1');

Como era de esperar, obtiene información sobre las tres columnas de la vista keycol, intcol y charcol:

column_name  column_id   data_type
------------ ----------- ----------
keycol       1           int
intcol       2           int
charcol      3           varchar

Observe los ID de columna (posiciones ordinales) que están asociados con las columnas.

Puede obtener información similar consultando la vista de esquema de información estándar INFORMACION_ESQUEMA.COLUMNAS, así:

SELECT COLUMN_NAME, ORDINAL_POSITION, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = N'dbo'
  AND TABLE_NAME = N'V1';

Para obtener la información de dependencia de la vista (objetos a los que se refiere), puede consultar sys.dm_sql_referenced_entities, así:

SELECT
  OBJECT_NAME(referenced_id) AS referenced_object,
  referenced_minor_id,
  COL_NAME(referenced_id, referenced_minor_id) AS column_name
FROM sys.dm_sql_referenced_entities(N'dbo.V1', N'OBJECT');

Encontrará la dependencia en la tabla T1 y en sus tres columnas:

referenced_object  referenced_minor_id column_name
------------------ ------------------- -----------
T1                 0                   NULL
T1                 1                   keycol
T1                 2                   intcol
T1                 3                   charcol

Como probablemente podría adivinar, el valor de reference_minor_id para las columnas es el ID de columna que vio anteriormente.

Si desea obtener los permisos de usuario1 contra V1, puede consultar sys.database_permissions, así:

SELECT 
  OBJECT_NAME(major_id) AS referenced_object,
  minor_id,
  COL_NAME(major_id, minor_id) AS column_name, 
  permission_name
FROM sys.database_permissions
WHERE major_id = OBJECT_ID(N'dbo.V1')
  AND grantee_principal_id = USER_ID(N'user1');

Este código genera el siguiente resultado, afirmando que, de hecho, el usuario 1 tiene permisos de selección solo contra keycol e intcol, pero no contra charcol:

referenced_object  minor_id    column_name  permission_name
------------------ ----------- ------------ ----------------
V1                 1           keycol       SELECT
V1                 2           intcol       SELECT

Nuevamente, el valor de minor_id es el ID de columna que vio anteriormente. Nuestro usuario, usuario1, tiene permisos para las columnas cuyos ID son 1 y 2.

A continuación, ejecute el siguiente código para suplantar al usuario1 e intentar consultar todas las columnas de V1:

EXECUTE AS USER = N'user1';
 
SELECT * FROM dbo.V1;

Como era de esperar, obtiene un error de permiso debido a la falta de permiso para consultar charcol:

Mensaje 230, Nivel 14, Estado 1, Línea 141
Se denegó el permiso SELECT en la columna 'charcol' del objeto 'V1', base de datos 'TSQLV5', esquema 'dbo'.

Intente consultar solo keycol e intcol:

SELECT keycol, intcol FROM dbo.V1;

Esta vez, la consulta se ejecuta correctamente y genera el siguiente resultado:

keycol      intcol
----------- -----------
1           10
2           20

Sin sorpresas hasta ahora.

Ejecute el siguiente código para volver a su usuario original:

REVERT;

Ahora apliquemos algunos cambios estructurales a la tabla subyacente dbo.T1. Ejecute el siguiente código para agregar primero dos columnas llamadas datecol y binarycol, y luego suelte la columna intcol:

ALTER TABLE dbo.T1
  ADD datecol DATE NOT NULL DEFAULT('99991231'),
      binarycol VARBINARY(3) NOT NULL DEFAULT(0x112233);
 
ALTER TABLE dbo.T1
  DROP COLUMN intcol;

SQL Server no rechazó los cambios estructurales en las columnas a las que hace referencia la vista, ya que la vista no se creó con el atributo SCHEMABINDING. Ahora, para la captura. En este punto, SQL Server aún no actualizó la información de metadatos de la vista en los diferentes objetos del catálogo.

Use el siguiente código para consultar la vista, aún con su usuario original (todavía no usuario1):

SELECT * FROM dbo.V1;

Obtienes el siguiente resultado:

keycol      intcol     charcol
----------- ---------- ----------
1           A          9999-12-31
2           B          9999-12-31

Tenga en cuenta que intcol en realidad devuelve el contenido de charcol y charcol devuelve el contenido de datecol. Recuerde, ya no hay intcol en la tabla pero hay datecol. Además, no recupera la nueva columna binarycol.

Para tratar de averiguar qué está pasando, use el siguiente código para consultar los metadatos de la columna de la vista:

SELECT name AS column_name, column_id, TYPE_NAME(system_type_id) AS data_type
FROM sys.columns
WHERE object_id = OBJECT_ID(N'dbo.V1');

Este código genera el siguiente resultado:

column_name  column_id   data_type
------------ ----------- ----------
keycol       1           int
intcol       2           int
charcol      3           varchar

Como puede ver, los metadatos de la vista aún no se actualizan. Puede ver intcol como ID de columna 2 y charcol como ID de columna 3. En la práctica, intcol ya no existe, se supone que charcol es la columna 2 y datecol se supone que es la columna 3.

Verifiquemos si hay algún cambio con la información del permiso:

SELECT 
  OBJECT_NAME(major_id) AS referenced_object,
  minor_id,
  COL_NAME(major_id, minor_id) AS column_name, 
  permission_name
FROM sys.database_permissions
WHERE major_id = OBJECT_ID(N'dbo.V1')
  AND grantee_principal_id = USER_ID(N'user1');

Obtienes el siguiente resultado:

referenced_object  minor_id    column_name  permission_name
------------------ ----------- ------------ ----------------
V1                 1           keycol       SELECT
V1                 2           intcol       SELECT

La información de permisos muestra que el usuario 1 tiene permisos para las columnas 1 y 2 de la vista. Sin embargo, aunque los metadatos piensan que la columna 2 se llama intcol, en realidad está asignada a charcol en T1 en la práctica. Eso es peligroso ya que se supone que user1 no tiene acceso a charcol. ¿Qué pasa si en la vida real esta columna contiene información confidencial como contraseñas?

Suplantemos de nuevo al usuario1 y consultemos todas las columnas de la vista:

EXECUTE AS USER = 'user1';
 
SELECT * FROM dbo.V1;

Obtiene un error de permiso que dice que no tiene acceso a charcol:

Mensaje 230, Nivel 14, Estado 1, Línea 211
Se denegó el permiso SELECT en la columna 'charcol' del objeto 'V1', base de datos 'TSQLV5', esquema 'dbo'.

Sin embargo, vea lo que sucede cuando solicita explícitamente keycol e intcol:

SELECT keycol, intcol FROM dbo.V1;

Obtienes el siguiente resultado:

keycol      intcol
----------- ----------
1           A
2           B

Esta consulta tiene éxito, solo que devuelve el contenido de charcol en intcol. Se supone que nuestro usuario, user1, no tiene acceso a esta información. ¡Uy!

En este punto, vuelva al usuario original ejecutando el siguiente código:

REVERT;

Actualizar módulo SQL

Puede ver claramente que usar SELECT * en la expresión de la tabla interna de la vista es una mala idea. Pero no es solo eso. En general, es una buena idea actualizar los metadatos de la vista después de cada cambio de DDL en los objetos y columnas a los que se hace referencia. Puede hacerlo usando sp_refreshview o el sp_refreshmodule más general, así:

EXEC sys.sp_refreshsqlmodule N'dbo.V1';

Consulta la vista nuevamente, ahora que sus metadatos se han actualizado:

SELECT * FROM dbo.V1;

Esta vez obtienes el resultado esperado:

keycol      charcol    datecol    binarycol
----------- ---------- ---------- ---------
1           A          9999-12-31 0x112233
2           B          9999-12-31 0x112233

La columna charcol tiene el nombre correcto y muestra los datos correctos; no ve intcol y sí ve las nuevas columnas datecol y binarycol.

Consulta los metadatos de la columna de la vista:

SELECT name AS column_name, column_id, TYPE_NAME(system_type_id) AS data_type
FROM sys.columns
WHERE object_id = OBJECT_ID(N'dbo.V1');

La salida ahora muestra la información de metadatos de columna correcta:

column_name  column_id   data_type
------------ ----------- ----------
keycol       1           int
charcol      2           varchar
datecol      3           date
binarycol    4           varbinary

Consulta los permisos del usuario1 contra la vista:

SELECT 
  OBJECT_NAME(major_id) AS referenced_object,
  minor_id,
  COL_NAME(major_id, minor_id) AS column_name, 
  permission_name
FROM sys.database_permissions
WHERE major_id = OBJECT_ID(N'dbo.V1')
  AND grantee_principal_id = USER_ID(N'user1');

Obtienes el siguiente resultado:

referenced_object  minor_id    column_name  permission_name
------------------ ----------- ------------ ----------------
V1                 1           keycol       SELECT

La información de permisos ahora es correcta. Nuestro usuario, user1, tiene permisos solo para seleccionar keycol y se eliminó la información de permisos para intcol.

Para asegurarnos de que todo está bien, probemos esto suplantando al usuario1 y consultando la vista:

EXECUTE AS USER = 'user1';
 
SELECT * FROM dbo.V1;

Obtiene dos errores de permiso debido a la falta de permisos contra datecol y binarycol:

Mensaje 230, Nivel 14, Estado 1, Línea 281
Se denegó el permiso SELECT en la columna 'datecol' del objeto 'V1', base de datos 'TSQLV5', esquema 'dbo'.

Mensaje 230, Nivel 14, Estado 1, Línea 281
Se denegó el permiso SELECT en la columna 'binarycol' del objeto 'V1', base de datos 'TSQLV5', esquema 'dbo'.

Intente consultar keycol e intcol:

SELECT keycol, intcol FROM dbo.V1;

Esta vez el error dice correctamente que no hay una columna llamada intcol:

Mensaje 207, Nivel 16, Estado 1, Línea 279

Nombre de columna no válido 'intcol'.

Consulta solo intcol:

SELECT keycol FROM dbo.V1;

Esta consulta se ejecuta correctamente y genera el siguiente resultado:

keycol
-----------
1
2

En este punto, vuelva a su usuario original ejecutando el siguiente código:

REVERT;

¿Es suficiente evitar SELECT * y usar nombres de columna explícitos?

Si sigue una práctica que dice no SELECT * en la expresión de la tabla interna de la vista, ¿sería esto suficiente para evitarle problemas? Bueno, veamos...

Use el siguiente código para recrear la tabla y la vista, solo que esta vez enumere las columnas explícitamente en la consulta interna de la vista:

DROP VIEW IF EXISTS dbo.V1;
DROP TABLE IF EXISTS dbo.T1;
GO
 
CREATE TABLE dbo.T1
(
  keycol INT NOT NULL IDENTITY
    CONSTRAINT PK_T1 PRIMARY KEY,
  intcol INT NOT NULL,
  charcol VARCHAR(10) NOT NULL
);
 
INSERT INTO dbo.T1(intcol, charcol) VALUES
  (10, 'A'),
  (20, 'B');
GO
 
CREATE OR ALTER VIEW dbo.V1
AS
  SELECT keycol, intcol, charcol
  FROM dbo.T1;
GO

Consultar la vista:

SELECT * FROM dbo.V1;

Obtienes el siguiente resultado:

keycol      intcol      charcol
----------- ----------- ----------
1           10          A
2           20          B

Nuevamente, otorgue permisos de usuario1 para seleccionar keycol e intcol:

GRANT SELECT ON dbo.V1(keycol, intcol) TO user1;

A continuación, aplique los mismos cambios estructurales que hizo antes:

ALTER TABLE dbo.T1
  ADD datecol DATE NOT NULL DEFAULT('99991231'),
      binarycol VARBINARY(3) NOT NULL DEFAULT(0x112233);
 
ALTER TABLE dbo.T1
  DROP COLUMN intcol;

Observe que SQL Server aceptó estos cambios, aunque la vista tiene una referencia explícita a intcol. Nuevamente, eso se debe a que la vista se creó sin la opción SCHEMABINDING.

Consultar la vista:

SELECT * FROM dbo.V1;

En este punto SQL Server genera el siguiente error:

Mensaje 207, Nivel 16, Estado 1, Procedimiento V1, Línea 5 [Batch Start Line 344]
Nombre de columna no válido 'intcol'.

Mensaje 4413, nivel 16, estado 1, línea 345
No se pudo usar la vista o la función 'dbo.V1' debido a errores de vinculación.

SQL Server intentó resolver la referencia intcol en la vista y, por supuesto, no tuvo éxito.

Pero, ¿y si su plan original fuera eliminar intcol y luego volver a agregarlo? Use el siguiente código para volver a agregarlo y luego consulte la vista:

ALTER TABLE dbo.T1
  ADD intcol INT NOT NULL DEFAULT(0);
 
SELECT * FROM dbo.V1;

Este código genera el siguiente resultado:

keycol      intcol      charcol
----------- ----------- ----------
1           0           A
2           0           B

El resultado parece correcto.

¿Qué hay de consultar la vista como usuario1? Intentémoslo:

EXECUTE AS USER = 'user1';

SELECT * FROM dbo.V1;

Al consultar todas las columnas, obtiene el error esperado debido a la falta de permisos contra charcol:

Mensaje 230, Nivel 14, Estado 1, Línea 367
Se denegó el permiso SELECT en la columna 'charcol' del objeto 'V1', base de datos 'TSQLV5', esquema 'dbo'.

Consulta keycol e intcol explícitamente:

SELECT keycol, intcol FROM dbo.V1;

Obtienes el siguiente resultado:

keycol      intcol
----------- -----------
1           0
2           0

Parece que todo está en orden gracias al hecho de que no usó SELECT * en la consulta interna de la vista, aunque no actualizó los metadatos de la vista. Aún así, podría ser una buena práctica actualizar los metadatos de la vista después de que DDL cambie a los objetos y columnas a los que se hace referencia para estar seguro.

En este punto, vuelva a su usuario original ejecutando el siguiente código:

REVERT;

VINCULACIÓN DE ESQUEMAS

Usando el atributo de vista SCHEMABINDING puede ahorrarse muchos de los problemas antes mencionados. Una de las claves para evitar el problema que vio anteriormente es no usar SELECT * en la consulta interna de la vista. Pero también está el problema de los cambios estructurales en los objetos dependientes, como eliminar las columnas a las que se hace referencia, que aún podrían generar errores al consultar la vista. Usando el atributo de vista SCHEMABINDING, no podrá usar SELECT * en la consulta interna. Además, SQL Server rechazará los intentos de aplicar cambios DDL relevantes a objetos y columnas dependientes. En relevante, me refiero a cambios como eliminar una tabla o columna a la que se hace referencia. Obviamente, agregar una columna a una tabla a la que se hace referencia no es un problema, por lo que SCHEMABINDING no evita dicho cambio.

Para demostrar esto, use el siguiente código para recrear la tabla y la vista, con SCHEMABINDING en la definición de la vista:

DROP VIEW IF EXISTS dbo.V1;
DROP TABLE IF EXISTS dbo.T1;
GO
 
CREATE TABLE dbo.T1
(
  keycol INT NOT NULL IDENTITY
    CONSTRAINT PK_T1 PRIMARY KEY,
  intcol INT NOT NULL,
  charcol VARCHAR(10) NOT NULL
);
 
INSERT INTO dbo.T1(intcol, charcol) VALUES
  (10, 'A'),
  (20, 'B');
GO
 
CREATE OR ALTER VIEW dbo.V1
  WITH SCHEMABINDING
AS
  SELECT *
  FROM dbo.T1;
GO

Obtiene un error:

Mensaje 1054, Nivel 15, Estado 6, Procedimiento V1, Línea 5 [Batch Start Line 387]
La sintaxis '*' no está permitida en objetos vinculados al esquema.

Cuando usa SCHEMABINDING, no puede usar SELECT * en la expresión de la tabla interna de la vista.

Intente crear la vista nuevamente, solo que esta vez con una lista de columnas explícita:

CREATE OR ALTER VIEW dbo.V1
  WITH SCHEMABINDING
AS
  SELECT keycol, intcol, charcol
  FROM dbo.T1;
GO

Esta vez, la vista se creó correctamente.

Otorgar permisos de usuario1 en keycol e intcol:

GRANT SELECT ON dbo.V1(keycol, intcol) TO user1;

A continuación, intente aplicar cambios estructurales a la tabla. Primero, agregue un par de columnas:

ALTER TABLE dbo.T1
  ADD datecol DATE NOT NULL DEFAULT('99991231'),
      binarycol VARBINARY(3) NOT NULL DEFAULT(0x112233);

Agregar columnas no es un problema porque no pueden ser parte de las vistas vinculadas al esquema existentes, por lo que este código se completa correctamente.

Intente eliminar la columna intcol:

ALTER TABLE dbo.T1
  DROP COLUMN intcol;

Obtiene el siguiente error:

Mensaje 5074, Nivel 16, Estado 1, Línea 418
El objeto 'V1' depende de la columna 'intcol'.

Mensaje 4922, Nivel 16, Estado 9, Línea 418
Falló el intcol de ALTER TABLE DROP COLUMN porque uno o más objetos acceden a esta columna.

No se permite eliminar o modificar las columnas a las que se hace referencia cuando existen objetos vinculados al esquema.

Si aún necesita eliminar intcol, primero deberá eliminar la vista de referencia vinculada al esquema, aplicar el cambio y luego volver a crear la vista y reasignar permisos, así:

DROP VIEW IF EXISTS dbo.V1;
GO
 
ALTER TABLE dbo.T1 DROP COLUMN intcol;
GO
 
CREATE OR ALTER VIEW dbo.V1
  WITH SCHEMABINDING
AS
  SELECT keycol, charcol, datecol, binarycol
  FROM dbo.T1;
GO
 
GRANT SELECT ON dbo.V1(keycol, datecol, binarycol) TO user1;
GO

Por supuesto, en este punto no hay necesidad de actualizar la definición de la vista, porque la creaste de nuevo.

Ahora que ha terminado de probar, ejecute el siguiente código para la limpieza:

DROP VIEW IF EXISTS dbo.V1;
DROP TABLE IF EXISTS dbo.T1;
DROP USER IF EXISTS user1;

Resumen

Usar SELECT * en la expresión de la tabla interna de la vista es una muy mala idea. Después de aplicar los cambios estructurales a los objetos a los que se hace referencia, podría obtener nombres de columna incorrectos e incluso permitir que los usuarios accedan a datos a los que se supone que no deben tener acceso. Es una práctica importante enumerar explícitamente los nombres de las columnas a las que se hace referencia.

Al usar SCHEMABINDING en la definición de la vista, se ve obligado a enumerar explícitamente los nombres de las columnas, y SQL Server rechaza los cambios estructurales relevantes en los objetos dependientes. Por lo tanto, puede parecer que crear vistas con SCHEMBINDING siempre es una buena idea. Sin embargo, hay una advertencia con esta opción. Como vio, aplicar cambios estructurales a objetos referenciados cuando se usa SCHEMBINDING se convierte en un proceso más largo y elaborado. Puede ser especialmente un problema en los sistemas que tienen que tener una disponibilidad muy alta. Imagine que necesita cambiar una columna definida como VARCHAR(50) a VARCHAR(60). Ese no es un cambio permitido si hay una vista definida con SCHEMABINDING que hace referencia a esta columna. Las implicaciones de descartar un montón de vistas de referencia, a las que podrían hacer referencia otras vistas, etc., podrían ser problemáticas para el sistema. En resumen, no siempre es tan trivial para las empresas adoptar una política que dice que SCHEMABINDING debe usarse en todos los objetos que lo admiten. Sin embargo, adoptar una política para no usar SELECT * en las consultas internas de las vistas debería ser más sencillo.

Hay mucho más para explorar con respecto a las vistas. Continuará el próximo mes…