bedingte eindeutige Einschränkung

92

Ich habe eine Situation, in der ich eine eindeutige Einschränkung für eine Reihe von Spalten erzwingen muss, aber nur für einen Wert einer Spalte.

So habe ich zum Beispiel eine Tabelle wie Table (ID, Name, RecordStatus).

RecordStatus kann nur den Wert 1 oder 2 (aktiv oder gelöscht) haben, und ich möchte nur dann eine eindeutige Einschränkung für (ID, RecordStatus) erstellen, wenn RecordStatus = 1 ist, da es mir egal ist, ob mehrere gelöschte Datensätze mit demselben vorhanden sind ICH WÜRDE.

Kann ich das tun, abgesehen vom Schreiben von Triggern?

Ich verwende SQL Server 2005.

np-hart
quelle
1
Dieser Entwurf ist ein allgemeiner Schmerz. Haben Sie darüber nachgedacht, das Design so zu ändern, dass die fiktiven "gelöschten" Datensätze physisch aus der Tabelle gelöscht und möglicherweise in eine "Archiv" -Tabelle verschoben werden?
Tag, wenn
1
... weil die Unfähigkeit, eine EINZIGARTIGE Einschränkung zum Erzwingen eines einfachen Schlüssels zu schreiben, als "Codegeruch" betrachtet werden sollte, IMO. Wenn Sie das Design (SQL DDL) nicht ändern können, weil viele andere Tabellen auf diese Tabelle verweisen, wette ich, dass Ihre SQL DML ebenfalls darunter leidet, dh Sie müssen daran denken, ... AND Table.RecordStatus = 1 'hinzuzufügen. zu den meisten Suchbedingungen und Verknüpfungsbedingungen, die diese Tabelle betreffen, und zu subtilen Fehlern, wenn sie gelegentlich unweigerlich weggelassen werden.
Tag, wenn

Antworten:

36

Fügen Sie eine solche Prüfbedingung hinzu. Der Unterschied besteht darin, dass Sie false zurückgeben, wenn Status = 1 und Count> 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;
D. Patrick
quelle
Ich habe mir die Einschränkungen für die Überprüfung auf Tabellenebene angesehen, aber es gibt keine Möglichkeit, die eingefügten oder aktualisierten Werte an die Funktion zu übergeben. Wissen Sie, wie das geht?
Np-Hard
Okay, ich habe ein Beispielskript veröffentlicht, mit dem Sie beweisen können, wovon ich spreche. Ich habe es getestet und es funktioniert. Wenn Sie sich die beiden kommentierten Zeilen ansehen, sehen Sie die Nachricht, die ich erhalte. Hinweis: In meiner Implementierung stelle ich lediglich sicher, dass Sie kein zweites Element mit derselben ID hinzufügen können, das aktiv ist, wenn bereits ein aktives Element vorhanden ist. Sie können die Logik so ändern, dass Sie bei einem aktiven Element kein Element mit derselben ID hinzufügen können. Mit diesem Muster sind die Möglichkeiten nahezu unbegrenzt.
D. Patrick
Ich würde die gleiche Logik in einem Trigger bevorzugen. "Eine Abfrage in einer Skalarfunktion ... kann große Probleme verursachen, wenn Ihre CHECK-Einschränkung auf einer Abfrage beruht und wenn mehr als eine Zeile von einer Aktualisierung betroffen ist. Was passiert, ist, dass die Einschränkung für jede Zeile einmal überprüft wird, bevor die Anweisung abgeschlossen ist Das bedeutet, dass die Atomizität der Anweisung unterbrochen ist und die Funktion in einem inkonsistenten Zustand für die Datenbank verfügbar gemacht wird. Die Ergebnisse sind nicht vorhersehbar und ungenau. " Siehe: blogs.conchango.com/davidportas/archive/2007/02/19/…
Uhr
Das ist nur teilweise wahr, wenn. Die Datenbank verhält sich konsistent und vorhersehbar. Die Prüfbedingung wird ausgeführt, nachdem die Zeile zur Tabelle hinzugefügt wurde und bevor die Transaktion von der Datenbank festgeschrieben wird, und Sie können sich darauf verlassen. In diesem Blog ging es um ein ziemlich einzigartiges Problem, bei dem Sie die Einschränkung für eine Reihe von Einfügungen ausführen müssen und nicht nur für jeweils eine Einfügung. ashish fordert jeweils eine Einschränkung für einen Einsatz an, und diese Einschränkung funktioniert genau, vorhersehbar und konsistent. Es tut mir leid, wenn das knapp klang; Mir gingen die Charaktere aus.
D. Patrick
3
Dies funktioniert gut für Einfügungen, scheint aber für Updates nicht zu funktionieren. EG Das Hinzufügen nach den anderen Beilagen funktioniert hier, wenn ich es nicht erwartet habe. INSERT INTO CheckConstraint VALUES (1, 'No ProblemsA', 2); Update CheckConstraint set Recordstatus = 1 wobei name = 'No ProblemsA'
dwidel
146

Siehe, der gefilterte Index . Aus der Dokumentation (Schwerpunkt Mine):

Ein gefilterter Index ist ein optimierter nicht gruppierter Index, der sich besonders für Abfragen eignet, die aus einer genau definierten Teilmenge von Daten auswählen. Es verwendet ein Filterprädikat, um einen Teil der Zeilen in der Tabelle zu indizieren. Ein gut gestalteter gefilterter Index kann die Abfrageleistung verbessern sowie die Indexwartungs- und Speicherkosten im Vergleich zu vollständigen Tabellenindizes senken.

Und hier ist ein Beispiel, das einen eindeutigen Index mit einem Filterprädikat kombiniert:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

Dies erzwingt im Wesentlichen Einzigartigkeit , IDwenn RecordStatusist 1.

Nach der Erstellung dieses Index führt eine Verletzung der Eindeutigkeit zu einem Arror:

Meldung 2601, Ebene 14,
Status 1, Zeile 13 Es kann keine doppelte Schlüsselzeile in das Objekt 'dbo.MyTable' mit dem eindeutigen Index 'MyIndex' eingefügt werden. Der doppelte Schlüsselwert ist (9999).

Hinweis: Der gefilterte Index wurde in SQL Server 2008 eingeführt. Frühere Versionen von SQL Server finden Sie in dieser Antwort .

Kanon
quelle
Beachten Sie, dass SQL Server ansi_paddinggefilterte Indizes benötigt. Stellen Sie daher sicher, dass diese Option durch Ausführen aktiviert ist, SET ANSI_PADDING ONbevor Sie einen gefilterten Index erstellen.
NaXa
10

Sie können die gelöschten Datensätze in eine Tabelle verschieben, in der die Einschränkung fehlt, und möglicherweise eine Ansicht mit UNION der beiden Tabellen verwenden, um das Erscheinungsbild einer einzelnen Tabelle beizubehalten.

Carl Manaster
quelle
2
Das ist eigentlich ziemlich klug, Carl. Es ist keine Antwort auf die Frage an sich, aber es ist eine gute Lösung. Wenn die Tabelle viele Zeilen enthält, kann dies auch die Suche nach einem aktiven Datensatz beschleunigen, da Sie sich die aktive Datensatztabelle ansehen können. Dies würde auch die Einschränkung beschleunigen, da die eindeutige Einschränkung einen Index verwendet, im Gegensatz zu der unten beschriebenen Prüfbedingung, die eine Zählung ausführen muss. Ich mag das.
D. Patrick
3

Sie können dies auf eine wirklich hackige Art und Weise tun ...

Erstellen Sie eine schemabundene Ansicht für Ihre Tabelle.

ANSICHT ERSTELLEN Was auch immer SELECT * FROM Table WHERE RecordStatus = 1

Erstellen Sie nun eine eindeutige Einschränkung für die Ansicht mit den gewünschten Feldern.

Ein Hinweis zu schemabunden Ansichten: Wenn Sie die zugrunde liegenden Tabellen ändern, müssen Sie die Ansicht neu erstellen. Viele Fallstricke deswegen.

Mindest
quelle
Dies ist ein ziemlich guter Vorschlag und nicht so "hacky". Hier finden Sie weitere Informationen zu dieser gefilterten Indexalternative .
Scott Whitlock
Es ist eine schlechte Idee. Die Frage ist es nicht.
FabianoLothor
Ich habe einmal eine schemabound-Ansicht verwendet und den Fehler nie wiederholt. Es kann ein königlicher Schmerz sein, mit ihnen zu arbeiten. Es ist nicht so, dass Sie die Ansicht neu erstellen müssen, wenn Sie die zugrunde liegende Tabelle ändern - dies müssen Sie möglicherweise für alle Ansichten tun, zumindest in SQL Server. Es ist so, dass Sie die Tabelle nicht ändern können, ohne zuvor die Ansicht gelöscht zu haben. Dies ist möglicherweise nicht möglich, ohne zuvor Verweise darauf zu löschen. Außerdem könnte der Speicher problematisch sein - entweder aufgrund des Speicherplatzes oder aufgrund der zusätzlichen Kosten für das Einfügen und Aktualisieren.
MattW
1

Da Sie Duplikate zulassen, funktioniert eine eindeutige Einschränkung nicht. Sie können eine Prüfbedingung für die RecordStatus-Spalte und eine gespeicherte Prozedur für INSERT erstellen, die die vorhandenen aktiven Datensätze überprüft, bevor Sie doppelte IDs einfügen.

Ichiban
quelle
1

Wenn Sie NULL nicht wie von Bill vorgeschlagen als RecordStatus verwenden können, können Sie seine Idee mit einem funktionsbasierten Index kombinieren. Erstellen Sie eine Funktion, die NULL zurückgibt, wenn der RecordStatus nicht einer der Werte ist, die Sie in Ihrer Einschränkung berücksichtigen möchten (und ansonsten der RecordStatus), und erstellen Sie darüber einen Index.

Dies hat den Vorteil, dass Sie andere Zeilen in der Tabelle in Ihrer Einschränkung nicht explizit untersuchen müssen, was zu Leistungsproblemen führen kann.

Ich sollte sagen, dass ich SQL Server überhaupt nicht kenne, aber ich habe diesen Ansatz in Oracle erfolgreich angewendet.

Hobo
quelle
Gute Idee, aber es gibt keine funktionsbasierten Indizes in SQL Server. Vielen Dank für die Antwort
np-hard