Как создать уникальное ограничение, которое также допускает нулевые значения? - PullRequest
563 голосов
/ 20 апреля 2009

Я хочу иметь уникальное ограничение для столбца, который я собираюсь заполнить GUID. Тем не менее, мои данные содержат нулевые значения для этих столбцов. Как создать ограничение, допускающее множественные нулевые значения?

Вот пример примера . Рассмотрим эту схему:

CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
  LibraryCardId UNIQUEIDENTIFIER NULL,
  CONSTRAINT UQ_People_LibraryCardId UNIQUE (LibraryCardId)
)

Тогда посмотрите этот код для того, чего я пытаюсь достичь:

-- This works fine:
INSERT INTO People (Name, LibraryCardId) 
 VALUES ('John Doe', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This also works fine, obviously:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marie Doe', 'BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB');

-- This would *correctly* fail:
--INSERT INTO People (Name, LibraryCardId) 
--VALUES ('John Doe the Second', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This works fine this one first time:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Richard Roe', NULL);

-- THE PROBLEM: This fails even though I'd like to be able to do this:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marcus Roe', NULL);

Последний оператор завершается ошибкой с сообщением:

Нарушение ограничения UNIQUE KEY 'UQ_People_LibraryCardId'. Невозможно вставить повторяющийся ключ в объект 'dbo.People'.

Как я могу изменить мою схему и / или ограничение уникальности так, чтобы оно допускало множественные значения NULL, в то же время проверяя уникальность реальных данных?

Ответы [ 14 ]

1209 голосов
/ 20 апреля 2009

То, что вы ищете, действительно является частью стандартов ANSI SQL: 92, SQL: 1999 и SQL: 2003, то есть ограничение UNIQUE должно запрещать повторяющиеся значения, отличные от NULL, но принимать несколько значений NULL.

Однако в мире Microsoft SQL Server допускается использование одного NULL, а нескольких NULL - нет ...

В SQL Server 2008 вы можете определить уникальный отфильтрованный индекс на основе предиката, который исключает NULL:

CREATE UNIQUE NONCLUSTERED INDEX idx_yourcolumn_notnull
ON YourTable(yourcolumn)
WHERE yourcolumn IS NOT NULL;

В более ранних версиях вы можете прибегнуть к VIEWS с предикатом NOT NULL, чтобы применить ограничение.

132 голосов
/ 20 апреля 2009

SQL Server 2008 +

Вы можете создать уникальный индекс, который принимает несколько значений NULL с предложением WHERE. См. ответ ниже .

До SQL Server 2008

Вы не можете создать УНИКАЛЬНОЕ ограничение и разрешить NULL. Вам нужно установить значение по умолчанию NEWID ().

Обновите существующие значения до NEWID (), где NULL, перед созданием ограничения UNIQUE.

31 голосов
/ 12 ноября 2010

SQL Server 2008 и выше

Просто отфильтруйте уникальный индекс:

CREATE UNIQUE NONCLUSTERED INDEX UQ_Party_SamAccountName
ON dbo.Party(SamAccountName)
WHERE SamAccountName IS NOT NULL;

В более низких версиях материализованное представление по-прежнему не требуется

Для SQL Server 2005 и более ранних версий вы можете сделать это без представления. Я только что добавил уникальное ограничение, как вы просите, к одной из моих таблиц. Учитывая, что мне нужна уникальность в столбце SamAccountName, но я хочу разрешить несколько значений NULL, я использовал материализованный столбец, а не материализованное представление:

ALTER TABLE dbo.Party ADD SamAccountNameUnique
   AS (Coalesce(SamAccountName, Convert(varchar(11), PartyID)))
ALTER TABLE dbo.Party ADD CONSTRAINT UQ_Party_SamAccountName
   UNIQUE (SamAccountNameUnique)

Вы просто должны поместить что-то в вычисляемый столбец, который будет гарантированно уникальным во всей таблице, когда фактический желаемый уникальный столбец равен NULL. В этом случае PartyID - это столбец идентификаторов, а числовое значение никогда не будет совпадать с любым SamAccountName, так что это сработало для меня. Вы можете попробовать свой собственный метод - убедитесь, что вы понимаете область своих данных, чтобы не было возможности пересечения с реальными данными. Это может быть так же просто, как добавить символ дифференцирования, как этот:

Coalesce('n' + SamAccountName, 'p' + Convert(varchar(11), PartyID))

Даже если когда-нибудь PartyID станет нечисловым и может совпадать с SamAccountName, теперь это не имеет значения.

Обратите внимание, что наличие индекса, включающего вычисляемый столбец, неявно приводит к тому, что каждый результат выражения сохраняется на диск вместе с другими данными в таблице, что ДОЛЖНО занимать дополнительное дисковое пространство.

Обратите внимание, что если вам не нужен индекс, вы все равно можете сохранить ЦП, сделав предварительный расчет выражения на диске, добавив ключевое слово PERSISTED в конец определения выражения столбца.

В SQL Server 2008 и выше обязательно используйте фильтрованное решение, если возможно!

Противоречие

Обратите внимание, что некоторые специалисты по базам данных будут рассматривать это как случай "суррогатных NULL", которые определенно имеют проблемы (в основном из-за проблем, связанных с попыткой определить, является ли что-то реальным значением или * 1038). * суррогатное значение для отсутствующих данных ; также могут быть проблемы с числом ненулевых суррогатных значений, умножающихся как сумасшедшие).

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

Итак, моя история такова, что это не суррогатный NULL, и я придерживаюсь его! Поскольку на самом деле мы не хотим, чтобы значение, отличное от NULL, использовалось не для того, чтобы обмануть индекс UNIQUE, чтобы игнорировать NULL, в нашем сценарии использования нет ни одной из проблем, возникающих при обычном создании суррогатного NULL.

Все это говорит о том, что у меня нет проблем с использованием вместо этого индексированного представления, но оно вызывает некоторые проблемы, такие как требование использования SCHEMABINDING. Получайте удовольствие, добавляя новый столбец в вашу базовую таблицу (вам как минимум придется отбросить индекс, а затем отбросить представление или изменить представление, чтобы оно не было привязано к схеме). См. Полный (длинный) список требований для создания индексированного представления в SQL Server (2005) (также более поздние версии), (2000) .

Update

Если ваш столбец числовой, может возникнуть проблема обеспечения того, чтобы уникальное ограничение, использующее Coalesce, не приводило к коллизиям. В этом случае есть несколько вариантов. Можно было бы использовать отрицательное число, чтобы поместить «суррогатные значения NULL» только в отрицательный диапазон, а «реальные значения» только в положительный диапазон. В качестве альтернативы можно использовать следующий шаблон. В таблице Issue (где IssueID - PRIMARY KEY) может быть или не быть TicketID, но если он есть, он должен быть уникальным.

ALTER TABLE dbo.Issue ADD TicketUnique
   AS (CASE WHEN TicketID IS NULL THEN IssueID END);
ALTER TABLE dbo.Issue ADD CONSTRAINT UQ_Issue_Ticket_AllowNull
   UNIQUE (TicketID, TicketUnique);

Если IssueID 1 имеет билет 123, ограничение UNIQUE будет на значения (123, NULL). Если IssueID 2 не имеет билета, он будет включен (NULL, 2). Некоторые думают, что это ограничение не может быть продублировано ни для одной строки в таблице, и все же допускает наличие нескольких пустых значений.

16 голосов
/ 20 сентября 2013

Для людей, которые используют Microsoft SQL Server Manager и хотят создать уникальный, но обнуляемый индекс, вы можете создать свой уникальный индекс, как обычно, затем в свойствах индекса для нового индекса выберите «Фильтр msgstr "с левой панели, затем введите свой фильтр (который является вашим предложением where). Следует читать что-то вроде этого:

([YourColumnName] IS NOT NULL)

Это работает с MSSQL 2012

9 голосов
/ 06 февраля 2013

Когда я применил уникальный индекс ниже:

CREATE UNIQUE NONCLUSTERED INDEX idx_badgeid_notnull
ON employee(badgeid)
WHERE badgeid IS NOT NULL;

каждое ненулевое обновление и вставка не выполнялись с ошибкой ниже:

ОБНОВЛЕНИЕ не удалось, потому что следующие параметры SET имеют неправильные настройки: 'ARITHABORT'.

Я нашел это на MSDN

SET ARITHABORT должен быть включен, когда вы создаете или изменяете индексы для вычисляемых столбцов или индексированных представлений. Если для параметра SET ARITHABORT установлено значение OFF, операторы CREATE, UPDATE, INSERT и DELETE для таблиц с индексами для вычисляемых столбцов или индексированных представлений завершатся сбоем.

Итак, чтобы заставить это работать правильно, я сделал это

Щелкните правой кнопкой мыши [База данных] -> Свойства -> Параметры -> Другое. Параметры -> Разное -> Арифметическое прерывание включено -> true

Я считаю, что можно установить эту опцию в коде, используя

ALTER DATABASE "DBNAME" SET ARITHABORT ON

но я не проверял это

6 голосов
/ 20 апреля 2009

Создайте представление, которое выбирает только не-1001 * столбцы, и создайте UNIQUE INDEX в представлении:

CREATE VIEW myview
AS
SELECT  *
FROM    mytable
WHERE   mycolumn IS NOT NULL

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Обратите внимание, что вам нужно выполнить INSERT и UPDATE в представлении вместо таблицы.

Вы можете сделать это с помощью INSTEAD OF триггера:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
4 голосов
/ 18 июля 2017

Это можно сделать и в конструкторе

Щелкните правой кнопкой мыши по Индексу> Свойства , чтобы открыть это окно

capture

4 голосов
/ 20 апреля 2009

Можно создать уникальное ограничение для кластеризованного индексированного представления

Вы можете создать вид следующим образом:

CREATE VIEW dbo.VIEW_OfYourTable WITH SCHEMABINDING AS
SELECT YourUniqueColumnWithNullValues FROM dbo.YourTable
WHERE YourUniqueColumnWithNullValues IS NOT NULL;

и уникальное ограничение, подобное этому:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)
2 голосов
/ 20 апреля 2009

Возможно, рассмотрите триггер "INSTEAD OF" и выполните проверку самостоятельно? С некластеризованным (неуникальным) индексом для столбца для включения поиска.

1 голос
/ 04 мая 2018

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

Вот пример:

CREATE TRIGGER PONY.trg_pony_unique_name ON PONY.tbl_pony
 INSTEAD OF INSERT, UPDATE
 AS
BEGIN
 IF EXISTS(
    SELECT TOP (1) 1 
    FROM inserted i
    GROUP BY i.pony_name
    HAVING COUNT(1) > 1     
    ) 
     OR EXISTS(
    SELECT TOP (1) 1 
    FROM PONY.tbl_pony t
    INNER JOIN inserted i
    ON i.pony_name = t.pony_name
    )
    THROW 911911, 'A pony must have a name as unique as s/he is. --PAS', 16;
 ELSE
    INSERT INTO PONY.tbl_pony (pony_name, stable_id, pet_human_id)
    SELECT pony_name, stable_id, pet_human_id
    FROM inserted
 END
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...