DEFAULT CONSTRAINT, стоит ли?

19

Я обычно проектирую свои базы данных по следующим правилам:

  • Никто кроме db_owner и sysadmin не имеет доступа к таблицам базы данных.
  • Роли пользователей контролируются на уровне приложений. Я обычно использую одну роль БД для предоставления доступа к представлениям, хранимым процедурам и функциям, но в некоторых случаях я добавляю второе правило для защиты некоторых хранимых процедур.
  • Я использую TRIGGERS для первоначальной проверки критической информации.

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 выполняется с использованием хранимых процедур:

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

Считаете ли вы, что в этом случае стоит использовать DEFAULT CONSTRAINTs, или я добавляю дополнительную и ненужную работу на сервер БД?

Обновить

Я понимаю, что с помощью ограничения DEFAULT я даю больше информации кому-то еще, кто должен управлять базой данных. Но я в основном заинтересован в производительности.

Я предполагаю, что база данных всегда проверяет значения по умолчанию, даже если я предоставляю правильное значение, поэтому я делаю одну и ту же работу дважды.

Например, есть ли способ избежать ограничения DEFAULT при выполнении триггера?

McNets
источник
1
Если вы используете процедуры для всех ваших DML, то я бы сделал вашу логику проверки там. Используйте ограничения, чтобы не допустить ошибок у тех, кто имеет доступ к базовым таблицам, но триггеры значительно замедляют вставки.
Джонатан
Какую проверку вы делаете в триггерах? Можно ли сделать это с помощью проверочных ограничений?
Мартин Смит
@MartinSmith это зависит, но проверочные ограничения всегда применяются, мне нужно убедиться, что начальные значения, такие как пользователь, текущее время и т. Д. Посмотрите на другие мои вопросы: dba.stackexchange.com/questions/164678/…
McNets
1
Если вы хотите получить «общий» ответ, вы должны удалить часть «ограничение». В SQL ограничения проверяют входные значения. Они не определяют значение по умолчанию . В SQL нет такого понятия, как «ограничение по умолчанию». Я добавил теги, sql-serverи tsqlпоскольку код в вашем вопросе очень специфичен для этой СУБД
a_horse_with_no_name

Ответы:

21

Я предполагаю, что база данных всегда проверяет значения по умолчанию, даже если я предоставляю правильное значение, поэтому я делаю одну и ту же работу дважды.

Хм, с чего ты это взял? ;-). Учитывая, что существуют значения по умолчанию для предоставления значения, когда столбец, к которому они присоединены, отсутствует в INSERTвыражении, я бы предположил прямо противоположное: они полностью игнорируются, если в INSERTвыражении присутствует соответствующий столбец .

К счастью, никому из нас не нужно ничего предполагать из-за этого утверждения в вопросе:

Я в основном заинтересован в производительности.

Вопросы о производительности почти всегда можно проверить. Поэтому нам просто нужно подготовить тест, чтобы SQL Server (настоящий авторитет здесь) мог ответить на этот вопрос.

НАСТРОИТЬ

Запустите следующее один раз:

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;

Выполните тесты 1A и 1B индивидуально, а не вместе, так как это искажает время. Запустите каждый из них несколько раз, чтобы получить представление о среднем времени для каждого из них.

Тест 1А

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

Тест 1Б

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

Выполните тесты 2A и 2B по отдельности, а не вместе, так как это искажает время. Запустите каждый из них несколько раз, чтобы получить представление о среднем времени для каждого из них.

Тест 2А

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

Тест 2В

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

Вы должны увидеть, что нет реальной разницы во времени между тестами 1A и 1B или между тестами 2A и 2B. Так что нет, нет снижения производительности, чтобы иметь DEFAULTопределенный, но не использованный.

Кроме того, помимо простого документирования предполагаемого поведения, вам необходимо помнить, что в основном вы заботитесь о том, чтобы DML-выражения полностью содержались в ваших хранимых процедурах. Поддержка людей не волнует. Будущие разработчики могут не знать о вашем желании инкапсулировать все DML в эти хранимые процедуры или не беспокоиться, даже если они это знают. А тому, кто поддерживает эту БД после вашего ухода (другой проект или задание), может быть наплевать, или он не сможет предотвратить использование ORM, независимо от того, сколько он протестует. Таким образом, значения по умолчанию могут помочь в том, что они дают людям «аут» при выполнении INSERT, особенно ad-hocINSERT сделанном представителем службы поддержки, поскольку это один столбец, который им не нужно включать (именно поэтому я всегда использую значения по умолчанию при аудите столбцы даты).


И мне просто пришло в голову, что довольно объективно можно показать, проверяется или нет DEFAULTфлажок, когда в INSERTвыражении присутствует связанный столбец : просто укажите недопустимое значение. Следующий тест делает именно это:

-- 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

Как видите, если указан столбец (и значение, а не ключевое слово DEFAULT), значение по умолчанию игнорируется на 100%. Мы знаем это, потому что это INSERTудается. Но если используется значение по умолчанию, возникает ошибка, поскольку он в конечном итоге выполняется.


Есть ли способ избежать ограничения DEFAULT при выполнении триггера?

Хотя необходимость избегать ограничений по умолчанию (по крайней мере, в этом контексте) совершенно не нужна, для полноты можно отметить, что можно было бы «избежать» ограничений по умолчанию только внутри INSTEAD OFтриггера, но не внутри AFTERтриггера. Согласно документации для CREATE TRIGGER :

Если для таблицы триггеров существуют ограничения, они проверяются после выполнения триггера INSTEAD OF и до выполнения триггера AFTER. Если ограничения нарушаются, действия триггера INSTEAD OF откатываются, а триггер AFTER не запускается.

Конечно, используя INSTEAD OF триггера потребует:

  1. Отключение ограничения по умолчанию
  2. Создание AFTERтриггера, который включает ограничение

Однако я бы точно не рекомендовал это делать.

Соломон Руцкий
источник
1
@McNets Нет проблем :-). Этот вопрос фактически предоставил прекрасную возможность что-то исследовать. При этом я также попытался протестировать MySQL (поскольку вы изначально спрашивали о СУБД в целом) и обнаружил, что значения по умолчанию в MySQL должны быть константами, за исключением только для CURRENT_TIMESTAMPстолбцов datetime. Таким образом, «простой» тест с использованием недопустимого выражения там не сработает (и недопустимое значение не может быть использовано, CREATE TABLEпоскольку он проверен), и у меня нет времени на создание теста производительности. Но я подозреваю, что большинство РСУБД будут относиться к ним одинаково.
Соломон Руцкий
9

Я не вижу существенного вреда в наличии ограничений по умолчанию. На самом деле, я вижу одно конкретное преимущество - вы определили значение по умолчанию на том же уровне логики, что и само определение таблицы. Если у вас есть значение по умолчанию, которое вы предоставляете в своей хранимой процедуре, кто-то должен пойти туда, чтобы узнать, каково значение по умолчанию; и это не то, что было бы очевидно для кого-то новичка в системе, обязательно (если, например, вы унаследуете миллиард долларов завтра, купите свой собственный тропический остров, выйдете и переедете туда, оставив какой-то другой бедный сок, чтобы понять вещи) самостоятельно).

RDFozz
источник