условное уникальное ограничение

95

У меня есть ситуация, когда мне нужно применить уникальное ограничение для набора столбцов, но только для одного значения столбца.

Так, например, у меня есть таблица типа Table (ID, Name, RecordStatus).

RecordStatus может иметь только значение 1 или 2 (активен или удален), и я хочу создать уникальное ограничение для (ID, RecordStatus) только тогда, когда RecordStatus = 1, поскольку мне все равно, есть ли несколько удаленных записей с одинаковыми МНЕ БЫ.

Могу ли я это сделать, помимо написания триггеров?

Я использую SQL Server 2005.

нп-жесткий
источник
1
Этот дизайн - обычная боль. Рассматривали ли вы изменение дизайна, чтобы условно «удаленные» записи физически удалялись из таблицы и, возможно, перемещались в «архивную» таблицу?
однажды, когда
1
... потому что невозможность написать ограничение UNIQUE для обеспечения соблюдения простого ключа следует рассматривать как «запах кода», ИМО. Если вы не можете изменить дизайн (SQL DDL), потому что многие другие таблицы ссылаются на эту таблицу, то держу пари, что ваш SQL DML также страдает в результате, т.е. вы должны не забыть добавить ... AND Table.RecordStatus = 1 ' к большинству условий поиска и условий присоединения, связанных с этой таблицей и испытывающих небольшие ошибки, когда она неизбежно иногда пропускается.
однажды, когда

Ответы:

37

Добавьте такое ограничение проверки. Разница в том, что вы вернете false, если Status = 1 и Count> 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;
Д. Патрик
источник
Я посмотрел на ограничения проверки на уровне таблицы, но не заметил, что есть способ передать значения, которые вставляются или обновляются в функцию, вы знаете, как это сделать?
np-hard
Хорошо, я опубликовал образец сценария, который поможет вам доказать, о чем я говорю. Я протестировал, и он работает. Если вы посмотрите на две прокомментированные строки, вы увидите сообщение, которое я получаю. Замечание: в моей реализации я просто гарантирую, что вы не сможете добавить второй элемент с тем же идентификатором, который активен, если уже есть один активный. Вы можете изменить логику таким образом, чтобы при наличии активного элемента нельзя было добавить какой-либо элемент с тем же идентификатором. С этим шаблоном возможности практически безграничны.
Д. Патрик
Я бы предпочел ту же логику в триггере. "запрос в скалярной функции ... может создать большие проблемы, если ваше ограничение CHECK основано на запросе и если какое-либо обновление затрагивает более одной строки. Что происходит, так это то, что ограничение проверяется один раз для каждой строки до завершения оператора Это означает, что атомарность оператора нарушена, и функция будет представлена ​​базе данных в несогласованном состоянии. Результаты непредсказуемы и неточны ». См .: blogs.conchango.com/davidportas/archive/2007/02/19/…
onedaywhen
Однажды это верно лишь частично. База данных ведет себя согласованно и предсказуемо. Ограничение проверки будет выполнено после добавления строки в таблицу и до того, как транзакция будет зафиксирована СУБД, и вы можете рассчитывать на это. В этом блоге говорилось о довольно уникальной проблеме, когда вам нужно выполнить ограничение для набора вставок, а не только для одной вставки за раз. ashish запрашивает ограничение для одной вставки за раз, и это ограничение будет работать точно, предсказуемо и последовательно. Прошу прощения, если это прозвучало лаконично; У меня заканчивались персонажи.
Д. Патрик
3
Это отлично подходит для вставок, но, похоже, не работает для обновлений. EG Добавление этого после других вставок работает здесь, когда я этого не ожидал. ВСТАВИТЬ В ЗНАЧЕНИЯ CheckConstraint (1, 'Нет проблемA', 2); update CheckConstraint устанавливает Recordstatus = 1, где name = 'No ProblemsA'
dwidel
152

Вот отфильтрованный индекс . Из документации (выделено мной):

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

А вот пример объединения уникального индекса с предикатом фильтра:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

По сути, это обеспечивает уникальность того, IDкогда RecordStatusесть 1.

После создания этого индекса нарушение уникальности вызовет ошибку:

Msg 2601, уровень 14, состояние 1, строка 13
Невозможно вставить повторяющуюся ключевую строку в объект «dbo.MyTable» с уникальным индексом «MyIndex». Повторяющееся значение ключа - (9999).

Примечание: отфильтрованный индекс был введен в SQL Server 2008. Для более ранних версий SQL Server см. Этот ответ .

каноник
источник
Обратите внимание, что SQL Server требует наличия ansi_paddingотфильтрованных индексов, поэтому убедитесь, что эта опция включена, выполнив SET ANSI_PADDING ONперед созданием отфильтрованного индекса.
naXa
10

Вы можете переместить удаленные записи в таблицу, в которой отсутствует ограничение, и, возможно, использовать представление с UNION двух таблиц, чтобы сохранить внешний вид одной таблицы.

Карл Манастер
источник
2
На самом деле это довольно умный Карл. Сам по себе это не ответ на вопрос, но хорошее решение. Если в таблице много строк, это также может ускорить поиск активной записи, поскольку вы можете просмотреть активную таблицу записей. Это также ускорило бы ограничение, потому что уникальное ограничение использует индекс, а не проверочное ограничение, которое я написал ниже, которое должно выполнять счет. Мне это нравится.
Д. Патрик
3

Вы можете сделать это по-настоящему хакерским способом ...

Создайте для своей таблицы представление, связанное со схемой.

СОЗДАТЬ ВИД Независимо от того, что ВЫБРАТЬ * ИЗ таблицы, ГДЕ RecordStatus = 1

Теперь создайте уникальное ограничение для представления с нужными полями.

Одно замечание о представлениях, связанных со схемой: если вы измените базовые таблицы, вам придется воссоздать представление. Из-за этого много подводных камней.

Мин.
источник
Это довольно хорошее предложение, и не такое уж "хакерское". Вот дополнительная информация об этой альтернативе отфильтрованного индекса .
Скотт Уитлок
Это плохая идея. Вопрос не в этом.
FabianoLothor 01
Я однажды воспользовался схематичным представлением и ни разу не повторил ошибку. Работа с ними может быть настоящей головной болью. Дело не в том, что вам нужно воссоздавать представление, если вы изменяете базовую таблицу - вы потенциально должны делать это для всех представлений, по крайней мере, на сервере SQL. Дело в том, что вы не можете изменить таблицу, предварительно не отбросив представление, что вы, возможно, не сможете сделать без предварительного удаления ссылок на него. Да, плюс хранилище может быть проблематичным - либо из-за места, либо из-за затрат, которые оно добавляет для вставки и обновления.
MattW,
1

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

ичибан
источник
1

Если вы не можете использовать NULL в качестве RecordStatus, как предлагал Билл, вы можете объединить его идею с индексом на основе функций. Создайте функцию, которая возвращает NULL, если RecordStatus не является одним из значений, которые вы хотите учитывать в своем ограничении (и RecordStatus в противном случае), и создайте индекс над этим.

Это будет иметь то преимущество, что вам не придется явно проверять другие строки в таблице в вашем ограничении, что может вызвать проблемы с производительностью.

Я должен сказать, что вообще не знаю SQL-сервер, но я успешно использовал этот подход в Oracle.

Бродяга
источник
хорошая идея, но на сервере sql нет индексированных функций, спасибо за ответ
np-hard