Posso adicionar uma restrição exclusiva que ignora violações existentes?

40

Eu tenho uma tabela que atualmente tem valores duplicados em uma coluna.

Não consigo remover essas duplicatas incorretas, mas gostaria de impedir que valores adicionais não exclusivos sejam adicionados.

Posso criar um UNIQUEque não verifique a conformidade existente?

Eu tentei usar, NOCHECKmas não obtive sucesso.

Nesse caso, tenho uma tabela que vincula informações de licenciamento a "CompanyName"

EDIT: Ter várias linhas com o mesmo "CompanyName" é um dado inválido, mas não podemos remover ou atualizar essas duplicatas no momento. Uma abordagem é fazer com que eles INSERTusem um procedimento armazenado que falhe para duplicatas ... Se fosse possível que o SQL verifique a exclusividade por si só, isso seria preferível.

Esses dados são consultados pelo nome da empresa. Para as poucas duplicatas existentes, isso significa que várias linhas são retornadas e exibidas ... Embora isso esteja errado, é aceitável em nosso caso de uso. O objetivo é evitá-lo no futuro. Parece-me pelos comentários que eu tenho que fazer essa lógica nos procedimentos armazenados.

Mateus
fonte
Você tem permissão para alterar a tabela (adicione mais uma coluna)?
precisa saber é o seguinte
@ypercube infelizmente não.
Matthew

Respostas:

33

A resposta é sim". Você pode fazer isso com um índice filtrado (veja aqui para documentação).

Por exemplo, você pode fazer:

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

Isso cria um índice exclusivo, apenas em novas linhas, e não nas linhas antigas. Esta formulação específica permitiria duplicatas com os valores existentes.

Se você tiver apenas algumas cópias, poderá fazer algo como:

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);
Gordon Linoff
fonte
2
Se isso é bom ou não, depende de itens existentes "antigos" impedirem a criação de novos itens com o mesmo valor.
precisa
11
@supercat. . . Dei uma formulação alternativa para criar o índice em tudo, exceto nos valores duplicados existentes.
Gordon Linoff 4/13
11
Para que o último funcione, seria necessário garantir que um deles fosse omitido na lista, um ID para cada valor de chave distinto duplicado e também seria necessário garantir que, se o item que foi deliberadamente omitido da lista fosse removido da tabela , um item com uma chave igual será removido da lista.
precisa
@supercat. . . Concordo. Manter o índice consistente para atualizações e exclusões é ainda mais desafiador, porque você não pode recriar o índice em um gatilho. De qualquer forma, tive a impressão do OP de que os dados - ou pelo menos as duplicatas - não estão mudando com frequência, se é que mudam.
Gordon Linoff
Por que não excluir uma lista de valores em vez de uma lista de IDs? Então você não precisa excluir um ID por valor duplicado da lista de IDs excluídos
JMD Coalesce
23

Sim, você pode fazer isso.

Aqui está uma tabela com duplicatas:

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

Vamos ignorar os existentes e garantir que nenhuma nova duplicata possa ser adicionada:

-- 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

Vamos testar esta solução:

-- 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'.
AK
fonte
4
Exceto que ele não pode adicionar uma coluna à tabela.
Aaron Bertrand
3
Eu gosto de como essa resposta transforma como os valores NULL são tratados de maneira não-padrão em restrições exclusivas em algo útil. Truque astuto.
precisa saber é o seguinte
@ ypercubeᵀᴹ, você poderia explicar o que não é padrão no manuseio de NULL em restrições exclusivas? Como é diferente do que você esperaria? Obrigado!
Noach 29/11
11
@Noach no SQL Server, uma UNIQUErestrição em uma coluna anulável garante que haja no máximo um único NULLvalor. O padrão SQL (e quase todos os outros DBMSs SQL) diz que deve permitir qualquer número de NULLvalores (ou seja, a restrição deve ignorar valores nulos).
ypercubeᵀᴹ
@ ypercubeᵀᴹ Então, para implementar isso em um DBMS diferente, precisamos usar DEFAULT 0 em vez de NULL. Corrigir?
Noach 29/11
16

O índice exclusivo filtrado é uma idéia brilhante, mas possui uma pequena desvantagem - não importa se você usa a WHERE identity_column > <current value>condição ou o WHERE identity_column NOT IN (<list of ids for duplicate values here>).

Com a primeira abordagem, você ainda poderá inserir dados duplicados no futuro, duplicados dos dados existentes (agora). Por exemplo, se você tiver (mesmo apenas uma) linha agora CompanyName = 'Software Inc.', o índice não proibirá a inserção de mais uma linha com o mesmo nome da empresa. Só o proibirá se você tentar duas vezes.

Com a segunda abordagem, há uma melhoria, o acima não funcionará (o que é bom.) No entanto, você ainda poderá inserir mais duplicatas ou duplicatas existentes. Por exemplo, se você tiver (duas ou mais) linhas agora com CompanyName = 'DoubleData Co.', o índice não proibirá a inserção de mais uma linha com o mesmo nome da empresa. Só o proibirá se você tentar duas vezes.

(Atualização) Isso pode ser corrigido se, para cada nome duplicado, você mantiver fora da lista de exclusões um ID. Se, como no exemplo acima, houver 4 linhas com CompanyName = DoubleData Co.IDs e duplicados 4,6,8,9, a lista de exclusão deverá ter apenas 3 desses IDs.

Com a segunda abordagem, outra desvantagem é a condição complicada (quanto pesada depende de quantas duplicatas existem), pois o SQL-Server parece não oferecer suporte ao NOT INoperador na WHEREparte dos índices filtrados. Veja SQL-Fiddle . Em vez disso WHERE (CompanyID NOT IN (3,7,4,6,8,9)), você terá que ter algo como WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9)não tenho certeza se há implicações de eficiência com essa condição, se você tiver centenas de nomes duplicados.


Outra solução (semelhante à do @Alex Kuznetsov) é adicionar outra coluna, preenchê-la com números de classificação e adicionar um índice exclusivo, incluindo esta coluna:

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) ; 

Em seguida, a inserção de uma linha com nome duplicado falhará devido à DEFAULT 1propriedade e ao índice exclusivo. Isso ainda não é 100% infalível (enquanto o de Alex é). As duplicatas ainda serão Rninseridas se estiver explicitamente definido na INSERTinstrução ou se os Rnvalores forem atualizados com códigos maliciosos.

SQL-Fiddle-2

ypercubeᵀᴹ
fonte
-2

Outra alternativa é escrever uma função escalar que verifique se já existe um valor na tabela e, em seguida, chame essa função a partir de uma restrição de verificação.

Isso fará coisas horríveis para o desempenho.

Andarilho de Pedra Verde
fonte
Além dos problemas apontados por Aaron, a resposta não explica como essa restrição de verificação pode ser adicionada e ignora as duplicatas existentes.
precisa saber é o seguinte
-2

Estou procurando o mesmo - crie um índice exclusivo não confiável para que os dados ruins existentes sejam ignorados, mas novos registros não podem ser duplicados de qualquer coisa que já exista.

Ao ler este tópico, me parece que uma solução melhor é escrever um gatilho que verifique se há duplicatas na tabela pai, e se existem duplicatas entre essas tabelas, ROLLBACK TRAN.

Brad
fonte