从父子表生成字符串树分支

时间:2015-12-22 09:29:01

标签: sql sql-server tsql common-table-expression

我有一个如下的父子表:

child   |   father
H       :   G
F       :   G
G       :   D
E       :   D
A       :   E
B       :   C
C       :   E

我希望sql server能够生成类似这样的内容(正如在{问题Convert a series of parent-child relationships into a hierarchical tree?中提到的那样,但是在tsql而不是在php中):

 D
 ├── E
 │   ├── C
 │   │   └── B
 │   └── A   
 └── G
     ├── F
     └── H

当然结果可以是我可以在文本编辑器中复制的字符串列。

我还希望有第二个查询生成如下内容:

 father |   descendants
 D      |   D -> E -> C -> B
 D      |   D -> E -> A
 D      |   D -> G -> F
 D      |   D -> G -> H

在前一种情况下,只有一棵树有一个父亲,但在表格中可能会有更多的父亲有多个父亲,如果D不存在,这棵树也会如此。

如果requast的第一部分(伪视觉树)不可能做到这一点很好。重要的是表格。

我已经尝试做了类似的事情,因为我无法获得想要的结果。

TNX

2 个答案:

答案 0 :(得分:3)

这很有趣。但是,在SQL而不是其他语言中执行此操作可能效率低下。想想还是很有趣。

这是我的表现。

初始化表格:

SET NOCOUNT ON
DECLARE @Table TABLE ([Child] NVARCHAR(10), [Parent] NVARCHAR(10))
INSERT @Table VALUES ('H','G'),('F','G'),('G','D'),('E','D')
,('A','E'),('B','C'),('C','E'),('D', NULL),('Z','E'),('X','Z'),('Y','Z')
,('L',NULL),('M','L'),('N','L'),('P','N'),('Q','L'), ('R',NULL),('S', 'R')
IF OBJECT_ID('tempdb..#tmptable') IS NOT NULL DROP TABLE #tmptable
; WITH T AS (
    SELECT Parent, Child, 1 [Level]
    FROM @Table
    WHERE Parent IS NULL
    UNION ALL
    SELECT a.Parent, a.Child, T.[Level] + 1
    FROM @Table a
    JOIN T ON a.Parent = T.Child)
SELECT *
INTO #tmptable
FROM T

对于查询1,我假设您不知道任何父母可能拥有的最大后代数量,我使用动态SQL:

DECLARE @SQL NVARCHAR(MAX)
DECLARE @a INT = (SELECT MAX(Level) FROM #tmptable)
DECLARE @b INT = 2
SET @SQL = 
'; WITH CTE AS (
    SELECT T1.Child Father'
WHILE @b<= @a BEGIN
    SET @SQL += '
        , ISNULL(T' + CONVERT(NVARCHAR, @b) + '.Child, '''') Child' + CONVERT(NVARCHAR, @b - 1)
    SET @b += 1
END
SET @SQL +='
        , ROW_NUMBER() OVER (ORDER BY T1.Child'
SET @b =  2 
WHILE @b <= @a BEGIN        
    SET @SQL += ', T' + CONVERT(NVARCHAR, @b) + '.Child'
    SET @b += 1
END
SET @SQL += ') RN
    FROM #tmptable T1'
SET @b = 2
WHILE @b <= @a BEGIN
    SET @SQL += '
    LEFT JOIN #tmptable T' + CONVERT(NVARCHAR, @b) + ' ON T' + CONVERT(NVARCHAR, @b) +'.Parent = T' + CONVERT(NVARCHAR, @b - 1) + '.Child'
    SET @b += 1
END
SET @SQL += '
    WHERE T1.Parent IS NULL
    GROUP BY T1.Child'
SET @b = 2
WHILE @b <= @a BEGIN
    SET @SQL += ', T' + CONVERT(NVARCHAR, @b) + '.Child'
    SET @b += 1
END
SET @SQL += ')
SELECT ''<ul>'' + REPLACE(REPLACE(CONVERT(NVARCHAR(MAX), (
    SELECT CASE WHEN RN = 1 THEN ''<li>''
            WHEN (SELECT Father FROM CTE WHERE RN = C.RN - 1) <> Father THEN ''<li>''
            ELSE '''' END --Fatherli
        , CASE WHEN RN = 1 THEN Father
            WHEN (SELECT Father FROM CTE WHERE RN = C.RN - 1) <> Father THEN Father
            ELSE '''' END --Father
        , CASE WHEN RN = 1 THEN ''</li>''
            WHEN (SELECT Father FROM CTE WHERE RN = C.RN - 1) <> Father THEN ''</li>''
            ELSE '''' END --Fathercli
        , CASE WHEN RN = 1 AND Child1 <> '''' THEN ''<ul>''
            WHEN (SELECT Father FROM CTE WHERE RN = C.RN - 1) <> Father AND Child1 <> '''' THEN ''<ul>''
            ELSE '''' END --Fatherul'
SET @b = 2
WHILE @b <= @a BEGIN
    SET @SQL += '
        , CASE WHEN RN = 1 AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' THEN ''<li>''
            WHEN (SELECT Child' + CONVERT(NVARCHAR, @b-1) + ' FROM CTE WHERE RN = C.RN - 1) <> Child' + CONVERT(NVARCHAR, @b-1) + ' AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' THEN ''<li>''
            ELSE '''' END --Child' + CONVERT(NVARCHAR, @b-1) + 'li
        , CASE WHEN RN = 1 AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' THEN Child' + CONVERT(NVARCHAR, @b-1) + '
            WHEN (SELECT Child' + CONVERT(NVARCHAR, @b-1) + ' FROM CTE WHERE RN = C.RN - 1) <> Child' + CONVERT(NVARCHAR, @b-1) + ' AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' THEN Child' + CONVERT(NVARCHAR, @b-1) + '
            ELSE '''' END --Child' + CONVERT(NVARCHAR, @b-1) + '
        , CASE WHEN RN = 1 AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' THEN ''</li>''
            WHEN (SELECT Child' + CONVERT(NVARCHAR, @b-1) + ' FROM CTE WHERE RN = C.RN - 1) <> Child' + CONVERT(NVARCHAR, @b-1) + ' AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' THEN ''</li>''
            ELSE '''' END --Child' + CONVERT(NVARCHAR, @b-1) + 'cli'
    IF @a <> @b 
        SET @SQL += '
        , CASE WHEN RN = 1 AND Child' + CONVERT(NVARCHAR, @b-1) + ' <> '''' AND Child' + CONVERT(NVARCHAR, @b) + ' <> '''' THEN ''<ul>''
            WHEN (SELECT Child' + CONVERT(NVARCHAR, @b-1) + ' FROM CTE WHERE RN = C.RN - 1) <> Child' + CONVERT(NVARCHAR, @b-1) + ' AND Child' + CONVERT(NVARCHAR, @b) + ' <> '''' THEN ''<ul>''
            ELSE '''' END --Child' + CONVERT(NVARCHAR, @b-1) + 'ul'
    SET @b += 1
END
SET @b -= 3
WHILE @b > 0 BEGIN
    SET @SQL += '
        , CASE WHEN RN = (SELECT MAX(RN) FROM CTE) AND Child' + CONVERT(NVARCHAR, @b+1) + ' <> '''' THEN ''</ul>''
            WHEN (SELECT Child' + CONVERT(NVARCHAR, @b) + ' FROM CTE WHERE RN = C.RN + 1) <> Child' + CONVERT(NVARCHAR, @b) + ' AND Child' + CONVERT(NVARCHAR, @b+1) + ' <> '''' THEN ''</ul>''
            ELSE '''' END --Child' + CONVERT(NVARCHAR, @b) + 'cul'
    SET @b -= 1
END
SET @SQL += '
        , CASE WHEN RN = (SELECT MAX(RN) FROM CTE) AND Child1 <> '''' THEN ''</ul>''
            WHEN (SELECT Father FROM CTE WHERE RN = C.RN + 1) <> Father AND Child1 <> '''' THEN ''</ul>''
            ELSE '''' END --Fathercul
    FROM CTE C
    FOR XML PATH (''''))), ''&lt;'', ''<''), ''&gt;'', ''>'') + ''</ul>'''
EXEC(@SQL)
-- PRINT @SQL

输出(对于我输入的值)是<ul><li>D</li><ul><li>E</li><ul><li>A</li><li>C</li><ul><li>B</li></ul><li>Z</li><ul><li>X</li><li>Y</li></ul></ul><li>G</li><ul><li>F</li><li>H</li></ul></ul><li>L</li><ul><li>M</li><li>N</li><ul><li>P</li></ul><li>Q</li></ul><li>R</li><ul><li>S</li></ul></ul>,显示如下:

  • d
    • 电子
      • A
      • C
      • ž
        • X
        • ý
      < UL>
    • ˚F
    • ħ
    • 中号
    • 名词< / LI>
      • P
    • Q
  • - [R
    • 取值
    • < / UL>

    对于第二个查询,可能有更简单的方法,但我想为什么不使用更动态的SQL?

    DECLARE @i INT = (SELECT MAX([Level]) FROM #tmptable), @j INT = 2
    DECLARE @SQL2 NVARCHAR(MAX)
    SET @SQL2 = 'SELECT T1.Child Father, T1.Child '
    WHILE @j <= @i BEGIN
        SET @SQL2 += '+ ISNULL('' -> '' + T' + CONVERT(NVARCHAR, @j) + '.Child, '''')'
        SET @j += 1
    END
    SET @j = 2
    SET @SQL2 += ' Descendants FROM #tmptable T1'
    WHILE @j <= @i BEGIN
        SET @SQL2 += ' LEFT JOIN #tmptable T' + CONVERT(NVARCHAR, @j) + ' ON T' + CONVERT(NVARCHAR, @j) + '.[Parent] = T' + CONVERT(NVARCHAR, @j-1) + '.[Child]'
        SET @j += 1
    END
    SET @j = 2
    SET @SQL2 += ' WHERE T1.[Parent] IS NULL ORDER BY T1.[Child]'
    WHILE @j <= @i BEGIN
        SET @SQL2 += ', T' + CONVERT(NVARCHAR, @j) + '.[Child]'
        SET @j += 1
    END
    EXEC(@SQL2)
    

    答案 1 :(得分:2)

    为了避免扫描整个表格以获得最大的祖先,我会添加father = NULL行或任何可以指示它的行。

    要获取从祖先到叶子的路径,您可以使用CTE:

    CREATE TABLE #tab(
       child  VARCHAR(8) NOT NULL PRIMARY KEY
      ,father VARCHAR(4) NULL
    );
    INSERT INTO #tab(child,father) VALUES ('H','G');
    INSERT INTO #tab(child,father) VALUES ('F','G');
    INSERT INTO #tab(child,father) VALUES ('G','D');
    INSERT INTO #tab(child,father) VALUES ('E','D');
    INSERT INTO #tab(child,father) VALUES ('A','E');
    INSERT INTO #tab(child,father) VALUES ('B','C');
    INSERT INTO #tab(child,father) VALUES ('C','E');
    
    INSERT INTO #tab(child,father) VALUES ('D',NULL);  -- add by me
    
    INSERT INTO #tab(child,father) VALUES ('Z',NULL);  -- for testing
    INSERT INTO #tab(child,father) VALUES ('Z1','Z');
    INSERT INTO #tab(child,father) VALUES ('Z2','Z');
    

    查询:

    ;WITH cte AS
    (
      SELECT 
           Child
          ,Father
          ,Level = 1
          ,Path = CAST(Child AS NVARCHAR(MAX))
          ,Ancestor = Child
      FROM #tab
      WHERE father IS NULL
      UNION ALL 
      SELECT
           t.Child
          ,t.Father
          ,Level = Level + 1
          ,Path = c.Path + ' -> ' + t.Child
          ,c.Ancestor
      FROM #tab t
      JOIN cte c
        ON t.Father = c.Child
    )
    SELECT Father = c.Ancestor
           ,c.Path
    FROM cte c
    LEFT JOIN cte c2
      ON c2.Path LIKE c.Path + ' -> ' +  '%'
    WHERE c2.Path IS NULL;
    

    LiveDemo

    这是递归CTE的经典用法,添加了两个字段:从祖先到叶子的PathAncestor。然后,您需要自我加入结果并使用LIKE根据Path过滤出仅仅叶子节点。