web-dev-qa-db-ja.com

既存の違反を無視する一意の制約を追加できますか?

現在、列の値が重複しているテーブルがあります。

これらの誤った重複を削除することはできませんが、一意でない値が追加されないようにしたいと思います。

既存のコンプライアンスをチェックしないUNIQUEを作成できますか?

NOCHECKを使用してみましたが、失敗しました。

この場合、ライセンス情報を「CompanyName」に関連付けるテーブルがあります。

編集:同じ "CompanyName"を持つ複数の行を持つことは悪いデータですが、現時点ではそれらの重複を削除または更新することはできません。 1つのアプローチは、INSERTsに、重複して失敗するストアドプロシージャを使用させることです... SQLが独自に一意性をチェックすることが可能である場合、それが望ましいでしょう。

このデータは、会社名で照会されます。いくつかの既存の重複については、これは複数の行が返されて表示されることを意味します...これは間違っていますが、私たちのユースケースでは許容されます。目標は、将来的にそれを防ぐことです。コメントから、ストアドプロシージャでこのロジックを実行する必要があるようです。

41
Matthew

答えは「はい」です。フィルターされたインデックスを使用してこれを行うことができます(ドキュメントについては here を参照してください)。

たとえば、次のことができます。

create unique index t_col on t(col) where id > 1000;

これにより、古い行ではなく、new行のみに一意のインデックスが作成されます。この特定の定式化により、既存の値との重複が可能になります。

重複が少ししかない場合は、次のようにすることができます。

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);
34
Gordon Linoff

はい、できます。

重複した表は次のとおりです。

CREATE TABLE dbo.Party
  (
    ID INT NOT NULL
           IDENTITY ,
    CONSTRAINT PK_Party PRIMARY KEY ( ID ) ,
    Name VARCHAR(30) NOT NULL
  ) ;
GO

INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' ),
        ( 'Luke Skywalker' ),
        ( 'Luke Skywalker' ),
        ( 'Harry Potter' ) ;
GO

既存のものを無視して、新しい重複が追加されないようにします:

-- Add a new column to mark grandfathered duplicates.
ALTER TABLE dbo.Party ADD IgnoreThisDuplicate INT NULL ;
GO

-- The *first* instance will be left NULL.
-- *Secondary* instances will be set to their ID (a unique value).
UPDATE  dbo.Party
SET     IgnoreThisDuplicate = ID
FROM    dbo.Party AS my
WHERE   EXISTS ( SELECT *
                 FROM   dbo.Party AS other
                 WHERE  other.Name = my.Name
                        AND other.ID < my.ID ) ;
GO

-- This constraint is not strictly necessary.
-- It prevents granting further exemptions beyond the ones we made above.
ALTER TABLE dbo.Party WITH NOCHECK
ADD CONSTRAINT CHK_Party_NoNewExemptions 
CHECK(IgnoreThisDuplicate IS NULL);
GO

SELECT * FROM dbo.Party;
GO

-- **THIS** is our pseudo-unique constraint.
-- It works because the grandfathered duplicates have a unique value (== their ID).
-- Non-grandfathered records just have NULL, which is not unique.
CREATE UNIQUE INDEX UNQ_Party_UniqueNewNames ON dbo.Party(Name, IgnoreThisDuplicate);
GO

このソリューションをテストしてみましょう:

-- cannot add a name that exists
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.

-- cannot add a name that exists and has an ignored duplicate
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Luke Skywalker' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.


-- can add a new name 
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

-- but only once
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.
23
A-K

フィルター処理された一意のインデックスは素晴らしいアイデアですが、_WHERE identity_column > <current value>_条件またはWHERE identity_column NOT IN (<list of ids for duplicate values here>)を使用するかどうかに関係なく、マイナーな欠点があります。

最初のアプローチでは、将来的に既存の(現在の)データの複製である重複データを挿入することができます。たとえば、_CompanyName = 'Software Inc.'_を含む行が(1つでも)ある場合、インデックスは同じ会社名の行をもう1つ挿入することを禁止しません。二度試した場合にのみ禁止されます。

2番目のアプローチには改善点がありますが、上記は機能しません(これは良いことです)。ただし、重複または既存の重複を挿入することはできます。たとえば、2つ以上の行に_CompanyName = 'DoubleData Co.'_が含まれている場合、インデックスは同じ会社名の行をもう1つ挿入することを禁止しません。二度試した場合にのみ禁止されます。

(Update)これは、重複する名前ごとに1つのIDを除外リストに含めない場合に修正できます。上記の例のように、重複する_CompanyName = DoubleData Co._およびID _4,6,8,9_が4行ある場合、除外リストにはこれらのIDが3つだけ含まれている必要があります。

2番目のアプローチでは、SQL-ServerがWHEREで_NOT IN_演算子をサポートしていないように見えるため、もう1つの欠点は厄介な条件です(どのくらい厄介なのかは、最初から重複の数によって異なります)。フィルターされたインデックスの一部。 SQL-Fiddle を参照してください。 WHERE (CompanyID NOT IN (3,7,4,6,8,9))の代わりに、WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9)のようなものが必要になります。このような条件で効率に影響があるかどうか、名前が重複している場合はわかりません。


別のソリューション(@Alex Kuznetsovと同様)は、別の列を追加し、ランク番号を入力して、この列を含む一意のインデックスを追加することです。

_ALTER TABLE Company
  ADD Rn TINYINT DEFAULT 1;

UPDATE x
SET Rn = Rnk
FROM
  ( SELECT 
      CompanyID,
      Rn,
      Rnk = ROW_NUMBER() OVER (PARTITION BY CompanyName 
                               ORDER BY CompanyID)
    FROM Company 
  ) x ;

CREATE UNIQUE INDEX CompanyName_UQ 
  ON Company (CompanyName, Rn) ; 
_

次に、_DEFAULT 1_プロパティと一意のインデックスが原因で、名前が重複する行を挿入すると失敗します。これはまだ100%フールプルーフではありません(アレックスの場合はそうです)。 RnステートメントでINSERTが明示的に設定されている場合、またはRnの値が悪意を持って更新されている場合は、重複が引き続き発生します。

SQL-Fiddle-2

16
ypercubeᵀᴹ