sql中的惰性向上递归查询

sql中的惰性向上递归查询,sql,sql-server,tsql,recursion,recursive-query,Sql,Sql Server,Tsql,Recursion,Recursive Query,我正在尝试从项的父项的名称生成路径。例如,如果测试具有父级dad,则路径为dad/test;如果爸爸有父母格兰,测试的路径就是格兰/爸爸/测试 我只有孩子的id,到目前为止,我只有一个递归生成每个人的路径的查询,然后选择正确的路径,但这似乎不是很有效 WITH SubItems AS ( SELECT CAST([Name] AS VARCHAR(255)) AS [Path], Id, ParentId, 0 AS Depth

我正在尝试从项的父项的名称生成路径。例如,如果测试具有父级dad,则路径为dad/test;如果爸爸有父母格兰,测试的路径就是格兰/爸爸/测试

我只有孩子的id,到目前为止,我只有一个递归生成每个人的路径的查询,然后选择正确的路径,但这似乎不是很有效

WITH    SubItems
AS (
    SELECT  CAST([Name] AS VARCHAR(255)) AS [Path],
        Id,
        ParentId,
        0 AS Depth
    FROM    Items
    WHERE   Id = 1 -- First parent of everyone
    UNION ALL
    SELECT  CAST(CONCAT(parent.[Path], '/', sub.[Name]) AS VARCHAR(255)),
        sub.Id,
        sub.ParentId,
        parent.Depth + 1
    FROM    Items sub
        JOIN SubItems parent ON parent.Id = sub.ParentId
)
SELECT      [Path]
FROM        SubItems
WHERE       Id = 1425 -- SubItem I want the path of
我也可以往上走,这会更快,但我不能这样创建路径。我可以尝试按深度顺序连接所有结果,但这似乎又不对

DECLARE @Path;
WITH    ParentItems
AS (
    SELECT  [Name],
        Id,
        ParentId,
        0 AS Depth
    FROM    Items
    WHERE   Id = 1425 -- SubItem I want the path of
    UNION ALL
    SELECT  [Name],
        parent.Id,
        parent.ParentId,
        sub.Depth - 1
    FROM    Items parent
        JOIN  ParentItems sub ON sub.ParentId = parent.Id
)
SELECT      @Path = COALESCE(@Path + '/', '') + [Name]
FROM        ParentItems
ORDER BY    Depth;
SELECT @Path;
有没有一种方法可以递归地往上走? 例如,其中ParentPath将等于CONCATParentPath,“/”,[Path]:

我知道在C语言中,您可以执行以下操作:

function getPath() {
  return (parent?.getPath() ?? "") + "/" + this.Name;
}
编辑:为什么我不能构建向上的路径,如下所示:

WITH ParentItems AS (
      SELECT i.Name, i.Id, i.ParentId, 0 AS Depth,
             CONVERT(VARCHAR(MAX), i.Name) as path
      FROM Items i
      WHERE i.Id = 1425 -- SubItem I want the path of
      UNION ALL
      SELECT i.Name, i.Id, i.ParentId, pi.Depth - 1,
             CONCAT(pi.Name, '/', i.[Path])
      FROM Items i JOIN
           ParentItems pi
           ON pi.ParentId = parent.Id
     )
SELECT *
FROM ParentItems
ORDER BY Depth;
假设上面的示例中,gran是要测试的父对象,而dad是要测试的父对象,则此查询的结果将是:

| name | path          |
|------|---------------|
| gran | gran/dad/test |
| dad  | dad/test      |
| test | test          |
而事实恰恰相反:

| name | path          |
|------|---------------|
| gran | gran/         |
| dad  | gran/dad      |
| test | gran/dad/test |

这是因为查询向上传递子级名称的方式,将其添加到其父级路径而不是相反的路径。

为什么不能构建向上的路径

WITH ParentItems AS (
      SELECT i.Name, i.Id, i.ParentId, 0 AS Depth,
             CONVERT(VARCHAR(MAX), i.Name) as path
      FROM Items i
      WHERE i.Id = 1425 -- SubItem I want the path of
      UNION ALL
      SELECT i.Name, i.Id, i.ParentId, pi.Depth + 1,
             CONCAT(i.Name, '/', pi.path)
      FROM Items i JOIN
           ParentItems pi
           ON pi.ParentId = parent.Id
     )
SELECT *
FROM ParentItems
ORDER BY Depth DESC;

对于将来的参考,使用For XML PATH似乎是可行的

WITH    ParentItems
AS (
    SELECT  [Name],
        Id,
        ParentId,
        0 AS Depth
    FROM    Items
    WHERE   Id = 1425 -- SubItem I want the path of
    UNION ALL
    SELECT  [Name],
        parent.Id,
        parent.ParentId,
        sub.Depth - 1
    FROM    Items parent
        JOIN  ParentItems sub ON sub.ParentId = parent.Id
)
SELECT (
    SELECT '/' + [Name]
    FROM ParentItems
    ORDER BY Depth
    FOR XML PATH('')
)
以下代码:

组装路径时,将树从孩子一直走到最年长的祖先。 获取最早祖先的路径并将其拆分为多个个体。 在组合路径时,将个人列表从最早的祖先返回到起始子代。 注意:这段代码没有使用,因为它是有文档记录的:输出行可能是任意顺序的。顺序不能保证与输入字符串中子字符串的顺序匹配。使用了确保结果顺序的

请注意,您可以选择任何中间CTE的结果,以查看过程如何进行。只需将最终的select语句替换为注释中提供的备选语句之一

坦白:我没有试图在期望输出gran/的第一行生成奇怪的悬空索里达,而不是更一致的gran。假设样本数据中存在印刷错误

-- Sample data.
declare @Samples as Table ( Id Int Identity, Name VarChar(10), ParentName VarChar(10) );
insert into @Samples ( Name, ParentName ) values
  ( 'test', 'dad' ),
  ( 'dad', 'gran' ),
  ( 'gran', null );
select * from @Samples;

-- Starting point.
declare @ChildName as VarChar(10) = 'test';

-- Walk the tree.
with
  Tree as (
    -- Note that paths in this initial tree are built using   Id , not   Name .
    --   This keeps the path length down, ensures rows are uniquely identified, avoids problems with "funny" names, ... .
    -- Start at the target child name.
    select Id, Name, ParentName, 0 as Depth,
      Cast( Id as VarChar(100) ) as Path
      from @Samples
      where Name = @ChildName
    union all
    -- Walk up the tree one level at a time.
    select S.Id, S.Name, S.ParentName, T.Depth + 1,
      Cast( Cast( S.Id as VarChar(100) ) + '/' + T.Path as VarChar(100) )
      from Tree as T inner join
        @Samples as S on S.Name = T.ParentName
    ),
  TreePath as (
    -- Take the path of the oldest ancestor and split it apart.
    select ItemNumber, Cast( Item as Int ) as Item from Tree as T cross apply
      dbo.DelimitedSplit8K( T.Path, '/' ) where T.ParentName is NULL ),
  InvertedTree as (
    -- Start at the first item on path, i.e. the oldest ancestor.
    select S.Name, 1 as Depth,
      Cast( S.Name as VarChar(100) ) as Path
      from TreePath as TP inner join
        @Samples as S on S.Id = TP.Item
      where TP.ItemNumber = 1
    union all
    -- Add chldren on the way down.
    select S.Name, IT.Depth + 1,
      Cast( IT.Path + '/' + S.Name as VarChar(100) )
      from InvertedTree as IT inner join
        TreePath as TP on TP.ItemNumber = IT.Depth + 1 inner join
        @Samples as S on S.Id = TP.Item
      )
  -- To see the intermediate results use one of the following   select   statements:
  --  select * from Tree;
  --  select * from TreePath;
  --  select * from InvertedTree;
  select Name, Path
    from InvertedTree
    order by Depth;
委员会:


我确实试过了。你的例子会给我测试/爸爸/奶奶。如果我移动命令,它会给我gran/dad/test,但作为gran的路径,test作为test的路径。@Halhex-我不理解你最后的反对意见。你能解释得更详细一点吗?@BenThul我将对问题进行编辑以保持清楚
-- Sample data.
declare @Samples as Table ( Id Int Identity, Name VarChar(10), ParentName VarChar(10) );
insert into @Samples ( Name, ParentName ) values
  ( 'test', 'dad' ),
  ( 'dad', 'gran' ),
  ( 'gran', null );
select * from @Samples;

-- Starting point.
declare @ChildName as VarChar(10) = 'test';

-- Walk the tree.
with
  Tree as (
    -- Note that paths in this initial tree are built using   Id , not   Name .
    --   This keeps the path length down, ensures rows are uniquely identified, avoids problems with "funny" names, ... .
    -- Start at the target child name.
    select Id, Name, ParentName, 0 as Depth,
      Cast( Id as VarChar(100) ) as Path
      from @Samples
      where Name = @ChildName
    union all
    -- Walk up the tree one level at a time.
    select S.Id, S.Name, S.ParentName, T.Depth + 1,
      Cast( Cast( S.Id as VarChar(100) ) + '/' + T.Path as VarChar(100) )
      from Tree as T inner join
        @Samples as S on S.Name = T.ParentName
    ),
  TreePath as (
    -- Take the path of the oldest ancestor and split it apart.
    select ItemNumber, Cast( Item as Int ) as Item from Tree as T cross apply
      dbo.DelimitedSplit8K( T.Path, '/' ) where T.ParentName is NULL ),
  InvertedTree as (
    -- Start at the first item on path, i.e. the oldest ancestor.
    select S.Name, 1 as Depth,
      Cast( S.Name as VarChar(100) ) as Path
      from TreePath as TP inner join
        @Samples as S on S.Id = TP.Item
      where TP.ItemNumber = 1
    union all
    -- Add chldren on the way down.
    select S.Name, IT.Depth + 1,
      Cast( IT.Path + '/' + S.Name as VarChar(100) )
      from InvertedTree as IT inner join
        TreePath as TP on TP.ItemNumber = IT.Depth + 1 inner join
        @Samples as S on S.Id = TP.Item
      )
  -- To see the intermediate results use one of the following   select   statements:
  --  select * from Tree;
  --  select * from TreePath;
  --  select * from InvertedTree;
  select Name, Path
    from InvertedTree
    order by Depth;
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
        (@pString VARCHAR(8000), @pDelimiter VARCHAR(16))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE!  IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  WITH E1(N) AS (
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
                ),                          --10E+1 or 10 rows
       E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
 cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
                ),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
                 SELECT 1 UNION ALL
                 SELECT t.N+ Len( @pDelimiter ) FROM cteTally t WHERE SUBSTRING(@pString,t.N, Len( @pDelimiter ) ) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1 ,8000)
                   FROM cteStart s
                )
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
 SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l
;