MySQL中的递归组结构

2ul0zpep  于 2022-10-22  发布在  Mysql
关注(0)|答案(7)|浏览(171)

我正在开发一个需要允许用户分组的系统。系统中的其他特权用户可以自由创建、编辑和删除这些组。这一部分很容易;只需创建一个group_users表,将用户链接到组中。(如果你是一个坚持标准化的人,那么你可以创建一个group表,只列出组,然后创建一个将它们链接在一起的group_users表格——这也很好)
这就是问题的症结所在。客户端希望组也包含任意深度和任意重叠的组(组可以在多个组中,组可以包含多个组)。这很容易存储(使用group_groups表),但如果没有像Oracle的CONNECT BY这样的排序扩展,就很难查询。
这个递归层次结构还需要具有追溯性——这意味着如果组A包含组B,并且组B被修改,那么组A也将被修改——所以我不能作弊,只需将结构扁平化。如果你不相信我,它不能简单地被压平,考虑一下这种情况。你有一个名为“酷人”的组,其中包含用户1和2。有人创建了一个名“真正酷人”组,其中包括用户3,并包含组“酷人。”。当我询问“真正酷的人”时,我应该得出结论:用户1、2和3在组中。现在假设有人认为用户2不再是一个酷的人,并将用户2从“酷的人”中删除。在那个时间点之后,“真正酷的人”只包含用户1和3。如果我最初将结构扁平化,当我将用户2从“酷的人(cool people)”中删除时,我不知道如何将其从“真正酷”中删除。
因此,在这种情况下,一个微不足道的平坦化是行不通的。我考虑过的其他选项:

  • 在代码中执行递归。
  • 对于复杂的组来说太慢了,而且还需要在内存中而不是在数据库中执行相关连接
  • 将结构扁平化为group_users_flattened,但还要维护group_groups表。在INSERT/UPDATE/DELETE上为group_users_flattened创建一个触发器,该触发器将转到group_groups表,找到包含该组的所有组,并动态地对group_users_flattened进行适当的更改。
  • 我可以想象这是可行的,但它看起来很复杂,容易出错,我觉得有一个我没有看到的问题。

还有其他我没有考虑过的想法吗?

bvk5enib

bvk5enib1#

请参阅我对“将平面表解析为树的最有效/最优雅的方法是什么?”的回答?。我描述了一种我称之为“闭包表”的设计。
在您的示例中,您将拥有表UsersGroupsUserGroupMembers,这是用户和组之间的交集表(多对多)。
然后需要另一个表来描述组是如何嵌套的。例如,称其为SubgroupPaths。这将记录将给定组与其子组关联的每条路径。

CREATE TABLE SubgroupPaths (
  supergroup INT UNSIGNED NOT NULL,
  subgroup   INT UNSIGNED NOT NULL,
  pathlength SMALLINT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (supergroup, subgroup),
  FOREIGN KEY (supergroup) REFERENCES Groups(groupid),
  FOREIGN KEY (subgroup) REFERENCES Groups(groupid)
);

您可能还需要一些复合索引的排列来支持对该表运行的某些查询。
这种设计允许您有多个层次结构,因此您可以让组“酷人”成为其每个超级组的后代。

INSERT INTO Groups (groupid, groupname) VALUES
(1, 'REALLY cool people'),
(2, 'slightly cool people'),
(3, 'cool people');

INSERT INTO SubgroupPaths (supergroup, subgroup, pathlength) VALUES
(1,1,0), (2,2,0), (3,3,0), -- every group points to itself
(1,3,1), -- REALLY is parent of cool people
(2,3,1); -- slightly is also parent of cool people

现在你可以得到所有应该被认为是酷人的用户的列表,不管他们是酷人、稍微酷的人还是真正酷的人。我们甚至可以添加一个DISTINCT,以防某些用户与这些组中的多个关联。

SELECT DISTINCT u.*
FROM SubgroupPaths AS cool
JOIN SubgroupPaths AS supercool ON cool.subgroup=supercool.subgroup
JOIN Groups AS g ON supercool.supergroup = g.groupid
JOIN UserGroupMembers AS m ON m.groupid = g.groupid
JOIN Users AS u ON u.userid = m.userid
WHERE cool.subgroup = 3;

我更喜欢闭包表,而不是其他答案建议的嵌套集设计,因为闭包表支持引用完整性约束,并且有一些查询在嵌套集中很难,但在闭包表中更容易。
有关闭包表的更多信息,请参阅我的书SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming
所有这些的脚注:小心违反YAGNI原则。
我曾经实现了一个数据库来存储这样的任意深度组,以及用于显示、报告和管理层次结构的所有PHP代码。此外,我必须在使用分层集合时克隆它们,因为稍后可以重新组织层次结构,并且我们需要保留层次结构中元素如何使用的历史数据。编码和测试花费了数周时间。当这一切都完成后,应用程序的用户实际上从未存储过任何层次结构,只有一层深。
如果你告诉一些决策者实施和测试需要做多少工作(即预算),他们会改变对需求范围的看法。

yhxst69z

yhxst69z2#

使用嵌套集的查询可能比使用存储过程遍历邻接列表的查询更快,对于缺少本地递归查询结构(如MySQL)的数据库来说,也是更快的选择
http://en.wikipedia.org/wiki/Nested_set_model
https://docs.joomla.org/Using_nested_sets
然而,插入新节点(行)将需要相应地更新所有行

tzdcorbm

tzdcorbm3#

我会使用嵌套集。此处提供详细信息:
http://www.alandelevie.com/2008/07/12/recursion-less-storage-of-hierarchical-data-in-a-relational-database/
虽然我从未用它来表示重叠。

epggiuax

epggiuax4#

您能有一个users_groups表(每行有一列,以区分用户条目和组条目)和一个单独的多连接表,列出所有user_group_memberships吗?
我猜想连接表需要一个约束来确保groups列是第一个表的FK,并且类型是group。(换句话说,如果连接表有两列:member_ID和group_ID,则member_ID可以是对成员或组的引用,而group_ID只能引用组。
这将允许任何用户或组被包括在任何组的成员中,同时防止任何用户或用户组成为用户的“成员”。
(顺便说一句:我对MySQL还不够精通,现在还没有准备好一个工作示例;但如果这个建议可行,我希望看到一个)。

7d7tgy0s

7d7tgy0s5#

这样的结构怎么样


小时
一种关系,比如真正酷的人与酷的人之间的关系是“连锁”的(因此是适当的级联),反之亦然。

ergxz8rk

ergxz8rk6#

您是否考虑过分组表中的自参照结构?假设你放了一个名为“超类”的列。就像OOP一样,子类继承自超类。然后给它一个ID列,这样你就可以:
[ID|组名称|任何其他列|超类]
以及ID和超类之间的外键约束。
这样,假设您有组heffalump,ID=3。它的超类可以是1,其中ID=1对应于组名winniethepooh。
假设Woozle的ID为4。它也可以有超类1。所以它仍然处于winniethepooh之下。
相当简单,但应该不会有太多麻烦。这样,按照你的例子,“真正酷的人”将被划分为“酷的人(cool people)”之下的等级,所以“真正酷”中唯一不属于“酷的”的人将是那些一开始就不属于“冷的人”的人。因此,如果你把一个人从“酷人”中剔除,他就不会被归类为“真正酷的人”,但如果你把他从“真正酷人”里剔除,它不会影响“酷的人。”
抱歉解释得太复杂了,我希望这能有所帮助!

  • 编辑:我注意到这基本上是另一个链接中给出的第一个示例。那样的话,我就没有其他想法了。很抱歉
uidvcgyl

uidvcgyl7#

我将研究使用公共表表达式(CTE)进行递归。根据我的经验,这是在SQLServer中查询分层数据的最有效方法。
以下链接解释了如何使用CTE:http://msdn.microsoft.com/en-us/library/ms190766.aspx
下面是一个简单的示例,说明如何使用CTE查询层次结构。显然,您必须调整应用程序的代码,但这应该为您指明正确的方向。

WITH Groups AS
(
   --initialization
   SELECT ParentGroups.GroupID, ParentGroups.GroupName, ParentGroups.ParentGroupID
   FROM ParentGroups
   WHERE ParentGroups.ParentGroupID IS NULL
   UNION ALL
   --recursive execution
   SELECT SubGroups.GroupID, SubGroups.GroupName, SubGroups.ParentGroupID
   FROM Groups SubGroups INNER JOIN Groups ParentGroups 
   ON SubGroups.ParentGroupID = ParentGroups.GroupID
)
SELECT * FROM Groups

此外,您不需要有group_groups表。通过添加ParentGroupID列,可以在组表中保留整个层次结构。

相关问题