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


31

たとえば、次のような表の場合:

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

フラグがa char(1)、a bitまたは何として実装されているかは問題ではありません。単一の行にのみ設定できるという制約を強制できるようにしたいだけです。


MySQLに限定されているこの質問に触発
ジャックダグラス

2
質問の言い回しは、テーブルの使用が間違った答えでなければならないことを示唆しています。しかし、時には(ほとんどの場合)別のテーブルを追加することをお勧めします。また、テーブルの追加は完全にデータベースに依存しません。
マイクシェリル 'キャットリコール'

回答:



16

SQL Server 2000、2005:

一意のインデックスでは1つの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);

2000年には、SET ARITHABORT ONこの情報が必要な場合があります(@gbnに感謝します)


14

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なりますYNULL。最大で1行のみが値を持つことができますY.


chkにはnot null制約が必要ですか?
ジャックダグラス

@jack:null not nullが必要ない場合は、制約を追加できます(質問の仕様からは明らかではありませんでした)。いずれの場合も、値「Y」を持つことができるのは1行のみです。
ビンセントマルグラート

+1私はあなたの意味がわかります-あなたは正しいことは必要ではありません(しかし、おそらく、と組み合わせた場合はおそらく少しすっきりしていますdefault)?
ジャックダグラス

2
@jack:あなたの発言は、列がまたはのいずれYnullであることに同意する場合、さらに簡単な可能性があることを認識させてくれました。私の更新を参照してください。
ビンセントマルグラート

1
オプション2は、追加の利点として、インデックスは小さなになります持っているnullかもしれないいくつかの明確さを犠牲に- sがスキップされる
ジャック・ダグラス

13

これは、データベーステーブルを正しく構成する場合だと思います。もっと具体的に言うと、複数のアドレスを持つ人がいて、そのうちの1人をデフォルトにしたい場合、デフォルトのアドレスのaddressIDをアドレステーブルに保存するのではなく、個人のテーブルに保存する必要があると思います。

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

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

DefaultAddressIDをNULL可能にすることもできますが、この方法では構造が制約を強制します。


12

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ではチェック制約は無視されるため、false nullまたはtrue falseとして検討する必要があり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上の以降のすべての後に良い解決策ではない、booleanの同義語でtinyint(1)あり、そのことは可能である0または1以上の非NULL値ができbit、より良い選択であると


これは、RolandoMySQLDBAの回答に対する私のコメントに答える可能性があります。DRIを使用したMySQLソリューションはありますか?
gbn

それはちょっと、いですが、- null、もっときれいなものがあるのか​​な?falsetrue
ジャックダグラス

@Jack-+1は、MySQLの純粋なDRIアプローチをうまく試します。
-RolandoMySQLDBA

ここでは、falseの使用を避けることをお勧めします。一意の制約では、そのようなfalse値を1つしか指定できないためです。nullがfalseを表す場合、一貫して一貫して使用する必要があります-追加の検証が利用可能な場合(たとえば、JSR-303 / hibernate-validator)はfalseの回避を強制できます。
スティーブチェンバーズ

1
MySQL / MariaDBの最近のバージョンでは、仮想カラムを実装しています。これにより、以下に概要を示すdba.stackexchange.com/a/144847/94908
MattWの

10

SQLサーバー:

どうやってするの:

  1. 最適な方法は、フィルター選択されたインデックスです。DRI
    SQL Server 2008+を使用

  2. 一意性を持つ計算列。DRIの使用
    Jack Douglasの回答を参照してください。SQL Server 2005以前

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

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

それをしない方法:

  1. UDFを使用して制約を確認します。これ、並行性とスナップショットの分離にとって安全はありません
    参照してください。一つ 二つ つの

10

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"

6

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

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

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


これは素晴らしい提案です。それは、正規化された設計に沿ったものであり、あらゆるデータベースプラットフォームで動作し、実装が最も簡単です。
ニックチャマス

+1ですが、RDBMSによっては「列全体」が物理的なスペースを使用しない場合があることに注意してください:)
ジャックダグラス

6

広く実装されている技術を使用した可能なアプローチ:

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整数列を追加します。に一意の制約を設定しseqます。考慮される「デフォルト」がseq値が1 である行、またはseq値が最大/最小値などの行になるように、アプリケーションのセマンティクスを変更します。おそらくビューを使用して、このロジックをカプセル化します。


5

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;

試してみる !!!


3
MySQL用のDRIベースのソリューションはありませんか?コードのみ?ますますMySQLを使用し始めているので興味があります
...-gbn

4

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

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


この回答では、コードの短い断片のようにもう少し「肉」を提供できますか?今のところ、それはほんのいくつかの一般的なアイデアとリンクです。
ニックチャマス

ここに例を合わせるのは少し難しいでしょう。リンクをクリックすると、探している「肉」が見つかります。
スパゲッティ

3

SQL Server 2000以降など、広く実装されている標準の移行SQL-92:

テーブルから「ライター」権限を取り消します。WHERE chk = 'Y'とをWHERE chk = 'N'それぞれ含む2つのビューを作成しますWITH CHECK OPTIONWHERE 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

あなたのRDBMSがこれをサポートしている場合でも、それはあなたが複数のユーザーを持っている場合、あなたは問題がある可能性がありますので、狂ったようにシリアライズされます
ジャック・ダグラス

複数のユーザーが同時に変更している場合、それらは並んでいる必要があります(シリアル化)-これは大丈夫ですが、しばしばそうではありません(重いOLTPや長いトランザクションを考えてください)。
ジャックダグラス

3
明確にしてくれてありがとう。複数のユーザーが単独のデフォルト行を頻繁に設定している場合、デザインの選択(同じテーブルのフラグ列)が疑わしいと言わなければなりません。
11

3

これは、もう少しエレガントな仮想列を使用した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の場合はSTOREDPERSISTENT。の代わりに使用します。)

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

1

標準FULL SQL-92:CHECK制約でサブクエリを使用します。たとえば、Access2000(ACE2007、Jet 4.0など)およびANSI-92クエリモードの場合は上記でサポートされるなど、広く実装されていません。

サンプルコード:CHECKAccessのノート制約は常にテーブルレベルです。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
私が使用したRDBMSでは良くありません... 警告がたくさんあります
ジャックダグラス

0

私は答えをざっと読んだだけなので、同様の答えを逃したかもしれません。アイデアは、pkまたはpkの値として存在しない定数のいずれかである生成列を使用することです

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)
);

知る限り、これはSQL2003で有効です(どこにとらわれないソリューションを探しているのか)。DB2はそれを許可しますが、それを受け入れる他のベンダーの数はわかりません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.