it-swarm.com.de

Wie erzwinge ich, dass ein Datensatz einen wahren Wert für eine boolesche Spalte und alle anderen einen falschen Wert hat?

Ich möchte erzwingen, dass nur ein Datensatz in einer Tabelle als "Standard" -Wert für andere Abfragen oder Ansichten betrachtet wird, die möglicherweise auf diese Tabelle zugreifen.

Grundsätzlich möchte ich sicherstellen, dass diese Abfrage immer genau eine Zeile zurückgibt:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Wie würde ich das in SQL machen?

20
emaynard

Bearbeiten: Dies ist auf SQL Server ausgerichtet, bevor wir MySQL kannten

Es gibt 4 5 Möglichkeiten, dies zu tun: in der Reihenfolge der am meisten gewünschten bis am wenigsten gewünschten

Wir wissen nicht, welches RDBMS dies ist, obwohl möglicherweise nicht alle zutreffen

  1. Der beste Weg ist ein gefilterter Index. Dies verwendet DRI, um die Eindeutigkeit aufrechtzuerhalten.

  2. Berechnete Spalte mit Eindeutigkeit (siehe Antwort von Jack Douglas) (hinzugefügt durch Bearbeitung 2)

  3. Eine indizierte/materialisierte Ansicht, die einem gefilterten Index mit DRI ähnelt

  4. Auslöser (wie bei anderen Antworten)

  5. Überprüfen Sie die Einschränkung mit einer UDF. Dies ist nicht sicher für Parallelität und Snapshot-Isolation. Siehe EinsZweiDreiVier

Beachten Sie, dass die Anforderung für "eine Standardeinstellung" in der Tabelle und nicht in der gespeicherten Prozedur enthalten ist. Die gespeicherte Prozedur hat dieselben Parallelitätsprobleme wie eine Prüfbedingung mit einem udf

Hinweis: gefragt am SO viele Male:

8
gbn

Hinweis: Diese Antwort wurde gegeben, bevor klar war, dass der Fragesteller etwas MySQL-spezifisches wollte. Diese Antwort ist voreingenommen gegenüber SQL Server.

Wenden Sie eine CHECK -Einschränkung auf die Tabelle an, die ein UDF aufruft und dessen Rückgabe sicherstellt Wert ist <= 1. Die UDF kann nur die Zeilen in der Tabelle WHERE isDefault = TRUE zählen. Dadurch wird sichergestellt, dass die Tabelle nicht mehr als eine Standardzeile enthält.

Sie sollten einen Bitmap oder gefilterten Index in die Spalte isDefault einfügen (abhängig von Ihrem Platforom), um sicherzustellen, dass diese UDF sehr schnell ausgeführt wird.

Vorsichtsmaßnahmen

  • Überprüfen Sie, ob die Einschränkungen bestimmteEinschränkungen haben. Sie werden nämlich für jede geänderte Zeile aufgerufen, auch wenn diese Zeilen noch nicht in einer Transaktion festgeschrieben wurden. Wenn Sie also eine Transaktion starten, eine neue Zeile als Standard festlegen und dann die alte Zeile deaktivieren, wird sich Ihre Prüfbeschränkung beschweren. Dies liegt daran, dass es ausgeführt wird, nachdem Sie die neue Zeile als Standard festgelegt haben, feststellen, dass jetzt zwei Standardeinstellungen vorhanden sind, und fehlschlagen. Die Lösung besteht dann darin, sicherzustellen, dass Sie zuerst die Standardzeile deaktivieren, bevor Sie einen neuen Standard festlegen, selbst wenn Sie diese Arbeit in einer Transaktion ausführen.
  • Als gbnhervorgehoben können UDFs inkonsistente Ergebnisse zurückgeben, wenn Sie die Snapshot-Isolation in SQL Server verwenden. Bei einer Inline-UDF tritt dieses Problem jedoch nicht auf. Da eine UDF, die nur zählt, inline-fähig ist, ist dies hier kein Problem.
  • Die Stärke der Verarbeitungsleistung eines Datenbankmoduls liegt in seiner Fähigkeit, Datensätze gleichzeitig zu bearbeiten. Mit einer UDF-gestützten Prüfbeschränkung kann die Engine diese UDF nur seriell auf jede geänderte Zeile anwenden. In Ihrem Anwendungsfall ist es unwahrscheinlich, dass Sie Massenaktualisierungen für die Tabelle PostalCodes durchführen. Dies bleibt jedoch ein Leistungsproblem für Situationen, in denen satzbasierte Aktivitäten wahrscheinlich sind.

Angesichts all dieser Einschränkungen empfehle ich, gbn's Vorschlag eines gefilterten eindeutigen Index anstelle einer Prüfbedingung zu verwenden.

10
Nick Chammas

Hier ist eine gespeicherte Prozedur (MySQL Dialect):

DELIMITER $$
DROP PROCEDURE IF EXISTS SetDefaultForZip;
CREATE PROCEDURE SetDefaultForZip (NEWID INT)
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;

    SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
    IF FOUND_TRUE = 1 THEN
        SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
        IF NEWID <> OLDID THEN
            UPDATE PostalCode SET isDefault = FALSE WHERE ID = OLDID;
            UPDATE PostalCode SET isDefault = TRUE  WHERE ID = NEWID;
        END IF;
    ELSE
        UPDATE PostalCode SET isDefault = TRUE WHERE ID = NEWID;
    END IF;
END;
$$
DELIMITER ;

Führen Sie die folgenden Schritte aus, um sicherzustellen, dass Ihre Tabelle sauber ist und die gespeicherte Prozedur funktioniert, vorausgesetzt, ID 200 ist die Standardeinstellung:

ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
CALL SetDefaultForZip(200);
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

Wie wäre es mit einem Trigger anstelle einer gespeicherten Prozedur?

DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;

Führen Sie die folgenden Schritte aus, um sicherzustellen, dass Ihre Tabelle sauber ist und der Trigger funktioniert, vorausgesetzt, ID 200 ist die Standardeinstellung:

DROP TRIGGER postalcodes_bu;
ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;
UPDATE PostalCodes SET isDefault = TRUE WHERE ID = 200;
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;
4
RolandoMySQLDBA

Sie können einen Auslöser verwenden, um die Regeln anzuwenden. Wenn eine UPDATE- oder INSERT-Anweisung isDefault auf True setzt, kann die SQL im Trigger alle anderen Zeilen auf False setzen.

Sie müssen andere Situationen berücksichtigen, wenn Sie dies anwenden. Was soll beispielsweise passieren, wenn mehr als eine Zeile in und UPDATE oder INSERT isDefault auf True setzt? Welche Regeln gelten, um einen Verstoß gegen die Regel zu vermeiden?

Ist es auch möglich, dass es keine Standardeinstellung gibt? Was passiert, wenn ein Update den isDefault von True auf False setzt?.

Sobald Sie die Regeln definiert haben, können Sie sie im Trigger erstellen.

Wie in einer anderen Antwort angegeben, möchten Sie dann eine Überprüfungsbeschränkung anwenden, um die Durchsetzung der Regeln zu unterstützen.

3
bobs

Im Folgenden werden eine Beispieltabelle und Daten erstellt:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[MyTable]') AND type in (N'U'))
DROP TABLE [dbo].[MyTable]
GO

CREATE TABLE dbo.MyTable
(
    [id] INT IDENTITY(1,1) PRIMARY KEY CLUSTERED
    , [IsDefault] BIT DEFAULT 0
)
GO

INSERT dbo.MyTable DEFAULT VALUES
GO 100

Wenn Sie eine gespeicherte Prozedur erstellen, um das IsDefault-Flag zu setzen und Berechtigungen zum Ändern der Basistabelle zu entfernen, können Sie dies mit der folgenden Abfrage erzwingen. Es würde jedes Mal einen Tabellenscan erfordern.

DECLARE @Id INT
SET @Id = 10

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END 
GO

Abhängig von der Größe der Tabelle kann ein Index für IsDefault (DESC) dazu führen, dass eine oder beide der folgenden Abfragen einen vollständigen Scan vermeiden.

DECLARE @Id INT
SET @Id = 10

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
FROM
    dbo.MyTable
WHERE
    [id] = @Id
OR  IsDefault = 1

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
FROM
    dbo.MyTable
WHERE
    [id] = @Id
OR  ([id] != @Id AND IsDefault = 1)

Wenn Sie keine Berechtigungen aus der Basistabelle entfernen können, die Integrität auf andere Weise erzwingen müssen und SQL2008 verwenden, können Sie einen gefilterten eindeutigen Index verwenden:

CREATE UNIQUE INDEX IX_MyTable_IsDefault  ON dbo.MyTable (IsDefault) WHERE IsDefault = 1
3

Für MySQL ohne gefilterte Indizes können Sie Folgendes tun. Es nutzt die Tatsache aus, dass MySQL mehrere NULL-Werte in eindeutigen Indizes zulässt und eine dedizierte Tabelle und einen Fremdschlüssel verwendet, um einen Datentyp zu simulieren, der nur NULL und 1 als Werte haben kann.

Mit dieser Lösung würden Sie anstelle von wahr/falsch nur 1/NULL verwenden.

CREATE TABLE DefaultFlag (id TINYINT UNSIGNED NOT NULL PRIMARY KEY);
INSERT INTO DefaultFlag (id) VALUES (1);

CREATE TABLE PostalCodes (
    ID INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    Zip VARCHAR (32) NOT NULL,
    isDefault TINYINT UNSIGNED NULL DEFAULT NULL,
    CONSTRAINT FOREIGN KEY (isDefault) REFERENCES DefaultFlag (id),
    UNIQUE KEY (isDefault)
);

INSERT INTO PostalCodes (Zip, isDefault) VALUES ('123', 1   );
/* Only one row can be default */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('abc', 1   );
/* ERROR 1062 (23000): Duplicate entry '1' for key 'isDefault' */

/* MySQL allows multiple NULLs in unique indexes */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('456', NULL);
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('789', NULL);

/* only NULL and 1 are admitted in isDefault thanks to foreign key */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('789', 2   );
/* ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`PostalCodes`, CONSTRAINT `PostalCodes_ibfk_1` FOREIGN KEY (`isDefault`) REFERENCES `DefaultFlag` (`id`))*/

Ich denke, das einzige, was schief gehen kann, ist, wenn jemand der Tabelle DefaultFlag eine Zeile hinzufügt. Ich denke, dies ist kein großes Problem, und Sie können es jederzeit mit Berechtigungen beheben, wenn Sie wirklich sicher sein möchten.

1
djfm
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

Dies gab Ihnen im Wesentlichen Probleme, weil Sie das Feld an der falschen Stelle platziert haben und das ist alles.

Haben Sie eine 'Defaults'-Tabelle mit PostalCode, die die ID enthält, nach der Sie suchen. Im Allgemeinen ist es auch gut, Ihre Tabellen gemäß einem Datensatz zu benennen, sodass der Name Ihrer ursprünglichen Tabelle besser als "PostalCode" bezeichnet wird. Die Standardeinstellungen sind eine einzelne Zeilentabelle, bis Sie Ihre Standardeinstellungen auf eine andere Konfiguration (z. B. Verlaufsdaten) spezialisieren müssen.

Die letzte Abfrage, die Sie möchten, lautet wie folgt:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
0
Konchog