Como parte da série sobre expressões de tabela, no mês passado iniciei a cobertura de visualizações. Especificamente, iniciei a cobertura dos aspectos lógicos das visualizações e comparei seu design com o de tabelas derivadas e CTEs. Este mês vou continuar a cobertura dos aspectos lógicos das views, focando minha atenção nas mudanças SELECT * e DDL.
O código que usarei neste artigo pode ser executado em qualquer banco de dados, mas em minhas demonstrações, usarei o TSQLV5 — o mesmo banco de dados de exemplo que usei nos artigos anteriores. Você pode encontrar o script que cria e preenche o TSQLV5 aqui e seu diagrama ER aqui.
Usar SELECT * na consulta interna da visualização é uma má ideia
Na seção de conclusão do artigo do mês passado, fiz uma pergunta para reflexão. Expliquei que, no início da série, defendi o uso de SELECT * nas expressões de tabela internas usadas com tabelas derivadas e CTEs. Consulte a Parte 3 da série para obter detalhes se precisar refrescar a memória. Pedi então que você pensasse se a mesma recomendação ainda seria válida para a expressão de tabela interna usada para definir a visualização. Talvez o título desta seção já fosse um spoiler, mas direi logo de cara que com visualizações na verdade é uma péssima ideia.
Começarei com visualizações que não são definidas com o atributo SCHEMABINDING, que impede alterações relevantes de DDL em objetos dependentes, e depois explicarei como as coisas mudam quando você usa esse atributo.
Vou pular direto para um exemplo, pois essa será a maneira mais fácil de apresentar meu argumento.
Use o código a seguir para criar uma tabela chamada dbo.T1 e uma exibição chamada dbo.V1 com base em uma consulta com SELECT * na tabela:
USE TSQLV5; DROP VIEW SE EXISTE dbo.V1;DROP TABLE SE EXISTE 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.V1AS SELECT * FROM dbo.T1;GO
Observe que a tabela atualmente possui as colunas keycol, intcol e charcol.
Use o seguinte código para consultar a exibição:
SELECT * FROM dbo.V1;
Você obtém a seguinte saída:
keycol intcol charcol----------- ----------- ----------1 10 A2 20 B
Nada muito especial aqui.
Quando você cria uma exibição, o SQL Server registra informações de metadados em vários objetos de catálogo. Ele registra algumas informações gerais, que você pode consultar por meio de sys.views, a definição de exibição que você pode consultar por meio de sys.sql_modules, informações de coluna que você pode consultar por meio de sys.columns e mais informações estão disponíveis por meio de outros objetos. O que também é relevante para nossa discussão é que o SQL Server permite controlar as permissões de acesso em relação às visualizações. O que quero avisá-lo ao usar SELECT * na expressão de tabela interna da exibição é o que pode acontecer quando as alterações de DDL são aplicadas a objetos dependentes subjacentes.
Use o código a seguir para criar um usuário chamado user1 e conceder ao usuário permissões para selecionar as colunas keycol e intcol da exibição, mas não charcol:
DROP USER SE EXISTE user1; CRIAR USUÁRIO user1 SEM LOGIN; GRANT SELECT ON dbo.V1(keycol, intcol) TO user1;
Neste ponto, vamos inspecionar alguns dos metadados registrados relacionados à nossa visão. Use o código a seguir para retornar a entrada que representa a exibição de sys.views:
SELECT SCHEMA_NAME(schema_id) AS schemaname, name, object_id, type_descFROM sys.viewsWHERE object_id =OBJECT_ID(N'dbo.V1');
Este código gera a seguinte saída:
nome do esquema object_id type_desc--------------- ----- ----------- ----------dbo V1 130099504 VIEW
Use o código a seguir para obter a definição de exibição de sys.modules:
SELECT definição FROM sys.sql_modulesWHERE object_id =OBJECT_ID(N'dbo.V1');
Outra opção é usar a função OBJECT_DEFINITION assim:
SELECT OBJECT_DEFINITION(OBJECT_ID(N'dbo.V1'));
Você obtém a seguinte saída:
CREATE VIEW dbo.V1AS SELECT * FROM dbo.T1;
Use o código a seguir para consultar as definições de coluna da exibição em sys.columns:
SELECT name AS column_name, column_id, TYPE_NAME(system_type_id) AS data_typeFROM sys.columnsWHERE object_id =OBJECT_ID(N'dbo.V1');
Como esperado, você obtém informações sobre as três colunas da view keycol, intcol e charcol:
column_name column_id data_type----------- ----------- ----------keycol 1 intintcol 2 intcharcol 3 varchar
Observe os IDs de coluna (posições ordinais) que estão associados às colunas.
Você pode obter informações semelhantes consultando a visualização de esquema de informações padrão INFORMATION_SCHEMA.COLUMNS, assim:
SELECT COLUMN_NAME, ORDINAL_POSITION, DATA_TYPEFROM INFORMATION_SCHEMA.COLUMNSWHERE TABLE_SCHEMA =N'dbo' AND TABLE_NAME =N'V1';
Para obter as informações de dependência da exibição (objetos aos quais ela se refere), você pode consultar sys.dm_sql_referenced_entities, assim:
SELECT OBJECT_NAME(referenced_id) AS referenced_object, referenced_minor_id, COL_NAME(referenced_id, referenced_minor_id) AS column_nameFROM sys.dm_sql_referenced_entities(N'dbo.V1', N'OBJECT');
Você encontrará a dependência na tabela T1 e em suas três colunas:
referenced_object referenced_minor_id column_name------- ------------------- ------- ----T1 0 NULLT1 1 keycolT1 2 intcolT1 3 charcol
Como você provavelmente pode adivinhar, o valor reference_minor_id para colunas é o ID da coluna que você viu anteriormente.
Se você quiser obter as permissões de user1 em relação a V1, poderá consultar sys.database_permissions, assim:
SELECT OBJECT_NAME(major_id) AS referenced_object, minor_id, COL_NAME(major_id, minor_id) AS column_name, permission_nameFROM sys.database_permissionsWHERE major_id =OBJECT_ID(N'dbo.V1') AND grantee_principal_id =USER_ID(N'user1');
Este código gera a seguinte saída, afirmando que, de fato, user1 tem permissões select apenas para keycol e intcol, mas não para charcol:
referenced_object minor_id column_name permission_name----------- ----------- ------------ -- --------------V1 1 keycol SELECTV1 2 intcol SELECT
Novamente, o valor minor_id é o ID da coluna que você viu anteriormente. Nosso usuário, user1, tem permissões para as colunas cujos IDs são 1 e 2.
Em seguida, execute o seguinte código para representar user1 e tentar consultar todas as colunas da V1:
EXECUTAR COMO USUÁRIO =N'usuário1'; SELECT * FROM dbo.V1;
Como seria de esperar, você recebe um erro de permissão devido à falta de permissão para consultar charcol:
Msg 230, Level 14, State 1, Line 141
A permissão SELECT foi negada na coluna 'charcol' do objeto 'V1', banco de dados 'TSQLV5', esquema 'dbo'.
Tente consultar apenas keycol e intcol:
SELECT keycol, intcol FROM dbo.V1;
Desta vez, a consulta é executada com sucesso, gerando a seguinte saída:
keycol intcol----------- -----------1 102 20
Sem surpresas até agora.
Execute o seguinte código para reverter para seu usuário original:
REVERTER;
Agora vamos aplicar algumas mudanças estruturais na tabela subjacente dbo.T1. Execute o código a seguir para primeiro adicionar duas colunas chamadas datecol e binarycol e, em seguida, para descartar a coluna 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;
O SQL Server não rejeitou as alterações estruturais nas colunas referenciadas pela exibição, pois a exibição não foi criada com o atributo SCHEMABINDING. Agora, para a captura. Nesse ponto, o SQL Server ainda não atualizou as informações de metadados da exibição nos diferentes objetos de catálogo.
Use o código a seguir para consultar a visualização, ainda com seu usuário original (ainda não user1):
SELECT * FROM dbo.V1;
Você obtém a seguinte saída:
keycol intcol charcol----------- ---------- ----------1 A 9999-12-312 B 9999-12-31
Observe que intcol realmente retorna o conteúdo de charcol e charcol retorna o conteúdo de datecol. Lembre-se, não há mais intcol na tabela, mas há datecol. Além disso, você não recebe de volta a nova coluna binarycol.
Para tentar descobrir o que está acontecendo, use o código a seguir para consultar os metadados da coluna da exibição:
SELECT name AS column_name, column_id, TYPE_NAME(system_type_id) AS data_typeFROM sys.columnsWHERE object_id =OBJECT_ID(N'dbo.V1');
Este código gera a seguinte saída:
column_name column_id data_type----------- ----------- ----------keycol 1 intintcol 2 intcharcol 3 varchar
Como você pode ver, os metadados da visualização ainda não foram atualizados. Você pode ver intcol como coluna ID 2 e charcol como coluna ID 3. Na prática, intcol não existe mais, charcol deveria ser a coluna 2 e datecol deveria ser a coluna 3.
Vamos verificar se há alguma alteração nas informações de permissão:
SELECT OBJECT_NAME(major_id) AS referenced_object, minor_id, COL_NAME(major_id, minor_id) AS column_name, permission_nameFROM sys.database_permissionsWHERE major_id =OBJECT_ID(N'dbo.V1') AND grantee_principal_id =USER_ID(N'user1');
Você obtém a seguinte saída:
referenced_object minor_id column_name permission_name----------- ----------- ------------ -- --------------V1 1 keycol SELECTV1 2 intcol SELECT
As informações de permissão mostram que user1 tem permissões para as colunas 1 e 2 na exibição. No entanto, embora os metadados pensem que a coluna 2 é chamada intcol, na prática ela é mapeada para charcol em T1. Isso é perigoso, pois o usuário1 não deve ter acesso ao charcol. E se na vida real esta coluna contiver informações confidenciais, como senhas.
Vamos representar user1 novamente e consultar todas as colunas de exibição:
EXECUTAR COMO USUÁRIO ='usuário1'; SELECT * FROM dbo.V1;
Você recebe um erro de permissão dizendo que não tem acesso ao charcol:
Msg 230, Level 14, State 1, Line 211
A permissão SELECT foi negada na coluna 'charcol' do objeto 'V1', banco de dados 'TSQLV5', esquema 'dbo'.
No entanto, veja o que acontece quando você solicita explicitamente keycol e intcol:
SELECT keycol, intcol FROM dbo.V1;
Você obtém a seguinte saída:
keycol intcol----------- ----------1 A2 B
Essa consulta é bem-sucedida, apenas retorna o conteúdo de charcol em intcol. Nosso usuário, user1, não deve ter acesso a essas informações. Ops!
Neste ponto, reverta para o usuário original executando o seguinte código:
REVERTER;Atualizar módulo SQL
Você pode ver claramente que usar SELECT * na expressão de tabela interna da exibição é uma má ideia. Mas não é só isso. Geralmente, é uma boa ideia atualizar os metadados da exibição após cada alteração de DDL para objetos e colunas referenciados. Você pode fazer isso usando sp_refreshview ou o sp_refreshmodule mais geral, assim:
EXEC sys.sp_refreshsqlmodule N'dbo.V1';
Consulte a visualização novamente, agora que seus metadados foram atualizados:
SELECT * FROM dbo.V1;
Desta vez, você obtém a saída esperada:
keycol charcol datecol binarycol--------------- ---------- ---------- ---------1 A 9999 -12-31 0x1122332 B 9999-12-31 0x112233
A coluna charcol é nomeada corretamente e mostra os dados corretos; você não vê intcol e vê as novas colunas datecol e binarycol.
Consulte os metadados da coluna da visualização:
SELECT name AS column_name, column_id, TYPE_NAME(system_type_id) AS data_typeFROM sys.columnsWHERE object_id =OBJECT_ID(N'dbo.V1');
A saída agora mostra as informações corretas dos metadados da coluna:
column_name column_id data_type----------- ----------- ----------keycol 1 intcharcol 2 varchardatecol 3 datebinarycol 4 varbinary
Consulte as permissões do user1 na visualização:
SELECT OBJECT_NAME(major_id) AS referenced_object, minor_id, COL_NAME(major_id, minor_id) AS column_name, permission_nameFROM sys.database_permissionsWHERE major_id =OBJECT_ID(N'dbo.V1') AND grantee_principal_id =USER_ID(N'user1');
Você obtém a seguinte saída:
referenced_object minor_id column_name permission_name----------- ----------- ------------ -- --------------V1 1 keycol SELECT
As informações de permissão agora estão corretas. Nosso usuário, user1, tem permissões apenas para selecionar keycol e as informações de permissão para intcol foram removidas.
Para ter certeza de que tudo está bem, vamos testar isso representando o usuário1 e consultando a visualização:
EXECUTAR COMO USUÁRIO ='usuário1'; SELECT * FROM dbo.V1;
Você recebe dois erros de permissão devido à falta de permissões para datecol e binarycol:
Msg 230, Level 14, State 1, Line 281
A permissão SELECT foi negada na coluna 'datecol' do objeto 'V1', banco de dados 'TSQLV5', esquema 'dbo'.
Msg 230, Level 14, State 1, Line 281
A permissão SELECT foi negada na coluna 'binarycol' do objeto 'V1', banco de dados 'TSQLV5', esquema 'dbo'.
Tente consultar keycol e intcol:
SELECT keycol, intcol FROM dbo.V1;
Desta vez, o erro diz corretamente que não há coluna chamada intcol:
Msg 207, Nível 16, Estado 1, Linha 279
Nome de coluna inválido 'intcol'.
Consulta apenas intcol:
SELECT keycol FROM dbo.V1;
Essa consulta é executada com sucesso, gerando a seguinte saída:
keycol-----------12
Neste ponto, volte ao seu usuário original executando o seguinte código:
REVERTER;É suficiente evitar SELECT * e usar nomes de coluna explícitos?
Se você seguir uma prática que diz sem SELECT * na expressão de tabela interna da exibição, isso seria suficiente para evitar problemas? Bem vamos ver…
Use o código a seguir para recriar a tabela e a visualização, só que desta vez liste as colunas explicitamente na consulta interna da visualização:
DROP VIEW SE EXISTE dbo.V1;DROP TABLE SE EXISTE 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.V1AS SELECT keycol, intcol, charcol FROM dbo.T1;GO
Consulte a visualização:
SELECT * FROM dbo.V1;
Você obtém a seguinte saída:
keycol intcol charcol----------- ----------- ----------1 10 A2 20 B
Novamente, conceda permissões de user1 para selecionar keycol e intcol:
GRANT SELECT ON dbo.V1(keycol, intcol) TO user1;
Em seguida, aplique as mesmas alterações estruturais como você fez 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 o SQL Server aceitou essas alterações, mesmo que a exibição tenha uma referência explícita a intcol. Novamente, isso ocorre porque a visualização foi criada sem a opção SCHEMABINDING.
Consulte a visualização:
SELECT * FROM dbo.V1;
Neste ponto o SQL Server gera o seguinte erro:
Msg 207, Level 16, State 1, Procedure V1, Line 5 [Batch Start Line 344]
Nome de coluna inválido 'intcol'.
Msg 4413, Level 16, State 1, Line 345
Não foi possível usar a visualização ou função 'dbo.V1' devido a erros de ligação.
O SQL Server tentou resolver a referência intcol na exibição e, é claro, não teve êxito.
Mas e se seu plano original fosse descartar intcol e depois adicioná-lo de volta? Use o código a seguir para adicioná-lo de volta e, em seguida, consulte a exibição:
ALTER TABLE dbo.T1 ADD intcol INT NOT NULL DEFAULT(0); SELECT * FROM dbo.V1;
Este código gera a seguinte saída:
keycol intcol charcol----------- ----------- ----------1 0 A2 0 B
O resultado parece correto.
Que tal consultar a view como user1? Vamos tentar:
EXECUTAR COMO USUÁRIO ='usuário1';SELECT * FROM dbo.V1;
Ao consultar todas as colunas, você obtém o erro esperado devido à falta de permissões em relação ao charcol:
Msg 230, Level 14, State 1, Line 367
A permissão SELECT foi negada na coluna 'charcol' do objeto 'V1', banco de dados 'TSQLV5', esquema 'dbo'.
Consulte keycol e intcol explicitamente:
SELECT keycol, intcol FROM dbo.V1;
Você obtém a seguinte saída:
keycol intcol----------- -----------1 02 0
Parece que tudo está em ordem graças ao fato de que você não usou SELECT * na consulta interna da visualização, mesmo que você não tenha atualizado os metadados da visualização. Ainda assim, pode ser uma boa prática atualizar os metadados da exibição depois que o DDL for alterado para objetos e colunas referenciados para garantir a segurança.
Neste ponto, reverta para seu usuário original executando o seguinte código:
REVERTER;COMBINAÇÃO DE ESQUEMAS
Usando o atributo de visualização SCHEMABINDING você pode evitar muitos dos problemas mencionados acima. Uma das chaves para evitar o problema que você viu anteriormente é não usar SELECT * na consulta interna da visualização. Mas também há o problema de alterações estruturais em objetos dependentes, como descartar colunas referenciadas, que ainda podem resultar em erros ao consultar a exibição. Usando o atributo de visualização SCHEMABINDING, você não terá permissão para usar SELECT * na consulta interna. Além disso, o SQL Server rejeitará tentativas de aplicar alterações de DDL relevantes a objetos e colunas dependentes. Em relevante, quero dizer alterações como descartar uma tabela ou coluna referenciada. Adicionar uma coluna a uma tabela referenciada obviamente não é um problema, portanto SCHEMABINDING não impede tal alteração.
Para demonstrar isso, use o código a seguir para recriar a tabela e a exibição, com SCHEMABINDING na definição da exibição:
DROP VIEW SE EXISTE dbo.V1;DROP TABLE SE EXISTE 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 SCHEMABINDINGAS SELECT * FROM dbo.T1;GO
Você recebe um erro:
Msg 1054, Level 15, State 6, Procedure V1, Line 5 [Batch Start Line 387]
A sintaxe '*' não é permitida em objetos vinculados ao esquema.
Ao usar SCHEMABINDING, você não tem permissão para usar SELECT * na expressão de tabela interna da exibição.
Tente criar a view novamente, só que desta vez com uma lista de colunas explícita:
CREATE OR ALTER VIEW dbo.V1 WITH SCHEMABINDINGAS SELECT keycol, intcol, charcol FROM dbo.T1;GO
Desta vez, a visualização é criada com sucesso.
Conceda permissões user1 em keycol e intcol:
GRANT SELECT ON dbo.V1(keycol, intcol) TO user1;
Em seguida, tente aplicar as alterações estruturais à tabela. Primeiro, adicione algumas colunas:
ALTER TABLE dbo.T1 ADD datecol DATE NOT NULL DEFAULT('99991231'), binarycol VARBINARY(3) NOT NULL DEFAULT(0x112233);
Adicionar colunas não é um problema porque elas não podem fazer parte de exibições vinculadas ao esquema existentes, portanto, esse código é concluído com êxito.
Tente eliminar o intcol da coluna:
ALTER TABLE dbo.T1 DROP COLUMN intcol;
Você recebe o seguinte erro:
Msg 5074, Level 16, State 1, Line 418
O objeto 'V1' depende da coluna 'intcol'.
Msg 4922, Level 16, State 9, Line 418
ALTER TABLE DROP COLUMN intcol falhou porque um ou mais objetos acessam esta coluna.
A eliminação ou alteração de colunas referenciadas não é permitida quando existem objetos vinculados ao esquema.
Se você ainda precisar descartar intcol, terá que descartar a visualização de referência vinculada ao esquema primeiro, aplicar a alteração e, em seguida, recriar a visualização e reatribuir permissões, assim:
DROP VIEW SE EXISTE dbo.V1;GO ALTER TABLE dbo.T1 DROP COLUMN intcol;GO CREATE OU ALTER VIEW dbo.V1 WITH SCHEMABINDINGAS SELECT keycol, charcol, datecol, binarycol FROM dbo.T1;GO GRANT SELECT ON dbo. V1(keycol, datecol, binarycol) TO user1;GO
É claro que neste ponto não há necessidade de atualizar a definição de visualização, porque você a criou novamente.
Agora que você terminou de testar, execute o seguinte código para limpeza:
DROP VIEW IF EXISTS dbo.V1;DROP TABLE IF EXISTS dbo.T1;DROP USER IF EXISTS user1;Resumo
Usar SELECT * na expressão de tabela interna da visão é uma péssima ideia. Depois que as alterações estruturais são aplicadas aos objetos referenciados, você pode obter nomes de coluna incorretos e até permitir que os usuários acessem dados aos quais não deveriam ter acesso. É uma prática importante listar explicitamente os nomes das colunas referenciadas.
Usando SCHEMABINDING na definição de exibição, você é forçado a listar explicitamente os nomes das colunas e as alterações estruturais relevantes em objetos dependentes são rejeitadas pelo SQL Server. Portanto, pode parecer que criar visualizações com SCHEMBINDING é sempre uma boa ideia. No entanto, há uma ressalva com essa opção. Como você viu, aplicar alterações estruturais a objetos referenciados quando SCHEMBINDING é usado torna-se um processo mais longo e elaborado. Pode ser especialmente um problema em sistemas que precisam ter disponibilidade muito alta. Imagine que você precisa alterar uma coluna definida como VARCHAR(50) para VARCHAR(60). Essa não é uma alteração permitida se houver uma visualização definida com SCHEMABINDING referenciando esta coluna. As implicações de descartar várias visualizações de referência, que podem ser referenciadas por outras visualizações e assim por diante, podem ser problemáticas para o sistema. Em suma, nem sempre é tão trivial para as empresas apenas adotar uma política que diz que SCHEMABINDING deve ser usado em todos os objetos que o suportam. No entanto, adotar uma política para não usar SELECT * nas consultas internas das visualizações deve ser mais simples.
Há muito mais para explorar em relação às visualizações. Continua no próximo mês…