Могу ли я добавить уникальное ограничение, которое игнорирует существующие нарушения?

41

У меня есть таблица, которая в настоящее время имеет повторяющиеся значения в столбце.

Я не могу удалить эти ошибочные дубликаты, но хотел бы предотвратить добавление дополнительных неуникальных значений.

Могу ли я создать документ UNIQUE, который не проверяет наличие соответствия?

Я пытался использовать, NOCHECKно безуспешно.

В этом случае у меня есть таблица, которая связывает информацию о лицензировании с «CompanyName»

РЕДАКТИРОВАТЬ: наличие нескольких строк с одним и тем же «CompanyName» является неверными данными, но мы не можем удалить или обновить эти дубликаты в настоящее время. Один из подходов состоит в том, чтобы INSERTиспользовать хранимую процедуру, которая не будет работать для дубликатов ... Если бы было возможно, чтобы SQL самостоятельно проверял уникальность, это было бы предпочтительнее.

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

Мэтью
источник
Вам разрешено менять таблицу (добавить еще один столбец)?
ypercubeᵀᴹ
@ypercube, к сожалению, нет.
Матфея

Ответы:

34

Ответ "да". Вы можете сделать это с помощью отфильтрованного индекса (см. Документацию здесь ).

Например, вы можете сделать:

create unique index t_col on t(col) where id > 1000;

Это создает уникальный индекс, только на новый строк, а не для старых. Эта конкретная формулировка позволит дублировать с существующими значениями.

Если у вас есть всего несколько дубликатов, вы можете сделать что-то вроде:

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);
Гордон Линофф
источник
2
Будет ли это хорошо, будет зависеть от того, должны ли «старые» существующие элементы предотвращать создание новых элементов с той же ценностью.
суперкат
1
@supercat. , , Я дал альтернативную формулировку для построения индекса на все, кроме существующих повторяющихся значений.
Гордон Линофф
1
Чтобы последние работали, нужно было бы убедиться, что один из списка пропустил один идентификатор для каждого отдельного значения ключа, имеющего дубликаты, а также обеспечить, чтобы элемент, который был намеренно пропущен из списка, был удален из таблицы. элемент с равным ключом будет удален из списка.
суперкат
@supercat. , , Я согласен. Поддержание согласованности индекса для обновлений и удалений является еще более сложной задачей, поскольку вы не можете заново создать индекс в триггере. В любом случае, у ОП сложилось впечатление, что данные - или, по крайней мере, дубликаты - меняются не часто, если вообще.
Гордон Линофф
Почему бы не исключить список значений вместо списка идентификаторов? Тогда вам не нужно исключать один идентификатор на дублированное значение из списка исключенных идентификаторов
JMD Coalesce
23

Да, вы можете сделать это.

Вот таблица с дубликатами:

CREATE TABLE dbo.Party
  (
    ID INT NOT NULL
           IDENTITY ,
    CONSTRAINT PK_Party PRIMARY KEY ( ID ) ,
    Name VARCHAR(30) NOT NULL
  ) ;
GO

INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' ),
        ( 'Luke Skywalker' ),
        ( 'Luke Skywalker' ),
        ( 'Harry Potter' ) ;
GO

Давайте проигнорируем существующие и убедимся, что новые дубликаты не могут быть добавлены:

-- Add a new column to mark grandfathered duplicates.
ALTER TABLE dbo.Party ADD IgnoreThisDuplicate INT NULL ;
GO

-- The *first* instance will be left NULL.
-- *Secondary* instances will be set to their ID (a unique value).
UPDATE  dbo.Party
SET     IgnoreThisDuplicate = ID
FROM    dbo.Party AS my
WHERE   EXISTS ( SELECT *
                 FROM   dbo.Party AS other
                 WHERE  other.Name = my.Name
                        AND other.ID < my.ID ) ;
GO

-- This constraint is not strictly necessary.
-- It prevents granting further exemptions beyond the ones we made above.
ALTER TABLE dbo.Party WITH NOCHECK
ADD CONSTRAINT CHK_Party_NoNewExemptions 
CHECK(IgnoreThisDuplicate IS NULL);
GO

SELECT * FROM dbo.Party;
GO

-- **THIS** is our pseudo-unique constraint.
-- It works because the grandfathered duplicates have a unique value (== their ID).
-- Non-grandfathered records just have NULL, which is not unique.
CREATE UNIQUE INDEX UNQ_Party_UniqueNewNames ON dbo.Party(Name, IgnoreThisDuplicate);
GO

Давайте проверим это решение:

-- cannot add a name that exists
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.

-- cannot add a name that exists and has an ignored duplicate
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Luke Skywalker' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.


-- can add a new name 
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

-- but only once
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.
Аляска
источник
4
За исключением того, что он не может добавить столбец в таблицу.
Аарон Бертран
3
Мне нравится, как этот ответ превращает то, как значения NULL обрабатываются нестандартным способом в уникальном ограничении, во что-то полезное. Хитрый трюк.
ypercubeᵀᴹ
@ ypercubeᵀᴹ, не могли бы вы объяснить, что нестандартно в обработке NULL в уникальных ограничениях? Чем он отличается от того, что вы ожидали? Благодарность!
Ноах
1
@Noach в SQL Server, UNIQUEограничение в столбце, допускающем значение NULL, гарантирует, что существует не более одного NULLзначения. Стандарт SQL (и почти все другие СУБД SQL) говорит, что он должен разрешать любое количество NULLзначений (т. Е. Ограничение должно игнорировать нулевые значения).
ypercubeᵀᴹ
@ ypercubeᵀᴹ Итак, чтобы реализовать это в другой СУБД, нам просто нужно использовать DEFAULT 0, а не NULL. Правильный?
Ноах
16

Отфильтрованный уникальный индекс - блестящая идея, но он имеет небольшой недостаток - независимо от того, используете ли вы WHERE identity_column > <current value>условие или WHERE identity_column NOT IN (<list of ids for duplicate values here>).

При первом подходе вы все равно сможете вставлять дубликаты данных в будущем, дубликаты существующих (сейчас) данных. Например, если у вас есть (даже только одна) строка с CompanyName = 'Software Inc.'индексом, индекс не будет запрещать вставку еще одной строки с таким же названием компании. Это только запретит, если вы попробуете дважды.

Со вторым подходом есть улучшение, вышеупомянутое не будет работать (что хорошо.) Однако вы все равно сможете вставить больше дубликатов или существующих дубликатов. Например, если у вас есть (две или более) строки теперь с CompanyName = 'DoubleData Co.'индексом, индекс не будет запрещать вставку еще одной строки с таким же названием компании. Это только запретит, если вы попробуете дважды.

(Обновление) Это можно исправить, если для каждого дублированного имени вы исключаете из списка исключений один идентификатор. Если, как и в приведенном выше примере, имеется 4 строки с дубликатами CompanyName = DoubleData Co.и идентификаторами 4,6,8,9, в списке исключений должно быть только 3 из этих идентификаторов.

При втором подходе еще одним недостатком является громоздкое условие (насколько громоздким оно зависит от того, сколько дубликатов существует в первую очередь), поскольку SQL-Server, похоже, не поддерживает NOT INоператор в WHEREчасти отфильтрованных индексов. Смотрите SQL-Fiddle . Вместо этого у WHERE (CompanyID NOT IN (3,7,4,6,8,9))вас должно быть что-то вроде того, WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9)что я не уверен, есть ли последствия для эффективности с таким условием, если у вас есть сотни повторяющихся имен.


Другое решение (похожее на @Alex Kuznetsov's) заключается в добавлении еще одного столбца, заполнении его номерами рангов и добавлении уникального индекса, включающего этот столбец:

ALTER TABLE Company
  ADD Rn TINYINT DEFAULT 1;

UPDATE x
SET Rn = Rnk
FROM
  ( SELECT 
      CompanyID,
      Rn,
      Rnk = ROW_NUMBER() OVER (PARTITION BY CompanyName 
                               ORDER BY CompanyID)
    FROM Company 
  ) x ;

CREATE UNIQUE INDEX CompanyName_UQ 
  ON Company (CompanyName, Rn) ; 

Затем вставка строки с повторяющимся именем завершится неудачно из-за DEFAULT 1свойства и уникального индекса. Это все еще не на 100% надежно (в то время как у Алекса). Дубликаты по-прежнему будут добавляться, если Rnв INSERTоператоре явно указано значение или Rnзначения были злонамеренно обновлены.

SQL-Fiddle-2

ypercubeᵀᴹ
источник
-2

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

Это будет делать ужасные вещи для производительности.

Гринстоун Уолкер
источник
Помимо проблем, указанных Аароном, в ответе не объясняется, как можно добавить это проверочное ограничение, чтобы игнорировать существующие дубликаты.
ypercubeᵀᴹ
-2

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

При чтении этой темы мне приходит в голову, что лучшим решением является написание триггера, который будет проверять [вставлено] в родительской таблице наличие дубликатов, и, если между этими таблицами есть дубликаты, ROLLBACK TRAN.

штифтик
источник