階層のあるテーブル:外部キーによる循環を防ぐための制約を作成します


10

次のように、テーブル自体に外部キー制約があるテーブルがあるとします。

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

INSERT INTO Foo (FooId, ParentFooId) 
VALUES (1, NULL), (2, 1), (3, 2)

UPDATE Foo SET ParentFooId = 3 WHERE FooId = 1

このテーブルには次のレコードがあります。

FooId  ParentFooId
-----  -----------
1      3
2      1
3      2

この種の設計が意味をなす場合(例:典型的な「従業員と上司と従業員」の関係)があり、いずれの場合も、スキーマにこれがある状況にあります。

この種の設計では、残念ながら、上の例に示すように、データレコードの循環性が考慮されています。

私の質問は次のとおりです:

  1. これをチェックする制約を書くことは可能ですか?そして
  2. これをチェックする制約を書くことは可能ですか?(特定の深さまでのみ必要な場合)

この質問のパート(2)については、テーブルに数百、場合によっては数千のレコードしかないと予想され、通常は約5〜10レベルより深くネストされていないことを言及することは適切かもしれません。

PS。MS SQL Server 2008


2012年3月14日更新
いくつかの良い答えがありました。言及された可能性/実現可能性を理解するのに役立つものを受け入れました。ただし、他にもいくつかの優れた回答があり、実装に関する提案も含まれているため、同じ質問でここに上陸した場合は、すべての回答を確認してください;)

回答:


6

このような制約を適用することが困難な隣接リストモデルを使用しています。

入れ子になったセットモデルを調べることができます。この場合、真の階層しか表現できません(循環パスはありません)。ただし、挿入や更新が遅いなどの欠点もあります。


素晴らしいリンクを+1して、Darnit入れ子のセットモデルを試してみて、この答えを私にとってうまくいったものとして受け入れたいと思います。
Jeroen、2012年

この回答は、可能性実現可能性を理解するのに役立った回答、つまり、質問の答えになったため、受け入れます。しかし、この質問では、誰の着陸は、@ a1ex07のを見ていなければならない答えの簡単な例では、とJohnGietzenの@働く制約の答えに偉大なリンクについてはHIERARCHYID、ネストされたセットモデルのネイティブMSSQL2008の実装であるように思われます。
Jeroen、2012年

7

これを強制する2つの主な方法を見てきました。

1、古い方法:

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     ParentFooId BIGINT,
     FooHierarchy VARCHAR(256),
     FOREIGN KEY([ParentFooId]) REFERENCES Foo ([FooId]) )

FooHierarchy列には、次のような値が含まれます。

"|1|27|425"

数値がFooId列にマッピングされる場所。次に、Hierarchy列が「| id」で終わり、残りの文字列がPARENTのFooHieratchyと一致するように強制します。

2、新しい方法:

SQL Server 2008には、HierarchyIDと呼ばれる新しいデータ型があり、これがすべて自動的に行われます。

古い方法と同じプリンシパルで動作しますが、SQL Serverによって効率的に処理され、「ParentID」列のREPLACEMENTとして使用するのに適しています。

CREATE TABLE Foo 
    (FooId BIGINT PRIMARY KEY,
     FooHierarchy HIERARCHYID )

1
HIERARCHYID階層ループの作成を妨げるソースまたは簡単なデモはありますか?
Nick Chammas

6

それは一種の可能性です:CHECK制約からスカラーUDFを呼び出すことができ、あらゆる長さのサイクルを検出できます。残念ながら、このアプローチは非常に遅く、信頼性が低く、偽陽性と偽陰性になる可能性があります。

代わりに、具体化されたパスを使用します。

循環を回避するもう1つの方法は、CHECK(ID> ParentID)を使用することです。これもおそらくあまり実行可能ではありません。

循環を回避するさらに別の方法は、さらに2つの列LevelInHierarchyとParentLevelInHierarchyを追加し、(ParentID、ParentLevelInHierarchy)に(ID、LevelInHierarchy)を参照させ、CHECK(LevelInHierarchy> ParentLevelInHierarchy)させることです。


CHECK制約のUDFは機能しません。一度に1つの行で実行される関数から、更新後の提案された状態のテーブルレベルの一貫した図を取得することはできません。AFTERトリガーを使用してロールバックするか、INSTEAD OFトリガーを使用して更新を拒否する必要があります。
ErikE、2012年

しかし今、私は複数行の更新に関する他の回答に関するコメントを参照しています。
ErikE 2012年

@ErikEその通り、CHECK制約のUDFは機能しません。
AK

@アレックス同意。私はこれを一度しっかりと証明するのに数時間かかりました。
ErikE 2012年

4

私はそれが可能であると信じています:

create function test_foo (@id bigint) returns bit
as
begin
declare @retval bit;

with t1 as (select @id as FooId, 0 as lvl  
union all 
 select f.FooId , t1.lvl+1 from t1 
 inner join Foo f ON (f.ParentFooId = t1.FooId)
 where lvl<11) -- you said that max nested level 10, so if there is any circular   
-- dependency, we don't need to go deeper than 11 levels to detect it

 select @retval =
 CASE(COUNT(*)) 
 WHEN 0 THEN 0 -- for records that don't have children
 WHEN 1 THEN 0 -- if a record has children
  ELSE 1 -- recursion detected
 END
 from t1
 where t1.FooId = @id ;

return @retval; 
end;
GO
alter table Foo add constraint CHK_REC1 CHECK (dbo.test_foo(ParentFooId) = 0)

私は何かを見逃したかもしれませんが(申し訳ありませんが、それを完全にテストすることはできません)、それはうまくいくようです。


1
「動作しているようだ」と私は同意しますが、複数行の更新では失敗する可能性があり、スナップショット分離では失敗し、非常に遅くなります。
AK

@AlexKuznetsov:再帰クエリが比較的遅いことを理解しており、複数行の更新が問題になる可能性があることに同意します(ただし、無効にすることはできます)。
a1ex07

@ a1ex07この提案に対するThx。私はそれを試しました、そして単純なケースではそれは確かにうまくいくようです。複数行の更新の失敗が問題であるかどうかはまだわかりません(おそらく問題です)。「無効化できる」という意味がわかりませんか?
Jeroen、2012年

私の理解では、このタスクはカーソル(または行)ベースのロジックを意味します。したがって、複数の行を変更する更新を無効にすることは理にかなっています(挿入されたテーブルに複数の行がある場合にエラーを発生させる更新トリガーの代わりに単純です)。
a1ex07

テーブルを再設計できない場合は、すべての制約をチェックしてレコードを追加/更新するプロシージャを作成します。次に、このsp以外の誰もこのテーブルを挿入/更新できないことを確認します。
a1ex07

3

これは別のオプションです。複数行の更新を許可し、サイクルを強制しないトリガーです。ルート要素(親NULLを持つ)が見つかるまで祖先チェーンをたどることによって機能し、サイクルがないことを証明します。もちろんサイクルは無限なので、10世代に限定されます。

これは、変更された行の現在のセットでのみ機能するため、更新がテーブル内の非常に多くの非常に深いアイテムに影響を与えない限り、パフォーマンスはそれほど悪くないはずです。要素ごとにチェーンを上に移動する必要があるため、パフォーマンスにある程度の影響があります。

真に「インテリジェントな」トリガーは、アイテムがそれ自体に到達したかどうかを確認してからベイルすることによって、サイクルを直接探します。ただし、これには、各ループ中に以前に見つかったすべてのノードの状態をチェックする必要があるため、WHILEループが必要になり、今は思っていた以上のコーディングが必要になります。通常の操作ではサイクルが発生しないため、これが実際に高額になることはありません。この場合、各ループで以前のすべてのノードではなく、前の世代のみを処理する方が速くなります。

@AlexKuznetsovまたは他の誰からも、これがスナップショット分離でどのように機能するかについての入力が大好きです。私はそれがあまりよくないのではないかと思うが、それをよりよく理解したい。

CREATE TRIGGER TR_Foo_PreventCycles_IU ON Foo FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;

IF EXISTS (
   SELECT *
   FROM sys.dm_exec_session
   WHERE session_id = @@SPID
   AND transaction_isolation_level = 5
)
BEGIN;
  SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
END;
DECLARE
   @CycledFooId bigint,
   @Message varchar(8000);

WITH Cycles AS (
   SELECT
      FooId SourceFooId,
      ParentFooId AncestorFooId,
      1 Generation
   FROM Inserted
   UNION ALL
   SELECT
      C.SourceFooId,
      F.ParentFooId,
      C.Generation + 1
   FROM
      Cycles C
      INNER JOIN dbo.Foo F
         ON C.AncestorFooId = F.FooId
   WHERE
      C.Generation <= 10
)
SELECT TOP 1 @CycledFooId = SourceFooId
FROM Cycles C
GROUP BY SourceFooId
HAVING Count(*) = Count(AncestorFooId); -- Doesn't have a NULL AncestorFooId in any row

IF @@RowCount > 0 BEGIN
   SET @Message = CASE WHEN EXISTS (SELECT * FROM Deleted) THEN 'UPDATE' ELSE 'INSERT' END + ' statement violated TRIGGER ''TR_Foo_PreventCycles_IU'' on table "dbo.Foo". A Foo cannot be its own ancestor. Example value is FooId ' + QuoteName(@CycledFooId, '"') + ' with ParentFooId ' + Quotename((SELECT ParentFooId FROM Inserted WHERE FooID = @CycledFooId), '"');
   RAISERROR(@Message, 16, 1);
   ROLLBACK TRAN;   
END;

更新

挿入されたテーブルへの余分な結合を回避する方法を見つけました。NULLを含まないものを検出するためにGROUP BYを行うより良い方法を誰かが見た場合は、私に知らせてください。

また、現在のセッションがSNAPSHOT ISOLATIONレベルの場合、READ COMMITTEDへのスイッチを追加しました。これにより不整合が防止されますが、残念ながらブロッキングが増加します。それは、目の前の仕事ではやむを得ないことです。


WITH(READCOMMITTEDLOCK)ヒントを使用する必要があります。ヒューゴKornelisは、例を書いた:sqlblog.com/blogs/hugo_kornelis/archive/2006/09/15/...
AK

@Alexに感謝します。これらの記事はダイナマイトであり、スナップショット分離をよりよく理解するのに役立ちました。コミットされていないコードを読み取るための条件付きスイッチを追加しました。
ErikE 2012年

2

レコードが複数のレベルでネストされている場合、制約は機能しません(たとえば、レコード1がレコード2の親であり、レコード3がレコード1の親であるという意味です)。これを行う唯一の方法は、親コードまたはトリガーを使用することですが、大きなテーブルと複数のレベルを表示している場合、これはかなり集中的になります。

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