たとえば、次のようなテーブルがあるとします。
_create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));
_
フラグがchar(1)
、bit
などとして実装されているかどうかは関係ありません。単一の行にのみ設定できるという制約を適用できるようにしたいだけです。
SQL Server 2008-フィルター処理された一意のインデックス
CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
SQL Server 2000、2005:
一意のインデックスではnullが1つだけ許可されるという事実を利用できます。
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);
2000の場合、SET ARITHABORT ON
(この情報について@gbnに感謝)
Oracle:
Oracleは、すべてのインデックス付き列がnullであるエントリにインデックスを付けないため、関数ベースの一意のインデックスを使用できます。
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);
このインデックスは、せいぜい1つの行にしかインデックスを付けません。
このインデックスの事実を知っていれば、ビット列を少し異なる方法で実装することもできます。
create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);
ここで、列chk
に可能な値はY
およびNULL
です。値Y.
を持つことができるのは、せいぜい1行だけです。
これは、データベーステーブルを正しく構造化した場合だと思います。より具体的に言うと、複数の住所を持つ人物がいて、その住所をデフォルトにしたい場合は、住所テーブルにデフォルトの列ではなく、人物テーブルにデフォルト住所のaddressIDを格納する必要があると思います。
Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)
Address
--------
AddressID
Street
City, State, Zip, etc.
DefaultAddressIDをnullにできるようにすることもできますが、この方法で構造が制約を適用します。
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
_
MySQLではチェック制約は無視されるため、null
またはfalse
をfalseと見なし、true
をtrueと見なす必要があります。最大で1行に_chk=true
_を含めることができます
チェック制約がないための回避策として、挿入/更新時にfalse
をtrue
に変更するトリガーを追加することを改善と考えるかもしれません-IMOは改善ではありません。
Char(0)を使用できるようにしたかったので、
2つの値のみを取ることができる列が必要な場合も非常に便利です。CHAR(0)NULLとして定義されている列は1ビットしか占有せず、値NULLおよび '')のみを取ることができます
残念ながら、少なくともMyISAMとInnoDBでは、
_ERROR 1167 (42000): The used storage engine can't index column 'chk'
_
-編集
mySQLでは boolean
はtinyint(1)
の同義語であるため、これは結局のところ良い解決策ではありません。したがって、0または1以外の非null値を許可します。 bit
がより良い選択である可能性があります
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"
-編集
または(はるかに良い) 一意の部分インデックス を使用します。
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"
広く実装されているテクノロジーを使用した可能なアプローチ:
1)テーブルの「書き込み」権限を取り消します。トランザクション境界で制約が適用されることを保証するCRUDプロシージャを作成します。
2)6NF:CHAR(1)
列を削除します。カーディナリティが1を超えないように制約された参照テーブルを追加します。
_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)
);
_
考慮される「デフォルト」が新しいテーブルの行になるように、アプリケーションのセマンティクスを変更します。おそらくビューを使用して、このロジックをカプセル化します。
3)CHAR(1)
列を削除します。 seq
integer列を追加します。 seq
に一意の制約を設定します。考慮される「デフォルト」がseq
値が1である行、またはseq
値が最大/最小値などである行になるように、アプリケーションのセマンティクスを変更します。おそらくビューを使用して、このロジックをカプセル化します。
この種の問題は、私がこの質問をしたもう1つの理由です。
データベースにアプリケーション設定テーブルがある場合、「特別」と見なしたい1つのレコードのIDを参照するエントリを作成できます。次に、設定テーブルからIDが何であるかをルックアップします。このようにして、設定されている1つのアイテムだけに列全体を指定する必要はありません。
MySQLを使用する人のために、ここに適切なストアドプロシージャがあります。
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 ;
テーブルがクリーンで、ストアドプロシージャが機能していることを確認するには、ID 200がデフォルトであると想定して、次の手順を実行します。
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;
これも役立つトリガーです:
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 ;
ID 200がデフォルトであると想定して、テーブルがクリーンでトリガーが機能していることを確認するには、次の手順を実行します。
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;
試してみる !!!
SQL Server 2000以降では、インデックス付きビューを使用して、求めているような複雑な(またはマルチテーブルの)制約を実装できます。
また、Oracleには、遅延チェック制約を持つマテリアライズドビューの同様の実装があります。
私の投稿を参照してくださいここ。
標準のTransitional SQL-92。広く実装されています。 SQL Server 2000以降:
テーブルから「書き込み」権限を取り消します。 WHERE chk = 'Y'
を含むWHERE chk = 'N'
とWITH CHECK OPTION
の2つのビューをそれぞれ作成します。 WHERE chk = 'Y'
ビューの場合、カーディナリティーが1を超えないように検索条件を含めます。ビューに対する「書き込み」権限を付与します。
ビューのサンプルコード:
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
MySQLとMariaDBの仮想列を使用したソリューションが、もう少しエレガントになっています。 MySQL> = 5.7.6または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)
一意の制約を強制しない場合は、NULLの仮想列を作成します。
MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;
(MySQLの場合、STORED
の代わりに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 |
+------+------+-------------+
標準FULL SQL-92:CHECK
制約でサブクエリを使用します。広く実装されていません。 ANSI-92クエリモード のAccess2000(ACE2007、Jet 4.0など)以降でサポートされます。
コード例:AccessのCHECK
制約は常にテーブルレベルです。質問のCREATE TABLE
ステートメントは行レベルのCHECK
制約を使用しているため、コンマを追加して少し修正する必要があります。
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'
));
私は答えをざっと目を通しただけだったので、私は同じような答えを逃したかもしれません。アイデアは、p.kの値として存在しない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これはSQL2003で有効です(不可知論的なソリューションを探しているため)。 DB2はそれを許可しますが、それを受け入れる他のベンダーの数はわかりません。