умовне унікальне обмеження


93

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

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

RecordStatus може мати значення лише 1 або 2 (активне чи видалене), і я хочу створити унікальне обмеження для (ID, RecordStatus) лише тоді, коли RecordStatus = 1, оскільки мені все одно, чи є кілька видалених записів з однаковим Посвідчення особи.

Крім написання тригерів, чи можу я це зробити?

Я використовую SQL Server 2005.


1
Цей дизайн є загальним болем. Ви задумувались про зміну дизайну таким чином, щоб умовно «видалені» записи фізично видалялися з таблиці і, можливо, переходили до таблиці «архіву»?
одного дня, коли

1
... оскільки неможливість написати УНІКАЛЬНЕ обмеження для забезпечення простого ключа слід розглядати як «запах коду», IMO. Якщо ви не можете змінити конструкцію (SQL DDL), оскільки багато інших таблиць посилаються на цю таблицю, я покладу пари, що ваш DML SQL також страждає в результаті, тобто вам потрібно пам'ятати про додавання ... AND Table.RecordStatus = 1 ' для більшості умов пошуку та приєднання до цієї таблиці та випробовування тонких помилок, коли це неминуче випадково опускається.
одного дня, коли

Відповіді:


36

Додайте таке обмеження перевірки. Різниця полягає в тому, що ви повернете 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/…
день, коли

Це лише частково вірно колись. База даних поводиться послідовно і передбачувано. Обмеження перевірки буде виконуватися після додавання рядка до таблиці і до того, як транзакція буде здійснена dbms, і ви можете на це розраховувати. Цей блог говорив про досить унікальну проблему, коли вам потрібно виконати обмеження щодо набору вставок, а не лише для однієї вставки за раз. ashish просить обмеження для однієї вставки за раз, і це обмеження буде працювати точно, передбачувано та послідовно. Мені шкода, якщо це звучало стисло; У мене закінчувалися персонажі.
Д. Патрік

3
Це чудово працює для вставок, але, схоже, не працює для оновлень. EG Додавання цього після інших вставок працює тут, коли я не очікував. ВСТАВИТИ У ЗНАЧЕННЯ Контрольних обмежень (1, 'No ProblemsA', 2); оновити CheckConstraint встановити Recordstatus = 1 де name = 'No ProblemsA'
dwidel

149

Ось, відфільтрований індекс . З документації (курсив мій):

Відфільтрований індекс - це оптимізований некластеризований індекс, який особливо підходить для охоплення запитів, які вибираються з чітко визначеної підмножини даних. Він використовує предикат фільтру для індексації частини рядків у таблиці. Добре розроблений відфільтрований індекс може покращити ефективність запитів, а також зменшити витрати на обслуговування індексу та зберігання порівняно з індексами з повною таблицею.

І ось приклад поєднання унікального індексу з предикатом фільтру:

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

Це, по суті, підтверджує унікальність того, IDколи RecordStatusє 1.

Після створення цього індексу порушення унікальності призведе до помилки:

Повідомлення 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

Ви можете зробити це по-справжньому хакі ...

Створіть подання для схеми на вашій таблиці.

СТВОРИТИ ПЕРЕГЛЯД Незалежно від SELECT * FROM Table WHERE RecordStatus = 1

Тепер створіть унікальне обмеження для подання з потрібними полями.

Однак одне примітка про подання схем, якщо ви зміните базові таблиці, вам доведеться відтворити подання. Багато причепів через це.


Це досить гарна пропозиція, і не така "хакі". Ось додаткова інформація про цю альтернативу відфільтрованого індексу .
Скотт Уітлок,

Це погана ідея. Питання не в цьому.
FabianoLothor

Я використав подання схеми один раз і ніколи не повторював помилку. З ними може працювати королівський біль. Це не те, що вам доведеться відтворити подання, якщо ви зміните базову таблицю - вам, можливо, доведеться це робити для всіх подань, принаймні на сервері SQL. Це те, що ви не можете змінити таблицю без попереднього видалення подання, чого ви, можливо, не зможете зробити без попереднього скидання посилань на неї. О, плюс зберігання може бути проблематичним - або через простір, або через вартість, яку він додає для вставки та оновлення.
MattW

1

Оскільки ви збираєтеся дозволити дублікати, унікальне обмеження не буде працювати. Ви можете створити обмеження перевірки для стовпця RecordStatus та збережену процедуру для INSERT, яка перевіряє наявні активні записи перед тим, як вставляти повторювані ідентифікатори.


1

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

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

Слід сказати, що взагалі не знаю SQL-сервера, але я успішно застосував цей підхід в Oracle.


хороша ідея, але на сервері sql немає індексованих функцій, дякую за відповідь
np-hard
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.