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

20

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

Grundsätzlich möchte ich garantieren, 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?

Emaynard
quelle
1
"Ich möchte garantieren, dass diese Abfrage immer genau eine Zeile zurückgibt" - immer? Was ist, wenn PostalCodesleer ist? Wenn eine Zeile bereits die Eigenschaft a hat, sollte verhindert werden, dass sie auf false gesetzt wird, es sei denn, eine andere Zeile (falls vorhanden) wird in derselben SQL-Anweisung auf true gesetzt. Können Nullzeilen die Eigenschaft zwischen Transaktionsgrenzen haben? Soll die letzte Zeile in der Tabelle gezwungen werden, die Eigenschaft zu besitzen, und soll verhindert werden, dass sie gelöscht wird? Die Erfahrung zeigt mir, dass "genau eine Reihe garantieren" in der Realität etwas anderes bedeutet, oft einfach "höchstens eine Reihe".
Tag, wenn der

Antworten:

8

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

Es gibt 4 bis 5 Möglichkeiten, dies zu tun: in der Reihenfolge vom Gewünschten zum Wenigsten

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

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

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

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

  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 Eins Zwei Drei Vier

Beachten Sie, dass die Anforderung für "einen Standard" in der Tabelle und nicht in der gespeicherten Prozedur enthalten ist. Die gespeicherte Prozedur hat dieselben Parallelitätsprobleme wie eine Check-Einschränkung mit einem udf

Hinweis: SO oft gefragt:

gbn
quelle
gbn - sieht aus wie die gefilterte Index / DRI-Lösung ist SQL 2008 nur so sieht es aus, als würde ich versuchen, Option 2 überspringen.
emaynard
10

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 eine UDF aufruft, und stellen Sie sicher, dass der Rückgabewert <= 1 ist. Die UDF kann nur die Zeilen in der Tabelle WHERE zählen isDefault = TRUE. Dadurch wird sichergestellt, dass die Tabelle nicht mehr als eine Standardzeile enthält.

Sie sollten der Spalte (je nach Plattform) eine Bitmap oder einen gefilterten Index isDefaulthinzufügen, um sicherzustellen, dass diese UDF sehr schnell ausgeführt wird.

Vorbehalte

  • Prüfungsbeschränkungen weisen bestimmte Einschränkungen auf . Sie werden nämlich für jede Zeile aufgerufen, die geändert wird, 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, festgestellt haben, dass es jetzt zwei Standardeinstellungen gibt und dies fehlschlägt. Die Lösung besteht dann darin, sicherzustellen, dass Sie zuerst die Standardzeile deaktivieren, bevor Sie eine neue Standardzeile festlegen, auch wenn Sie diese Aufgabe in einer Transaktion ausführen.
  • Wie von gbn bereits erwähnt , können UDFs inkonsistente Ergebnisse zurückgeben, wenn Sie die Snapshot-Isolierung 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 der Fähigkeit, Datenmengen gleichzeitig zu bearbeiten. Bei einer von UDF unterstützten Prüfbedingung ist die Engine darauf beschränkt, diese UDF seriell auf jede Zeile anzuwenden, die geändert wird. In Ihrem Anwendungsfall ist es unwahrscheinlich, dass Sie Massenaktualisierungen für die PostalCodesTabelle durchführen. Dies ist jedoch weiterhin ein Leistungsproblem in Situationen, in denen Aktivitäten auf Gruppenbasis wahrscheinlich sind.

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

Nick Chammas
quelle
4

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 ;

Gehen Sie folgendermaßen vor, 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 der Standardwert:

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;
RolandoMySQLDBA
quelle
3

Sie können einen Trigger 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 zum Beispiel passieren, wenn mehr als eine Zeile in 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 keinen Standard 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.

Anschließend möchten Sie, wie in einer anderen Antwort angegeben, eine Prüfbedingung anwenden, um die Durchsetzung der Regeln zu unterstützen.

Bob
quelle
3

Folgendes erstellt eine Beispieltabelle und Daten:

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

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 Berechtigungen nicht 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
Mark Storey-Smith
quelle
1

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 mithilfe einer dedizierten Tabelle und eines Fremdschlüssels einen Datentyp simuliert, der nur NULL und 1 als Werte haben kann.

Bei dieser Lösung würden Sie anstelle von true / false 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 DefaultFlagTabelle eine Zeile hinzufügt . Ich denke, das ist kein großes Problem, und Sie können es immer mit Berechtigungen beheben, wenn Sie wirklich sicher sein möchten.

djfm
quelle
0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

Das hat Ihnen im Wesentlichen Probleme bereitet, weil Sie das Feld an die falsche Stelle gelegt 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 in Übereinstimmung mit einem Datensatz zu benennen, sodass der Name Ihrer Anfangstabelle besser als "PostalCode" bezeichnet werden sollte. Die Standardeinstellungen sind eine einzelne Zeilentabelle, bis Sie Ihre Standardeinstellungen auf eine andere Konfiguration (z. B. Verlauf) 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;
Konchog
quelle