带有父节点和子节点的SQL表(导航表)

时间:2009-11-17 09:44:09

标签: asp.net sql-server

我有一个包含父节点和子节点的表,每个节点都有一个订单号。

我正在尝试编写单个查询以按顺序输出它们,它用于包含类别和子类别的导航列表。

我可以在代码中而不是在SQL查询中管理它,但它涉及从查询循环中调用查询 - 我想避免这种情况。

表:

DBID | Title | ParentID | OrderNum
 1      aaa       0          1
 2      bbb       0          2
 3      ccc       1          1
 4      ddd       1          2
 5      eee       2          1

我希望获得如下结果集:

DBID | Title | ParentID | OrderNum
 1      aaa       0          1      <<< main 
 3      ccc       1          1      <<< child
 4      ddd       1          2      <<< 2nd child
 2      bbb       0          2      <<< main 
 5      eee       2          1      <<< child

我一直在考虑使用递归SQL选择或公用表表达式(CTE),但还没有弄明白。

任何人都可以帮我指出正确的方向吗?

(使用SQL Server 2005 / ASP.Net C#)

编辑: 我应该澄清一点,我只需要它到两个级别 - 即父级和小级,所以它不需要无限期地重新诅咒,所以我认为答案比我看到的要简单得多。

6 个答案:

答案 0 :(得分:2)

是否需要递归?这似乎给出了正确的结果:

declare @t table 
(DBID           int
,Title          varchar(10)
,ParentID       int
,OrderNum       int
)

insert @t
      select 1,'aaa',0,1
union select 2,'bbb',0,2
union select 3,'ccc',1,1
union select 4,'ddd',1,2
union select 5,'eee',2,1

select * 
from @t
order by ISNULL(NULLIF(ParentID,0),DBID)
        ,ParentID
        ,OrderNum
        ,DBID

修改

解释:

由于输入行与输出行对应1:1,而且只有行顺序发生变化,因此必须是排序问题。解决方案是识别每一行所属的父子组,然后按第一个排序,然后排序所有其他值。

执行此操作的唯一困难部分是父行(ParentID = 0)从其DBID隐式获取其组成员资格,而所有其他行从ParentID获取它

第一个WHERE条件(ISNULL(NULLIF(ParentID,0),DBID))处理此问题。它也可以写成

CASE WHEN ParentID = 0
     THEN DBID
     ELSE ParentID
END

我使用NULLIF...ISNULL因为它更简洁,但如果排序条件更复杂,我会使用CASE...WHEN

答案 1 :(得分:1)

几年前,我发现最简单的方法(在打猎之后)是在表格中添加“深度”和“路径”列:

DBID | Title | ParentID | OrderNum | Path  | Depth
 0      Root     null        1       /         0
 1      aaa       0          1       /1/       1
 2      bbb       0          2       /2/       1
 3      ccc       1          1       /1/3/     2
 4      ddd       1          2       /1/4/     2
 5      eee       2          1       /2/5/     2

然后我在表上有一对触发器,以便在插入和更新时自动更新这些触发器:

插入触发器

ALTER TRIGGER [dbo].[SiteMap_InsertTrigger] 
  ON  [dbo].[SiteMap] 
  AFTER INSERT
AS 
BEGIN
  -- SET NOCOUNT ON added to prevent extra result sets from
  -- interfering with SELECT statements.
  SET NOCOUNT ON;

  UPDATE child
    -- set the depth of this "child" to be the
    -- depth of the parent, plus one.
    SET Depth = ISNULL(parent.depth + 1, 0),
      -- the lineage is simply the lineage of the parent,
      -- plus the child's ID (and appropriate '/' characters
      Path = ISNULL(parent.Path, '/') + LTrim(child.DBID) + '/'

    -- we can't update the "inserted" table directly,
    -- so we find the corresponding child in the
    -- "real" table
    FROM SiteMap child INNER JOIN inserted i ON i.Id = child.DBID
      -- now, we attempt to find the parent of this
      -- "child" - but it might not exist, so these
      -- values may well be NULL
      LEFT OUTER JOIN SiteMap parent ON child.ParentId = parent.DBID
END

更新触发器

ALTER TRIGGER [dbo].[SiteMap_UpdateTrigger] 
  ON  [dbo].[SiteMap] 
  AFTER UPDATE
AS 
BEGIN
  -- SET NOCOUNT ON added to prevent extra result sets from
  -- interfering with SELECT statements.
  SET NOCOUNT ON;

  -- if we've modified the parentId, then we
  -- need to do some calculations
  IF UPDATE (ParentId)
  BEGIN
    UPDATE child
      /*
      to calculate the correct depth of a node, remember that
        - old.depth is the depth of its old parent
        - child.depth is the original depth of the node
          we're looking at before a parent node moved.
          note that this is not necessarily old.depth + 1,
          as we are looking at all depths below the modified
          node
      The depth of the node relative to the old parent is
      (child.depth - old.depth), then we simply add this to the
      depth of the new parent, plus one.
      */
      SET Depth = child.Depth - old.Depth + ISNULL(parent.Depth + 1,0),
        Path = ISNULL(parent.Path,'/') + LTrim(old.DBID) + '/' +
        right(child.Path, len(child.Path) - len(old.Path))
        -- if the parentId has been changed for some row
        -- in the "inserted" table, we need to update the
        -- fields in all children of that node, and the
        -- node itself                 
      FROM SiteMap child
      INNER JOIN inserted old ON child.Path LIKE old.Path + '%'
        -- as with the insert trigger, attempt to find the parent
        -- of the updated row
      LEFT OUTER JOIN SiteMap parent ON old.ParentId=parent.DBID
  END;
END;

通过搜索“路径”或“深度”,可以更快地跳转到特定类别并快速拉出所有子项和子项。

有趣的是,我刚刚发现SQL Server 2008包含一个新类型:HierarchyId,它基本上将Path列转换为紧凑类型。

答案 2 :(得分:0)

是的,您可以使用递归sql或cte。

来实现此目的

我认为这个解决方案可以帮助您解决这个分层表选择。

http://geekswithblogs.net/dotNETPlayground/archive/2007/12/28/118017.aspx

答案 3 :(得分:0)

您可以使用nested-set model。这允许在一个SQL语句中提取整个树。缺点是写操作变得更复杂。

或者你可以写一个递归存储过程。

答案 4 :(得分:0)

这是一种递归方法。使用CTE。

with cte
as
(
select *, 1 as [level], [DBID] as [father]
from @t
where parentID=0
union all
select t.*, [level]+1, cte.[father]
from @t t
inner join cte on cte.DBID=t.parentID
)
select  [DBID],Title, ParentID,  OrderNum  
from cte
order by [father], [level],  OrderNum  

答案 5 :(得分:0)

接受的答案是现场。这里也是linq:

var orderedItems = from item in dataContext.items
                   orderby (item.ParentID == 0 ? item.DBID : item.ParentID),
                       item.ParentID , item.OrderNum, item.DBID
                   select item;