Làm thế nào để viết một truy vấn tìm thấy tất cả các tham chiếu vòng tròn khi một bảng tham chiếu chính nó?


26

Tôi có lược đồ sau (tên đã thay đổi), mà tôi không thể thay đổi:

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

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

Đó là, mỗi bản ghi là một đứa con của một bản ghi khác. Nếu một bản ghi ParentIdbằng với bản ghi của nó Id, thì bản ghi được coi là nút gốc.

Tôi muốn chạy truy vấn sẽ tìm thấy tất cả các tham chiếu tròn. Ví dụ: với dữ liệu

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

truy vấn sẽ trả về

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

Tôi đã viết truy vấn sau cho SQL Server 2008 R2 và tôi tự hỏi liệu truy vấn này có thể được cải thiện không:

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;

Không 0 > 0nên được coi là một chu kỳ?
ypercubeᵀᴹ

1
Không, 0 là một nút gốc, vì nó ParentIdbằng với nó Id, vì vậy nó không phải là một chu kỳ cho kịch bản này.
cubetwo1729

Câu trả lời:


30

Điều này đòi hỏi một CTE đệ quy:

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;

Xem nó trong hành động ở đây: SQL Fiddle


Cập nhật:

Đã thêm khoảng cách để có thể loại trừ tất cả các chu kỳ tự (xem bình luận của 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;

Câu đố SQL

Cái nào bạn nên sử dụng phụ thuộc vào yêu cầu của bạn.


Điều này cần được sửa chữa. Hiện tại nó cũng hiển thị 1 chu kỳ, giống như 6 > 6, miễn là nó không 0 > 0.
ypercubeᵀᴹ

Tôi hiểu rằng OP chỉ loại trừ các nút gốc thực sự chu kỳ. Tuy nhiên, bạn có thể dễ dàng thêm yêu cầu đó bằng cách kiểm tra R.Path như '%>%' trong mệnh đề where cuối cùng. Hoặc bạn có thể thêm một cột đếm chiều dài chu kỳ bên trong CTE đệ quy.
Sebastian Meine

2
Bạn chỉ có thể thêm WHERE Id <> ParentIdvào phần đầu tiên của CTE.
ypercubeᵀᴹ

AND C.ParentId <> C.IdKhông đủ. Đó là một đường dẫn dẫn vào một vòng tròn dài hơn ( A->B, B->C, C->B), bạn vẫn nhận được đệ quy vô hạn để xây dựng các đường dẫn bắt đầu A. Bạn thực sự cần phải kiểm tra toàn bộ đường dẫn.
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
Và làm thế nào để làm việc này? Đó thường là lời giải thích làm cho một câu trả lời tốt. Các bài viết chỉ có mã được nhăn mặt ở đây (thường, ít nhất).
dezso

2
Điều này có vẻ như nó trả lời một câu hỏi tương tự nhưng khác nhau.
ypercubeᵀᴹ
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.