【问题标题】:Is there a way to select Parent IDs in SQL without recursion or looping?有没有办法在 SQL 中选择父 ID 而无需递归或循环?
【发布时间】:2012-04-26 06:26:09
【问题描述】:

我有一个包含以下列的表:group_id、parent_id、name

在此表中,parent_id 是另一条记录的 group_id。父母与孩子之间存在一对多的关系。这形成了一个层次结构,其中只有一个顶级组的 parent_id 为 NULL。可以有任意数量的深度,但实际上我的层次结构永远不会超过 20 层。

我想检索具有给定 group_id 的组的每个祖先(父母的父母等)。我担心返回的特定方式。

我正在使用 MS SQL 2005,但我也对使用其他 RDBMS 的解决方案感兴趣。

我发现了一些类似的问题,但它们似乎都分解为递归、循环或嵌套集。我不能使用嵌套集,因为我不能改变数据结构。我想尽可能避免递归或循环,或者至少了解为什么不可能。

这是我在研究时发现的一些问题:

How to select parent ids

Sql recursion without recursion

【问题讨论】:

  • 你唯一的目标是为每个id获取最上层的顶级parent_id?
  • 您发布的第一个链接将使用递归公用表表达式 (with ...) 解决您的问题,这也适用于大多数现代 DBMS(稍作语法更改)
  • @a_horse_with_no_name:这就是我的目标。看起来微不足道。
  • @wildplasser 让我澄清一下“我想检索每个父母”,就像该组的每个父母一样,包括中间的每个父母,直到您到达顶部。您可以通过以下方式轻松到达最顶层: Select * from table where parent_id is null;
  • 你应该说你也想避免 implicit 递归(因为 SQL 本身不是递归的,只是它的执行)。但是你为什么要避免这种情况呢? CTE 可能是解决此问题的最有效方法。

标签: sql parent-child


【解决方案1】:

操作本质上是循环的。由于每个节点与其根没有任何有限关系,因此您必须遍历才能发现它。

例如,如果您知道最大深度为 N,那么您可以在单个语句中创建 N 个 LEFT OUTER JOINs 并显示以这种方式返回的最后一个非空父 ID。

循环的要求是你根本不知道 N 是什么,而且你不能要求像 SQL 这样的声明性语言来“弄清楚”

即使你可以使用一些内置方法来完成它,它仍然是一个循环或递归,只是被你混淆了。

【讨论】:

  • 在我发布的第二个链接的第一个答案中,有一个以 Oracle 为中心的答案,它可以在一个干净的查询中完成(虽然我还不明白)。即使数据库必须执行一些循环,它在创建 DBMS 的本机代码中的性能是否仍然比在 SQL 中重新实现该循环更好?现代 DMBS 做了多少缓存和执行计划准备工作?
【解决方案2】:

您可以创建一个类似于您的临时表并像这样填充它:

INSERT INTO #T(group_id, parent_id) SELECT group_id, parent_id FROM Your_Table

现在执行五次完全相同的 SQL:

INSERT INTO #T(group_id, parent_id) SELECT T2.group_id, T1.parent_id FROM #T T1 JOIN #T T2 ON T2.parent_id = T1.group_id LEFT JOIN #T C ON T2.group_id = C.group_id AND T1.parent_id = C.parent_id  AND C.group_id IS NULL
INSERT INTO #T(group_id, parent_id) SELECT T2.group_id, T1.parent_id FROM #T T1 JOIN #T T2 ON T2.parent_id = T1.group_id LEFT JOIN #T C ON T2.group_id = C.group_id AND T1.parent_id = C.parent_id  AND C.group_id IS NULL
INSERT INTO #T(group_id, parent_id) SELECT T2.group_id, T1.parent_id FROM #T T1 JOIN #T T2 ON T2.parent_id = T1.group_id LEFT JOIN #T C ON T2.group_id = C.group_id AND T1.parent_id = C.parent_id  AND C.group_id IS NULL
INSERT INTO #T(group_id, parent_id) SELECT T2.group_id, T1.parent_id FROM #T T1 JOIN #T T2 ON T2.parent_id = T1.group_id LEFT JOIN #T C ON T2.group_id = C.group_id AND T1.parent_id = C.parent_id  AND C.group_id IS NULL
INSERT INTO #T(group_id, parent_id) SELECT T2.group_id, T1.parent_id FROM #T T1 JOIN #T T2 ON T2.parent_id = T1.group_id LEFT JOIN #T C ON T2.group_id = C.group_id AND T1.parent_id = C.parent_id  AND C.group_id IS NULL

在此之后,您的表格现在可以跟踪祖先而不是父母,最多可跟踪 32 级距离。 (2^5 = 32 和 32 > 20)。

这是计算“传递闭包”的一种有效方法,尽管如果您添加循环而不是仅仅重复相同的 INSERT 五次,您将不再需要您假设 20 个级别。当INSERT 插入零个新行时,您应该停止。 这种循环将有助于而不是损害性能,并且迭代次数将非常少。

【讨论】:

  • 这需要我一点时间来消化,+1 表示新鲜感。
  • @Squeaky - 以下是如何消化它:第一个查询使祖父母成为普通父母,但父母也留在#T。那是距离 1 或 2。下一次迭代类似地添加距离最多为 2+2 = 4 的祖先。下一次迭代添加距离最多为 4+4 = 8 的祖先。依此类推。这是由(内部)JOIN 完成的。解释 LEFT JOIN:永远不要添加已经在 #T 中的祖先关系,以防止它随着重复行增长。
  • 有多少可能会被缓存以防止每次调用都重复如此庞大的工作量?
  • @Squeaky - 如果它适合 RAM,那么一切。此外,应使用由 group_id 和 parent_id 组成的主键创建临时表,以获得最佳访问速度。
【解决方案3】:

如果你确切地知道你的数据结构有多深,你可以手写代码:

DECLARE
    @parentId1 int
   ,@parentId2 int
   ...
   ,@parentId19 int
   ,@parentId20 int

SELECT
    @parentId1 = parent_id
FROM
    myTable
WHERE
    group_id = <someid>

SELECT
    @parentId2 = parent_id
FROM
    myTable
WHERE
    group_id = @parentId1

等等。然而,这会给你一大堆额外的代码,并且不会比循环更好,而且它非常脆弱。向树中添加新级别需要您修改代码,这应该是即时的代码味道。

用任何其他语言来考虑它。您必须总共执行任务 X N 次,其中 N 是可变的。你打算怎么写这个?你会使用一个循环。现在假设您的数据结构是一棵树(这就是您在这里得到的)。你会怎么写这个?您可能会使用递归,除非您将递归扁平化为循环。

MSSQL 唯一需要注意的是,默认情况下,递归堆栈的深度限制为 16。在 MSSQL 中使用循环比使用递归要好得多。

我通常会这样做:

-- Temp table will hold the results starting from the ID of the source item
-- through all its ancestors in ascending order
DECLARE @table TABLE (
    sequence int IDENTITY(1, 1)
   ,group_id int
)

DECLARE @groupId int

SELECT @groupId = <someid>

-- Loop backwards through the group's hierarchy inserting all parent IDs
-- into the temporary table
WHILE @groupId IS NOT NULL
BEGIN
    INSERT INTO @table (
        group_id
    )

    VALUES (
        @groupId
    )

    -- Get the ID of the group's parent ready to loop again
    SELECT @groupId = parent_id
    FROM mutable
    WHERE group_id = @groupId
END

-- Print the results
SELECT group_id
FROM @table

可能有更好的方法,但它会以一种您可以轻松操作的形式为您提供所有 ID,而且它们的顺序也正确。

【讨论】:

  • 这是无效的,因为您不知道嵌套深度。假设嵌套深度为一百万,那么代码复制粘贴一百万次才能正常运行。
  • @Matthew:这正是答案的重点。您需要阅读代码后的其余文本...
  • 但 OP 明确表示他知道嵌套深度。
  • @MatthewPK:我知道,但在不使用循环或递归的情况下,唯一可能的方法是手动编写代码。答案说明了这种可能性,然后指出了它的问题,它们是:a)您需要知道最大深度(您可以通过在每个后续 SELECT 之前执行 NULL 检查来应对较短的深度)和 b)这是糟糕的代码。在问题的最后一部分,OP 问为什么他必须使用循环或递归,我在这里回答。
  • 在我发布的第二个链接中,有一个 Oracle Centric 可以通过一次选择来完成,我希望有一个巧妙的解决方案可以在其他地方工作。还考虑到 SQL 所做的基本上是遍历数据集并执行多重比较并以复杂的方式构造数据,这让我怀疑大多数查询实际上调用了 RDBMS 中的一些循环。
猜你喜欢
  • 2010-10-21
  • 2013-11-27
  • 2019-08-22
  • 2012-04-18
  • 2022-01-05
  • 1970-01-01
  • 1970-01-01
  • 2019-09-05
  • 2021-06-29
相关资源
最近更新 更多