테이블이 자신을 참조 할 때 모든 순환 참조를 찾는 쿼리를 작성하는 방법은 무엇입니까?


26

다음 스키마 (이름이 변경됨)가 있는데 변경할 수 없습니다.

CREATE TABLE MyTable (
    Id INT NOT NULL PRIMARY KEY,
    ParentId INT NOT NULL
);

ALTER TABLE MyTable ADD FOREIGN KEY (ParentId) REFERENCES MyTable(Id);

즉, 각 레코드는 다른 레코드의 자식입니다. 레코드가의 레코드 ParentId와 같으면 Id레코드는 루트 노드로 간주됩니다.

모든 순환 참조를 찾을 수있는 쿼리를 실행하고 싶습니다. 예를 들어

INSERT INTO MyTable (Id, ParentId) VALUES
    (0, 0),
    (1, 0),
    (2, 4),
    (3, 2),
    (4, 3);

쿼리는

Id | Cycle
2  | 2 < 4 < 3 < 2
3  | 3 < 2 < 4 < 3
4  | 4 < 3 < 2 < 4

SQL Server 2008 R2에 대해 다음 쿼리를 작성했으며이 쿼리를 개선 할 수 있는지 궁금합니다.

IF OBJECT_ID(N'tempdb..#Results') IS NOT NULL DROP TABLE #Results;
CREATE TABLE #Results (Id INT, HasParentalCycle BIT, Cycle VARCHAR(MAX));

DECLARE @i INT,
    @j INT,
    @flag BIT,
    @isRoot BIT,
    @ids VARCHAR(MAX);

DECLARE MyCursor CURSOR FAST_FORWARD FOR
    SELECT Id
    FROM MyTable;

OPEN MyCursor;
FETCH NEXT FROM MyCursor INTO @i;
WHILE @@FETCH_STATUS = 0
BEGIN
    IF OBJECT_ID(N'tempdb..#Parents') IS NOT NULL DROP TABLE #Parents;
    CREATE TABLE #Parents (Id INT);

    SET @ids = NULL;
    SET @isRoot = 0;
    SET @flag = 0;
    SET @j = @i;
    INSERT INTO #Parents (Id) VALUES (@j);

    WHILE (1=1)
    BEGIN
        SELECT
            @j = ParentId,
            @isRoot = CASE WHEN ParentId = Id THEN 1 ELSE 0 END
        FROM MyTable
        WHERE Id = @j;

        IF (@isRoot = 1)
        BEGIN
            SET @flag = 0;
            BREAK;
        END        

        IF EXISTS (SELECT 1 FROM #Parents WHERE Id = @j)
        BEGIN
            INSERT INTO #Parents (Id) VALUES (@j);
            SET @flag = 1;
            SELECT @ids = COALESCE(@ids + ' < ', '') + CAST(Id AS VARCHAR) FROM #Parents;
            BREAK;
        END
        ELSE
        BEGIN
            INSERT INTO #Parents (Id) VALUES (@j);
        END        
    END

    INSERT INTO #Results (Id, HasParentalCycle, Cycle) VALUES (@i, @flag, @ids);

    FETCH NEXT FROM MyCursor INTO @i;
END
CLOSE MyCursor;
DEALLOCATE MyCursor;

SELECT Id, Cycle
FROM #Results
WHERE HasParentalCycle = 1;

0 > 0주기 간주되어서는 안된다?
ypercubeᵀᴹ

1
아니요, 0은 루트 노드이므로 루트 노드 ParentId이므로이 Id시나리오에서는주기가 아닙니다.
cubetwo1729

답변:


30

재귀 CTE가 필요합니다.

WITH FindRoot AS
(
    SELECT Id,ParentId, CAST(Id AS NVARCHAR(MAX)) Path
    FROM dbo.MyTable

    UNION ALL

    SELECT C.Id, P.ParentId, C.Path + N' > ' + CAST(P.Id AS NVARCHAR(MAX))
    FROM dbo.MyTable P
    JOIN FindRoot C
    ON C.ParentId = P.Id AND P.ParentId <> P.Id AND C.ParentId <> C.Id
 )
SELECT *
FROM FindRoot R
WHERE R.Id = R.ParentId 
  AND R.ParentId <> 0;

여기에서 실제로 확인하십시오 : SQL Fiddle


최신 정보:

모든 자체 사이클을 제외 할 수있는 거리가 추가되었습니다 (ypercube의 설명 참조).

WITH FindRoot AS
(
    SELECT Id,ParentId, CAST(Id AS NVARCHAR(MAX)) Path, 0 Distance
    FROM dbo.MyTable

    UNION ALL

    SELECT C.Id, P.ParentId, C.Path + N' > ' + CAST(P.Id AS NVARCHAR(MAX)), C.Distance + 1
    FROM dbo.MyTable P
    JOIN FindRoot C
    ON C.ParentId = P.Id AND P.ParentId <> P.Id AND C.ParentId <> C.Id
 )
SELECT *
FROM FindRoot R
WHERE R.Id = R.ParentId 
  AND R.ParentId <> 0
  AND R.Distance > 0;

SQL 바이올린

어느 것을 사용해야하는지 요구 사항에 따라 다릅니다.


이 문제를 해결해야합니다. 현재가 6 > 6아닌 한 1주기를 표시 0 > 0합니다.
ypercubeᵀᴹ

실제 루트 노드 자체 주기만 제외해야한다는 OP를 이해했습니다. 그러나 최종 where 절에서 '%> %'와 같은 R.Path를 확인하여 해당 요구 사항을 쉽게 추가 할 수 있습니다. 또는 순환 CTE 내부에주기 길이 카운트 열을 추가 할 수 있습니다.
Sebastian Meine

2
WHERE Id <> ParentIdCTE의 첫 부분 만 추가하면 됩니다.
ypercubeᵀᴹ

AND C.ParentId <> C.Id충분하지 않다. 경로는 더 긴 원으로 이어지고 ( A->B, B->C, C->B) 시작하는 경로를 구성하기 위해 무한 재귀가 발생 A합니다. 전체 경로를 확인해야합니다.
Bergi

2
SELECT RC.CONSTRAINT_NAME FK_Name
, KF.TABLE_SCHEMA FK_Schema
, KF.TABLE_NAME FK_Table
, KF.COLUMN_NAME FK_Column
, RC.UNIQUE_CONSTRAINT_NAME PK_Name
, KP.TABLE_SCHEMA PK_Schema
, KP.TABLE_NAME PK_Table
, KP.COLUMN_NAME PK_Column
, RC.MATCH_OPTION MatchOption
, RC.UPDATE_RULE UpdateRule
, RC.DELETE_RULE DeleteRule
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KF ON RC.CONSTRAINT_NAME = KF.CONSTRAINT_NAME
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KP ON RC.UNIQUE_CONSTRAINT_NAME = KP.CONSTRAINT_NAME
WHERE KF.TABLE_NAME = KP.TABLE_NAME

1
그리고 어떻게 작동합니까? 일반적으로 좋은 대답을하는 설명입니다. 코드 전용 게시물은 여기에 찡그립니다 (보통 적어도).
dezso 2016 년

2
이것은 비슷하지만 다른 질문에 대답하는 것처럼 보입니다.
ypercubeᵀᴹ
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.