如何防止在SQL中插入循环引用

时间:2017-10-11 11:42:37

标签: sql-server tsql sql-server-2017 sql-server-2017-graph

我有下表:

create table dbo.Link
(
    FromNodeId int not null,
    ToNodeId int not null
)

此表中的行表示节点之间的链接。

我想阻止对此表的插入或更新在节点之间创建循环关系。

所以如果表包含:

(1,2)
(2,3)

不应该允许包含以下任何内容:

(1,1)
(2,1)
(3,1)

我很乐意单独处理(1,1)(例如使用CHECK CONSTRAINT),如果它使解决方案更直接。

我正在考虑使用递归CTE创建一个AFTER INSERT触发器(虽然可能有一种更简单的方法)。

假设这是要走的路,触发器定义是什么?如果有一种更优雅的方式,它是什么?

2 个答案:

答案 0 :(得分:3)

首先请注意,最好在另一个环境中检测周期,因为递归CTE不是因其良好的性能而闻名,也不是每个插入语句都会运行的触发器。对于大型图表,基于以下解决方案的解决方案可能效率低下。

假设您按如下方式创建表:

CREATE TABLE dbo.lnk (
    node_from INT NOT NULL,
    node_to INT NOT NULL,
    CONSTRAINT CHK_self_link CHECK (node_from<>node_to),
    CONSTRAINT PK_lnk_node_from_node_to PRIMARY KEY(node_from,node_to)
);

这会阻止node_from等于node_to的插入,以及已存在的行。

如果检测到循环引用,则以下触发器应通过抛出异常来检测循环引用:

CREATE TRIGGER TRG_no_circulars_on_lnk ON dbo.lnk AFTER INSERT
AS
BEGIN
    DECLARE @cd INT;
    WITH det_path AS (
        SELECT
            anchor=i.node_from,
            node_to=l.node_to,
            is_cycle=CASE WHEN i.node_from/*anchor*/=l.node_to THEN 1 ELSE 0 END
        FROM
            inserted AS i
            INNER JOIN dbo.lnk AS l ON
                l.node_from=i.node_to
        UNION ALL
        SELECT
            dp.anchor,
            node_to=l.node_to,
            is_cycle=CASE WHEN dp.anchor=l.node_to THEN 1 ELSE 0 END
        FROM
            det_path AS dp
            INNER JOIN dbo.lnk AS l ON
                l.node_from=dp.node_to
        WHERE
            dp.is_cycle=0
    )
    SELECT TOP 1
        @cd=is_cycle 
    FROM 
        det_path
    WHERE
        is_cycle=1
    OPTION 
        (MAXRECURSION 0);

    IF @cd IS NOT NULL 
        THROW 67890, 'Insert would cause cyclic reference', 1;
END

我测试了这个数量有限的插页。

INSERT INTO dbo.lnk(node_from,node_to)VALUES(1,2); -- OK
INSERT INTO dbo.lnk(node_from,node_to)VALUES(2,3); -- OK
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,4); -- OK

INSERT INTO dbo.lnk(node_from,node_to)VALUES(2,3); -- PK violation
INSERT INTO dbo.lnk(node_from,node_to)VALUES(1,1); -- Check constraint violation
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,2); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(3,1); -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(4,1); -- Exception: Insert would cause cyclic reference

如果一次插入多个行,或者如果在图中引入长于一个边的路径,它还会检测已插入行中已存在的循环引用。关于相同的初始插入:

INSERT INTO dbo.lnk(node_from,node_to)VALUES(8,9),(9,8);       -- Exception: Insert would cause cyclic reference
INSERT INTO dbo.lnk(node_from,node_to)VALUES(4,5),(5,6),(6,1); -- Exception: Insert would cause cyclic reference

答案 1 :(得分:0)

编辑:处理多记录插入,在单独的函数中移动逻辑

我考虑过一种程序方法,它非常快,几乎与链接表和图形“密度”中的记录数无关

我在一个有10'000个链接的表上测试了它,节点值从1到1000 这真的非常快,不会受到链接表维度或“密度”的影响

此外,该函数可用于在插入之前测试值,或者(例如)如果您不想在客户端的所有移动测试逻辑上使用触发器。

关于递归CTE的考虑:小心!
我已经在我的测试表(10k行)上测试了接受的答案但是在 25分钟之后,我已经取消了一行的插入操作,因为查询被挂起而没有结果...... 将表格缩小到5k行,单个记录的插入可以持续 2-3分钟。 它非常依赖于图的“人口”。如果您插入一个新路径,或者您正在将一个节点添加到具有较低“分支”的路径,则它非常快,但您无法控制它。 当图表更加“密集”时,这个解决方案会在你脸上爆炸。

非常仔细地考虑您的需求。

所以,让我们看看如何......

首先,我已将表的// Not need to cast to `Button`, since all views can have an onClickListener rootView.findViewById(R.id.enable).setOnClickListener(clickListener) rootView.findViewById(R.id.enable).setOnClickListener(clickListener) // Put this as a member of your Fragment class. View.OnClickListener clickListener = new View.OnClickListener() { @Override public void onClick(View v) { if (v.getId() == R.id.enable) { // Save your preference here // ... listener.themechanged(2); enable.setVisibility(View.GONE); disable.setVisibility(View.VISIBLE); } if (v.getId() == R.id.R.id.disable) { // Save your preference here // ... listener.themechanged(2); disable.setVisibility(View.GONE); enable.setVisibility(View.VISIBLE); } } } 设置为两列,并在第二列上添加了索引以进行完全覆盖。 (不需要FromNodeId&lt;&gt; ToNodeId上的PK因为算法已经涵盖了这种情况。)

CHECK

然后我构建了一个函数来测试单个链接的有效性:

CREATE TABLE [dbo].[Link](
    [FromNodeId] [int] NOT NULL,
    [ToNodeId] [int] NOT NULL,
 CONSTRAINT [PK_Link] PRIMARY KEY CLUSTERED ([FromNodeId],[ToNodeId])
) 
GO

CREATE NONCLUSTERED INDEX [ToNodeId] ON [dbo].[Link] ([ToNodeId])
GO

现在让我们从触发器中调用它 如果将插入多行,则触发器将针对整个插入测试每个链接(旧表+新recs),如果所有这些都有效且最终表将是一致的,则插入将完成,如果其中一个无效,则插入将中止。

drop function fn_test_link
go
create function fn_test_link(@f int, @t int)
returns int
as
begin
    --SET NOCOUNT ON

    declare @p table (id int identity primary key, l int, t int, unique (l,t,id))
    declare @r int = 0
    declare @i int = 0

    -- link is not self-referencing
    if @f<>@t begin 
        -- there are links that starts from where new link wants to end (possible cycle)
        if exists(select 1 from link where fromnodeid=@t) begin

            -- PAY ATTENTION.. HERE LINK TABLE ALREADY HAVE ALL RECORDS ADDED (ALSO NEW ONES IF PROCEDURE IS CALLED FROM A TRIGGER AFTER INSERT)

            -- LOAD ALL THE PATHS TOUCHED BY DESTINATION OF TEST NODE
            set @i = 0
            insert into @p 
            select distinct @i, ToNodeId 
            from link 
            where fromnodeid=@t

            set @i = 1
            -- THERE IS AT LEAST A STEP TO FOLLOW DOWN THE PATHS
            while exists(select 1 from @p where l=@i-1) begin

                -- LOAD THE NEXT STEP FOR ALL THE PATHS TOUCHED
                insert into @p 
                select distinct @i, l.ToNodeId
                from link l
                join @p p on p.l = @i-1 and p.t = l.fromnodeid

                -- CHECK IF THIS STEP HAVE REACHED THE TEST NODE START
                if exists(select 1 from @p where l=@i and t=@f) begin
                    -- WE ARE EATING OUR OWN TAIL! CIRCULAR REFERENCE FOUND
                    set @r = -1
                    break
                end

                -- THE NODE IS STILL GOOD
                -- DELETE FROM LIST DUPLICATED ALREADY TESTED PATHS
                -- (THIS IS A BIG OPTIMIZATION, WHEN PATHS CROSSES EACH OTHER YOU RISK TO TEST MANY TIMES SAME PATHS)
                delete p 
                from @p p 
                where l = @i 
                and (exists(select 1 from @p px where px.l < p.l and px.t = p.t)) 

                set @i = @i + 1
            end
            if @r<0
                -- a circular reference was found 
                set @r = 0
            else
                -- no circular reference was found
                set @r = 1

        end else begin 
            -- THERE ARE NO LINKS THAT STARTS FROM TESTED NODE DESTINATIO (CIRCULAR REFERENCE NOT POSSIBLE)
            set @r = 1
        end
    end; -- link is not self-referencing

    --select * from @p 

    return @r

end
GO

我希望这会有所帮助