More nested XML with SQL Server: n-level tree recursion

During my foray into XML SQL queries this week, I was presented with another challenge. Instead of getting just the immediate children of a category, I now needed to recursively select all children from a tree – to an unlimited depth.

A Common Table Expression (CTE, aka WITH statement) can also be called recursively, but requires UNION ALL to join the recursive and anchor members — and XML columns can’t be unioned.

Instead, we need a user defined function that returns XML TYPE. It’ll give us a rootless collection of products within a category, and call itself again to get sub-categories. Got it? Here’s the function definition, again using the AdventureWorks database:

CREATE FUNCTION GetProductCategoryChildren
(
	@ParentProductCategoryID INT
)
RETURNS XML
AS
BEGIN
	RETURN
	(
	SELECT
		-- Map columns to XML attributes/elements with XPath selector
		category.ProductCategoryID AS '@id',
		category.Name AS '@name',
		(
			-- Use a sub query for child elements.
			SELECT
				ProductID AS '@id',
				Name AS '@name',
				ListPrice AS '@price'
			FROM
				SalesLT.Product
			WHERE
				ProductCategoryID = category.ProductCategoryID
			FOR
				XML PATH('product'),  -- The element name for each row.
				TYPE -- Column is typed so it nests as XML, not text.
		) AS 'products',
		dbo.GetProductCategoryChildren(category.ProductCategoryID)
			AS 'categories' -- Recursive call to get child categories.
	FROM
		SalesLT.ProductCategory category
	WHERE
		category.ParentProductCategoryID = @ParentProductCategoryID
	FOR
		XML PATH('category'),  -- The element name for each row.
		TYPE -- The root element name for this result set.
	)
END

This function works great. But we still want to get details about the group itself (not just its children), and we still need a root node so we can load it into an XmlDocument. Here’s how to wrap the call to this function to get a root node and details about the parent:

-- Get the parent group's name and child products.
SELECT
	category.ProductCategoryID AS '@id',
	category.Name AS '@name',
	(
		SELECT
			ProductID AS '@id',
			Name AS '@name',
			ListPrice AS '@price'
		FROM
			SalesLT.Product
		WHERE
			ProductCategoryID = category.ProductCategoryID
		FOR
			XML PATH('product'), TYPE
	) AS 'products',
	-- start recursing to get child categories.
	dbo.GetProductCategoryChildren(category.ProductCategoryID) AS 'categories'
FROM
	SalesLT.ProductCategory category
WHERE
	category.CategoryID = 2
FOR
	XML PATH('category'), ROOT('categories')

This is what the output looks like. It’ll go for as many levels of depth as your tree does.

<categories>
  <category id="2" name="Components">
    <categories>
      <category id="8" name="Handlebars">
        <products>
          <product id="808" name="LL Mountain Handlebars" price="44.5400" />
          <product id="809" name="ML Mountain Handlebars" price="61.9200" />
          <product id="810" name="HL Mountain Handlebars" price="120.2700" />
          <product id="811" name="LL Road Handlebars" price="44.5400" />
          <product id="812" name="ML Road Handlebars" price="61.9200" />
        </products>
        <categories>
          <category id="9" name="Bottom Brackets">
            <products>
              <product id="994" name="LL Bottom Bracket" price="53.9900" />
              <product id="995" name="ML Bottom Bracket" price="101.2400" />
              <product id="996" name="HL Bottom Bracket" price="121.4900" />
            </products>
            <categories>
              <category id="11" name="Chains">
                <products>
                  <product id="952" name="Chain" price="20.2400" />
                </products>
              </category>
            </categories>
          </category>
          <category id="10" name="Brakes">
            <products>
              <product id="907" name="Rear Brakes" price="106.5000" />
              <product id="948" name="Front Brakes" price="106.5000" />
            </products>
          </category>
        </categories>
      </category>
      <category id="12" name="Cranksets">
        <products>
          <product id="949" name="LL Crankset" price="175.4900" />
          <product id="950" name="ML Crankset" price="256.4900" />
          <product id="951" name="HL Crankset" price="404.9900" />
        </products>
      </category>
    </categories>
  </category>
</categories>

Note I had to rearrange some of the categories in the AdventureWorks database to get deeper nesting.

August 30, 2008

3 Comments

Steve K. on September 30, 2011 at 4:33 pm.

Thanks for this post. Though it is several years old, it helped me in my need for some menu building for a website I’m working on.

Dana on December 2, 2011 at 8:26 pm.

Thanks. This was one of the best explanations of xml I have seen and I’ve been looking around internet for a couple of months.

Rob on January 26, 2012 at 3:59 pm.

I agree. These are great examples. I needed to generate an XML file using some data in our database, to act as a configuration file for a SQLSpec – a great little documenting tool. This really helped, and saved me from having to deal with XSLT.

Leave Your Comment

Your email will not be published or shared. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>