自加入数据到分层XML

时间:2014-02-25 00:33:38

标签: sql-server xml recursive-query

我在SQL Server 2012中有一个表,其中包含以下自加数据:

 ID   | ChildA_ID | ChildB_ID
(int) |   (int)   |   (int)
-----------------------------
 1    |           |     
 2    | 1         |     
 3    | 1         |     
 4    | 3         |     
 6    | 2         | 4
 7    | 6         |     

使用T-SQL(或者,压缩,CLR代码),我想把它变成看起来像这样的XML:

<row>
  <ID>7</ID>
  <ChildA>
    <row>
      <ID>6</ID>
      <ChildA>
        <row>
          <ID>2</ID>
          <ChildA>
            <row>
              <ID>1</ID>
            </row>
          </ChildA>
        </row>
      </ChildA>
      <ChildB>
        <row>
          <ID>4</ID>
          <ChildA>
            <row>
              <ID>3</ID>
              <ChildA>
                <row>
                  <ID>1</ID>
                </row>
              </ChildA>
            </row>
          </ChildA>
        </row>
      </ChildB>
    </row>
  </ChildA>
</row>

我对SQL Server中的递归CTE和FOR XML子句有相当多的经验,但这个让我很好并且真正难倒。 (我不打算说我只是拉扯哑巴的可能性。)

欢迎提出任何建议,包括构建数据的其他方法,以达到预期的效果。 (但请注意,“ChildA”和“ChildB”代表父子关系的不同类型,而不是非规范化。)

编辑:

当使用我的样本数据尝试Mitch的解决方案时,我遇到了一些重复的行:

<row>
  <ID>7</ID>
  <ChildA>
    <row>
      <ID>6</ID>
      <ChildA>
        <row>
          <ID>2</ID>
          <ChildA>
            <row>
              <ID>1</ID>
            </row>
          </ChildA>
        </row>
<!--This is the first duplicate row:-->
        <row>
          <ID>2</ID>
          <ChildA>
            <row>
              <ID>1</ID>
            </row>
          </ChildA>
        </row>
      </ChildA>
      <ChildB>
        <row>
          <ID>4</ID>
          <ChildA>
<!--This is the second "duplicate" row:-->
            <row>
              <ID>3</ID>
            </row>
            <row>
              <ID>3</ID>
              <ChildA>
                <row>
                  <ID>1</ID>
                </row>
              </ChildA>
            </row>
          </ChildA>
        </row>
      </ChildB>
    </row>
  </ChildA>
</row>

编辑2:

以下是我用于生成上述结果的代码(Mitch解决方案的略微修改版本):

IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Nodes') 
    CREATE TABLE Nodes (id int, leftid int, rightid int);

DELETE FROM Nodes

INSERT INTO Nodes
VALUES (1, null, null),
       (2, 1, null),
       (3, 1, null),
       (4, 3, null), 
       (6, 2, 4),
       (7, 6, null);

CREATE TABLE #xml (id int, depth int, data xml);

DECLARE @rootNode int = 7;

WITH cte (cid, depth, child) as 
(
  -- anchor
  SELECT id, 1, leftid
  FROM nodes
  WHERE id = @rootNode and leftid is not null
  UNION ALL 
  SELECT id, 1, rightid
  FROM nodes
  WHERE id = @rootNode and rightid is not null
  UNION ALL 
  -- recursive
  SELECT n.id, depth + 1, leftid
  FROM cte c
  INNER JOIN nodes n on c.child = n.id
  WHERE leftid is not null or (rightid is null and leftid is null)
  UNION ALL 
  SELECT n.id, depth + 1, rightid
  FROM cte c
  INNER JOIN nodes n on c.child = n.id
  WHERE rightid is not null
)
INSERT INTO #xml (id, depth, data)
SELECT DISTINCT cid, depth, null
FROM CTE;

DECLARE @maxDepth int;
SET @maxDepth = (SELECT MAX(depth) FROM #xml)

WHILE @MaxDepth > 0
BEGIN
  UPDATE xu
  SET data = (
    SELECT n.ID, xl.data as ChildA, xr.data as ChildB
    FROM Nodes n
    LEFT OUTER JOIN #xml xl on n.leftid = xl.id
    LEFT OUTER JOIN #xml xr on n.rightid = xr.id
    WHERE xu.id = n.id
    FOR XML PATH('row'), TYPE
  )
  FROM #xml xu
  WHERE xu.Depth = @MaxDepth

  SET @MaxDepth = @MaxDepth - 1
END

SELECT data FROM #xml WHERE id = @RootNode

DROP TABLE #xml

2 个答案:

答案 0 :(得分:2)

您可以通过深度优先搜索来执行这些类型的翻译。使用递归CTE计算深度,然后使用循环转换为XML:

Example SQL (Fiddle):

CREATE TABLE Nodes (id int, leftid int, rightid int, name varchar(256));

INSERT INTO Nodes
VALUES (1, 2, 3, 'abcd'),
       (2, 4, 5, 'ab'),
       (4, null, null, 'a'), 
       (5, null, null, 'b'),
       (3, 6, 7, 'cd'),
       (6, null, null, 'c'),
       (7, null, null, 'd');

CREATE TABLE #xml (id int, depth int, data xml, parent int);

DECLARE @rootNode int = 1;

WITH cte (cid, depth, child, parent) as 
(
  -- anchor
  SELECT id, 1, leftid, cast(null as int)
  FROM nodes
  WHERE id = @rootNode and leftid is not null
  UNION ALL 
  SELECT id, 1, righted, null
  FROM nodes
  WHERE id = @rootNode and rightid is not null
  UNION ALL 
  -- recursive
  SELECT n.id, depth + 1, leftid, c.cid
  FROM cte c
  INNER JOIN nodes n on c.child = n.id
  WHERE leftid is not null or (rightid is null and leftid is null)
  UNION ALL 
  SELECT n.id, depth + 1, rightid, c.cid
  FROM cte c
  INNER JOIN nodes n on c.child = n.id
  WHERE rightid is not null
)
INSERT INTO #xml (id, depth, data, parent)
SELECT DISTINCT cid, depth, null, parent
FROM CTE;

DECLARE @maxDepth int;
SET @maxDepth = (SELECT MAX(depth) FROM #xml)

WHILE @MaxDepth > 0
BEGIN
  UPDATE xu
  SET data = (
    SELECT n.Name, xl.data as [Left], xr.data as [Right]
    FROM Nodes n
    LEFT OUTER JOIN #xml xl on n.leftid = xl.id and xl.parent = n.id
    LEFT OUTER JOIN #xml xr on n.rightid = xr.id and xr.parent = n.id
    WHERE xu.id = n.id
    FOR XML PATH('TreeNode'), TYPE
  )
  FROM #xml xu
  WHERE xu.Depth = @MaxDepth

  SET @MaxDepth = @MaxDepth - 1
END

SELECT data FROM #xml WHERE id = @RootNode

制作:

<TreeNode>
  <Name>abcd</Name>
  <Left>
    <TreeNode>
      <Name>ab</Name>
      <Left>
        <TreeNode>
          <Name>a</Name>
        </TreeNode>
      </Left>
      <Right>
        <TreeNode>
          <Name>b</Name>
        </TreeNode>
      </Right>
    </TreeNode>
  </Left>
  <Right>
    <TreeNode>
      <Name>cd</Name>
      <Left>
        <TreeNode>
          <Name>c</Name>
        </TreeNode>
      </Left>
      <Right>
        <TreeNode>
          <Name>d</Name>
        </TreeNode>
      </Right>
    </TreeNode>
  </Right>
</TreeNode>

答案 1 :(得分:1)

sql可以做很多丑陋的事情;诀窍是将递归移到手工制作的堆栈,因为反复调用的奢侈品在某种程度上是有限的:

 declare @tmp table(id int, ChildA_id int, ChildB_ID int)
 declare @xml xml
 select @xml = ''
 insert into @tmp
 select       1    , NULL      , NULL    
 union select 2    , 1         , NULL    
 union select 3    , 1         , NULL    
 union select 4    , 3         , NULL    
 union select 6    , 2         , 4
 union select 7    , 6         , NULL   

declare @stack table(id int, parent int, relation nvarchar, 
      stackindex int identity(1,1))

insert into @stack 
select max(id), null, 'R' from @tmp

declare @stackindex int, @id int, @relation nvarchar(50), 
      @parent int, @ChildA_id int, @ChildB_id int

while (select count(1) from @stack) > 0
begin
  select @stackindex = max(stackindex) from @stack
  select @id = id, @relation = relation, @parent = parent
  from @stack where stackindex = @stackindex

  if @id is not null
  begin

    select @childa_id = ChildA_id, @childb_id = ChildB_id
    from @tmp where id = @id 

    if @childa_id is not null
    insert into @stack(id,parent,relation)
    values(@childa_id, @id, 'A')

    if @childb_id is not null
    insert into @stack(id,parent,relation)
    values(@childb_id, @id, 'B')  

    if @relation = 'R'
    SET @xml.modify('       
insert <row><ID>{sql:variable("@id")}</ID></row>
as last
into (.)')

    if @relation = 'A'
    SET @xml.modify('       
insert <ChildA><row><ID>{sql:variable("@id")}</ID></row></ChildA>
after (//row/ID[text()=sql:variable("@parent")])[1]')

    if @relation = 'B'
    SET @xml.modify('       
insert <ChildB><row><ID>{sql:variable("@id")}</ID></row></ChildB>
as last
into (//row[ID/text()=sql:variable("@parent")])[1]')
  end

  delete from @stack where stackindex = @stackindex
end

select @xml