Kann ich eine eindeutige Einschränkung hinzufügen, die vorhandene Verstöße ignoriert?

39

Ich habe eine Tabelle, die derzeit doppelte Werte in einer Spalte enthält.

Ich kann diese fehlerhaften Duplikate nicht entfernen, möchte jedoch verhindern, dass zusätzliche nicht eindeutige Werte hinzugefügt werden.

Kann ich eine erstellen UNIQUE, die nicht auf vorhandene Konformität überprüft?

Ich habe versucht, NOCHECKaber war erfolglos.

In diesem Fall habe ich eine Tabelle, die Lizenzinformationen mit "CompanyName" verknüpft.

BEARBEITEN: Mehrere Zeilen mit demselben "CompanyName" sind fehlerhafte Daten, aber wir können diese Duplikate derzeit nicht entfernen oder aktualisieren. Ein Ansatz besteht darin, die INSERTs eine gespeicherte Prozedur verwenden zu lassen, die bei Duplikaten fehlschlägt. Wenn SQL die Eindeutigkeit selbst überprüfen könnte, wäre dies vorzuziehen.

Diese Daten werden nach Firmennamen abgefragt. Für die wenigen vorhandenen Duplikate bedeutet dies, dass mehrere Zeilen zurückgegeben und angezeigt werden. Dies ist zwar falsch, in unserem Anwendungsfall jedoch akzeptabel. Ziel ist es, dies in Zukunft zu verhindern. Aus den Kommentaren geht hervor, dass ich diese Logik in den gespeicherten Prozeduren ausführen muss.

Matthew
quelle
Dürfen Sie die Tabelle ändern (eine weitere Spalte hinzufügen)?
ypercubeᵀᴹ
@ypercube leider nicht.
Matthew

Antworten:

33

Die Antwort ist ja". Sie können dies mit einem gefilterten Index tun (siehe hier für Dokumentation).

Zum Beispiel können Sie Folgendes tun:

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

Dadurch wird ein eindeutiger Index nur für neue Zeilen und nicht für die alten Zeilen erstellt. Diese spezielle Formulierung würde Duplikate mit vorhandenen Werten ermöglichen.

Wenn Sie nur eine Handvoll Duplikate haben, können Sie Folgendes tun:

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);
Gordon Linoff
quelle
2
Ob dies gut ist oder nicht, hängt davon ab, ob "alte" vorhandene Elemente die Erstellung neuer Elemente mit demselben Wert verhindern sollen.
Supercat
1
@supercat. . . Ich habe eine alternative Formulierung für den Aufbau des Index für alles angegeben, außer für vorhandene doppelte Werte.
Gordon Linoff
1
Damit Letzteres funktioniert, müsste sichergestellt werden, dass für jeden eindeutigen Schlüsselwert, der Duplikate enthält, eine ID aus der Liste entfernt wird. Außerdem müsste sichergestellt werden, dass das Element, das absichtlich aus der Liste entfernt wurde, aus der Tabelle entfernt wird wird ein Eintrag mit gleichem Schlüssel aus der Liste entfernt.
Supercat
@supercat. . . Genau. Umso schwieriger ist es, den Index für Aktualisierungen und Löschvorgänge konsistent zu halten, da Sie den Index in einem Trigger nicht neu erstellen können. Auf jeden Fall hatte ich vom OP den Eindruck, dass sich die Daten - oder zumindest die Duplikate - nicht oder nur selten ändern.
Gordon Linoff
Warum nicht eine Liste von Werten anstelle einer Liste von IDs ausschließen? Dann müssen Sie nicht eine ID pro doppeltem Wert aus der Liste der ausgeschlossenen IDs ausschließen
JMD Coalesce
23

Ja, das kannst du.

Hier ist eine Tabelle mit Duplikaten:

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

Lassen Sie uns vorhandene ignorieren und sicherstellen, dass keine neuen Duplikate hinzugefügt werden können:

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

Lassen Sie uns diese Lösung testen:

-- 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
quelle
4
Es sei denn, er kann der Tabelle keine Spalte hinzufügen.
Aaron Bertrand
3
Mir gefällt, wie diese Antwort die Behandlung von NULL-Werten auf nicht standardmäßige Weise in einer eindeutigen Einschränkung in etwas Nützliches verwandelt. Schlauer Trick.
Ypercubeᵀᴹ
@ ypercubeᵀᴹ, könntest du erklären, was an der NULL-Behandlung in eindeutigen Einschränkungen nicht standardgemäß ist? Wie unterscheidet es sich von dem, was Sie erwartet hätten? Vielen Dank!
Noach
1
@Noach In SQL Server stellt eine UNIQUEEinschränkung in einer nullwertfähigen Spalte sicher, dass höchstens ein einziger NULLWert vorhanden ist. Der SQL-Standard (und fast alle anderen SQL-DBMS) gibt an, dass eine beliebige Anzahl von NULLWerten zulässig sein soll (dh, die Einschränkung sollte Nullwerte ignorieren).
ypercubeᵀᴹ
@ ypercubeᵀᴹ Um dies auf einem anderen DBMS zu implementieren, müssen wir nur DEFAULT 0 anstelle von NULL verwenden. Richtig?
Noach
16

Der gefilterte eindeutige Index ist eine brillante Idee, hat aber einen kleinen Nachteil - egal ob Sie die WHERE identity_column > <current value>Bedingung oder die verwenden WHERE identity_column NOT IN (<list of ids for duplicate values here>).

Mit dem ersten Ansatz können Sie auch in Zukunft doppelte Daten einfügen, Duplikate vorhandener (jetzt) ​​Daten. Wenn Sie beispielsweise jetzt (auch nur eine) Zeile mit haben CompanyName = 'Software Inc.', wird der Index das Einfügen einer weiteren Zeile mit demselben Firmennamen nicht verbieten. Es wird es nur verbieten, wenn Sie es zweimal versuchen.

Mit dem zweiten Ansatz gibt es eine Verbesserung, die oben genannten werden nicht funktionieren (was gut ist). Sie werden jedoch weiterhin in der Lage sein, mehr Duplikate oder vorhandene Duplikate einzufügen. Wenn Sie beispielsweise jetzt (zwei oder mehr) Zeilen mit haben CompanyName = 'DoubleData Co.', verbietet der Index das Einfügen einer weiteren Zeile mit demselben Firmennamen nicht. Es wird es nur verbieten, wenn Sie es zweimal versuchen.

(Update) Dies kann korrigiert werden, wenn Sie für jeden doppelten Namen eine ID aus der Ausschlussliste streichen. Wenn wie im obigen Beispiel vier Zeilen mit Duplikaten CompanyName = DoubleData Co.und IDs vorhanden sind 4,6,8,9, sollte die Ausschlussliste nur drei dieser IDs enthalten.

Beim zweiten Ansatz ist ein weiterer Nachteil die umständliche Bedingung (wie umständlich es ist, wie viele Duplikate überhaupt vorhanden sind), da SQL-Server den NOT INOperator im WHERETeil der gefilterten Indizes nicht zu unterstützen scheint . Siehe SQL-Fiddle . Stattdessen WHERE (CompanyID NOT IN (3,7,4,6,8,9))müssen Sie so etwas haben, dass WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9)ich nicht sicher bin, ob es bei einer solchen Bedingung Auswirkungen auf die Effizienz gibt, wenn Sie Hunderte von doppelten Namen haben.


Eine andere Lösung (ähnlich wie bei @Alex Kuznetsov) besteht darin, eine weitere Spalte hinzuzufügen, diese mit Rangnummern zu füllen und einen eindeutigen Index mit der folgenden Spalte hinzuzufügen:

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

Das Einfügen einer Zeile mit doppeltem Namen schlägt aufgrund der DEFAULT 1Eigenschaft und des eindeutigen Index fehl . Dies ist immer noch nicht 100% narrensicher (während es Alex ist). Duplikate werden weiterhin eingefügt, wenn das Rnexplizit in der INSERTAnweisung festgelegt ist oder wenn die RnWerte in böswilliger Absicht aktualisiert werden.

SQL-Fiddle-2

ypercubeᵀᴹ
quelle
-2

Eine andere Alternative besteht darin, eine Skalarfunktion zu schreiben, die prüft, ob in der Tabelle bereits ein Wert vorhanden ist, und diese Funktion dann über eine Prüfbedingung aufzurufen.

Dies wird schreckliche Dinge für die Leistung tun.

Greenstone Walker
quelle
Abgesehen von den von Aaron angesprochenen Problemen wird in der Antwort nicht erläutert, wie diese Prüfbedingung hinzugefügt werden kann, sodass vorhandene Duplikate ignoriert werden.
ypercubeᵀᴹ
-2

Ich suche nach dem gleichen - erstelle einen nicht vertrauenswürdigen eindeutigen Index, damit vorhandene fehlerhafte Daten ignoriert werden, aber neue Datensätze können keine Duplikate von allem sein, was bereits vorhanden ist.

Beim Lesen dieses Threads denke ich, dass eine bessere Lösung darin besteht, einen Trigger zu schreiben, der die übergeordnete Tabelle auf Duplikate überprüft und ROLLBACK TRAN, wenn zwischen diesen Tabellen Duplikate vorhanden sind.

Brad
quelle