web-dev-qa-db-ja.com

別の列値に基づいて列を一意として強制する

列を一意にしたいのですが、別の列が特定の値である場合のみです。

次の表を検討してください。

CREATE TABLE [SampleTable]
(
     [Id] INTEGER NOT NULL IDENTITY(1,1)
    ,CONSTRAINT [PK_SampleTable]
        PRIMARY KEY ([Id])

    ,[Code]      NVARCHAR(255) NOT NULL
    ,[Deleted]   BIT           NOT NULL DEFAULT 0
    ,[CreatedOn] DATETIME      NOT NULL DEFAULT GETDATE()
);

その目的は、[Deleted]列を1に設定することにより、アイテムを「削除」できるようにすることです。

必要なのは、[Code]列を一意として強制することですが、削除されていない行のみです。

データベースレベルでデータの正確さを強制することは、一般的に私の好みです。ただし、過去にこのパターンを頻繁に使用しましたが、この種の制約をデータベースレベルで適用できるかどうかは定かではありませんでした。締め切りのプレッシャーは彼らが何であるかであり、私は気にすることはありません。したがって、私は常にそれらをアプリケーションレベルで強制しました。

それを行う方法がある場合でも、それが何であるかを本当に知りたいです。

明確にするために、次のデータをサポートできるようにする必要があるため、結合された一意制約を使用することはできません。

    [ID]   [Code]   [Deleted]  [CreatedOn]
=====================================================================
     1     'ABC'    1          Ages ago
     2     'ABC'    1          A while ago
     3     'ABC'    1          Quite recently, actually
     4     'ABC'    0          Just a moment ago!

私の目的では、同じコードを持つ3つの異なる「削除済み」エントリが有効であるため、結合された一意制約は機能しません。

「アプリケーションレベルでこれを強制する」ポリシーにより、サードパーティのアプリケーションからのデータを統合することに関して、私は最近お尻に食い込んだので、この質問をします。データベースが不正な統合データを完全に拒否していたとしたら、それは良かったでしょう。統合が発生する前に修正することは、データが誤って発生した後にデータをクレンジングするよりもはるかに簡単だからです。

SQL Server 2008 R2を使用しています。ただし、この機能を取得する必要がある場合はアップグレードしてもかまいません。とにかくアップグレードするつもりです。

アーロンはこれにかなりすぐに答えました。フィルター処理された一意のインデックスを使用する必要がありました。

次のコードは、ソリューションを示しています。

IF EXISTS (SELECT * FROM SYS.TABLES WHERE [name] = 'SampleTable')
BEGIN
    PRINT 'Dropping Table [SampleTable]';
    DROP TABLE [SampleTable];
END;
GO

PRINT 'Creating Table [SampleTable]';
CREATE TABLE [SampleTable]
(
     [Id] INTEGER NOT NULL IDENTITY(1,1)
    ,CONSTRAINT [PK_SampleTable]
        PRIMARY KEY ([Id])

    ,[Code]      NVARCHAR(255) NOT NULL
    ,[Deleted]   BIT           NOT NULL DEFAULT 0
    ,[CreatedOn] DATETIME      NOT NULL DEFAULT GETDATE()
);

CREATE UNIQUE INDEX
    [UNQ_SampleTable_Code]
ON
    [SampleTable]([Code])
WHERE
    ([Deleted] = 0);

INSERT INTO [SampleTable] ([Code],[Deleted]) VALUES ('ABC', 1);
INSERT INTO [SampleTable] ([Code],[Deleted]) VALUES ('ABC', 1);
INSERT INTO [SampleTable] ([Code],[Deleted]) VALUES ('ABC', 1);
INSERT INTO [SampleTable] ([Code],[Deleted]) VALUES ('ABC', 1);
INSERT INTO [SampleTable] ([Code],[Deleted]) VALUES ('ABC', 0);
INSERT INTO [SampleTable] ([Code],[Deleted]) VALUES ('ABC', 0);

UPDATE [SampleTable] SET [Deleted] = 0 WHERE [Id] = 1;

SELECT * FROM [SampleTable];

6番目(最後)の挿入と更新の両方が、フィルター処理されたインデックスが原因で失敗します。

将来はこれを有効活用します。アーロン、ありがとう!

6

行のサブセットのみに適用する一意の制約がある場合、一意のフィルター処理されたインデックスを使用してこれを強制できます。この場合に効果があると思われるインデックスは次のとおりです。

CREATE UNIQUE INDEX [UNQ_SampleTable_Code]
  ON dbo.[SampleTable]([Code])
  WHERE   ([Deleted] = 0);

これにより、Codeが0の行にはDeletedの個別の値が1つだけ存在し、Deletedが1の場合は重複が存在することが保証されます。通常、これは一部のクエリは、アクティブな行のみに関心があるため(ソフト削除ではなく)、これがクエリをカバーしない場合は、INCLUDE句に列を追加することを検討することができます(SQL Serverはクラスター化されたスキャンを選択する場合があります)インデックス、または別のインデックス(ルックアップのコストが高すぎると思われる場合)。

9
Aaron Bertrand