【问题标题】:What is the most efficient way to concatenate a string from all parent rows using T-SQL?使用 T-SQL 连接所有父行中的字符串的最有效方法是什么?
【发布时间】:2011-07-14 03:32:58
【问题描述】:

我有一个表,它有一个代表其父行的自引用外键。为了以最简单的形式说明问题,我们将使用下表:

CREATE TABLE Folder(
    id int IDENTITY(1,1) NOT NULL, --PK
    parent_id int NULL,        --FK
    folder_name varchar(255) NOT NULL)

我想创建一个标量值函数,它将文件夹名称及其所有父文件夹名称的串联字符串返回到根文件夹,该根文件夹将由 null parent_id 值指定。

我目前的解决方案是一种我认为并不理想的程序方法。这是我正在做的事情:

CREATE FUNCTION dbo.GetEntireLineage
    (@folderId INT)
    RETURNS VARCHAR(MAX)
AS
BEGIN
    DECLARE @lineage VARCHAR(MAX)
    DECLARE @parentFolderId INT

    SELECT @lineage = folder_name, @parentFolderId = parent_id FROM Folder WHERE id = @folderId

WHILE NOT @parentFolderId IS NULL
    BEGIN
        SET @parentFolderId = (SELECT parent_id FROM Folder WHERE parent_id = @parentFolderId)
        SET @lineage = (SELECT @lineage + '-' + (SELECT folder_name FROM Folder WHERE parent_id = @parentFolderId))
    END
RETURN @lineage
END

有没有更理想的方法来做到这一点?我是一位经验丰富的程序员,但 T-SQL 对我来说并不熟悉,我知道由于基于集合的数据的性质,这些问题通常需要不同的方法。任何帮助寻找解决方案或处理 T-SQL 的任何其他提示和技巧将不胜感激。

【问题讨论】:

    标签: sql sql-server database tsql stored-procedures


    【解决方案1】:

    要确定您需要测试的性能。我已经使用您的版本(稍作修改)和其他人建议的递归 CTE 版本进行了一些测试。

    我在一个文件夹层次结构中使用了包含 2048 行的示例表,因此当将 2048 作为参数传递给函数时,完成了 2048 个连接。

    循环版本:

    create function GetEntireLineage1 (@id int)
    returns varchar(max)
    as
    begin
      declare @ret varchar(max)
    
      select @ret = folder_name,
             @id = parent_id
      from Folder
      where id = @id
    
      while @@rowcount > 0
      begin
        select @ret = @ret + '-' + folder_name,
               @id = parent_id
        from Folder
        where id = @id
      end
      return @ret
    end
    

    统计:

     SQL Server Execution Times:
       CPU time = 125 ms,  elapsed time = 122 ms.
    

    递归 CTE 版本:

    create function GetEntireLineage2(@id int)
    returns varchar(max)
    begin
      declare @ret varchar(max);
    
      with cte(id, name) as
      (
        select f.parent_id,
               cast(f.folder_name as varchar(max))
        from Folder as f
        where f.id = @id
        union all
        select f.parent_id,
               c.name + '-' + f.folder_name
        from Folder as f
          inner join cte as c
            on f.id = c.id
      )
      select @ret = name
      from cte
      where id is null
      option (maxrecursion 0)
    
      return @ret
    end
    

    统计:

     SQL Server Execution Times:
       CPU time = 187 ms,  elapsed time = 183 ms.
    

    所以在这两者之间,循环版本更有效,至少在我的测试数据上是这样。您需要对实际数据进行测试才能确定。

    编辑

    使用 for xml path('') 技巧的递归 CTE。

    create function [dbo].[GetEntireLineage4](@id int)
    returns varchar(max)
    begin
      declare @ret varchar(max) = '';
    
      with cte(id, lvl, name) as
      (
        select f.parent_id,
               1,
               f.folder_name
        from Folder as f
        where f.id = @id
        union all
        select f.parent_id,
               lvl + 1,
               f.folder_name
        from Folder as f
          inner join cte as c
            on f.id = c.id
      )
      select @ret = (select '-'+name
                     from cte
                     order by lvl
                     for xml path(''), type).value('.', 'varchar(max)')
      option (maxrecursion 0)
    
      return stuff(@ret, 1, 1, '')
    end
    

    统计:

     SQL Server Execution Times:
       CPU time = 31 ms,  elapsed time = 37 ms.
    

    【讨论】:

    • 哇,非常感谢您为此付出的努力,我真的很感激。这个地方太棒了。
    • 所以,我在我的实际数据库中实现了这个,似乎字符串连接查询(select @ret = @ret + '-' + name)没有连接所有行的名称。我查询了 cte 中的所有行,并且所有行都按预期存在,只是没有按照串联查询的预期串联。
    • 嗯,我已经缩小了范围,我有点困惑。我会“从 cte order by lvl 中选择 *;”它返回按级别适当排序的所有预期行。当我执行连接查询“select @ret = @ret + '-' + name from cte order by lvl”时,它不会连接所有行,只是连接最后一行。当我省略 order by 子句时,它会适当地连接所有行,我只是不能保证它们的顺序正确。
    • 使用带有 order 子句的 select 聚合字符串连接似乎是一个已知问题。请参阅:stackoverflow.com/questions/982286/…。 Mikael,它似乎在您的解决方案中有效吗?
    • 很高兴您从中获得了一些乐趣。再次感谢您的帮助:)
    【解决方案2】:

    使用recursive query 遍历父母,然后使用this method 连接成一个字符串。

    【讨论】:

      【解决方案3】:

      hierarchyid 通常是多余的,除非你有一个非常深的层次结构或非常大的数据集可以利用索引。在不更改架构的情况下,这是您可以获得的最快速度。

       with recursiveCTE (parent_id,concatenated_name) as (
          select parent_id,folder_name
          from folder
          union all
          select f.parent_id,r.concatenated_name +f.folder_name
          from folder f
          inner join recursiveCTE r on r.parent_id = f.id
          )
          select folder_name from recursiveCTE 
      

      【讨论】:

        【解决方案4】:

        这对你有用:

         with cte (Parent_id, Path) as 
            (
            select Parent_Id,Folder_Name
            from folder
            union all
            select f.Parent_Id,r.Path + '\' + f.Folder_Name
            from Folder as f
            inner join cte as c on c.Parent_Id = f.Id
            )
            select Folder_Name from cte
        

        【讨论】:

          猜你喜欢
          • 2010-11-21
          • 2013-05-17
          • 1970-01-01
          • 2010-09-06
          • 2010-10-25
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多