条件付き一意制約


92

列のセットに対して一意の制約を適用する必要があるが、列の1つの値に対してのみである状況があります。

したがって、たとえば、Table(ID、Name、RecordStatus)のようなテーブルがあります。

RecordStatusは値1または2(アクティブまたは削除済み)のみを持つことができ、(ID、RecordStatus)に一意の制約を作成したいのは、RecordStatus = 1の場合のみです。 ID。

トリガーの作成とは別に、それを行うことはできますか?

SQL Server 2005を使用しています。


1
このデザインは一般的な痛みです。概念的に「削除された」レコードがテーブルから物理的に削除され、おそらく「アーカイブ」テーブルに移動されるように設計を変更することを検討しましたか?
2009年

1
...単純なキーを強制するためにUNIQUE制約を記述できないことは、「コードのにおい」、IMOと見なされるべきだからです。他の多くのテーブルがこのテーブルを参照しているためにデザイン(SQL DDL)を変更できない場合、SQL DMLも結果として影響を受けることを賭けます。つまり、... AND Table.RecordStatus = 1 'を追加することを忘れないでください。このテーブルを含むほとんどの検索条件と結合条件に、そして時々それが必然的に省略されたときに微妙なバグが発生します。
2009年

回答:


36

このようなチェック制約を追加します。違いは、Status = 1かつCount> 0の場合はfalseを返すことです。

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;

テーブルレベルのチェック制約を調べましたが、挿入または更新される値を関数に渡す方法がないようです。方法はわかりますか?
np-hard

わかりました。私が話していることを証明するのに役立つサンプルスクリプトを投稿しました。私はそれをテストし、それは動作します。2つのコメント行を見ると、私が受け取ったメッセージが表示されます。注意:私の実装では、アクティブなアイテムが既に1つある場合、アクティブな同じIDを持つ2番目のアイテムを追加できないことを確認しています。ロジックを変更して、アクティブなものがある場合、同じIDのアイテムを追加できないようにすることができます。このパターンでは、可能性はほとんど無限です。
D.パトリック

トリガーでも同じロジックを使用します。「スカラー関数のクエリ... CHECK制約がクエリに依存し、複数の行が更新の影響を受ける場合、大きな問題が発生する可能性があります。何が起こるかというと、ステートメントが完了する前に各行に対して制約が一度チェックされるということです。 。これは、ステートメントの原子性が失われ、関数が一貫性のない状態でデータベースに公開されることを意味します。結果は予測不可能で不正確です。」参照:blogs.conchango.com/davidportas/archive/2007/02/19/...
onedaywhen

それはいつの日か部分的に本当です。データベースは一貫して予測どおりに動作します。チェック制約は、行がテーブルに追加された後、トランザクションがdbmsによってコミットされる前に実行されます。そのブログは、一度に1つの挿入だけではなく、一連の挿入に対して制約を実行する必要があるというかなり独特な問題について話していました。ashishは一度に1つの挿入に制約を求めています。この制約は正確に、予測可能に、そして一貫して機能します。これが簡潔に聞こえたら申し訳ありません。キャラクターが足りなくなりました。
D.パトリック

3
これは挿入には効果的ですが、更新には効果がないようです。EG他の挿入の後にこれを追加すると、予期しないときにここで機能します。INSERT INTO CheckConstraint VALUES(1、 'No ProblemsA'、2); UpdateConstraint set Recordstatus = 1 where name = 'No ProblemsA'
dwidel

147

ご覧のとおり、フィルターされたインデックスです。ドキュメントから(私の強調):

フィルター選択されたインデックスは、明確に定義されたデータのサブセットから選択するクエリをカバーするのに特に適した、最適化された非クラスター化インデックスです。フィルター述語を使用して、テーブル内の行の一部にインデックスを付けます。適切に設計されたフィルター処理されたインデックスは、クエリパフォーマンスを向上させるだけでなく、フルテーブルインデックスと比較してインデックスのメンテナンスとストレージのコストを削減できます。

一意のインデックスとフィルター述語を組み合わせる例を次に示します。

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

これは、本質的に一意性を強制IDする場合RecordStatusです1

そのインデックスの作成に続いて、一意性違反によりエラーが発生します。

メッセージ2601、レベル14、状態1、行13
行一意のインデックス 'MyIndex'を持つオブジェクト 'dbo.MyTable'に重複するキー行を挿入できません。重複するキーの値は(9999)です。

注:フィルター選択されたインデックスはSQL Server 2008で導入されました。以前のバージョンのSQL Serverについては、この回答を参照しください。


SQL Serverではansi_paddingフィルター処理されたインデックスが必要なのでSET ANSI_PADDING ON、フィルター処理されたインデックスを作成する前に、このオプションが実行されていることを確認してください。
naXa 2017年

10

削除されたレコードを制約のないテーブルに移動し、おそらく2つのテーブルのUNIONを持つビューを使用して、単一のテーブルの外観を保持できます。


2
それは実際にはかなり賢いカールです。それ自体は質問への回答ではありませんが、良い解決策です。テーブルに多くの行がある場合、アクティブレコードテーブルを確認できるため、アクティブレコードの検索も高速化できます。一意の制約は、カウントを実行する必要がある以下で説明するチェック制約とは対照的にインデックスを使用するため、制約も高速化されます。私はそれが好きです。
D.パトリック

3

あなたは本当にハッキーな方法でこれを行うことができます...

テーブルにスキーマバインドビューを作成します。

CREATE VIEW何でもSELECT * FROMテーブルWHERE RecordStatus = 1

次に、必要なフィールドを持つビューに一意の制約を作成します。

ただし、スキーマバインドビューに関する注意点として、基になるテーブルを変更する場合は、ビューを再作成する必要があります。そのため、多くの落とし穴があります。


これはかなり良い提案であり、「ハッキー」ではありません。これは、このフィルター選択されたインデックスの代替に関する詳細です。
スコットウィットロック

それは悪い考えです。問題はそれではない。
FabianoLothor

私は一度スキーマバインドされたビューを使用しましたが、間違いを繰り返したことはありません。彼らは一緒に仕事をするための王室の痛みになることがあります。基になるテーブルを変更した場合にビューを再作成する必要があるわけではありません。少なくともSQLサーバーでは、すべてのビューに対してそれを行う必要がある可能性があります。最初にビューを削除せずにテーブルを変更することはできません。最初にビューへの参照を削除せずに行うことはできない場合があります。ああ、それに加えて、ストレージが問題になる可能性があります-スペースのため、または挿入と更新に追加されるコストのためです。
MattW 2013年

1

重複を許可するため、一意の制約は機能しません。RecordStatus列のチェック制約と、重複するIDを挿入する前に既存のアクティブレコードをチェックするINSERTのストアドプロシージャを作成できます。


1

Billの提案どおりにRecordStatusにNULLを使用できない場合は、彼のアイデアを関数ベースのインデックスと組み合わせることができます。RecordStatusが制約で考慮したい値の1つでない場合はNULLを返す関数を作成し(そうでない場合はRecordStatus)、その上にインデックスを作成します。

これには、制約内のテーブル内の他の行を明示的に調べる必要がないという利点があります。これにより、パフォーマンスの問題が発生する可能性があります。

私はSQLサーバーをまったく知らないと言うべきですが、私はOracleでこのアプローチをうまく使用しました。


良いアイデアですが、SQLサーバーには関数ベースのインデックスがありません。ただし、回答をありがとう
np-hard
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.