Как создать уникальное ограничение, которое также допускает null?

Я хочу иметь уникальное ограничение для столбца, который я собираюсь заполнить с помощью 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, проверяя при этом уникальность на фактических данных?

Ответ 1

SQL Server 2008 +

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

До SQL Server 2008

Вы не можете создать ограничение UNIQUE и разрешить NULL. Вам нужно установить значение NEWID() по умолчанию.

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

Ответ 2

То, что вы ищете, действительно является частью стандартов 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 для принудительного применения ограничения.

Ответ 3

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 -нибудь PartyID станет нечисловым и может совпадать с SamAccountName, теперь это не имеет значения.

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

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

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

полемика

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

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

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

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

Обновить

Если ваш столбец числовой, может возникнуть проблема обеспечения того, чтобы уникальное ограничение, использующее 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). Некоторые рассуждения покажут, что это ограничение не может быть продублировано для какой-либо строки в таблице, и, тем не менее, допускает несколько значений NULL.

Ответ 4

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

([YourColumnName] IS NOT NULL)

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

Ответ 5

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

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

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

Ошибка UPDATE, потому что следующие параметры SET имеют неправильные настройки: "ARITHABORT".

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

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

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

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

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

ALTER DATABASE "DBNAME" SET ARITHABORT ON

но я не тестировал этот

Ответ 6

Создайте представление, которое выбирает только столбцы не NULL и создает 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

Ответ 7

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

Вы можете создать представление так:

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)

Ответ 8

Это можно сделать и в дизайнере

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

capture

Ответ 9

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

Ответ 10

Как указывалось ранее, SQL Server не реализует стандарт ANSI, когда дело доходит до UNIQUE CONSTRAINT. С этим 2007 годом существует билет на Microsoft Connect. Как предложено там, и здесь лучшие варианты на сегодняшний день - использовать отфильтрованный индекс, как указано в другом ответе или вычисленный столбец, например:

CREATE TABLE [Orders] (
  [OrderId] INT IDENTITY(1,1) NOT NULL,
  [TrackingId] varchar(11) NULL,
  ...
  [ComputedUniqueTrackingId] AS (
      CASE WHEN [TrackingId] IS NULL
      THEN '#' + cast([OrderId] as varchar(12))
      ELSE [TrackingId_Unique] END
  ),
  CONSTRAINT [UQ_TrackingId] UNIQUE ([ComputedUniqueTrackingId])
)

Ответ 11

Вы можете создать триггер 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

Ответ 12

Вы не можете сделать это с помощью ограничения UNIQUE, но вы можете сделать это в триггере.

    CREATE TRIGGER [dbo].[OnInsertMyTableTrigger]
   ON  [dbo].[MyTable]
   INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @Column1 INT;
    DECLARE @Column2 INT; -- allow nulls on this column

    SELECT @Column1=Column1, @Column2=Column2 FROM inserted;

    -- Check if an existing record already exists, if not allow the insert.
    IF NOT EXISTS(SELECT * FROM dbo.MyTable WHERE [email protected] AND [email protected] @Column2 IS NOT NULL)
    BEGIN
        INSERT INTO dbo.MyTable (Column1, Column2)
            SELECT @Column2, @Column2;
    END
    ELSE
    BEGIN
        RAISERROR('The unique constraint applies on Column1 %d, AND Column2 %d, unless Column2 is NULL.', 16, 1, @Column1, @Column2);
        ROLLBACK TRANSACTION;   
    END

END

Ответ 13

CREATE UNIQUE NONCLUSTERED INDEX [UIX_COLUMN_NAME]
ON [dbo].[Employee]([Username] ASC) WHERE ([Username] IS NOT NULL) 
WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF, 
DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF, ONLINE = OFF, 
MAXDOP = 0) ON [PRIMARY];

Ответ 14

этот код, если вы создаете регистрационную форму с помощью textBox и используете вставку, а ur textBox пуст и u нажимаете кнопку отправки.

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;