RESTRICCIÓN POR DEFECTO, ¿vale la pena?

19

Normalmente diseño mis bases de datos siguiendo las siguientes reglas:

  • Nadie más que db_owner y sysadmin tienen acceso a las tablas de la base de datos.
  • Los roles de usuario se controlan en la capa de aplicación. Usualmente uso un rol db para otorgar acceso a las vistas, procedimientos almacenados y funciones, pero en algunos casos, agrego una segunda regla para proteger algunos procedimientos almacenados.
  • Utilizo GATILLOS para validar inicialmente la información crítica.

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
  • DML se ejecuta utilizando procedimientos almacenados:

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

¿Cree que, en este escenario, vale la pena usar RESTRICCIONES POR DEFECTO, o estoy agregando un trabajo adicional e innecesario al servidor de base de datos?

Actualizar

Entiendo que al usar la restricción DEFAULT estoy dando más información a otra persona que debe administrar la base de datos. Pero estoy principalmente interesado en el rendimiento.

Supongo que la base de datos verifica siempre los valores predeterminados, incluso si proporciono el valor correcto, por lo tanto, estoy haciendo el mismo trabajo dos veces.

Por ejemplo, ¿hay alguna manera de evitar la restricción DEFAULT dentro de una ejecución de disparo?

McNets
fuente
1
Si está utilizando procedimientos para todos sus DML, entonces haría su lógica de validación allí. Use restricciones para evitar que cualquier persona que tenga acceso a las tablas base cometa errores, pero los disparadores disminuirán drásticamente las inserciones.
Jonathan Fite
¿Qué validación estás haciendo en los disparadores? ¿Se puede hacer con restricciones de verificación en su lugar?
Martin Smith
@MartinSmith depende, pero la restricción de verificación siempre se aplica, necesito asegurar los valores iniciales, como usuario, hora actual, etc. Eche un vistazo a otras de mis preguntas: dba.stackexchange.com/questions/164678/…
McNets
1
Si desea una respuesta "general", debe eliminar la parte de "restricción". En SQL, las restricciones validan los valores de entrada. No definen un valor predeterminado . No existe una "restricción predeterminada" en SQL. sql-servertsql
Agregué

Respuestas:

21

Supongo que la base de datos verifica siempre los valores predeterminados, incluso si proporciono el valor correcto, por lo tanto, estoy haciendo el mismo trabajo dos veces.

¿Por qué asumirías eso? ;-). Dado que los valores predeterminados existen para proporcionar un valor cuando la columna a la que están unidos no está presente en la INSERTdeclaración, asumiría exactamente lo contrario: que se ignoran por completo si la columna asociada está presente en la INSERTdeclaración.

Afortunadamente, ninguno de nosotros necesita asumir nada debido a esta declaración en la pregunta:

Estoy principalmente interesado en el rendimiento.

Las preguntas sobre el rendimiento son casi siempre comprobables. Por lo tanto, solo necesitamos una prueba para permitir que SQL Server (la verdadera autoridad aquí) responda esta pregunta.

PREPARAR

Ejecute lo siguiente una 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;

Ejecute las pruebas 1A y 1B individualmente, no juntas, ya que eso sesga el tiempo. Ejecute cada uno varias veces para tener una idea del tiempo promedio para cada uno.

Prueba 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

Prueba 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

Ejecute las pruebas 2A y 2B individualmente, no juntas, ya que eso sesga el tiempo. Ejecute cada uno varias veces para tener una idea del tiempo promedio para cada uno.

Prueba 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);

Prueba 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);

Debería ver que no hay una diferencia real en el tiempo entre las pruebas 1A y 1B, o entre las pruebas 2A y 2B. Entonces, no, no hay penalización de rendimiento para tener un DEFAULTdefinido pero no utilizado.

Además, además de simplemente documentar el comportamiento previsto, debe tener en cuenta que es principalmente a usted a quien le importa que las declaraciones DML estén completamente contenidas en sus procedimientos almacenados. La gente de apoyo no les importa. Es posible que los futuros desarrolladores no sean conscientes de su deseo de tener todo el DML encapsulado dentro de esos procedimientos almacenados, o se preocupen incluso si lo saben. Y a quien sea que mantenga este DB después de que usted se haya ido (ya sea otro proyecto o trabajo) puede que no le importe o que no pueda evitar el uso de un ORM sin importar cuánto protesten. Por lo tanto, los valores predeterminados pueden ayudar, ya que le dan a la gente un "out" cuando hacen un INSERT, especialmente un ad-hoc INSERThecho por un representante de soporte, ya que esa es una columna que no necesitan incluir (por eso siempre uso los valores predeterminados en la auditoría columnas de fecha).


Y, se me ocurrió que se puede mostrar de manera bastante objetiva si DEFAULTse verifica o no cuando la columna asociada está presente en la INSERTdeclaración: simplemente proporcione un valor no válido. La siguiente prueba hace exactamente eso:

-- 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 puede ver, cuando se proporciona una columna (y un valor, no la palabra clave DEFAULT), el valor predeterminado se ignora al 100%. Lo sabemos porque INSERTtiene éxito. Pero si se usa el valor predeterminado, hay un error, ya que finalmente se está ejecutando.


¿Hay alguna manera de evitar la restricción DEFAULT dentro de una ejecución de disparo?

Si bien es necesario evitar las Restricciones predeterminadas (al menos en este contexto) es completamente innecesario, en aras de la exhaustividad se puede observar que solo sería posible "evitar" una Restricción predeterminada dentro de un INSTEAD OFDisparador, pero no dentro de un AFTERDisparador. De acuerdo con la documentación de CREATE TRIGGER :

Si existen restricciones en la tabla de desencadenadores, se verifican después de la ejecución del desencadenador INSTEAD OF y antes de la ejecución del desencadenador DESPUÉS. Si se violan las restricciones, las acciones del disparador INSTEAD OF se deshacen y el disparador DESPUÉS no se dispara.

Por supuesto, usar un INSTEAD OFdisparador requeriría:

  1. Deshabilitar la restricción predeterminada
  2. Crear un AFTERdisparador que habilite la restricción

Sin embargo, no recomendaría exactamente hacer esto.

Solomon Rutzky
fuente
1
@McNets No hay problema :-). Esta pregunta en realidad brindó una gran oportunidad para investigar algo. Y al hacerlo, también intenté probar en MySQL (ya que originalmente preguntaste sobre RDBMS en general) y descubrí que los valores predeterminados en MySQL deben ser constantes, con solo una excepción para las CURRENT_TIMESTAMPcolumnas de fecha y hora. Por lo tanto, la prueba "fácil" de usar una expresión no válida no funcionará allí (y un valor no válido no se puede usar CREATE TABLEdesde que está validado), y no tengo tiempo para construir la prueba de rendimiento. Pero sospecho que la mayoría de los RDBMS los tratarían de la misma manera.
Solomon Rutzky
9

No veo ningún daño significativo al tener restricciones predeterminadas. De hecho, veo una ventaja particular: ha definido el valor predeterminado en el mismo nivel de lógica que la definición de la tabla en sí. Si tiene un valor predeterminado que proporciona en su procedimiento almacenado, alguien tiene que ir allí para averiguar cuál es el valor predeterminado; y, eso no es algo que sería obvio para alguien nuevo en el sistema, necesariamente (si, por ejemplo, hereda un billón de dólares mañana, compra su propia isla tropical, y renuncia y se muda allí, dejando otra savia pobre para resolver las cosas fuera por su cuenta).

RDFozz
fuente