É SELECT * ok em um gatilho. Ou estou pedindo problemas?

8

Estou preso em um debate no trabalho e preciso de alguns conselhos sobre possíveis armadilhas que eu poderia estar ignorando.

Imagine um cenário em que um acionador é usado para copiar registros excluídos em uma tabela de auditoria. O gatilho usa SELECT *. Todo mundo aponta e grita e nos diz o quanto isso é ruim.

No entanto, se uma modificação for feita na Estrutura da tabela principal, e a tabela de auditoria for ignorada, o gatilho gerará um erro informando às pessoas que a tabela de auditoria também precisa de modificação.

O erro será detectado durante o teste em nossos servidores DEV. Mas precisamos garantir a DEV de correspondências de produção, para que permitamos SELECT * em sistemas de produção (somente gatilhos).

Portanto, minha pergunta é: estou sendo pressionado a remover o SELECT *, mas não tenho certeza de que outra forma garantir que estamos capturando automaticamente erros de desenvolvimento dessa natureza, de alguma idéia ou é uma prática recomendada?

Reunimos um exemplo abaixo:

--Create Test Table
CREATE TABLE dbo.Test(ID INT IDENTITY(1,1), Person VARCHAR(255))
--Create Test Audit Table
CREATE TABLE dbo.TestAudit(AuditID INT IDENTITY(1,1),ID INT, Person VARCHAR(255))

--Create Trigger on Test
CREATE TRIGGER [dbo].[trTestDelete] ON [dbo].[Test] AFTER DELETE
NOT FOR REPLICATION
AS
BEGIN
    SET NOCOUNT ON;
    INSERT  dbo.TestAudit([ID], [Person])
    SELECT  *
    FROM    deleted
END

--Insert Test Data into Test
INSERT INTO dbo.Test VALUES
('Scooby')
,('Fred')
,('Shaggy')

--Perform a delete
DELETE dbo.Test WHERE Person = 'Scooby'

UPDATE (reformular a pergunta):

Sou DBA e preciso garantir que os desenvolvedores não forneçam scripts de implantação mal pensados, contribuindo para a documentação de práticas recomendadas. SELECT * causa um erro na DEV quando o desenvolvedor ignora a tabela de auditoria (esta é uma rede de segurança), portanto o erro é detectado no início do processo de desenvolvimento. Mas em algum lugar na Constituição do SQL - a segunda alteração diz "Não usaremos SELECT *". Então agora há um esforço para se livrar da rede de segurança.

Como você substitui a rede de segurança ou devo considerar essa prática recomendada para os acionadores?

ATUALIZAÇÃO 2: (solução)

Obrigado por todas as suas contribuições, não tenho certeza se tenho uma resposta clara, porque este parece ser um assunto muito cinzento. Mas, coletivamente, você forneceu pontos de discussão que podem ajudar nossos desenvolvedores a avançar na definição de suas Melhores Práticas.

Obrigado Daevinpor sua contribuição, sua resposta fornece as bases para alguns mecanismos de teste que nossos desenvolvedores podem implementar. +1

Obrigado CM_Dayton, suas sugestões que contribuem para as práticas recomendadas podem ser benéficas para qualquer um que esteja desenvolvendo os Acionadores de Auditoria. +1

Muito obrigado ypercube, você refletiu bastante sobre os problemas relacionados a tabelas que sofrem diferentes formas de alteração de definição. +1

Em conclusão:

Is Select * ok in a tigger? Sim, é uma área cinzenta, não siga cegamente a ideologia "Selecionar * é ruim".

Am I asking for Trouble? Sim, fazemos mais do que apenas adicionar novas colunas às tabelas.

pacificamente
fonte
você responde a si mesmo na pergunta. selecione * será interrompido se a tabela de origem for alterada. Para garantir que dev e prod sejam os mesmos, use alguma forma de controle de origem.
Bob Klimes
pergunta um pouco mais ampla, com que frequência você exclui registros e quantos como porcentagem total da tabela? uma alternativa aos gatilhos seria ter um sinalizador de bit que marque as linhas como excluídas e um trabalho do agente que seja executado em um agendamento para movê-las para uma tabela de log. Você pode incorporar as verificações do trabalho do agente para verificar se o esquema da tabela corresponde e o trabalho simplesmente falhará se houver um problema com essa etapa até que seja corrigido.
Tanner
Eu geralmente concordo com o fato de SELECT *ser preguiçoso, mas como você tem um motivo legítimo para usá-lo, é mais cinza que preto e branco. O que você deve tentar fazer é algo como isso , mas ajustá-lo para não só têm a mesma contagem de coluna, mas que os nomes de colunas e tipos de dados são os mesmos (desde que alguém poderia alterar os tipos de dados e ainda causa problemas no db normalmente não pegou com sua SELECT *'rede de segurança'.
Daevin
3
Eu gosto da idéia de usar SELECT *como rede de segurança, mas ela não pega todos os casos. Por exemplo, se você soltar uma coluna e adicioná-la novamente. Isso mudará a ordem das colunas e (a menos que todas as colunas sejam do mesmo tipo) as inserções na tabela de auditoria falharão ou resultarão em perda de dados devido às conversões implícitas de tipo.
precisa saber é o seguinte
2
Também me pergunto como seu design de auditoria funcionará quando uma coluna for descartada de uma tabela. Você também remove a coluna da tabela de auditoria (e perde todos os dados de auditoria anteriores)?
precisa saber é o seguinte

Respostas:

10

Normalmente, é considerado programação "lenta".

Como você está inserindo especificamente dois valores em sua TestAudittabela aqui, tenha cuidado para garantir que seu select também esteja recebendo exatamente dois valores. Porque se, por algum motivo, essa Testtabela tiver ou já obtiver uma terceira coluna, esse gatilho falhará.

Não está diretamente relacionado à sua pergunta, mas se você estiver configurando uma tabela de auditoria, também adicionarei algumas colunas adicionais à sua TestAudittabela para ...

  • acompanhar a ação que você está auditando (excluir neste caso, inserções ou atualizações)
  • coluna de data / hora para rastrear quando o evento de auditoria ocorreu
  • coluna de ID do usuário para rastrear quem executou a ação que você está auditando.

Portanto, isso resulta em uma consulta como:

INSERT dbo.TestAudit([ID], [Person], [AuditAction], [ChangedOn], [ChangedBy])
SELECT [ID], [Person], 
   'Delete', -- or a 'D' or a numeric lookup to an audit actions table...
   GetDate(), -- or SYSDATETIME() for greater precision
   SYSTEM_USER -- or some other value for WHO made the deletion
FROM deleted

Dessa forma, você obtém as colunas exatas necessárias e audita o que / quando / por que / quem é o evento de auditoria.

CaM
fonte
"ID do usuário" Este é complicado com a auditoria. Normalmente, as contas do banco de dados não correspondem aos usuários reais. Com muito mais frequência, eles correspondem a um único aplicativo da Web ou outro tipo de componente, com um único conjunto de credenciais usadas por esse componente. (E, às vezes, os componentes também compartilham credenciais.) Portanto, as credenciais do banco de dados são bastante inúteis como um identificador de quem fez o que, a menos que você esteja interessado apenas em qual componente fez. Mas transmitir dados de aplicativos que identificam o "quem" não é exatamente fácil com uma função de gatilho, tanto quanto eu sei.
Jpmc26
veja atualização para a pergunta.
pacreely 3/17/17
Outro problema que pode surgir com o SELECT * em geral (embora provavelmente não esteja no seu exemplo) é que, se as colunas da tabela subjacente não estiverem na mesma ordem que as colunas de inserção, a inserção falhará.
CaM
3

Comentei isso em sua pergunta, mas achei que tentaria realmente apresentar uma solução de código.

Eu geralmente concordo em SELECT *ser preguiçoso, mas como você tem um motivo legítimo para usá-lo, é mais cinza do que em preto e branco.

O que você deve (na minha opinião) tentar fazer é algo como isso , mas ajustá-lo para garantir que os nomes de colunas e tipos de dados são os mesmos (desde que alguém poderia alterar os tipos de dados e ainda causa problemas no db normalmente não pego com a sua SELECT *'segurança internet'.

Você pode até criar uma função que permita verificar rapidamente se a versão de auditoria da tabela corresponde à versão de não auditoria:

-- The lengths are, I believe, max values for the corresponding db objects. If I'm wrong, someone please correct me
CREATE FUNCTION TableMappingComparer(
    @TableCatalog VARCHAR(85) = NULL,
    @TableSchema VARCHAR(32) = NULL,
    @TableName VARCHAR(128) = NULL) RETURNS BIT
AS
BEGIN
    DECLARE @ReturnValue BIT = NULL;
    DECLARE @VaryingColumns INT = NULL;

    IF (@TableCatalog IS NOT NULL
            AND @TableSchema IS NOT NULL
            AND @TableName IS NOT NULL)
        SELECT @VaryingColumns = COUNT(COLUMN_NAME)
            FROM (SELECT COLUMN_NAME,
                        DATA_TYPE -- Add more columns that you want to ensure are identical
                    FROM INFORMATION_SCHEMA.COLUMNS
                    WHERE TABLE_CATALOG = @TableCatalog
                        AND TABLE_SCHEMA = @TableSchema
                        AND TABLE_NAME = @TableName
                EXCEPT
                    SELECT COLUMN_NAME,
                            DATA_TYPE -- Add more columns that you want to ensure are identical
                        FROM INFORMATION_SCHEMA.COLUMNS
                        WHERE (TABLE_CATALOG = @TableCatalog
                            AND TABLE_SCHEMA = @TableSchema
                            AND TABLE_NAME = @TableName + 'Audit')
                            AND (COLUMN_NAME != 'exclude your audit table specific columns')) adt;
    IF @VaryingColumns = 0
        SET @ReturnValue = 1
    ELSE IF @VaryingColumns > 0
        SET @ReturnValue = 0

    RETURN @ReturnValue;
END;

O SELECT ... EXCEPT SELECT ...Auditmostrará quais colunas da tabela não estão na tabela Auditoria. Você pode até alterar a função para retornar o nome de colunas que não são iguais, em vez de apenas mapearem ou não, ou até criar uma exceção.

Em seguida, você pode executar isso antes de passar DEVpara os PRODUCTIONservidores de cada tabela no banco de dados, usando um cursor sobre:

SELECT TABLE_NAME
    FROM INFORMATION_SCHEMA.TABLES
    WHERE NOT (TABLE_NAME LIKE '%Audit')
Daevin
fonte
11
veja update à pergunta
pacreely
Ainda bem que pude ajudar. Agradecemos a você por ler todas as respostas e trazê-las de volta à sua equipe para sugestões; adaptabilidade e vontade de melhorar são as maneiras pelas quais os departamentos de tecnologia mantêm as empresas funcionando e funcionando sem problemas! : D
Daevin 3/17/17
0

A instrução que indicará o gatilho falhará e o gatilho falhará. Seria uma prática melhor documentar o gatilho e a trilha de auditoria para que você modifique a consulta para adicionar as colunas em vez de especificar o *.

No mínimo, você deve modificar o gatilho para que ele falhe normalmente ao registrar erros em uma tabela e, talvez, coloque um alerta na tabela em que o gatilho está registrando os erros.

Isso também lembra: você pode colocar um gatilho ou alerta quando alguém altera a tabela e adiciona mais colunas ou remove colunas, para notificá-lo a acrescentar o gatilho.

Em termos de desempenho, acredito que * não muda nada, apenas aumenta as chances de falhas no caminho quando as coisas mudam e também pode causar latência da rede quando você extrai mais informações da rede quando necessário. Há um tempo e um lugar para *, mas, como descrito acima, você tem melhores soluções e ferramentas para tentar.

Shaulinator
fonte
0

Se a estrutura original ou a tabela de auditoria mudar, você garante que terá um problema com o seu select *.

INSERT INTO [AuditTable]
(Col1,Col2,Col3)
SELECT * 
FROM [OrigTable] or [deleted];

Se qualquer um deles mudar, o gatilho terá um erro.

Você poderia fazer:

INSERT INTO [AuditTable]
SELECT * 
FROM [OrigTable];

Mas, como diz CM_Dayton, isso é uma programação lenta e abre as portas para outras inconsistências. Para que esse cenário funcione, você precisa ter certeza absoluta de que está atualizando a estrutura de ambas as tabelas ao mesmo tempo.

MguerraTorres
fonte