【问题标题】:Preventing circular joining, recursive searches防止循环连接、递归搜索
【发布时间】:2018-09-06 15:25:43
【问题描述】:

所以在我的情况下,我有三个表:listitemlist_relation

每个item 都将通过list_id 外键链接到一个列表。

list_relation 看起来像这样:

CREATE TABLE list_relation
    (
        parent_id INT UNSIGNED NOT NULL,
        child_id INT UNSIGNED NOT NULL,

        UNIQUE(parent_id, child_id)

        FOREIGN KEY (parent_id)
            REFERENCES list (id)
                ON DELETE CASCADE,    

        FOREIGN KEY (child_id)
            REFERENCES list (id)
                ON DELETE CASCADE
    );

我也希望能够从多个列表中继承(包括相关项目)。

例如我有列表:1、2、3。

我想知道是否有任何 SQL 方法可以防止出现循环关系。例如

List 1 继承自 List 3,List 2 继承自 List 1,List 3 继承自 List 1。

1 -> 2 -> 3 -> 1

我目前的想法是,我必须先验证所需的继承,然后将其插入数据库,以确定它是否是循环的。

【问题讨论】:

  • 通常“list”和“item”表示1:many的关系,而不是“many:many”。那么,它是什么?然后给出一个显示“循环”的表格数据样本。请不要使用“继承”这个词,它在 SQL 中没有任何意义。
  • @RickJames 抱歉,我对 SQL 术语不是很熟悉,所以我只使用我认为相关的任何术语。 listitem 确实是1 to many 的关系,但是由于我希望有一个列表有一个父列表,这意味着当我调用select list 2 时,它实际上显示的是list 1 + list 2
  • 如果列表 3 是列表 1 的子项,列表 2 是列表 1 的子项,列表 3 是列表 2 的子项,那么出于您的目的,您是否认为这是循环的?
  • "Parent" 和 "child" 常用于指代一个single表和一个“self join”来实现一个“层次结构” 但是你好像有两个表“列表”和“项目”,加上它们之间的多对多关系表?将“parent_id”更改为“list_id”,将“child_id”更改为“item_id”可以避免我的困惑。

标签: mysql recursion


【解决方案1】:

在列表中放一列parent_id不是更好吗?

然后您可以通过在列表表上使用LEFT JOIN 进行查询来获取列表树,将parent_idlist_id 匹配,例如:

SELECT t1.list_id, t2.list_id, t3.list_id
FROM list AS t1
LEFT JOIN list as t2 ON t2.parent_id = t1.list_id
LEFT JOIN list as t3 ON t3.parent_id = t2.list_id
WHERE t1.list_id = #your_list_id#

它可以解决您的问题吗? 无论如何,我建议您阅读有关在 mysql 中管理分层数据的内容,您可以找到很多关于这个问题的信息!

【讨论】:

  • “我也希望能够从多个列表中继承” - 所以这是一个多对多的关系。
【解决方案2】:

您介意是否需要添加额外的表格?

执行此操作的一种 SQL 方法和有效方法是创建一个附加表,其中包含每个孩子的 ALL 父母。然后在继承建立之前检查当前节点的父列表中是否存在潜在子节点。

parent_list 表是这样的:

CREATE TABLE parent_list (
  list_id INT UNSIGNED NOT NULL,
  parent_list_id INT UNSIGNED NOT NULL,
  PRIMARY KEY (list_id, parent_list_id)
);

现在,让我们从头开始。

  1. 2 继承自 1 和 4。
    parent_list 为空,表示 1 和 4 都没有父级。所以在这种情况下很好。
    在这一步之后,parent_list 应该是:

    list_id, parent_list_id
    2、1
    2、4

  2. 3 继承自 2。
    2 有两个父母,1 和 4。3 不是其中之一。所以又好了。
    现在parent_list变成了(注意2的父母也应该是3的父母):

    list_id, parent_list_id
    2、1
    2、4
    3、1
    3、4
    3、2

  3. 4 继承自 3。
    4 存在于 3 的父列表中。这将导致一个循环。没办法!


要检查是否会发生循环,只需要一个简单的 SQL:

SELECT * FROM parent_list 
WHERE list_id = potential_parent_id AND parent_list_id = potential_child_id;

想通过一个电话完成所有这些事情吗?应用存储过程:

CREATE PROCEDURE 'inherit'(
IN in_parent_id INT UNSIGNED,
IN in_child_id INT UNSIGNED
)
BEGIN
    DECLARE result INT DEFAULT 0;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION 
    BEGIN
          ROLLBACK;
          SELECT -1;
    END;

    START TRANSACTION;

    IF EXISTS(SELECT * FROM parent_list WHERE list_id = in_parent_id AND parent_list_id = in_child_id) THEN
        SET result = 1; -- just some error code
    ELSE
        -- do your inserting here

        -- update parent_list
        INSERT INTO parent_list (SELECT in_child_id, parent_list_id FROM parent_list WHERE list_id = in_parent_id);
        INSERT INTO parent_list VALUES (in_child_id, in_parent_id);
    END IF;

    COMMIT;
    SELECT result;
END

当涉及到多重继承时,只需多次调用inherit

【讨论】:

  • 我觉得我可以使用递归 CTE 来做同样的事情
  • @A.Lau 嗯,我第一次看这个问题的时候就想到了递归的方式。但很快我就放弃了。首先,这实际上取决于您如何组织list_relation 的数据。其次,继承层次可能非常复杂。即使你可以使用递归 CTE 来解决它,它也一定很复杂而且超级慢。
  • 它真的只是看看parent_id 是否还有其他parent_ids,虽然我还没有实现它所以我真的不知道。
  • 简而言之,递归方式不确定,速度慢,风险高。如果我是你,在大多数情况下我不会接受。第二种方式绝对简单、清晰、高效。但是它确实有一个缺点,那就是占用空间比较大。如果真的有递归的方式,实际上就变成了时空权衡问题。
  • 是的,没错。如果您无论如何都在寻找递归方式,则应提供有关表如何工作的更多详细信息。那我可以试着找出来。但我还是不得不说,即使它真的存在,它也会非常缓慢和复杂。
【解决方案3】:

在您提供的示例中,错误关系很简单。这是 3 -> 1 和 1-> 3 关系。您可以在插入新行时简单地查找反向关系。如果存在,则不要插入新行。

如果您添加一个自动递增的列,那么您可以专门识别有问题的行。

另一方面,如果您正在查看现有行,则可以使用如下简单的 SQL 语句来识别错误行:

SELECT
    a.parent_id,
    a.child_id
FROM list_relation a
JOIN list_relation b
ON a.child_id = b.parent_id AND a.parent_id = b.child_id

如果您添加一个自动递增的列,那么您可以专门识别有问题的行。

您的问题标题包含“防止”一词,因此我认为您想避免添加行。为此,您需要一个 ON BEFORE INSERT 触发器来检查现有行并阻止插入。您还可以使用 ON BEFORE UPDATE 触发器来防止现有行被更改为可能出现问题的值。

【讨论】:

  • 这不会处理第 n 个深度关系。 IOW,如果“圆形”关系大于 1 个深度。示例:3 是 1 的子代,2 是 1 的子代,3 是 2 的子代。如果关系可以深几级,则必须使用循环来构建关系列表,查看 parent_id 和 child_id在其他关系中找到特定行的。
【解决方案4】:

如果您使用 MySQL 8.0MariaDB 10.2(或更高版本),您可以尝试递归 CTE(通用表表达式)

假设以下架构和数据:

CREATE TABLE `list_relation` (
  `child_id`  int unsigned NOT NULL,
  `parent_id` int unsigned NOT NULL,
  PRIMARY KEY (`child_id`,`parent_id`)
);
insert into list_relation (child_id, parent_id) values
    (2,1),
    (3,1),
    (4,2),
    (4,3),
    (5,3);

现在您尝试使用child_id = 1parent_id = 4 插入新行。但这会产生循环关系(1->4->2->11->4->3->1),这是您想要阻止的。要确定是否已经存在反向关系,可以使用以下查询,它将显示 list 4 的所有父级(包括继承/传递父级):

set @new_child_id  = 1;
set @new_parent_id = 4;

with recursive rcte as (
  select *
  from list_relation r
  where r.child_id = @new_parent_id
  union all
  select r.*
  from rcte
  join list_relation r on r.child_id = rcte.parent_id
)
select * from rcte

结果是:

child_id | parent_id
       4 |         2
       4 |         3
       2 |         1
       3 |         1

Demo

您可以在结果中看到,list 1list 4 的父项之一,您不会插入新记录。

由于你只想知道list 1是否在结果中,你可以把最后一行改成

select * from rcte where parent_id = @new_child_id limit 1

或到

select exists (select * from rcte where parent_id = @new_child_id)

顺便说一句:您可以使用相同的查询来防止冗余关系。 假设您要插入带有child_id = 4parent_id = 1 的记录。这将是多余的,因为 list 4 已经从 list 2list 3 继承了 list 1。下面的查询会告诉你:

set @new_child_id  = 4;
set @new_parent_id = 1;

with recursive rcte as (
  select *
  from list_relation r
  where r.child_id = @new_child_id
  union all
  select r.*
  from rcte
  join list_relation r on r.child_id = rcte.parent_id
)
select exists (select * from rcte where parent_id = @new_parent_id)

您可以使用类似的查询来获取所有继承的项目:

set @list = 4;

with recursive rcte (list_id) as (
  select @list
  union distinct
  select r.parent_id
  from rcte
  join list_relation r on r.child_id = rcte.list_id
)
select distinct i.*
from rcte
join item i on i.list_id = rcte.list_id

【讨论】:

    【解决方案5】:

    问:[是否有]任何 SQL 方法来防止循环关系

    答:简短的回答

    没有声明性约束会阻止 INSERT 或 UPDATE 创建循环关系(如问题中所述。)

    但是 BEFORE INSERTBEFORE UPDATE 触发器的组合可以防止它,使用查询和/或过程逻辑来检测插入或更新的成功完成会导致循环关系。

    当检测到这种情况时,触发器需要引发错误以阻止 INSERT/UPDATE 操作完成。

    【讨论】:

      【解决方案6】:

      对于那些没有 MySQL 8.0 或 Maria DB 并希望在 MySQL 5.7 中使用递归方法的人。 我只是希望你不必超过最大 rec.depth 255 manual:)

      MySQL 不允许递归函数,但它允许递归过程。将它们结合起来,您可以拥有可以在任何选择命令中使用的不错的小功能。

      递归 sp 将采用两个输入参数和一个输出。第一个输入是您在节点树中搜索的 ID,第二个输入由 sp 用于在执行期间保存结果。第三个参数是携带最终结果的输出参数。

      CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_list_relation_recursive`(
          in itemId       text,
          in iPreserve    text,
          out oResult     text
      
      )
      BEGIN
      
          DECLARE ChildId text default null;
      
          IF (coalesce(itemId,'') = '') then
              -- when no id received retun whatever we have in the preserve container
              set oResult = iPreserve;
          ELSE
              -- add the received id to the preserving container
              SET iPreserve = concat_ws(',',iPreserve,itemId);
              SET oResult = iPreserve;
      
              SET ChildId = 
              (
                  coalesce(
                  (
                      Select 
                          group_concat(TNode.child_id separator ',') -- get all children
                      from 
                          list_relation as TNode 
                      WHERE 
                          not find_in_set(TNode.child_id,     iPreserve) -- if we don't already have'em
                          AND find_in_set(TNode.parent_id,    itemId) -- from these parents
                  )
              ,'')
              );
      
              IF length(ChildId) >0 THEN
                  -- one or more child found, recursively search again for further child elements
                  CALL sp_list_relation_recursive(ChildId,iPreserve,oResult);
              END IF;
      
          END IF;
      
          -- uncomment this to see the progress looping steps
          -- select ChildId,iPreserve,oResult;
      END
      

      测试一下:

      SET MAX_SP_RECURSION_DEPTH = 250;
      set @list = '';
      call test.sp_list_relation_recursive(1,'',@list);
      select @list;
      
      +----------------+
      |     @list      |
      +----------------+
      | ,1,2,3,6,4,4,5 |
      +----------------+
      

      不介意重复的父节点或额外的逗号,我们只想知道节点中是否存在元素,而不需要太多 if 和 whens。

      目前看起来还不错,但是 SP 不能在 select 命令中使用,所以我们只为这个 sp 创建包装函数。

      CREATE DEFINER=`root`@`localhost` FUNCTION `fn_list_relation_recursive`(
          NodeId int
      ) RETURNS text CHARSET utf8
          READS SQL DATA
          DETERMINISTIC
      BEGIN
      
          /*
              Returns a tree of nodes
              branches out all possible branches
          */
          DECLARE mTree mediumtext;
          SET MAX_SP_RECURSION_DEPTH = 250;
      
      
          call sp_list_relation_recursive(NodeId,'',mTree);
      
          RETURN mTree;
      END
      

      现在检查一下:

      SELECT 
          *,
          FN_LIST_RELATION_RECURSIVE(parent_id) AS parents_children
      FROM
          list_relation;
      
      +----------+-----------+------------------+
      | child_id | parent_id | parents_children |
      +----------+-----------+------------------+
      |        1 |         7 | ,7,1,2,3,6,4,4,5 |
      |        2 |         1 |   ,1,2,3,6,4,4,5 |
      |        3 |         1 |   ,1,2,3,6,4,4,5 |
      |        4 |         2 |             ,2,4 |
      |        4 |         3 |           ,3,4,5 |
      |        5 |         3 |           ,3,4,5 |
      |        6 |         1 |   ,1,2,3,6,4,4,5 |
      |       51 |        50 |           ,50,51 |
      +----------+-----------+------------------+
      

      您的插入将如下所示:

      insert into list_relation (child_id,parent_id)
      select
          -- child, parent
          1,6
      where 
          -- parent not to be foud in child's children node
          not find_in_set(6,fn_list_relation_recursive(1));
      

      1,6 应该添加 0 条记录。但是 1,7 应该可以工作。

      一如既往,我只是在证明这个概念,非常欢迎你们 调整 sp 以返回父节点的子节点或子节点的父节点。或者为每个节点树设置两个单独的 SP,或者甚至将所有 SP 组合在一起,因此从单个 id 返回所有父节点和子节点。

      试试吧..没那么难:)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-01-25
        • 1970-01-01
        • 2019-03-03
        • 1970-01-01
        • 2016-01-25
        • 2021-04-03
        • 2020-02-25
        • 2022-07-27
        相关资源
        最近更新 更多