When you have to save hierarchical data inside a database you can traverse the tree with recursion and for each note call the database to save it. But that is not the most elegant solution; because your tree might be quite big and you can end up with lots of calls to the database. You may also need to wrap this whole thing inside a transaction but as we all know stretching a transaction between several calls is not the best approach.

What we actually want is to transfer the hierarchical tree inside a single stored procedure and process it there. But how can you pass a hierarchical data to a single stored procedure? Well the answer as often is XML. Let’s assume you have this hierarchical XML:

DECLARE @xml XML

SET @xml = '
<Root>
    <Folder Name="/">
        <Folder Name="One">
            <Folder Name="One_One" />
        </Folder>
        <Folder Name="Two">
            <Folder Name="Two_One">
                <Folder Name="Two_One_One">
                    <Folder Name="Two_One_One_One" />
                </Folder>
                <Folder Name="Two_One_Two" />
            </Folder>
        </Folder>
    </Folder>
</Root>';

Then in order to traverse it we have to combine common table expressions with CROSS APPLY call to the current child node. This may look as follow:

WITH CTE_ITEMS (Folders, [Name], [Path], Level)
AS
(
    SELECT 
        VIRT.node.query('./*') as [Folders],
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name],
        CAST('/' as nvarchar(max)) as [Path],
        0 as [Level]
        FROM @xml.nodes('/Root/Folder') as VIRT(node)
    UNION ALL
    SELECT 
        VIRT.node.query('./*') as [Folders], 
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name],
        [Path] + 
            CASE [Path] WHEN '/' THEN '' ELSE + '/' END + 
            VIRT.node.value('@Name', 'nvarchar(max)') as [Path],
        Level + 1 as [Level]
        FROM 
    CTE_ITEMS CROSS APPLY Folders.nodes('./Folder') as VIRT(node)
)
SELECT [Name],[Path],[Level] FROM CTE_ITEMS ORDER BY [Path]

And finally the result gives us all we need to traverse the tree:

Name Path Level
/ / 0
One /One 1
One_One /One/One_One 2
Two /Two 1
Two_One /Two/Two_One 2
Two_One_One /Two/Two_One/Two_One_One 3
Two_One_One_One /Two/Two_One/Two_One_One/Two_One_One_One 4
Two_One_Two /Two/Two_One/Two_One_Two 3

Share this post:   digg     Stumble Upon     del.icio.us     E-mail

enjio
Posted on 6/6/2008 1:57:09 PM

Is there a way to see the Parent Name in the list?

Vladimir Bodurov
Posted on 6/6/2008 2:05:45 PM

with the current query it won't be accessible with XPath because the recursive call does not include the parent node but you can get it from the path as:

WITH CTE_ITEMS ([Parent], Folders, [Name], [Path], Level)   
AS   
(   
    SELECT    
        CAST(NULL as nvarchar(max)) as [ParentName],   
        VIRT.node.query('./*') as [Folders],   
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name],   
        CAST('/' as nvarchar(max)) as [Path],   
        0 as [Level]   
        FROM @xml.nodes('/Root/Folder') as VIRT(node)   
    UNION ALL   
    SELECT   
        REVERSE(SUBSTRING( REVERSE([Path]) ,0 , CHARINDEX( '/', REVERSE([Path])) + 1 )) as [ParentName],    
        VIRT.node.query('./*') as [Folders],    
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name],   
        [Path] +    
            CASE [Path] WHEN '/' THEN '' ELSE + '/' END +    
            VIRT.node.value('@Name', 'nvarchar(max)') as [Path],   
        Level + 1 as [Level]   
        FROM    
    CTE_ITEMS CROSS APPLY Folders.nodes('./Folder') as VIRT(node)   
)   
SELECT [Parent],[Name],[Path],[Level] FROM CTE_ITEMS ORDER BY [Path]   
Kiran
Posted on 11/28/2008 12:08:36 AM

It was really a nice one. But, here in my case I need an xml to be generated from a table which had parent/child relation at multiple levels. That means exactly opposite to the example you have given.will it be possible to do in T-SQL.

Karsten Strobel
Posted on 3/23/2009 2:54:28 PM

Hi. Here's my approach...

WITH CTE_ITEMS ([HId], [Folders], [Name], [Path]) 
AS 
( 
    SELECT  
        hierarchyid::GetRoot() as [HId], 
        VIRT.node.query('./*') as [Folders], 
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name], 
        CAST('/' as nvarchar(max)) as [Path] 
        FROM @xml.nodes('/Root/*') as VIRT(node) 
    UNION ALL 
    SELECT  
        hierarchyid::Parse([HId].ToString() + VIRT.node.value('1+count(for $a in . return $a/../*[. << $a])', 'varchar(10)') + '/'), 
        VIRT.node.query('./*'),  
        VIRT.node.value('@Name', 'nvarchar(500)'), 
        [Path] +  
            CASE [Path] WHEN '/' THEN '' ELSE + '/' END +  
            VIRT.node.value('@Name', 'nvarchar(max)') 
    FROM  
    CTE_ITEMS CROSS APPLY Folders.nodes('./*') as VIRT(node) 
) 
SELECT [HId].ToString(),[Name],[Path],[HId].GetLevel() FROM CTE_ITEMS 
ORDER BY [HId] 
elhaix
Posted on 12/11/2009 12:11:38 AM

This is so elegant!

Given:

	SET @TestData = 
'<Receipt xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ReceiptID="0" UserID="0" UserCardID="0" PaymentMethodID="1" MerchantID="0" MerchantTaxID="MERCHANT_TAX_ID" MerchantReceiptID="MERCHANT_RECEIPT_ID" MerchantReceiptReferenceID="MERCHANT_RECEIPT_REF_ID" ReceiptTypeID="0" TransactionTypeID="2" MerchantReceiptDate="2009-12-10T18:01:14.2101141-07:00" Tax1PerCent="0" Tax2PerCent="0" Tax3PerCent="0" Tax1Total="0" Tax2Total="0" Tax3Total="0" TotalTax="5" Subtotal="100" ReceiptTotal="105" DateAdded="2009-12-10T18:01:14.2101141-07:00" MerchantStore="MERCHANT_STORE_NAME" StoreAddress1="228127 Long Street" StoreAddress2="" StoreCity="Los Angeles" StoreState="California" StoreZip="90212" StoreCountry="USA" StorePhone1="310-444-3333" StorePhone2="" StoreFax="310-333-2222" ReceiptHeader1="Test Receipt Header 1" ReceiptHeader2="Header 2" ReceiptHeader3="Header 3" ReceiptFooter1="Test Receipt Footer 1" ReceiptFooter2="Footer 2" ReceiptFooter3="Footer 3" ReceiptCreditCost="1" UserPaidForReceipt="false"> 
  <Errors /> 
  <ReceiptItem LineItemID="0" UserID="0" ReceiptID="0" MerchantItemID="111xxxTEST_ITEM_1" LineItemTypeID="1" ItemDesc1="Item 1 - Desc1: This is a test item purchased on a test receipt, line 1" ItemDesc2="Item 1 - Desc2: Item description, line 2" ItemDesc3="Item 1 - Desc3: Item description, line 3" Quantity="1" PricePerItem="50" LineItemTotal="50" DateAdded="2009-12-10T18:01:14.2101141-07:00"> 
    <Errors /> 
    <LineItemType LineItemTypeID="1" LineItemType="Purchase"> 
      <Errors /> 
    </LineItemType> 
  </ReceiptItem> 
. 
. 
. 

I was surprised at how simple it was there-after. Didn't really even have to go through specifying paths!

SET @UserID = 9 
SET @ReceiptID = 111222 
 
;WITH CTE_ITEMS (Receipt, MerchantItemID, LineItemTypeID, 
				ItemDesc1, ItemDesc2, ItemDec3, ItemNumber, Quantity, 
				PricePerItem, LineItemTotal, DateAdded) 
AS 
( 
	-- we're only interested in the receipt line items 
	-- AS we've already retrieved the receipt top-level items 
    SELECT  
        VIRT.node.query('./*') AS Receipt, 
        --@UserID AS UserID, 
        --@ReceiptID AS REceipt, 
        VIRT.node.value('@MerchantItemID', 'varchar(50)') AS MerchantItemID, 
        VIRT.node.value('@LineItemTypeID', 'int') AS LineItemTypeID, 
        VIRT.node.value('@ItemDesc1', 'varchar(128)') AS ItemDesc1, 
        VIRT.node.value('@ItemDesc2', 'varchar(128)') AS ItemDesc2, 
        VIRT.node.value('@ItemDesc3', 'varchar(128)') AS ItemDesc3, 
        VIRT.node.value('@ItemNumber', 'varchar(128)') AS ItemNumber, 
        VIRT.node.value('@Quantity', 'int') AS Quantity, 
        VIRT.node.value('@PricePerItem', 'decimal(18,2)') AS PricePerItem, 
        VIRT.node.value('@LineItemTotal', 'decimal(18,2)') AS LineItemTotal, 
        VIRT.node.value('@DateAdded', 'varchar(128)') AS DateAded 
    FROM @TestData.nodes('/Receipt/ReceiptItem') AS VIRT(node) 
     
    UNION ALL 
     
    SELECT  
        VIRT.node.query('./*') AS Receipt, 
        --@UserID AS UserID, 
        --@ReceiptID AS REceipt, 
        VIRT.node.value('@MerchantItemID', 'varchar(50)') AS MerchantItemID, 
        VIRT.node.value('@LineItemTypeID', 'int') AS LineItemTypeID, 
        VIRT.node.value('@ItemDesc1', 'varchar(128)') AS ItemDesc1, 
        VIRT.node.value('@ItemDesc2', 'varchar(128)') AS ItemDesc2, 
        VIRT.node.value('@ItemDesc3', 'varchar(128)') AS ItemDesc3, 
        VIRT.node.value('@ItemNumber', 'varchar(128)') AS ItemNumber, 
        VIRT.node.value('@Quantity', 'int') AS Quantity, 
        VIRT.node.value('@PricePerItem', 'decimal(18,2)') AS PricePerItem, 
        VIRT.node.value('@LineItemTotal', 'decimal(18,2)') AS LineItemTotal, 
        VIRT.node.value('@DateAdded', 'varchar(128)') AS DateAded 
    FROM CTE_ITEMS CROSS APPLY Receipt.nodes('./ReceiptItem') AS VIRT(node) 
) 
SELECT @UserID AS UserID, @ReceiptID AS ReceiptID, -- Receipt, 
		MerchantItemID, LineItemTypeID, 
				ItemDesc1, ItemDesc2, ItemDec3, ItemNumber, Quantity, 
				PricePerItem, LineItemTotal, DateAdded 
FROM CTE_ITEMS  
[/code 
 
I modified it to traverse my own parent/child XML tree - works like s charm! 
 
Thanks guys! 
website designing
Posted on 1/16/2010 10:10:09 AM

Hy, This is a very informative and interedted site, gud work done by you guys. keep it up. thanks

Designer
Posted on 4/18/2010 7:37:42 AM

Not bad, i'll try to use this code at our project with SOAP-extension.
In our Project, it will be very useful thing!

Thanks

Andreas Moehlenbrock
Posted on 11/8/2010 9:41:57 PM

Thank you all for sharing this, I had similar problem as Karsten, need parentId, but have only SQL Server 2005 available, hierarchyid::GetRoot() works only for SQL Server 2008. So I enhanced the sample shown above as follows:

DECLARE @xml XML 
 
SET @xml = ' 
<Root> 
    <Folder Id="1" Name="/"> 
        <Folder Id="2" Name="One"> 
            <Folder Id="3" Name="One_One" /> 
        </Folder> 
        <Folder Id="4" Name="Two"> 
            <Folder Id="5" Name="Two_One"> 
                <Folder Id="6" Name="Two_One_One"> 
                    <Folder Id="7" Name="Two_One_One_One" /> 
                </Folder> 
                <Folder Id="8" Name="Two_One_Two" /> 
            </Folder> 
        </Folder> 
    </Folder> 
</Root>'; 
WITH CTE_ITEMS ([Parent], [ParentId], Folders, [Id], [Name], [Path], [Level])    
AS    
(    
    SELECT     
        CAST(NULL as nvarchar(max)) as [Parent], 
		CAST(NULL as int) as [ParentId], 
        VIRT.node.query('./*') as [Folders],    
        VIRT.node.value('@Id', 'int') as [Id],    
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name],    
        CAST('/Root/Folder[@Id=' as nvarchar(max)) + VIRT.node.value('@Id', 'varchar(50)') + ']' as [Path], 
        0 as [Level]    
        FROM @xml.nodes('/Root/Folder') as VIRT(node)    
    UNION ALL    
    SELECT    
		[Path] AS [Parent], 
		CAST(SUBSTRING([Path], LEN([Path]) - CHARINDEX( '=', REVERSE([Path])) + 2, LEN([Path]) - CHARINDEX( ']', REVERSE([Path])) - (LEN([Path]) - CHARINDEX( '=', REVERSE([Path])) + 2) + 1) AS int) AS ParentId, 
        VIRT.node.query('./*') as [Folders], 
        VIRT.node.value('@Id', 'int') as [Id],    
        VIRT.node.value('@Name', 'nvarchar(500)') as [Name],    
        [Path] +     
            CASE [Path] WHEN '/' THEN '' ELSE + '/' END +     
            'Folder[@Id=' + VIRT.node.value('@Id', 'varchar(50)') + ']' as [Path], 
        Level + 1 as [Level] 
        FROM     
    CTE_ITEMS CROSS APPLY Folders.nodes('./Folder') as VIRT(node)    
)    
SELECT [Parent],[ParentId],[Id],[Name],[Path],[Level] FROM CTE_ITEMS ORDER BY [Path] 

Have fun.

Cloud Hosting
Posted on 4/19/2011 5:09:21 PM

I have been looking all over the web to figure out how to traverse hierarchical XML data With T-SQL in SQL Server 2005/2008. I didn't even think about using common table expressions. Your solution is brilliant and elegant. Thanks for all of the help.

Commenting temporarily disabled