9

我有一个使用邻接列表模型存储层次信息的表。(使用自引用键 - 下面的示例。此表可能看起来很熟悉):

category_id name                 parent
----------- -------------------- -----------
1           ELECTRONICS          NULL
2           TELEVISIONS          1
3           TUBE                 2
4           LCD                  2
5           PLASMA               2
6           PORTABLE ELECTRONICS 1
7           MP3 PLAYERS          6
8           FLASH                7
9           CD PLAYERS           6
10          2 WAY RADIOS         6

将上述数据“展平”成这样的最佳方法是什么?

category_id lvl1        lvl2        lvl3        lvl4
----------- ----------- ----------- ----------- -----------
1           1           NULL        NULL        NULL
2           1           2           NULL        NULL
6           1           6           NULL        NULL
3           1           2           3           NULL
4           1           2           4           NULL
5           1           2           5           NULL
7           1           6           7           NULL
9           1           6           9           NULL
10          1           6           10          NULL
8           1           6           7           8

每一行都是通过层次结构的一个“路径”,除了每​​个节点都有一行不仅仅是每个叶子节点)。category_id 列代表当前节点,“lvl”列是它的祖先。当前节点的值也必须在最右边的 lvl 列中。lvl1 列中的值将始终表示根节点,lvl2 中的值将始终表示 lvl1 的直接后代,依此类推。

如果可能,生成此输出的方法将使用 SQL,并且适用于 n 层层次结构。

4

4 回答 4

11

在一个简单的邻接列表中进行多级查询总是涉及到自左连接。制作右对齐表格很容易:

SELECT category.category_id,
    ancestor4.category_id AS lvl4,
    ancestor3.category_id AS lvl3,
    ancestor2.category_id AS lvl2,
    ancestor1.category_id AS lvl1
FROM categories AS category
    LEFT JOIN categories AS ancestor1 ON ancestor1.category_id=category.category_id
    LEFT JOIN categories AS ancestor2 ON ancestor2.category_id=ancestor1.parent
    LEFT JOIN categories AS ancestor3 ON ancestor3.category_id=ancestor2.parent
    LEFT JOIN categories AS ancestor4 ON ancestor4.category_id=ancestor3.parent;

像你的例子一样左对齐它有点棘手。想到这:

SELECT category.category_id,
    ancestor1.category_id AS lvl1,
    ancestor2.category_id AS lvl2,
    ancestor3.category_id AS lvl3,
    ancestor4.category_id AS lvl4
FROM categories AS category
    LEFT JOIN categories AS ancestor1 ON ancestor1.parent IS NULL
    LEFT JOIN categories AS ancestor2 ON ancestor1.category_id<>category.category_id AND ancestor2.parent=ancestor1.category_id
    LEFT JOIN categories AS ancestor3 ON ancestor2.category_id<>category.category_id AND ancestor3.parent=ancestor2.category_id
    LEFT JOIN categories AS ancestor4 ON ancestor3.category_id<>category.category_id AND ancestor4.parent=ancestor3.category_id
WHERE
    ancestor1.category_id=category.category_id OR
    ancestor2.category_id=category.category_id OR
    ancestor3.category_id=category.category_id OR
    ancestor4.category_id=category.category_id;

将适用于 n 层层次结构。

抱歉,在邻接列表模型中不可能进行任意深度的查询。如果您经常进行此类查询,则应将架构更改为存储分层信息的其他模型之一:完全邻接关系(存储所有祖先-后代关系)、物化路径或嵌套集。

如果类别不经常移动(对于像您的示例这样的商店来说通常是这种情况),我会倾向于嵌套集合。

于 2009-04-19T01:45:23.030 回答
9

如前所述,SQL 没有干净的方法来实现具有动态变化的列数的表。我之前使用的唯一两个解决方案是:1. 固定数量的 Self-Joins,给出固定数量的列(AS per BobInce) 2. 将结果生成为单个列中的字符串

第二个听起来很怪诞。将 ID 存储为字符串?!但是当输出被格式化为 XML 或其他格式时,人们似乎并没有那么在意。

同样,如果您想加入 SQL 中的结果,这也没什么用。如果要将结果提供给应用程序,它可能非常合适。然而,就个人而言,我更喜欢在应用程序中进行扁平化而不是 SQL


我被困在一个 10 英寸的屏幕上,无法访问 SQL,所以我无法提供测试代码,但基本方法是以某种方式利用递归;
- 递归标量函数可以做到这一点
- MS SQL 可以使用递归 WITH 语句做到这一点(更有效)

标量函数(类似):

CREATE FUNCTION getGraphWalk(@child_id INT)
RETURNS VARCHAR(4000)
AS
BEGIN

  DECLARE @graph VARCHAR(4000)

  -- This step assumes each child only has one parent
  SELECT
    @graph = dbo.getGraphWalk(parent_id)
  FROM
    mapping_table
  WHERE
    category_id = @child_id
    AND parent_id IS NOT NULL

  IF (@graph  IS NULL)
    SET @graph = CAST(@child_id AS VARCHAR(16))
  ELSE
    SET @graph = @graph + ',' + CAST(@child_id AS VARCHAR(16))

  RETURN @graph

END


SELECT
  category_id                         AS [category_id],
  dbo.getGraphWalk(category_id)       AS [graph_path]
FROM
  mapping_table
ORDER BY
  category_id

我有一段时间没有使用递归 WITH 了,但即使我没有 SQL 来测试任何东西,我也会尝试一下语法:)

递归的

WITH
  result (
    category_id,
    graph_path
  )
AS
(
  SELECT
    category_id,
    CAST(category_id AS VARCHAR(4000))
  FROM
    mapping_table
  WHERE
    parent_id IS NULL

  UNION ALL

  SELECT
    mapping_table.category_id,
    CAST(result.graph_path + ',' + CAST(mapping_table.category_id AS VARCHAR(16)) AS VARCHAR(4000))
  FROM
    result
  INNER JOIN
    mapping_table
      ON result.category_id = mapping_table.parent_id
)

SELECT
  *
FROM
  result
ORDER BY
  category_id


编辑 - 两者的输出是相同的:

1   '1'
2   '1,2'
3   '1,2,3'
4   '1,2,4'
5   '1,2,5'
6   '1,6'
7   '1,6,7'
8   '1,6,7,8'
9   '1,6,9'
于 2009-04-19T08:34:13.327 回答
1

遍历任意深度的树通常涉及递归过程代码,除非您使用某些 DBMS 的特殊功能。

在 Oracle 中,如果您使用邻接列表,则 CONNECT BY 子句将允许您以第一顺序深度遍历树,就像您在此处所做的那样。

如果您使用嵌套集,左边的序列号将为您提供访问节点的顺序。

于 2009-04-19T02:57:08.897 回答
0

实际上可以在存储过程中使用动态 SQL 来完成。然后,您将受限于存储过程可以做的事情。显然,将结果执行到一个临时表中,不知道有多少列是一个挑战。但是,如果目标是输出到网页或其他 UI,那么这可能是值得的……

于 2012-07-22T00:18:12.257 回答