계층 구조가있는 테이블 : 외래 키를 통한 순환을 방지하기위한 제약 조건 만들기


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 레벨보다 깊게 중첩되지 않은 테이블에 수백 개 또는 경우에 따라 수천 개의 레코드 만 기대한다는 언급과 관련이있을 수 있습니다.

추신. MS SQL Server 2008


2012 년 3 월 14 일 업데이트
몇 가지 좋은 답변이있었습니다. 나는 이제 언급 된 가능성 / 타당성을 이해 하는 데 도움이되는 것을 받아 들였습니다 . 그래도 구현 제안이있는 몇 가지 다른 훌륭한 답변이 있으므로 동일한 질문으로 여기에 도착하면 모든 답변을 살펴보십시오.)

답변:


6

이러한 구속 조건을 적용하기 어려운 인접 목록 모델을 사용하고 있습니다.

실제 계층 만 나타낼 수있는 중첩 집합 모델을 검사 할 수 있습니다 (원형 경로 없음). 그러나 느린 삽입 / 업데이트와 같은 다른 단점이 있습니다.


+1 훌륭한 링크, 그리고 대담하다 나는 중첩 된 세트 모델을 시도해 보고이 대답을 나를 위해 일한 것으로 받아 들일 수 있기를 바랍니다.
Jeroen

그것이 나를 이해하고 도와 준 하나 때문에 나는이 대답을 받아들이는거야 가능성feasability를 , 나를 위해 질문에 대답 즉. 그러나,이 질문에 사람이 착륙 @ a1ex07의에보고해야한다 대답 간단한 경우에 작품과의 JohnGietzen @하는 제한 조건을 대답 위대한 링크가 HIERARCHYID있는 중첩 된 세트 모델의 기본 MSSQL2008 구현 될 것으로 보인다.
Jeroen

7

나는 이것을 시행하는 두 가지 주요 방법을 보았다.

1, 오래된 방법 :

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

FooHierarchy 열에는 다음과 같은 값이 포함됩니다.

"|1|27|425"

숫자가 FooId 열에 매핑되는 위치 그런 다음 계층 열이 "| id"로 끝나고 나머지 문자열이 PARENT의 FooHieratchy와 일치하도록 강제합니다.

2, 새로운 방법 :

SQL Server 2008에는 HierarchyID 라는 새로운 데이터 유형 이 있습니다.

OLD 방식과 동일한 보안 주체에서 작동하지만 SQL Server에서 효율적으로 처리하며 "ParentID"열의 REPLACEMENT로 사용하기에 적합합니다.

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

1
HIERARCHYID계층 루프 생성 을 방해 하는 소스 또는 간단한 데모가 있습니까?
Nick Chammas

6

CHECK 제약 조건에서 스칼라 UDF를 호출 할 수 있으며 모든 길이의주기를 감지 할 수 있습니다. 불행히도이 접근 방식은 매우 느리고 신뢰할 수 없습니다. 오 탐지 및 오 탐지를 가질 수 있습니다.

대신 구체화 된 경로를 사용합니다.

사이클을 피하는 또 다른 방법은 CHECK (ID> ParentID)를 사용하는 것인데, 이는 아마도 그다지 실현되지 않을 것입니다.

순환을 피하는 또 다른 방법은 LevelInHierarchy 및 ParentLevelInHierarchy라는 두 개의 열을 추가하고 (ParentID, ParentLevelInHierarchy)가 (ID, LevelInHierarchy)를 참조하고 CHECK (LevelInHierarchy> ParentLevelInHierarchy)를 갖는 것입니다.


CHECK 제약 조건의 UDF가 작동하지 않습니다. 한 번에 한 행씩 실행되는 함수에서 업데이트 후 제안 상태에 대한 테이블 수준의 일관성있는 그림을 얻을 수 없습니다. AFTER 트리거와 롤백 또는 INSTEAD OF 트리거를 사용하고 업데이트를 거부해야합니다.
ErikE

그러나 이제 여러 행 업데이트에 대한 다른 답변에 대한 의견을 볼 수 있습니다.
ErikE

@ErikE 맞습니다 .CHECK 제약 조건의 UDF는 작동하지 않습니다.
AK

@Alex Agreed. 나는 이것을 한 번 확실하게 증명하기 위해 몇 시간이 걸렸다.
ErikE

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

내 이해 에서이 작업은 커서 (또는 행) 기반 논리를 의미합니다. 따라서 둘 이상의 행을 수정하는 업데이트를 비활성화하는 것이 좋습니다 (삽입 된 테이블에 둘 이상의 행이있는 경우 오류를 발생시키는 업데이트 트리거 대신).
a1ex07

테이블을 다시 디자인 할 수 없으면 모든 제약 조건을 확인하고 레코드를 추가 / 업데이트하는 프로 시저를 작성합니다. 그런 다음이 sp를 제외하고 아무도이 테이블을 삽입 / 업데이트 할 수 없도록하십시오.
a1ex07

3

다른 옵션은 다음과 같습니다. 여러 행 업데이트를 허용하고주기를 적용하지 않는 트리거입니다. 루트 요소 (부모 NULL 포함)를 찾을 때까지 조상 체인을 통과하여 작동하므로 사이클이 없음을 증명합니다. 물론 사이클은 끝이 없기 때문에 10 세대로 제한됩니다.

현재 수정 된 행 세트에서만 작동하므로 업데이트가 테이블의 매우 많은 수의 매우 깊은 항목에 닿지 않는 한 성능이 나쁘지 않아야합니다. 각 요소의 체인까지 올라 가야하므로 성능에 약간의 영향을 미칩니다.

진정으로 "지능적인"트리거는 항목이 자체에 도달했는지 확인한 다음 bailing하여 직접 사이클을 찾습니다. 그러나 이것은 각 루프 중에 이전에 찾은 모든 노드의 상태를 확인해야하므로 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;

최신 정보

Inserted 테이블에 추가 조인을 피하는 방법을 알아 냈습니다. 누구든지 NULL을 포함하지 않는 것을 감지하기 위해 GROUP BY를 수행하는 더 좋은 방법을 알고 있다면 알려주십시오.

또한 현재 세션이 SNAPSHOT ISOLATION 레벨 인 경우 READ COMMITTED에 스위치를 추가했습니다. 불행하게도 차단이 증가하지만 불일치를 방지합니다. 그것은 당면한 일에 피할 수없는 일입니다.


WITH (READCOMMITTEDLOCK) 힌트를 사용해야합니다. Hugo Kornelis는 sqlblog.com/blogs/hugo_kornelis/archive/2006/09/15/…를
AK

@Alex에게 감사의 말을 전합니다.이 기사는 다이너마이트였으며 스냅 샷 격리를 훨씬 잘 이해하는 데 도움이되었습니다. 커밋되지 않은 코드를 읽도록 조건부 스위치를 추가했습니다.
ErikE

2

레코드가 1 레벨 이상 중첩되면 제약 조건이 작동하지 않습니다 (예를 들어 레코드 1이 레코드 2의 부모이고 레코드 3이 레코드 1의 부모라는 것을 의미한다고 가정합니다). 이 작업을 수행하는 유일한 방법은 부모 코드 또는 트리거를 사용하는 것이지만 큰 테이블과 여러 수준을 보는 경우 상당히 집중적입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.