Posso aggiungere un vincolo unico che ignora le violazioni esistenti?


40

Ho una tabella che attualmente ha valori duplicati in una colonna.

Non riesco a rimuovere questi duplicati errati, ma vorrei impedire l'aggiunta di ulteriori valori non univoci.

Posso creare un oggetto UNIQUEche non verifica la conformità esistente?

Ho provato a usare NOCHECKma non ci sono riuscito.

In questo caso ho una tabella che lega le informazioni sulla licenza a "CompanyName"

EDIT: avere più righe con lo stesso "CompanyName" sono dati errati, ma al momento non possiamo rimuovere o aggiornare quei duplicati. Un approccio consiste INSERTnell'utilizzare una procedura memorizzata che fallirà per i duplicati ... Se fosse possibile che SQL controlli da solo l'unicità, sarebbe preferibile.

Questi dati vengono richiesti per nome dell'azienda. Per i pochi duplicati esistenti ciò significa che vengono restituite e visualizzate più righe ... Anche se questo è sbagliato, è accettabile nel nostro caso d'uso. L'obiettivo è prevenirlo in futuro. Mi sembra dai commenti che devo fare questa logica nelle procedure memorizzate.


Ti è permesso cambiare la tabella (aggiungi un'altra colonna)?
ypercubeᵀᴹ

@ypercube purtroppo no.
Matthew,

Risposte:


33

La risposta è si". Puoi farlo con un indice filtrato (vedi qui per la documentazione).

Ad esempio, puoi fare:

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

Questo crea un indice univoco, solo su nuove righe, anziché sulle vecchie righe. Questa particolare formulazione consentirebbe duplicati con valori esistenti.

Se hai solo una manciata di duplicati, potresti fare qualcosa del tipo:

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);

2
Se ciò sia positivo dipenderà dal fatto che "vecchi" articoli esistenti debbano impedire la creazione di nuovi articoli con lo stesso valore.
supercat

1
@supercat. . . Ho dato una formulazione alternativa per costruire l'indice su tutto tranne i valori duplicati esistenti.
Gordon Linoff,

1
Affinché quest'ultimo funzioni, si dovrebbe garantire che uno omesso dall'elenco sia un ID per ciascun valore chiave distinto che avesse duplicati e dovrebbe anche assicurarsi che se l'elemento che è stato deliberatamente omesso dall'elenco sia stato rimosso dalla tabella , un elemento con una chiave uguale verrà rimosso dall'elenco.
supercat

@supercat. . . Sono d'accordo. Mantenere l'indice coerente per gli aggiornamenti e le eliminazioni è tanto più impegnativo perché non è possibile ricreare l'indice in un trigger. In ogni caso, ho avuto l'impressione dall'OP che i dati - o almeno i duplicati - non cambino spesso, se non del tutto.
Gordon Linoff,

Perché non escludere un elenco di valori anziché un elenco di ID? Quindi non è necessario escludere un ID per valore duplicato dall'elenco degli ID esclusi
JMD Coalesce,

23

Si, puoi farlo.

Ecco una tabella con duplicati:

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

Ignoriamo quelli esistenti e assicuriamo che non sia possibile aggiungere nuovi duplicati:

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

Proviamo questa soluzione:

-- 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
Tranne che non può aggiungere una colonna alla tabella.
Aaron Bertrand

3
Mi piace come questa risposta trasforma il modo in cui i valori NULL vengono trattati in modo non standard in un unico vincolo in qualcosa di utile. Trucco astuto.
ypercubeᵀᴹ

@ ypercubeᵀᴹ, potresti spiegare cosa non è standard nella gestione NULL in vincoli univoci? In che cosa differisce da quello che ti aspettavi? Grazie!
Noach,

1
@Noach in SQL Server, un UNIQUEvincolo in una colonna nullable assicura che ci sia al massimo un singolo NULLvalore. Lo standard SQL (e quasi tutti gli altri DBMS SQL) afferma che dovrebbe consentire qualsiasi numero di NULLvalori (cioè il vincolo dovrebbe ignorare i valori null).
ypercubeᵀᴹ

@ ypercubeᵀᴹ Quindi per implementarlo su un DBMS diverso, dobbiamo solo usare DEFAULT 0 anziché NULL. Corretta?
Noach,

16

L'indice univoco filtrato è un'idea geniale ma presenta uno svantaggio minore, indipendentemente dal fatto che si utilizzi la WHERE identity_column > <current value>condizione o il WHERE identity_column NOT IN (<list of ids for duplicate values here>).

Con il primo approccio, sarai ancora in grado di inserire dati duplicati in futuro, duplicati di dati esistenti (ora). Ad esempio, se ora hai (anche solo una) riga con CompanyName = 'Software Inc.', l'indice non proibirà l'inserimento di un'altra riga con lo stesso nome di società. Lo proibirà solo se ci provi due volte.

Con il secondo approccio c'è un miglioramento, quanto sopra non funzionerà (il che è buono.) Tuttavia, sarai comunque in grado di inserire più duplicati o duplicati esistenti. Ad esempio, se ora disponi di (due o più) righe con CompanyName = 'DoubleData Co.', l'indice non proibirà l'inserimento di un'altra riga con lo stesso nome di società. Lo proibirà solo se ci provi due volte.

(Aggiorna) Questo può essere corretto se per ogni nome duplicato, si tiene fuori dalla lista di esclusione un ID. Se, come nell'esempio precedente, ci sono 4 righe con duplicati CompanyName = DoubleData Co.e ID 4,6,8,9, l'elenco di esclusione dovrebbe avere solo 3 di questi ID.

Con il secondo approccio un altro svantaggio è la condizione ingombrante (quanto ingombrante dipende da quanti duplicati ci sono in primo luogo), dal momento che SQL Server sembra non supportare l' NOT INoperatore nella WHEREparte degli indici filtrati. Vedi SQL-Fiddle . Invece WHERE (CompanyID NOT IN (3,7,4,6,8,9)), dovresti avere qualcosa di simile, WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9)non sono sicuro che ci siano implicazioni di efficienza con tale condizione, se hai centinaia di nomi duplicati.


Un'altra soluzione (simile a quella di @Alex Kuznetsov) è quella di aggiungere un'altra colonna, compilarla con i numeri di rango e aggiungere un indice univoco che includa questa colonna:

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

Quindi, l'inserimento di una riga con un nome duplicato non riuscirà a causa della DEFAULT 1proprietà e dell'indice univoco. Questo non è ancora sicuro al 100% (mentre quello di Alex lo è). I duplicati continueranno comunque a Rnessere inseriti se sono esplicitamente impostati INSERTnell'istruzione o se i Rnvalori vengono aggiornati in modo dannoso.

SQL-violino-2


-2

Un'altra alternativa è scrivere una funzione scalare che controlla se esiste già un valore nella tabella e quindi chiamare quella funzione da un vincolo di controllo.

Questo farà cose orribili per le prestazioni.



Oltre ai problemi segnalati da Aaron, la risposta non spiega come aggiungere questo vincolo di controllo, quindi ignora i duplicati esistenti.
ypercubeᵀᴹ

-2

Sto cercando lo stesso: creare un indice univoco non attendibile in modo che i dati errati esistenti vengano ignorati, ma i nuovi record non possono essere duplicati di tutto ciò che esiste già.

Durante la lettura di questo thread, mi viene in mente che una soluzione migliore è quella di scrivere un trigger che controllerà [inserito] sulla tabella padre per i duplicati e se esistono duplicati tra quelle tabelle, ROLLBACK TRAN.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.