如何使用MySQL存储过程进行分层查询?

时间:2011-10-12 21:13:01

标签: mysql stored-procedures hierarchical-data

在开始之前,我想说数据库结构已设置且无法更改。我必须使用我提供的结构。所以请不要建议我更改数据库。 =)

我有一个在线游戏的数据库。这个游戏有一个“技能树”,每个技能都有“先决条件”技能,你必须获得解锁它。每个先决条件技能可能具有自己的先决技能,依此类推。深度不固定,可以达到6或7级深度。

所以,让我说我有这个技能树,它是发射鱼雷所必需的:

Torpedos
|- Missile Launcher Operation 
|- Heavy Missiles
   |- Standard Missiles

(它还包括一些冗余的预制,比如标准导弹需要导弹发射器操作。那些为了简单而省略的)

由于数据库的性质,我正在考虑制作一个通过技能树进行递归的存储过程。每个技能都有一个与之相关的ID和一些先决条件(没有父母的权利)。这是我当前的SQL查询:

SELECT
IFNULL(SkillName.valueInt,SkillName.valueFloat) AS SkillID,
items.typeName AS Skill
FROM dgmtypeattributes AS SkillName
INNER JOIN dgmtypeattributes AS SkillLevel ON SkillLevel.typeID = SkillName.typeID AND SkillLevel.attributeID IN (277, 278, 279, 1286, 1287, 1288)
INNER JOIN invtypes  AS items ON IFNULL(SkillName.valueInt,SkillName.valueFloat) = items.typeID
WHERE SkillName.typeID = SKILLID AND
((SkillName.attributeID = 182 AND
SkillLevel.attributeID = 277) OR
(SkillName.attributeID = 183 AND
SkillLevel.attributeID = 278) OR
(SkillName.attributeID = 184 AND
SkillLevel.attributeID = 279) OR
(SkillName.attributeID = 1285 AND
SkillLevel.attributeID = 1286) OR
(SkillName.attributeID = 1289 AND
SkillLevel.attributeID = 1287) OR
(SkillName.attributeID = 1290 AND
SkillLevel.attributeID = 1288))

不要担心绒毛,这里最主要的是SKILLID。如果我将3325(鱼雷的ID)替换为SKILLID,它将返回:

+---------+----------------------------+
| SkillID | Skill                      |
+---------+----------------------------+
|    3319 | Missile Launcher Operation |
|    3324 | Heavy Missiles             |
+---------+----------------------------+

如果我输入3324(重型导弹的身份证),它将返回:

+---------+----------------------------+
| SkillID | Skill                      |
+---------+----------------------------+
|    3321 | Standard Missiles          |
+---------+----------------------------+

所以,基本上,我需要遍历这些查询并使用新查询中之前结果中的SkillID,最终结束。我还需要一个新列parent来指定哪个技能ID是该行的父级。

问题是,在存储过程方面,我不知道我在做什么。我已经阅读了它们,我仍然不知道如何去做。

我可以使用PHP和一些SQL查询轻松完成此操作,但我想尝试更改速度的程序。任何人都可以在这里右脚开始我吗? =)


更新1

我有这个功能,但它显示错误:

#1172 - Result consisted of more than one row

使用此查询时:

SELECT  test2(typeID) AS id, @level AS level
FROM    (
    SELECT  @start_with := 3325,
    @id := @start_with,
    @level := 0
) vars, dgmtypeattributes
WHERE   @id IS NOT NULL

这是功能:

DROP FUNCTION IF EXISTS test2;
CREATE FUNCTION test2(value INT) RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
        DECLARE _id INT;
        DECLARE _parent INT;
        DECLARE _next INT;
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET @id = NULL;

        SET _parent = @id;
        SET _id = -1;

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

        LOOP
                SELECT 
                    MIN(valueInt) AS id, CONCAT(@path, ',', MIN(valueInt))
                INTO @id, @path
                FROM `dgmtypeattributes` 
                WHERE 
                    typeID = _parent
                    AND attributeID > 181 
                    AND attributeID < 185
                    AND valueInt    > _id;

                IF @id IS NOT NULL OR _parent = @start_with THEN
                        SET @level = @level + 1;
                        RETURN @id;
                END IF;
                SET @level := @level - 1;

                SELECT  _parent, SUBSTRING_INDEX(@path, ',', -1)
                INTO    _id, _parent
                FROM    `dgmtypeattributes`
                WHERE   typeID = _parent;
        END LOOP;
END

我认为以SELECT _parent, SUBSTRING_INDEX(@path, ',', -1)开头的第二个选择是返回多行的那个...很可能我没有提供正确的查询并且让我的值混淆了。那第二个选择 - 应该返回什么?

表格解释

此外,由于很难使用虚数据,这里是我正在使用的两个表的链接(实际数据表和一个简单地用ID链接ID的表): http://www.2shared.com/file/17uFmKXc/sqlFile.html

我不希望任何人对数据做出正面或反面。很多都是与手头问题无关的额外内容(例如,invTypes表 - 它不仅包含'技能',而且包含整个游戏中的每一项)。

快速纲要:dgmtypeattributes是一个表格,用于定义游戏中各种项目的属性。这些属性包括使用所述项目所需的“先决条件”技能,这些属性ID为182-184。因此,如果您在typeID(鱼雷技能)中搜索3325属性范围为182-184(查询先决条件属性),我会回来:

mysql> SELECT * FROM `dgmtypeattributes` WHERE typeID = 3325 AND attributeID >181 AND attributeID <185;
+--------+-------------+----------+------------+
| typeID | attributeID | valueInt | valueFloat |
+--------+-------------+----------+------------+
|   3325 |         182 |     3319 |       NULL |
|   3325 |         183 |     3324 |       NULL |
+--------+-------------+----------+------------+
2 rows in set (0.00 sec)

valueInt下的两个值是先决技能(导弹发射器操作和重型导弹)的ID。


解决方案

今天还在玩弄它,我想我终于搞清楚了。我基本上不得不写出函数的整个流程,因为MySQL并没有真正提供任何调试这些东西的方法= /

我还在改进它,但这是我到目前为止所做的:

CREATE FUNCTION test2(value INT) RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN


        DECLARE _id INT;
        DECLARE _parent INT;
        DECLARE _next INT;
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET @id = NULL;

        SET _parent = @id;
        SET _id = -1;

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

        LOOP
                SELECT 
                    MIN(valueInt) AS id, IF(MIN(valueInt), CONCAT(@path, ',', _parent), @path)
                INTO @id, @path
                FROM `dgmtypeattributes` 
                WHERE 
                    typeID = _parent
                    AND attributeID > 181 
                    AND attributeID < 185
                    AND valueInt    > _id;

                IF @id IS NOT NULL OR _parent = @start_with THEN
                        SET @level = @level + 1;
                        SET @parent = _parent;
                        RETURN @id;
                END IF;

                IF @path = '' THEN
                    RETURN NULL;
                END IF;

                SET @level := @level - 1;

                SELECT  _parent, SUBSTRING_INDEX(@path, ',', -1), SUBSTRING(@path, 1, (LENGTH(@path)-(LENGTH(SUBSTRING_INDEX(@path, ',', -1)) +1)))
                INTO    _id, _parent, @path;
        END LOOP;
END

结果:

SELECT  test2(typeID) AS id, @level AS level
FROM    (
    SELECT  
        @start_with := 3325,
        @id := @start_with,
        @level := 0,
    @path := ''
) vars,dgmtypeattributes
WHERE   @id IS NOT NULL

+------+-------+
| id   | level |
+------+-------+
| 3319 |     1 |
| 3324 |     1 |
| 3319 |     2 |
| 3321 |     2 |
| 3319 |     3 |
| NULL |     1 |
+------+-------+
6 rows in set (0.09 sec)

再一次,要调整一些东西(当它不应该返回NULL时,我需要包含一个父列)但总而言之我已经有了它的工作!

2 个答案:

答案 0 :(得分:2)

通常,您可以使用本文中描述的方法:

,请记住,先决条件是(反直觉地)儿童,heavy missilesmissile launchers都是torpedoes的孩子,而不是父母。

但是,您的模型存在两个问题:

首先,您将关系分为两个表格(itemsdgmtypeattributes,而不是一个EAV)。

这不是一个大问题,可以很容易地解决。只需替换

SELECT  MIN(id)
INTO    @id
FROM    t_hierarchy
WHERE   parent = _parent
        AND id > _id;

使用等效查询,以“{1}}顺序”返回“id大于_id的第一个孩子。”

其次,你的血统不是树,也就是一个项目可以有多个父母。

这是一个问题,因为该过程不保留递归堆栈,而只是在两个方向上遍历树。由于可以有多个方向,相当于以下查询:

id

不知道要遵循哪个方向(导致此项目存储在SELECT id, parent INTO _id, _parent FROM t_hierarchy WHERE id = _parent; 277或其他属性中的父级)?

但是,可以通过将实际递归路径存储在以逗号分隔的会话变量中来重写该过程。只需用您的等效内容重写查询:

278

,并用

替换第二个查询(选择父项)
SELECT  MIN(id), CONCAT(@path, ',', MIN(id))
INTO    @id, @path
FROM    t_hierarchy
WHERE   parent = _parent
            AND id > _id;

答案 1 :(得分:0)

尝试将树表示为嵌套集,而不是存储父ID。给它一个左右值。现在你有一个节点A有两个孩子B和C,C有两个孩子,D和E.现在想象两次访问每个节点并分配它们的数字,一次是左侧,一次是右侧。当你向右上方并击中一个分支时,向左走。叶节点得到两个连续的数字。然后再回到你第二次击中根。

左:1右:8 B左:2右:7 C左:3右:4 D左:5右:6

现在,要查找一个叶子,只需查找一个右 - 左= 1的节点。找到D的祖先,寻找剩下的节点&gt;右1和左&lt; D.left和right&gt; D.right

CREATE PROCEDURE getancestors (@nodename NVARCHAR(50))
    BEGIN
      SELECT nodeID, left, right
      FROM mytree
      WHERE left > right-1 AND left < (SELECT left FROM mytree WHERE nodeID=@nodename) 
        AND right > (SELECT right FROM mytree WHERE nodeID=@nodename) 
      ORDER BY left;
    END;

这将为您提供父节点到您的节点的结果。要从节点提升树,请按右侧排序。请记住,在嵌套集中,所有子项的左值都大于且右值小于父值。这种技术的唯一问题是你需要频繁插入。

假设上面的例子,为了获得D的祖先,将其调用为:

CALL getancestors("D");

祝你好运,编码愉快; - }