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'
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 chk
Y
und NULL
. Maximal nur eine Zeile kann den Wert Y.
Haben.
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.
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
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:
Der beste Weg ist ein gefilterter Index. Verwendet DRI
SQL Server 2008+
Berechnete Spalte mit Eindeutigkeit. Verwendet DRI
Siehe Jack Douglas 'Antwort. SQL Server 2005 und früher
Eine indizierte/materialisierte Ansicht, die einem gefilterten Index ähnelt. Verwendet DRI
Alle Versionen.
Auslösen. Verwendet Code, nicht DRI.
Alle Versionen
Wie man es nicht macht:
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.
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.
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 !!!
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.
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
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 |
+------+------+-------------+
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'
));
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.