Implementieren eines Standardflags, das nur für eine einzelne Zeile festgelegt werden kann

31

Zum Beispiel mit einer Tabelle ähnlich der folgenden:

create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));

Es ist egal, ob das Flag als char(1), a bitoder was auch immer implementiert ist . Ich möchte nur in der Lage sein, die Einschränkung durchzusetzen, dass sie nur für eine einzelne Zeile festgelegt werden kann.

Jack Douglas
quelle
inspiriert von dieser Frage , die auf MySQL beschränkt ist
Jack Douglas
2
Die Art und Weise, wie die Frage formuliert ist, legt nahe, dass die Verwendung einer Tabelle die falsche Antwort sein muss. Aber manchmal (meistens?) Ist es eine gute Idee, eine weitere Tabelle hinzuzufügen. Das Hinzufügen einer Tabelle ist vollständig datenbankunabhängig.
Mike Sherrill 'Cat Recall'

Antworten:

31

SQL Server 2008 - Gefilterter eindeutiger Index

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
Mark Storey-Smith
quelle
16

SQL Server 2000, 2005:

Sie können die Tatsache nutzen, dass in einem eindeutigen Index nur eine Null zulässig ist:

create table t( id int identity, 
                chk1 char(1) not null default 'N' check(chk1 in('Y', 'N')), 
                chk2 as case chk1 when 'Y' then null else id end );
create unique index u_chk on t(chk2);

für 2000 benötigen Sie möglicherweise SET ARITHABORT ON(danke an @gbn für diese Info)

Jack Douglas
quelle
14

Orakel:

Da Oracle keine Einträge indiziert, bei denen alle indizierten Spalten null sind, können Sie einen funktionsbasierten eindeutigen Index verwenden:

create table foo(bar integer, chk char(1) not null check (chk in('Y', 'N')));
create unique index idx on foo(case when chk='Y' then 'Y' end);

Dieser Index indiziert immer höchstens eine einzelne Zeile.

In Kenntnis dieses Indexfaktors können Sie die Bitspalte auch etwas anders implementieren:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);

Hier werden die möglichen Werte für die Spalte chkwird Yund NULL. Maximal eine Zeile kann den Wert habenY.

Vincent Malgrat
quelle
chk braucht eine not nullEinschränkung?
Jack Douglas
@jack: Sie können eine not nullEinschränkung hinzufügen , wenn Sie keine Nullen möchten (das war mir aus den Fragenspezifikationen nicht klar). In jedem Fall kann nur eine Zeile den Wert 'Y' haben.
Vincent Malgrat
+1 Ich verstehe, was du meinst - du hast Recht, es ist nicht notwendig (aber vielleicht ist es ein bisschen ordentlicher, besonders wenn es mit einem kombiniert wird default)?
Jack Douglas
2
@jack: Ihre Bemerkung hat mich darauf aufmerksam gemacht, dass es eine noch einfachere Möglichkeit gibt, wenn Sie akzeptieren, dass die Spalte entweder Yoder sein nullkann. Sehen Sie sich mein Update an.
Vincent Malgrat
1
Option 2 hat den zusätzlichen Vorteil, dass der Index winzig wird, da nulls übersprungen werden - auf Kosten der Klarheit vielleicht
Jack Douglas
13

Ich denke, dies ist ein Fall der richtigen Strukturierung Ihrer Datenbanktabellen. Um es konkreter zu machen: Wenn Sie eine Person mit mehreren Adressen haben und eine als Standardadresse festlegen möchten, sollten Sie die Adress-ID der Standardadresse in der Personentabelle und keine Standardspalte in der Adresstabelle speichern:

Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)

Address
--------
AddressID
Street
City, State, Zip, etc.

Sie können die DefaultAddressID auf null setzen, aber auf diese Weise erzwingt die Struktur Ihre Einschränkung.

Decker97
quelle
12

MySQL:

create table foo(bar serial, chk boolean unique);
insert into foo(chk) values(null);
insert into foo(chk) values(null);
insert into foo(chk) values(false);
insert into foo(chk) values(true);

select * from foo;
+-----+------+
| bar | chk  |
+-----+------+
|   1 | NULL |
|   2 | NULL |
|   3 |    0 |
|   4 |    1 |
+-----+------+

insert into foo(chk) values(true);
ERROR 1062 (23000): Duplicate entry '1' for key 2
insert into foo(chk) values(false);
ERROR 1062 (23000): Duplicate entry '0' for key 2

Check - Bedingungen werden in MySQL ignoriert , so dass wir betrachten haben nulloder falseals falsch und trueals wahr. Höchstens 1 Reihe kann habenchk=true

Sie können es eine Verbesserung betrachten einen Trigger zu ändern hinzuzufügen falsein trueauf insert / update als Abhilfe für das Fehlen einer Check - Einschränkung - IMO es aber nicht eine Verbesserung.

Ich hatte gehofft, ein char (0) verwenden zu können, weil es

ist auch sehr schön, wenn Sie eine Spalte benötigen, die nur zwei Werte annehmen kann: Eine Spalte, die als CHAR (0) NULL definiert ist, belegt nur ein Bit und kann nur die Werte NULL und '' annehmen.

Leider bekomme ich zumindest mit MyISAM und InnoDB

ERROR 1167 (42000): The used storage engine can't index column 'chk'

--bearbeiten

Dies ist schließlich keine gute Lösung, da MySQL booleanein Synonym fürtinyint(1) ist und daher Werte ungleich Null als 0 oder 1 zulässt. Möglicherweise ist dies biteine bessere Wahl

Jack Douglas
quelle
Dies könnte meinen Kommentar zur Antwort von RolandoMySQLDBA beantworten: Können wir MySQL-Lösungen mit DRI haben?
gbn
Es ist ein bisschen hässlich , obwohl wegen der null, false, true- ich Wunder tun , wenn es etwas sauberer ist ...
Jack Douglas
@Jack - +1 für einen netten Versuch im reinen DRI-Ansatz in MySQL.
RolandoMySQLDBA
Ich würde raten, die Verwendung von false hier zu vermeiden, da die eindeutige Einschränkung nur die Bereitstellung eines solchen falschen Werts zulässt. Wenn null für false steht, sollte es durchgehend verwendet werden - die Vermeidung von false kann erzwungen werden, wenn eine zusätzliche Validierung verfügbar ist (z. B. JSR-303 / Hibernate-Validator).
Steve Chambers
1
Aktuelle Versionen von MySQL / MariaDB implementieren virtuelle Spalten, von denen ich glaube, dass sie eine etwas elegantere Lösung bieten,
MattW beschrieben wird.
10

SQL Server:

Wie es geht:

  1. Der beste Weg ist ein gefilterter Index. Verwendet DRI
    SQL Server 2008+

  2. Berechnete Spalte mit Eindeutigkeit. Verwendet DRI
    Siehe die Antwort von Jack Douglas. SQL Server 2005 und früher

  3. Eine indizierte / materialisierte Ansicht, die einem gefilterten Index ähnelt. Verwendet DRI
    Alle Versionen.

  4. Auslösen. Verwendet Code, nicht DRI.
    Alle Versionen

Wie man es nicht macht:

  1. Ü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
gbn
quelle
10

PostgreSQL:

create table foo(bar serial, chk char(1) unique check(chk='Y'));
insert into foo default values;
insert into foo default values;
insert into foo(chk) values('Y');

select * from foo;
 bar | chk
-----+-----
   1 |
   2 |
   3 | Y

insert into foo(chk) values('Y');
ERROR:  duplicate key value violates unique constraint "foo_chk_key"

--bearbeiten

oder (viel besser) einen eindeutigen Teilindex verwenden :

create table foo(bar serial, chk boolean not null default false);
create unique index foo_i on foo(chk) where chk;
insert into foo default values;
insert into foo default values;
insert into foo(chk) values(true);

select * from foo;
 bar | chk
-----+-----
   1 | f
   2 | f
   3 | t
(3 rows)

insert into foo(chk) values(true);
ERROR:  duplicate key value violates unique constraint "foo_i"
Jack Douglas
quelle
6

Diese Art von Problem ist ein weiterer Grund, warum ich diese Frage gestellt habe:

Anwendungseinstellungen in der Datenbank

Wenn Sie eine Anwendungseinstellungstabelle in Ihrer Datenbank haben, könnten Sie einen Eintrag haben, der auf die ID des einen Datensatzes verweist, den Sie als "speziell" betrachten möchten. Dann sehen Sie einfach in Ihrer Einstelltabelle nach, um welche ID es sich handelt. Auf diese Weise benötigen Sie keine ganze Spalte für nur einen einzustellenden Eintrag.

CenterOrbit
quelle
Dies ist ein großartiger Vorschlag: Er entspricht eher dem normalisierten Design, funktioniert mit jeder Datenbankplattform und ist am einfachsten zu implementieren.
Nick Chammas
+1, aber beachten Sie, dass "eine ganze Spalte" möglicherweise keinen physischen Speicherplatz verwendet, abhängig von Ihrem RDBMS :)
Jack Douglas
6

Mögliche Ansätze unter Verwendung weit verbreiteter Technologien:

1) Entziehen Sie der Tabelle die Schreibrechte. Erstellen Sie CRUD-Prozeduren, die sicherstellen, dass die Einschränkung an den Transaktionsgrenzen erzwungen wird.

2) 6NF: Lassen Sie die CHAR(1)Säule fallen. Fügen Sie eine Referenzierungstabelle hinzu, die eingeschränkt ist, um sicherzustellen, dass ihre Kardinalität eine nicht überschreitet:

alter table foo ADD UNIQUE (bar);

create table foo_Y
(
 x CHAR(1) DEFAULT 'x' NOT NULL UNIQUE CHECK (x = 'x'), 
 bar int references foo (bar)
);

Ändern Sie die Anwendungssemantik so, dass als "Standard" die Zeile in der neuen Tabelle gilt. Verwenden Sie möglicherweise Ansichten, um diese Logik zu kapseln.

3) Lassen Sie die CHAR(1)Säule fallen. Fügen Sie eine seqGanzzahlspalte hinzu. Legen Sie eine eindeutige Einschränkung auf seq. Ändern Sie die Anwendungssemantik so, dass als "Standard" die Zeile gilt, in der der seqWert eins oder der seqWert der größte / kleinste Wert oder ähnliches ist. Verwenden Sie möglicherweise Ansichten, um diese Logik zu kapseln.

eines Tages, wenn
quelle
5

Für diejenigen, die MySQL verwenden, ist hier eine geeignete gespeicherte Prozedur:

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;

Hier ist ein Trigger, der ebenfalls hilft:

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;

Versuche es !!!

RolandoMySQLDBA
quelle
3
Gibt es keine DRI-basierte Lösung für MySQL? Nur Code? Ich bin neugierig, weil ich
anfange,
4

In SQL Server 2000 und höher können Sie indizierte Ansichten verwenden, um komplexe Einschränkungen (oder Einschränkungen für mehrere Tabellen) wie die von Ihnen angeforderten zu implementieren.
Auch Oracle hat eine ähnliche Implementierung für materialisierte Ansichten mit verzögerten Prüfbedingungen.

Siehe meinen Beitrag hier .

Spaghettidba
quelle
Könnten Sie in dieser Antwort etwas mehr "Fleisch" liefern, wie einen kurzen Codeausschnitt? Im Moment sind es nur ein paar allgemeine Ideen und ein Link.
Nick Chammas
Es wäre ein bisschen schwierig, hier ein Beispiel zu finden. Wenn Sie auf den Link klicken, finden Sie das "Fleisch", das Sie suchen.
Spaghettidba
3

Standard Transitional SQL-92, weit verbreitet, z. B. SQL Server 2000 und höher:

Entziehen Sie der Tabelle die Schreibrechte. Erstellen Sie zwei Ansichten für WHERE chk = 'Y'und WHERE chk = 'N'jeweils einschließlich WITH CHECK OPTION. Für dieWHERE chk = 'Y' Ansicht eine Suchbedingung ein, deren Kardinalität eine solche nicht überschreiten darf. Gewähren Sie Schreibberechtigungen für die Ansichten.

Beispielcode für die Views:

CREATE VIEW foo_chk_N
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'N' 
WITH CHECK OPTION

CREATE VIEW foo_chk_Y
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'Y' 
       AND 1 >= (
                 SELECT COUNT(*)
                   FROM foo AS f2
                  WHERE f2.chk = 'Y'
                )
WITH CHECK OPTION
eines Tages, wenn
quelle
Selbst wenn Ihr RDBMS dies unterstützt, wird es wie verrückt serialisiert. Wenn Sie also mehr als einen Benutzer haben, haben Sie möglicherweise ein Problem
Jack Douglas
Wenn mehrere Benutzer gleichzeitig Änderungen vornehmen, müssen sie in der Schlange stehen (serialisieren) - manchmal ist dies in Ordnung, oft nicht (denken Sie an schweres OLTP oder lange Transaktionen).
Jack Douglas
3
Danke fürs klarstellen. Ich muss sagen, wenn mehrere Benutzer häufig die einzige Standardzeile festlegen, ist die Auswahl des Designs (Markierungsspalte in derselben Tabelle) fraglich.
Eintägig, wenn der
3

Hier ist eine Lösung für MySQL und MariaDB mit virtuellen Spalten, die etwas eleganter ist. Es erfordert MySQL> = 5.7.6 oder MariaDB> = 5.2:

MariaDB [db]> create table foo(bar varchar(255), chk boolean);

MariaDB [db]> describe foo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| bar   | varchar(255) | YES  |     | NULL    |       |
| chk   | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Erstellen Sie eine virtuelle Spalte mit dem Wert NULL, wenn Sie die Unique-Einschränkung nicht erzwingen möchten:

MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;

(Verwenden Sie für MySQL STOREDanstelle von PERSISTENT.)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.01 sec)

MariaDB [salt_dev]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
ERROR 1062 (23000): Duplicate entry 'a' for key 'checked_bar'

MariaDB [db]> insert into foo(bar, chk) values('b', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> select * from foo;
+------+------+-------------+
| bar  | chk  | checked_bar |
+------+------+-------------+
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    1 | a           |
| b    |    1 | b           |
+------+------+-------------+
MattW.
quelle
1

VOLLSTÄNDIGES SQL-92-Standard: Verwenden Sie eine Unterabfrage in einer CHECKEinschränkung, die nicht allgemein implementiert ist, z. B. in Access2000 (ACE2007, Jet 4.0, was auch immer) und höher, wenn Sie sich im ANSI-92-Abfragemodus befinden .

Beispielcode: Beachten Sie, dass CHECKEinschränkungen in Access immer auf Tabellenebene gelten. Da die CREATE TABLEAnweisung in der Frage eine CHECKEinschränkung auf Zeilenebene verwendet, muss sie leicht durch Hinzufügen eines Kommas geändert werden:

create table foo(bar int identity, chk char(1), check (chk in('Y', 'N')));

ALTER TABLE foo ADD 
   CHECK (1 >= (
                SELECT COUNT(*) 
                  FROM foo AS f2 
                 WHERE f2.chk = 'Y'
               ));
eines Tages, wenn
quelle
1
Nicht gut in jedem RDBMS, das ich verwendet habe ... es gibt viele Vorbehalte
Jack Douglas
0

Ich habe nur die Antworten durchgesehen, sodass ich möglicherweise eine ähnliche Antwort verpasst habe. Die Idee ist, eine generierte Spalte zu verwenden, die entweder pk oder eine Konstante ist, die nicht als Wert für pk existiert

create table foo 
(  bar int not null primary key
,  chk char(1) check (chk in('Y', 'N'))
,  some_name generated always as ( case when chk = 'N' 
                                        then bar 
                                        else -1 
                                   end )
, unique (somename)
);

AFAIK dies ist in SQL2003 gültig (da Sie nach einer agnostischen Lösung suchen). DB2 lässt dies zu, nicht sicher, wie viele andere Anbieter dies akzeptieren.

Lennart
quelle