Například s tabulkou podobnou této:
create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));
Nezáleží na tom, zda je příznak implementován jako char(1)
, bit
nebo cokoli. Chci jen vynutit omezení, které lze nastavit pouze na jeden řádek.
SQL Server 2008 - Filtrovaný jedinečný index
CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
SQL Server 2000, 2005:
Můžete využít skutečnosti, že v jedinečném indexu je povolen pouze jeden null:
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);
pro rok 2000 možná budete potřebovat SET ARITHABORT ON
(díky @ info za tuto informaci)
Věštec:
Protože Oracle neindexuje položky, ve kterých jsou všechny indexované sloupce nulové, můžete použít jedinečný index založený na funkcích:
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);
Tento index bude vždy indexovat pouze jeden řádek.
Znáte-li tuto skutečnost indexu, můžete implementovat bitový sloupec trochu jinak:
create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);
Zde budou možné hodnoty pro sloupec chk
Y
a NULL
. Pouze jeden řádek může mít hodnotu Y.
Myslím, že se jedná o případ správné strukturování databázových tabulek. Aby to bylo konkrétnější, pokud máte osobu s více adresami a chcete, aby byla výchozí, myslím, že byste měli uložit ID adresy výchozí adresy do tabulky osob, neměli byste mít výchozí sloupec v tabulce adres:
Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)
Address
--------
AddressID
Street
City, State, Zip, etc.
Můžete nastavit DefaultAddressID na null, ale tímto způsobem struktura vynutí vaše omezení.
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
Omezení kontroly jsou v MySQL ignorována, takže musíme považovat null
nebo false
za false a true
za true. Maximálně jeden řádek může mít chk=true
Můžete považovat za vylepšení přidat spouštěč pro změnu false
na true
na insert/update jako řešení pro nedostatek omezení kontroly - IMO to však není vylepšení.
Doufal jsem, že budu moci použít znak (0), protože to je
Bohužel, alespoň u MyISAM a InnoDB
ERROR 1167 (42000): The used storage engine can't index column 'chk'
--Upravit
od MySQL to nakonec není dobré řešení, boolean
je synonymum pro tinyint(1)
, a tak umožňuje nenulové hodnoty než 0 nebo 1. Je to je možné, že bit
by byla lepší volbou
SQL Server:
Jak to udělat:
Nejlepší způsob je filtrovaný index. Používá DRI
SQL Server 2008+
Počítačový sloupec s jedinečností. Používá DRI
Viz odpověď Jacka Douglase. SQL Server 2005 a starší
Indexované/materializované zobrazení, které je jako filtrovaný index. Používá DRI
Všechny verze.
Spoušť. Používá kód, nikoli DRI.
Všechny verze
Jak to udělat:
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"
--Upravit
nebo (mnohem lépe), použijte jedinečný částečný index :
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"
Možné přístupy využívající široce implementované technologie:
1) Zrušte u stolu oprávnění „spisovatele“. Vytvořte procedury CRUD, které zajistí vynucení omezení na hranici transakce.
2) 6NF: zrušte sloupec CHAR(1)
. Přidejte referenční tabulku omezenou, aby její mohutnost nepřesáhla jednu:
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)
);
Změňte sémantiku aplikace tak, aby byla považována za „výchozí“ řádek v nové tabulce. Tuto logiku lze zapouzdřit pomocí pohledů.
3) Vypusťte sloupec CHAR(1)
. Přidejte sloupec seq
integer. Vložte jedinečné omezení pro seq
. Změňte sémantiku aplikace tak, aby uvažovaná „výchozí“ byla řádek, kde hodnota seq
je jedna nebo hodnota seq
největší/nejmenší nebo podobná hodnota. Tuto logiku lze zapouzdřit pomocí pohledů.
Tento druh problému je dalším důvodem, proč jsem se zeptal na tuto otázku:
Pokud máte v databázi tabulku nastavení aplikace, můžete mít záznam, který by odkazoval na ID jednoho záznamu, který chcete považovat za „speciální“. Pak byste jen hledali, co je ID z tabulky nastavení, takže nepotřebujete celý sloupec pouze pro jednu nastavenou položku.
Pro ty, kteří používají MySQL, je zde vhodná uložená procedura:
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 ;
Chcete-li se ujistit, že je tabulka čistá a zda uložená procedura funguje, za předpokladu, že je výchozí hodnota ID 200, spusťte tyto kroky:
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;
Tady je spouštěč, který také pomáhá:
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 ;
Chcete-li se ujistit, že je tabulka čistá a že spouštěč funguje, za předpokladu, že je výchozí hodnota ID 200, spusťte tyto kroky:
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;
Pokusit se !!!
V SQL Server 2000 a více můžete použít indexovaná zobrazení k implementaci složitých (nebo vícerozměrných) omezení, jako jsou ta, která požadujete.
Také Oracle má podobnou implementaci pro zhmotněné pohledy s odloženými omezeními kontroly.
Viz můj příspěvek zde.
Standardní přechodný SQL-92, široce implementovaný např. SQL Server 2000 a vyšší:
Z tabulky zrušíte práva pro spisovatele. Vytvořte dva pohledy pro WHERE chk = 'Y'
a WHERE chk = 'N'
, včetně WITH CHECK OPTION
. Pro WHERE chk = 'Y'
view, zahrňte podmínku vyhledávání v tom smyslu, že její mohutnost nesmí překročit jednu. Udělte oprávnění „spisovatel“ na zobrazení.
Příklad kódu pro zobrazení:
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
Zde je řešení pro MySQL a MariaDB pomocí virtuálních sloupců, které jsou o něco elegantnější. Vyžaduje MySQL> = 5.7.6 nebo 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)
Vytvořte virtuální sloupec, který je NULL, pokud si nepřejete vynutit jedinečný rozpor:
MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;
(Pro MySQL použijte STORED
místo 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 |
+------+------+-------------+
Standardní PLNÝ SQL-92: použijte poddotaz v omezení CHECK
, který není široce implementován např. podporováno v Access2000 (ACE2007, Jet 4.0, cokoli) a výše, pokud je v ANSI-92 Query Mode .
Příklad kódu: poznámka CHECK
omezení v aplikaci Access jsou vždy na úrovni tabulky. Protože příkaz CREATE TABLE
V otázce používá omezení na úrovni řádku CHECK
, je třeba jej mírně změnit přidáním čárky:
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'
));
Procházel jsem pouze odpověďmi, takže jsem mohl vynechat podobnou odpověď. Cílem je použít generovaný sloupec, který je buď p.k, nebo konstanta, která neexistuje jako hodnota pro p.k.
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 to platí v SQL2003 (protože jste hledali agnostické řešení). DB2 to umožňuje, není si jistý, kolik dalších dodavatelů to přijalo.