RESTRIÇÃO PADRÃO, vale a pena?

18

Eu costumo criar meus bancos de dados seguindo as próximas regras:

  • Ninguém além de db_owner e sysadmin tem acesso às tabelas do banco de dados.
  • As funções de usuário são controladas na camada do aplicativo. Normalmente, uso uma função de banco de dados para conceder acesso aos modos de exibição, procedimentos e funções armazenados, mas em alguns casos, adiciono uma segunda regra para proteger alguns procedimentos armazenados.
  • Uso TRIGGERS para validar inicialmente informações críticas.

CREATE TRIGGER <TriggerName>
ON <MyTable>
[BEFORE | AFTER] INSERT
AS
    IF EXISTS (SELECT 1 
               FROM   inserted
               WHERE  Field1 <> <some_initial_value>
               OR     Field2 <> <other_initial_value>)
    BEGIN
        UPDATE MyTable
        SET    Field1 = <some_initial_value>,  
               Field2 = <other_initial_value>  
        ...  
    END
  • O DML é executado usando procedimentos armazenados:

sp_MyTable_Insert(@Field1, @Field2, @Field3, ...);
sp_MyTable_Delete(@Key1, @Key2, ...);
sp_MyTable_Update(@Key1, @Key2, @Field3, ...);

Você acha que, nesse cenário, vale a pena usar restrições padrão ou estou adicionando um trabalho extra e desnecessário ao servidor de banco de dados?

Atualizar

Entendo que, usando a restrição DEFAULT, estou fornecendo mais informações a alguém que precise administrar o banco de dados. Mas estou mais interessado em desempenho.

Presumo que o banco de dados esteja sempre verificando os valores padrão, mesmo que eu forneça o valor correto, portanto, estou fazendo o mesmo trabalho duas vezes.

Por exemplo, existe uma maneira de evitar restrições DEFAULT em uma execução de acionador?

McNets
fonte
1
Se você estiver usando procedimentos para todo o seu DML, eu faria sua lógica de validação lá. Use restrições para impedir que qualquer pessoa que tenha acesso às tabelas base cometa erros, mas os acionadores desacelerarão drasticamente as inserções.
Jonathan Fite
Que validação você está fazendo nos gatilhos? Pode ser feito com restrições de verificação?
Martin Smith
@MartinSmith depende, mas a restrição de verificação é sempre aplicada. Preciso garantir valores iniciais, como usuário, horário atual, etc. Dê uma olhada em outras perguntas: dba.stackexchange.com/questions/164678/…
McNets
1
Se você deseja uma resposta "geral", remova a parte "restrição". No SQL, restrições validam valores de entrada. Eles não definem um valor padrão . Não existe uma restrição "padrão" no SQL. Eu adicionei as tags sql-servere tsqlcomo o código na sua pergunta é altamente específico para esse DBMS
a_horse_with_no_name

Respostas:

20

Presumo que o banco de dados esteja sempre verificando os valores padrão, mesmo que eu forneça o valor correto, portanto, estou fazendo o mesmo trabalho duas vezes.

Hum, por que você assumiria isso? ;-). Dado que os padrões existem para fornecer um valor quando a coluna à qual eles estão anexados não está presente na INSERTdeclaração, eu assumiria exatamente o oposto: que eles serão completamente ignorados se a coluna associada estiver presente na INSERTdeclaração.

Felizmente, nenhum de nós precisa assumir algo devido a esta afirmação na pergunta:

Estou mais interessado em desempenho.

Perguntas sobre desempenho são quase sempre testáveis. Então, precisamos apenas fazer um teste para permitir que o SQL Server (a verdadeira autoridade aqui) responda a essa pergunta.

CONFIGURAÇÃO

Execute o seguinte uma vez:

SET NOCOUNT ON;

-- DROP TABLE #HasDefault;
CREATE TABLE #HasDefault
(
  [HasDefaultID] INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
  [SomeInt] INT NULL,
  [SomeDate] DATETIME NOT NULL DEFAULT (GETDATE())
);

-- DROP TABLE #NoDefault;
CREATE TABLE #NoDefault
(
  [NoDefaultID] INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
  [SomeInt] INT NULL,
  [SomeDate] DATETIME NOT NULL
);

-- make sure that data file and Tran Log file are grown, if need be, ahead of time:
INSERT INTO #HasDefault ([SomeInt])
  SELECT TOP (2000000) NULL
  FROM   [master].sys.[all_columns] ac1
  CROSS JOIN [master].sys.[all_columns] ac2;

Execute os testes 1A e 1B individualmente, não juntos, pois isso distorce o tempo. Execute cada uma delas várias vezes para ter uma noção do tempo médio de cada uma.

Teste 1A

TRUNCATE TABLE #HasDefault;
GO

PRINT '#HasDefault:';
SET STATISTICS TIME ON;
INSERT INTO #HasDefault ([SomeDate])
  SELECT TOP (1000000) '2017-05-15 10:11:12.000'
  FROM   [master].sys.[all_columns] ac1
  CROSS JOIN [master].sys.[all_columns] ac2;
SET STATISTICS TIME OFF;
GO

Teste 1B

TRUNCATE TABLE #NoDefault;
GO

PRINT '#NoDefault:';
SET STATISTICS TIME ON;
INSERT INTO #NoDefault ([SomeDate])
  SELECT TOP (1000000) '2017-05-15 10:11:12.000'
  FROM   [master].sys.[all_columns] ac1
  CROSS JOIN [master].sys.[all_columns] ac2;
SET STATISTICS TIME OFF;
GO

Execute os testes 2A e 2B individualmente, não juntos, pois isso distorce o tempo. Execute cada uma delas várias vezes para ter uma noção do tempo médio de cada uma.

Teste 2A

TRUNCATE TABLE #HasDefault;
GO

DECLARE @Counter INT = 0,
        @StartTime DATETIME,
        @EndTime DATETIME;

BEGIN TRAN;
--SET STATISTICS TIME ON;
SET @StartTime = GETDATE();
WHILE (@Counter < 100000)
BEGIN
  INSERT INTO #HasDefault ([SomeDate]) VALUES ('2017-05-15 10:11:12.000');
  SET @Counter = @Counter + 1;
END;
SET @EndTime = GETDATE();
--SET STATISTICS TIME OFF;
COMMIT TRAN;
PRINT DATEDIFF(MILLISECOND, @StartTime, @EndTime);

Teste 2B

TRUNCATE TABLE #NoDefault;
GO

DECLARE @Counter INT = 0,
        @StartTime DATETIME,
        @EndTime DATETIME;

BEGIN TRAN;
--SET STATISTICS TIME ON;
SET @StartTime = GETDATE();
WHILE (@Counter < 100000)
BEGIN
  INSERT INTO #NoDefault ([SomeDate]) VALUES ('2017-05-15 10:11:12.000');
  SET @Counter = @Counter + 1;
END;
SET @EndTime = GETDATE();
--SET STATISTICS TIME OFF;
COMMIT TRAN;
PRINT DATEDIFF(MILLISECOND, @StartTime, @EndTime);

Você deve ver que não há diferença real no tempo entre os testes 1A e 1B, ou entre os testes 2A e 2B. Portanto, não, não há penalidade de desempenho para ter um DEFAULTdefinido, mas não usado.

Além disso, além de documentar apenas o comportamento pretendido, você deve ter em mente que é principalmente você quem se importa com as instruções DML sendo completamente contidas nos procedimentos armazenados. O pessoal de suporte não se importa. Os desenvolvedores futuros podem não estar cientes do seu desejo de ter todo o DML encapsulado nesses procedimentos armazenados ou se importar, mesmo que eles saibam. E quem mantiver esse banco de dados após a sua saída (outro projeto ou trabalho) pode não se importar, ou pode não ser capaz de impedir o uso de um ORM, não importa o quanto eles protestem. Portanto, os Padrões podem ajudar a dar uma folga às pessoas ao fazer um INSERT, especialmente um ad-hoc INSERTfeito por um representante de suporte, pois essa é uma coluna que eles não precisam incluir (é por isso que eu sempre uso padrões na auditoria colunas de data).


E, ocorreu-me apenas que pode ser mostrado objetivamente se a DEFAULTestá marcada ou não quando a coluna associada está presente na INSERTinstrução: simplesmente forneça um valor inválido. O teste a seguir faz exatamente isso:

-- DROP TABLE #BadDefault;
CREATE TABLE #BadDefault
(
  [BadDefaultID] INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
  [SomeInt] INT NOT NULL DEFAULT (1 / 0)
);


INSERT INTO #BadDefault ([SomeInt]) VALUES (1234); -- Success!!!
SELECT * FROM #BadDefault; -- just to be sure ;-)



INSERT INTO #BadDefault ([SomeInt]) VALUES (DEFAULT); -- Error:
/*
Msg 8134, Level 16, State 1, Line xxxxx
Divide by zero error encountered.
The statement has been terminated.
*/
SELECT * FROM #BadDefault; -- just to be sure ;-)
GO

Como você pode ver, quando uma coluna (e um valor, não a palavra-chave DEFAULT) é fornecida, o Padrão é 100% ignorado. Sabemos disso porque o INSERTsucesso. Mas se o padrão for usado, ocorrerá um erro quando ele estiver sendo finalmente executado.


Existe uma maneira de evitar restrições DEFAULT em uma execução de acionador?

Embora a necessidade de evitar restrições padrão (pelo menos nesse contexto) seja completamente desnecessária, por uma questão de integridade, pode-se observar que só seria possível "evitar" uma restrição padrão dentro de um INSTEAD OFgatilho, mas não dentro de um AFTERgatilho. De acordo com a documentação para CREATE TRIGGER :

Se existirem restrições na tabela de acionadores, elas serão verificadas após a execução do acionador INSTEAD OF e antes da execução do acionador AFTER. Se as restrições forem violadas, as ações do gatilho INSTEAD OF serão revertidas e o gatilho AFTER não será acionado.

Obviamente, o uso de um INSTEAD OFacionador exigiria:

  1. Desabilitando a restrição padrão
  2. Criando um AFTERgatilho que habilita a restrição

No entanto, eu não recomendaria exatamente isso.

Solomon Rutzky
fonte
1
@McNets Não há problema :-). Esta questão realmente forneceu uma grande oportunidade para pesquisar algo. E, ao fazer isso, também tentei testar no MySQL (desde que você perguntou inicialmente sobre RDBMSs em geral) e descobri que os padrões no MySQL precisam ser constantes, com apenas uma exceção para as CURRENT_TIMESTAMPcolunas de data e hora. Portanto, o teste "fácil" de usar uma expressão inválida não funcionará lá (e um valor inválido não pode ser usado no método CREATE TABLEdesde que foi validado) e não tenho tempo para construir o teste de desempenho. Mas suspeito que a maioria dos RDBMSs os trataria da mesma maneira.
Solomon Rutzky
8

Não vejo nenhum dano significativo em ter restrições padrão. De fato, vejo uma vantagem específica - você definiu o padrão no mesmo nível de lógica que a própria definição de tabela. Se você possui um valor padrão fornecido no procedimento armazenado, alguém precisa ir até lá para descobrir qual é o valor padrão; e isso não é algo óbvio para alguém novo no sistema, necessariamente (se, por exemplo, você herdar um bilhão de dólares amanhã, comprar sua própria ilha tropical e sair e se mudar para lá, deixando outra seiva pobre para entender as coisas por conta própria).

RDFozz
fonte