it-swarm.com.de

So implementieren Sie ein Standardflag, das nur für eine einzelne Zeile gesetzt werden kann

Zum Beispiel mit einer ähnlichen Tabelle:

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

Es spielt keine Rolle, ob das Flag als char(1), bit oder was auch immer implementiert ist. Ich möchte nur in der Lage sein, die Einschränkung zu erzwingen, dass sie nur für eine einzelne Zeile festgelegt werden kann.

SQL Server 2008 - Gefilterter eindeutiger Index

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
31

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)

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 höchstens eine einzelne Zeile.

Wenn Sie diese Index-Tatsache kennen, können Sie die Bit-Spalte auch etwas anders implementieren:

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

Hier sind die möglichen Werte für die Spalte chkY und NULL. Maximal nur eine Zeile kann den Wert Y. Haben.

14
Vincent Malgrat

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

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.

13
Decker97

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

Überprüfungsbeschränkungen werden in MySQL ignoriert, daher müssen wir null oder false als falsch und true als wahr betrachten. Höchstens 1 Zeile kann chk=true Haben.

Sie können es als Verbesserung betrachten, einen Auslöser hinzuzufügen, um false beim Einfügen/Aktualisieren in true zu ändern, um das Fehlen einer Prüfbeschränkung zu umgehen - IMO ist dies jedoch keine Verbesserung.

Ich hoffte, ein Zeichen (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 unter MySQL boolean ein Synonym für tinyint(1) ist und daher Werte ungleich Null als 0 oder 1 zulässt ist möglich, dass bit eine bessere Wahl wäre

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 Jack Douglas 'Antwort. 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 EinsZweiDreiVier
10
gbn

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) verwenden Sie einen eindeutigen Teilindex :

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"

Mögliche Ansätze mit weit verbreiteten Technologien:

1) Widerrufen Sie die 'Writer'-Berechtigungen für die Tabelle. Erstellen Sie CRUD-Prozeduren, die sicherstellen, dass die Einschränkung an Transaktionsgrenzen erzwungen wird.

2) 6NF: Löschen Sie die Spalte CHAR(1). 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 der betrachtete Standard die Zeile in der neuen Tabelle ist. Verwenden Sie möglicherweise Ansichten, um diese Logik zu kapseln.

3) Löschen Sie die Spalte CHAR(1). Fügen Sie eine Ganzzahlspalte seq hinzu. Legen Sie eine eindeutige Einschränkung für seq fest. Ändern Sie die Anwendungssemantik so, dass der betrachtete 'Standard' die Zeile ist, in der der Wert seq eins ist, oder der Wert seq der größte/kleinste Wert oder ähnliches. Verwenden Sie möglicherweise Ansichten, um diese Logik zu kapseln.

6
onedaywhen

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önnen Sie einen Eintrag haben, der auf die ID des einen Datensatzes verweist, den Sie als "speziell" betrachten möchten. Dann würden Sie einfach nachschlagen, wie die ID in Ihrer Einstellungstabelle lautet. Auf diese Weise benötigen Sie keine ganze Spalte für nur ein Element, das festgelegt wird.

6
CenterOrbit

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 ;

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;

Hier ist ein Auslöser, 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 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;

Versuche es !!!

5
RolandoMySQLDBA

In SQL Server 2000 und höher können Sie indizierte Ansichten verwenden, um komplexe (oder mehrtabellige) Einschränkungen wie die gewünschte zu implementieren.
Auch Oracle hat eine ähnliche Implementierung für materialisierte Ansichten mit verzögerten Überprüfungsbeschränkungen.

Siehe meinen Beitrag hier.

4
spaghettidba

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

Widerrufen Sie 'Writer'-Berechtigungen aus der Tabelle. Erstellen Sie zwei Ansichten für WHERE chk = 'Y' und WHERE chk = 'N' jeweils einschließlich WITH CHECK OPTION. Für die WHERE chk = 'Y' view, fügen Sie eine Suchbedingung hinzu, deren Kardinalität eine nicht überschreiten darf. Gewähren Sie "Schreibrechte" für die Ansichten.

Beispielcode für die Ansichten:

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

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, die NULL ist, wenn Sie die eindeutige 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 STORED anstelle 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           |
+------+------+-------------+
3

Standard FULL SQL-92: Verwenden Sie eine Unterabfrage in einer CHECK -Einschränkung, die nicht weit verbreitet ist, z. wird in Access2000 (ACE2007, Jet 4.0, was auch immer) und höher unterstützt, wenn in ANSI-92-Abfragemodus .

Beispielcode: Hinweis CHECK Einschränkungen in Access sind immer auf Tabellenebene. Weil der CREATE TABLE Anweisung in der Frage verwendet eine Einschränkung auf Zeilenebene CHECK. Sie muss geringfügig 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'
               ));
1
onedaywhen

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 p.k oder eine Konstante ist, die nicht als Wert für p.k 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 gilt in SQL2003 (da Sie nach einer agnostischen Lösung gesucht haben). DB2 erlaubt es, nicht sicher, wie viele andere Anbieter es akzeptieren.

0
Lennart