web-dev-qa-db-ja.com

単一行にのみ設定できる「デフォルト」フラグを実装する方法

たとえば、次のようなテーブルがあるとします。

_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'
31

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行だけです。

14
Vincent Malgrat

これは、データベーステーブルを正しく構造化した場合だと思います。より具体的に言うと、複数の住所を持つ人物がいて、その住所をデフォルトにしたい場合は、住所テーブルにデフォルトの列ではなく、人物テーブルにデフォルト住所のaddressIDを格納する必要があると思います。

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

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

DefaultAddressIDをnullにできるようにすることもできますが、この方法で構造が制約を適用します。

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
_

MySQLではチェック制約は無視されるため、nullまたはfalseをfalseと見なし、trueをtrueと見なす必要があります。最大で1行に_chk=true_を含めることができます

チェック制約がないための回避策として、挿入/更新時にfalsetrueに変更するトリガーを追加することを改善と考えるかもしれません-IMOは改善ではありません。

Char(0)を使用できるようにしたかったので、

2つの値のみを取ることができる列が必要な場合も非常に便利です。CHAR(0)NULLとして定義されている列は1ビットしか占有せず、値NULLおよび '')のみを取ることができます

残念ながら、少なくともMyISAMとInnoDBでは、

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

-編集

mySQLでは booleantinyint(1) の同義語であるため、これは結局のところ良い解決策ではありません。したがって、0または1以外の非null値を許可します。 bit がより良い選択である可能性があります

SQLサーバー:

どうやってするの:

  1. 最善の方法は、フィルターされたインデックスです。 DRIを使用
    SQL Server 2008以降

  2. 一意性のある計算列。 DRIを使用
    ジャックダグラスの回答をご覧ください。 SQL Server 2005以前

  3. フィルター処理されたインデックスのようなインデックス付き/マテリアライズドビュー。 DRIを使用
    すべてのバージョン。

  4. 引き金。 DRIではなくコードを使用します。
    すべてのバージョン

それをしない方法:

  1. UDFで制約を確認してください。これは、同時実行性とスナップショット分離のために安全ではありません
    参照 OneTwoThreeFour
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"

-編集

または(はるかに良い) 一意の部分インデックス を使用します。

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値が最大/最小値などである行になるように、アプリケーションのセマンティクスを変更します。おそらくビューを使用して、このロジックをカプセル化します。

6
onedaywhen

この種の問題は、私がこの質問をしたもう1つの理由です。

データベースのアプリケーション設定

データベースにアプリケーション設定テーブルがある場合、「特別」と見なしたい1つのレコードのIDを参照するエントリを作成できます。次に、設定テーブルからIDが何であるかをルックアップします。このようにして、設定されている1つのアイテムだけに列全体を指定する必要はありません。

6
CenterOrbit

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;

試してみる !!!

5
RolandoMySQLDBA

SQL Server 2000以降では、インデックス付きビューを使用して、求めているような複雑な(またはマルチテーブルの)制約を実装できます。
また、Oracleには、遅延チェック制約を持つマテリアライズドビューの同様の実装があります。

私の投稿を参照してくださいここ

4
spaghettidba

標準の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
3
onedaywhen

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

標準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'
               ));
1
onedaywhen

私は答えをざっと目を通しただけだったので、私は同じような答えを逃したかもしれません。アイデアは、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はそれを許可しますが、それを受け入れる他のベンダーの数はわかりません。

0
Lennart