【问题标题】:How to query a MySql table to display the root and its subchild.如何查询 MySql 表以显示根及其子表。
【发布时间】:2011-11-12 01:26:08
【问题描述】:
UserID      UserName       ParentID      TopID
  1         abc            Null           Null
  2         edf             1             1
  3         gef             1             1
  4         huj             3             1
  5         jdi             4             1
  6         das             2             1
  7         new            Null           Null
  8         gka             7             7

TopID 和 ParentID 来自用户 ID

我想获取用户记录及其子记录和子记录。这里 userid1 是 root 并且它的孩子是 userid2 和 userid 3。所以如果用户 id 是 1 我必须显示从 userid 1 到 userid 6 的所有记录,因为它们都是 root 的孩子和 SUBchild。同样对于 userid3 我必须显示 userid3 及其子 Userid 4 和 Child of Userid 4 Userid5 如果用户 ID 为 3

输出应该是

Userid  Username
3          gef
4          huj
5          jdi

我会知道 userid 和 topID 那么我该如何查询才能获得上述结果。

SELECT UserID, UserName  FROM tbl_User WHERE ParentID=3 OR UserID=3 And TopID=1;

通过上面的查询,我可以显示用户 ID 3 和用户 ID 4,但我无法显示用户 ID 5,这有点让人印象深刻。需要帮忙。谢谢

【问题讨论】:

  • MySQL 不支持分层或递归查询。如果您有明确定义的最大深度,则可以使用那么多自连接。

标签: mysql sql parent-child hierarchy


【解决方案1】:

使用存储过程在 MySQL 中进行递归分层查询在技术上是可行的。

这里有一个适合你的场景:

CREATE TABLE `user` (
  `UserID` int(16) unsigned NOT NULL,
  `UserName` varchar(32),
  `ParentID` int(16) DEFAULT NULL,
  `TopID` int(16) DEFAULT NULL,
  PRIMARY KEY (`UserID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO user VALUES (1, 'abc', NULL, NULL), (2, 'edf', 1, 1), (3, 'gef', 1, 1), 
 (4, 'huj', 3, 1), (5, 'jdi', 4, 1), (6, 'das', 2, 1), (7, 'new', NULL, NULL), 
 (8, 'gka', 7, 7);

DELIMITER $$
DROP PROCEDURE IF EXISTS `Hierarchy` $$
CREATE PROCEDURE `Hierarchy` (IN GivenID INT, IN initial INT)
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE next_id INT;

    -- CURSOR TO LOOP THROUGH RESULTS --
    DECLARE cur1 CURSOR FOR SELECT UserID FROM user WHERE ParentID = GivenID;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    -- CREATE A TEMPORARY TABLE TO HOLD RESULTS --
    IF initial=1 THEN
        -- MAKE SURE TABLE DOESN'T CONTAIN OUTDATED INFO IF IT EXISTS (USUALLY ON ERROR) --
        DROP TABLE IF EXISTS OUT_TEMP; 
        CREATE TEMPORARY TABLE OUT_TEMP (userID int, UserName varchar(32));
    END IF;

    -- ADD OURSELF TO THE TEMPORARY TABLE --
    INSERT INTO OUT_TEMP SELECT UserID, UserName FROM user WHERE UserID = GivenID;

    -- AND LOOP THROUGH THE CURSOR --
    OPEN cur1;
    read_loop: LOOP
        FETCH cur1 INTO next_id;

        -- NO ROWS FOUND, LEAVE LOOP --
        IF done THEN
        LEAVE read_loop;
        END IF;

        -- NEXT ROUND --
        CALL Hierarchy(next_id, 0);     
    END LOOP;

    CLOSE cur1;

    -- THIS IS THE INITIAL CALL, LET'S GET THE RESULTS --
    IF initial=1 THEN
    SELECT * FROM OUT_TEMP;
        -- CLEAN UP AFTER OURSELVES --
        DROP TABLE OUT_TEMP; 
    END IF;
END $$
DELIMITER ;

CALL Hierarchy(3,1);
+--------+----------+
| userID | UserName |
+--------+----------+
|      3 | gef      |
|      4 | huj      |
|      5 | jdi      |
+--------+----------+
3 rows in set (0.07 sec)

Query OK, 0 rows affected (0.07 sec)

CALL Hierarchy(1,1);
+--------+----------+
| userID | UserName |
+--------+----------+
|      1 | abc      |
|      2 | edf      |
|      6 | das      |
|      3 | gef      |
|      4 | huj      |
|      5 | jdi      |
+--------+----------+
6 rows in set (0.10 sec)

Query OK, 0 rows affected (0.10 sec)

是时候指出一些注意事项了:

  • 由于这是递归调用存储过程,需要增加max_sp_recursion_depth的大小,最大值为255(默认为0)。

  • 我在具有有限测试数据(user 表的 10 个元组)的非繁忙服务器上的结果需要 0.07-0.10 秒才能完成。性能如此之好,最好将递归放在您的应用程序层中。

  • 我没有利用您的TopID 列,因此可能存在逻辑缺陷。但是这两个测试用例给了我预期的结果。

免责声明:这个例子只是为了表明它可以在 MySQL 中完成,而不是无论如何我都认可它。存储过程、临时表和游标可能不是解决这个问题的最佳方法。

【讨论】:

  • 我复制了您的存储过程并在 myphp admin 中执行并执行了,如何在 myphpadmin 中进行查询以查看结果
  • 应该是 SQL 查询:CALL Hierarchy($userID, 1);
  • 调用层次结构(2, 1);我在 mysql 中执行了这个,它抛出了错误 #1456 - 例程层次结构超出了递归限制 0(由 max_sp_recursion_depth 变量设置),
  • 是的,先生,正如我的第一个警告要点所述,您必须增加此变量。为了进行测试,您可以在发出 CALL 命令之前将其设置为会话:SET max_sp_recursion_depth=10
【解决方案2】:

这不是一个非常干净的实现,但由于您只需要孩子和子孩子,因此其中任何一个都可以工作:

查询1:

SELECT UserID, UserName
FROM tbl_user
WHERE ParentID = 3 OR UserID = 3
UNION
SELECT UserID, UserName
FROM tbl_user
WHERE ParentID IN (SELECT UserID
FROM tbl_user
WHERE ParentID = 3);

查询 2:

SELECT UserID, UserName
FROM tbl_user 
WHERE UserID = 3
OR ParentID = 3
OR ParentID IN (SELECT UserID
    FROM tbl_user
    WHERE ParentID = 3);

编辑 1:或者,您可以修改表结构,以便更方便地查询特定类别的所有子项。请点击此链接阅读更多关于storing hierarchical data in MySQL的信息。

此外,您可能会考虑以树状方式分层存储数据,这在in this article 中有很好的解释。

请注意,每种方法在检索所需结果与添加/删除类别方面都有其权衡,但我相信您会喜欢阅读。

【讨论】:

  • 我看到投反对票,但选民没有解释他投反对票的原因。嗯!
  • 我没有对您投反对票,现在您的评分为+2,但如果层次结构中的级别超过 3 个,您的示例将不起作用。如果您将一个子用户添加到用户 5,它将不会在您的示例中返回。它确实适用于 OP 提供的情况。
【解决方案3】:

这是我看过的最好的文章之一,用于解释在 SQL 样式数据库中存储树状数据的“修改的预序树遍历”方法。

http://www.sitepoint.com/hierarchical-data-database/

MPTT 内容从第 2 页开始。

本质上,您为树中的每个节点存储一个“Left”和一个“Right”值,以便获取 ParentA 的所有子节点,然后获取 ParentA 的 Left 和 Right,然后

SELECT * 
FROM TableName
WHERE Left > ParentLeft 
AND Right < ParentRight

【讨论】:

  • +1 这对提高效率很有好处,尽管我总是将父字段留在其中,以便在插入错误时可以重建。
  • 确实,父母 Id 对于重建很重要。我也将 MPTT 用于 20k 记录表,因此插入可能会很慢,所以如果我插入许多记录,我只进行插入,然后更新左侧和右侧的质量。
【解决方案4】:

获取所选孩子的所有父母(本例中 user_id = 3):

         SELECT
              @parent_id AS _user_id,
              user_name,
              (
                 SELECT @parent_id := parent_id
                 FROM users
                 WHERE user_id = _user_id
              ) AS parent
           FROM (
        -- initialize variables
              SELECT
                 @parent_id := 3
              ) vars,
              users u
           WHERE @parent_id <> 0;

获取选定 user_id 的所有子代

SELECT ui.user_id AS 'user_id', ui.user_name AS 'user_name', parent_id,
FROM
(
  SELECT connect_by_parent(user_id) AS user_id
  FROM (
    SELECT
      @start_user := 3,
      @user_id := @start_user
  ) vars, users
  WHERE @user_id IS NOT NULL
  ) uo
JOIN users ui ON ui.user_id = uo.user_id

这需要以下函数

CREATE FUNCTION connect_by_parent(value INT) RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
        DECLARE _user_id INT;
        DECLARE _parent_id INT;
        DECLARE _next INT;
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET @user_id = NULL;

SET _parent_id = @user_id;
SET _user_id = -1;

IF @user_id IS NULL THEN
        RETURN NULL;
END IF;

LOOP
        SELECT  MIN(user_id)
        INTO    @user_id
        FROM    users
        WHERE   parent_id = _parent_id
                AND user_id > _user_id;

        IF @user_id IS NOT NULL OR _parent_id = @start_with THEN
                RETURN @user_id;
        END IF;

        SELECT  user_id, parent_id
        INTO    _user_id, _parent_id
        FROM    users
        WHERE   user_id = _parent_id;
END LOOP;

结束

这个例子大量使用了许多 sql 用户可能不熟悉的会话变量,所以这里有一个链接可以提供一些见解:session variables

【讨论】:

  • 想知道您是否自己测试过这个?例如,您的函数接受一个“值”参数,但从不使用它。
  • @DTest MySQL 忽略函数定义中的 NON DETERMINISTIC 子句,缓存它的返回值,如果使用相同的参数调用它,则实际上不会调用它。所以它只是在那里让mysql执行该功能。我之前在稍微不同的数据集上使用过这个函数/查询示例,只是将其调整为 OP。
猜你喜欢
  • 1970-01-01
  • 2011-06-24
  • 1970-01-01
  • 1970-01-01
  • 2011-03-30
  • 1970-01-01
  • 2016-05-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多