Вы находитесь на странице: 1из 32

FOR XML EXPLICIT Tutorial

With SQL Server 2005 we can generate XML output using different methods.
Using TSQL keyword FOR XML along with AUTO, RAW, PATH and EXPLICIT we
could generate almost any XML structure that we might need. PATH is a very
powerful keyword which allows a great deal of customization on the structure
of the generated XML and is relatively easy to use. EXPLICIT provides more
control over the generated XML structure but it is much more complex then
other methods. Most of the times, we could generate the same output as
EXPLICIT by using PATH. But some times, the structure of the XML output
might be too complex for PATH to generate, and we will have to go with
EXPLICIT. PATH is available only in SQL Server 2005. If you are working with
SQL server 2000, you will have to work with EXPLICIT if you need control
over the XML structure being generated.

I had been helping some people on writing TSQL queries with EXPLICIT
recently, at some of the Internet forums. My observation is that most of the
times people get an error because of the sort order of the result set being
passed to FOR XML EXPLICIT I worked with Vimal Rughani recently on such
a query. After we wrote the query, he asked me if I could explain the flow of
the code. I thought that it would be a good idea to write down the steps I
went through while writing the query, so that it will help other people around
too. He wanted to generate the following XML output using FOR XML
EXPLICIT.

<Agents>
<Agent AgentID="1">
<Fname>Vimal</Fname>
<SSN>123-23-4521</SSN>
<AddressCollection>
<Address>
<AddressType>Home</AddressType>
<Address1>abc</Address1>
<Address2>xyz road</Address2>
<City>RJ</City>
</Address>
<Address>
<AddressType>Office</AddressType>
<Address1>temp</Address1>
<Address2>ppp road</Address2>
<City>RJ</City>
</Address>
</AddressCollection>
</Agent>
<Agent AgentID="2">
<Fname>Jacob</Fname>
<SSN>321-52-4562</SSN>
<AddressCollection>
<Address>
<AddressType>Home</AddressType>
<Address1>xxx</Address1>
<Address2>aaa road</Address2>
<City>NY</City>
</Address>
<Address>
<AddressType>Office</AddressType>
<Address1>ccc</Address1>
<Address2>oli Com</Address2>
<City>CL</City>
</Address>
<Address>
<AddressType>Temp</AddressType>
<Address1>eee</Address1>
<Address2>olkiu road</Address2>
<City>CL</City>
</Address>
</AddressCollection>
</Agent>
<Agent AgentID="3">
<Fname>Tom</Fname>
<SSN>252-52-4563</SSN>
<AddressCollection>
<Address>
<AddressType>Home</AddressType>
<Address1>ttt</Address1>
<Address2>loik road</Address2>
<City>NY</City>
</Address>
</AddressCollection>
</Agent>
</Agents>

The data should come from two tables Agents and Addresses. Before we
write the query, we need to create those tables and populate them with
some data. For the purpose of this example, we may not need any physical
tables. We could go with memory tables. The following code will create two
memory tables and fill them with data. The code is written by Kent in one of
the MSDN forums.

/*
Borrowed from Kent's code
*/
declare @agent table
(
AgentID int,
Fname varchar(5),
SSN varchar(11)
)
insert into @agent
select 1, 'Vimal', '123-23-4521' union all
select 2, 'Jacob', '321-52-4562' union all
select 3, 'Tom', '252-52-4563'
declare @address table
(
AddressID int,
AddressType varchar(12),
Address1 varchar(20),
Address2 varchar(20),
City varchar(25),
AgentID int
)
insert into @address
select 1, 'Home', 'abc', 'xyz road', 'RJ', 1 union all
select 2, 'Office', 'temp', 'ppp road', 'RJ', 1 union all
select 3, 'Home', 'xxx', 'aaa road', 'NY', 2 union all
select 4, 'Office', 'ccc', 'oli Com', 'CL', 2 union all
select 5, 'Temp', 'eee', 'olkiu road', 'CL', 2 union all
select 6, 'Home', 'ttt', 'loik road', 'NY', 3

Let us start writing the query. Because we write this query for learning
purpose, I would like to take an approach by which we will progressively
develop the complete query.

So let us start with the root node. Let us first create the query for generating
the root node.

SELECT

1 AS Tag,
NULL AS Parent,
NULL AS 'Agents!1!'
FOR XML EXPLICIT

This will generate the root node that we need.

<Agents />
Now let us write the code for generating next level. The next level is the
agent node. This information should come from the agent table. Let us add
the code for that.

SELECT
1 AS Tag,
NULL AS Parent,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID'
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
NULL,
AgentID
FROM @agent
FOR XML EXPLICIT

Note the code in yellow. This is what we added to the previous version. This
query generates the following output.

<Agents>
<Agent AgentID="1" />
<Agent AgentID="2" />
<Agent AgentID="3" />
</Agents>

Good so far. Let us add fname and ssn under the agent node as child
elements.

SELECT
1 AS Tag,
NULL AS Parent,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element'
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
NULL,
AgentID,
Fname,
SSN
FROM @agent
FOR XML EXPLICIT

This version will give us the following output.

<Agents>
<Agent AgentID="1">
<Fname>Vimal</Fname>
<SSN>123-23-4521</SSN>
</Agent>
<Agent AgentID="2">
<Fname>Jacob</Fname>
<SSN>321-52-4562</SSN>
</Agent>
<Agent AgentID="3">
<Fname>Tom</Fname>
<SSN>252-52-4563</SSN>
</Agent>
</Agents>

Let us move ahead. Under each agent, we need a node named


AddressCollection. Let us add the code for that.

SELECT
1 AS Tag,
NULL AS Parent,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element',
NULL AS 'AddressCollection!3!Element'
UNION ALL
SELECT
2 AS Tag, 1 AS Parent,
NULL, AgentID, Fname, SSN,
NULL
FROM @agent
UNION ALL
SELECT
3 AS Tag, 2 AS Parent,
NULL, NULL, NULL, NULL,
NULL
FROM @agent
FOR XML EXPLICIT
We added a new level for AddressCollection element. I used FROM @agent
because we need an AddressCollection element for each agent record. Here
is the output.

<Agents>
<Agent AgentID="1">
<Fname>Vimal</Fname>
<SSN>123-23-4521</SSN>
</Agent>
<Agent AgentID="2">
<Fname>Jacob</Fname>
<SSN>321-52-4562</SSN>
</Agent>
<Agent AgentID="3">
<Fname>Tom</Fname>
<SSN>252-52-4563</SSN>
<AddressCollection />
<AddressCollection />
<AddressCollection />
</Agent>
</Agents>

Wait a second! we have a problem. Note that the 3 AddressCollection


elements were created as part of the last node. Why does this happen? To
understand that we need to look at the query results that we passed to FOR
XML EXPLICIT. Let us run the query without FOR XML EXPLICIT.

Paren Agents! Agent!2! Agent!2! Agent!2! AddressCollectio


Tag
t 1! AgentID Fname ssn n!3
1 NULL NULL NULL NULL NULL NULL
123-23-
2 1 NULL 1 Vimal NULL
4521
321-52-
2 1 NULL 2 Jacob NULL
4562
252-52-
2 1 NULL 3 Tom NULL
4563
3 2 NULL NULL NULL NULL NULL
3 2 NULL NULL NULL NULL NULL
3 2 NULL NULL NULL NULL NULL

Note the rows in red. These are the records with tag 3. Note that they appear
at the bottom of the result set. That is the reason why they appear at the
bottom of the XML result. So to fix this, we need to change the order of the
rows. So to get the correct XML we need to have the query results in the
following order.

Paren Agents! Agent!2! Agent!2! Agent!2! AddressCollectio


Tag
t 1! AgentID Fname ssn n!3
1 NULL NULL NULL NULL NULL NULL
123-23-
2 1 NULL 1 Vimal NULL
4521
3 2 NULL NULL NULL NULL NULL
321-52-
2 1 NULL 2 Jacob NULL
4562
3 2 NULL NULL NULL NULL NULL
252-52-
2 1 NULL 3 Tom NULL
4563
3 2 NULL NULL NULL NULL NULL

So at this stage, we need to write some kind of code to alter the sort order of
the records. There might be different ways to do that. What I did was to add
a calculated column for the sort order based on the AgentID. Here is the new
code.

SELECT
1 AS Tag,
NULL AS
Parent,
0 AS Sort,
NULL AS
'Agents!1!',
NULL AS
'Agent!2!AgentID',
NULL AS
'Agent!2!Fname!Element',
NULL AS
'Agent!2!SSN!Element',
NULL AS
'AddressCollection!3!Element'
UNION ALL
SELECT
2 AS
Tag, 1 AS Parent,
AgentID * 100 AS
Sort,
NULL, AgentID, Fname, SSN,

NULL
FROM @agent
UNION ALL
SELECT
3 AS Tag, 2 AS
Parent,
AgentID * 100 + 1 AS Sort,

NULL, NULL, NULL, NULL,


NULL
FROM @agent
ORDER BY
Sort

Paren Agents! Agent!2! Agent!2! Agent!2! AddressCollectio


Tag
t 1! AgentID Fname ssn n!3
1 NULL NULL NULL NULL NULL NULL
123-23-
2 1 NULL 1 Vimal NULL
4521
3 2 NULL NULL NULL NULL NULL
321-52-
2 1 NULL 2 Jacob NULL
4562
3 2 NULL NULL NULL NULL NULL
252-52-
2 1 NULL 3 Tom NULL
4563
3 2 NULL NULL NULL NULL NULL

Well, that worked. Note that the sort order holds correct values so that we
get the records in the desired order. There might be different ways to
generate the sort order column. For the purpose of this example, I made it
by multiplying the AgentID with 100, 101 etc. This approach may not work in
a different situation. It worked for the example. The KEY here is to sort the
records in the correct order (exactly in the order that we need them in the
XML results). You can apply your own logic that you feel right, to achieve
this. Let us generate the XML now.

SELECT
1 AS Tag,
NULL AS Parent,
0 AS Sort,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element',
NULL AS 'AddressCollection!3!Element'
UNION ALL
SELECT
2 AS Tag, 1 AS Parent,
AgentID * 100 AS Sort,
NULL, AgentID, Fname, SSN,
NULL
FROM @agent
UNION ALL
SELECT
3 AS Tag, 2 AS Parent,
AgentID * 100 + 1 AS Sort,
NULL, NULL, NULL, NULL,
NULL
FROM @agent
ORDER BY Sort
FOR XML EXPLICIT

unfortunately, this code will not work. If you try to run this, you will get the
following error.

TOSHIBA-USER\SQL2005(TOSHIBA-USER\Jacob Sebastian): Msg 6802, Level 16,


State
1, Line 33
FOR XML EXPLICIT query contains the invalid column name 'Sort'. Use the
TAGNAME!TAGID!ATTRIBUTENAME[!..] format where TAGID is a positive integer.

The error is caused by the "Sort" column that we just added. When we use
FOR XML EXPLICIT, all columns other than "Tag" and "Parent" should be in
the form of "[TAG]![TAGID]!ATTRIBUTE...". We need to hide the "Sort"
column. Lets create an outer query to do this.

SELECT
Tag,
Parent,
[Agents!1!],
[Agent!2!AgentID],
[Agent!2!Fname!Element],
[Agent!2!SSN!Element],
[AddressCollection!3!Element]
FROM (
SELECT
1 AS Tag,
NULL AS Parent,
0 AS Sort,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element',
NULL AS 'AddressCollection!3!Element'
UNION ALL
SELECT
2 AS Tag, 1 AS Parent,
AgentID * 100 AS Sort,
NULL, AgentID, Fname, SSN,
NULL
FROM @agent
UNION ALL
SELECT
3 AS Tag, 2 AS Parent,
AgentID * 100 + 1 AS Sort,
NULL, NULL, NULL, NULL,
NULL
FROM @agent
)A
ORDER BY Sort
FOR XML EXPLICIT

Here is the result.


<Agents>
<Agent AgentID="1">
<Fname>Vimal</Fname>
<SSN>123-23-4521</SSN>
<AddressCollection />
</Agent>
<Agent AgentID="2">
<Fname>Jacob</Fname>
<SSN>321-52-4562</SSN>
<AddressCollection />
</Agent>
<Agent AgentID="3">
<Fname>Tom</Fname>
<SSN>252-52-4563</SSN>
<AddressCollection />
</Agent>
</Agents>

Having fixed the problem with the sort order, let us go ahead with the rest of
the code. Let us add Addresses under the AddressCollection node and come
up with the final version of the code. We need to add a new level, Tag 4.
Note that I used AgentID * 102 to make sure that this record will come right
below the AddressCollection row of each Agent.

SELECT Tag, Parent,


[Agents!1!],
[Agent!2!AgentID],
[Agent!2!Fname!Element],
[Agent!2!SSN!Element],
[AddressCollection!3!Element],
[Address!4!AddressType!Element],
[Address!4!Address1!Element],
[Address!4!Address2!Element],
[Address!4!City!Element]
FROM (
SELECT
1 AS Tag,
NULL AS Parent,
0 AS Sort,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element',
NULL AS 'AddressCollection!3!Element',
NULL AS 'Address!4!AddressType!Element',
NULL AS 'Address!4!Address1!Element',
NULL AS 'Address!4!Address2!Element',
NULL AS 'Address!4!City!Element'
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
AgentID * 100,
NULL, AgentID, Fname, SSN,
NULL,NULL, NULL, NULL, NULL
FROM @Agent
UNION ALL
SELECT
3 AS Tag,
2 AS Parent,
AgentID * 100 + 1,
NULL,NULL,NULL, NULL,
NULL, NULL, NULL, NULL, NULL
FROM @Agent
UNION ALL
SELECT
4 AS Tag,
3 AS Parent,
AgentID * 100 + 2,
NULL,NULL,NULL,NULL,NULL,
AddressType, Address1, Address2, City
FROM @Address
)A
ORDER BY Sort
FOR XML EXPLICIT
Here is the complete listing of the code.

/*
Borrowed from Kent's code
*/
declare @agent table
(
AgentID int,
Fname varchar(5),
SSN varchar(11)
)
insert into @agent
select 1, 'Vimal', '123-23-4521' union all
select 2, 'Jacob', '321-52-4562' union all
select 3, 'Tom', '252-52-4563'
declare @address table
(
AddressID int,
AddressType varchar(12),
Address1 varchar(20),
Address2 varchar(20),
City varchar(25),
AgentID int
)
insert into @address
select 1, 'Home', 'abc', 'xyz road', 'RJ', 1 union all
select 2, 'Office', 'temp', 'ppp road', 'RJ', 1 union all
select 3, 'Home', 'xxx', 'aaa road', 'NY', 2 union all
select 4, 'Office', 'ccc', 'oli Com', 'CL', 2 union all
select 5, 'Temp', 'eee', 'olkiu road', 'CL', 2 union all
select 6, 'Home', 'ttt', 'loik road', 'NY', 3
/*
End Borrow
*/
SELECT Tag, Parent,
[Agents!1!],
[Agent!2!AgentID],
[Agent!2!Fname!Element],
[Agent!2!SSN!Element],
[AddressCollection!3!Element],
[Address!4!AddressType!Element],
[Address!4!Address1!Element],
[Address!4!Address2!Element],
[Address!4!City!Element]
FROM (
SELECT
1 AS Tag,
NULL AS Parent,
0 AS Sort,
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element',
NULL AS 'AddressCollection!3!Element',
NULL AS 'Address!4!AddressType!Element',
NULL AS 'Address!4!Address1!Element',
NULL AS 'Address!4!Address2!Element',
NULL AS 'Address!4!City!Element'
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
AgentID * 100,
NULL, AgentID, Fname, SSN,
NULL,NULL, NULL, NULL, NULL
FROM @Agent
UNION ALL
SELECT
3 AS Tag,
2 AS Parent,
AgentID * 100 + 1,
NULL,NULL,NULL, NULL,
NULL, NULL, NULL, NULL, NULL
FROM @Agent
UNION ALL
SELECT
4 AS Tag,
3 AS Parent,
AgentID * 100 + 2,
NULL,NULL,NULL,NULL,NULL,
AddressType, Address1, Address2, City
FROM @Address
)A
ORDER BY Sort
FOR XML EXPLICIT

Output:

<Agents>
<Agent AgentID="1">
<Fname>Vimal</Fname>
<SSN>123-23-4521</SSN>
<AddressCollection>
<Address>
<AddressType>Home</AddressType>
<Address1>abc</Address1>
<Address2>xyz road</Address2>
<City>RJ</City>
</Address>
<Address>
<AddressType>Office</AddressType>
<Address1>temp</Address1>
<Address2>ppp road</Address2>
<City>RJ</City>
</Address>
</AddressCollection>
</Agent>
<Agent AgentID="2">
<Fname>Jacob</Fname>
<SSN>321-52-4562</SSN>
<AddressCollection>
<Address>
<AddressType>Home</AddressType>
<Address1>xxx</Address1>
<Address2>aaa road</Address2>
<City>NY</City>
</Address>
<Address>
<AddressType>Office</AddressType>
<Address1>ccc</Address1>
<Address2>oli Com</Address2>
<City>CL</City>
</Address>
<Address>
<AddressType>Temp</AddressType>
<Address1>eee</Address1>
<Address2>olkiu road</Address2>
<City>CL</City>
</Address>
</AddressCollection>
</Agent>
<Agent AgentID="3">
<Fname>Tom</Fname>
<SSN>252-52-4563</SSN>
<AddressCollection>
<Address>
<AddressType>Home</AddressType>
<Address1>ttt</Address1>
<Address2>loik road</Address2>
<City>NY</City>
</Address>
</AddressCollection>
</Agent>
</Agents>

This is a late addition to the 3 part FOR XML TUTORIAL I wrote last year. You
can find Part 1 here, Part 2 here and Part 3 here.
When generating the XML document, FOR XML EXPLICIT processes rows in
the same order as they are returned by the query. So, most of the times, you
need to specify an ORDER BY clause in your query, so that the XML output
will contain information in the desired order. In Part 3, we used a calculated
column to generate certain values and used those values to order the result.
Since we did not want the 'artificial sort column' in the XML output, we used
an outer query to filter out the sort column.

One of the readers, GooseCandy, suggested that it will be a better idea to


use the 'hide' directive, rather than using an outer query to hide the 'sort'
column. He is right and I would like to post a new version of the sample code
we saw in Part 3, using the 'hide' directive.

Here is the new version of the query using the 'hide' directive.

declare @agent table


(
AgentID int,
Fname varchar(5),
SSN varchar(11)
)

insert into @agent


select 1, 'Vimal', '123-23-4521' union all
select 2, 'Jacob', '321-52-4562' union all
select 3, 'Tom', '252-52-4563'

declare @address table


(
AddressID int,
AddressType varchar(12),
Address1 varchar(20),
Address2 varchar(20),
City varchar(25),
AgentID int
)

insert into @address


select 1, 'Home', 'abc', 'xyz road', 'RJ', 1 union all
select 2, 'Office', 'temp', 'ppp road', 'RJ', 1 union all
select 3, 'Home', 'xxx', 'aaa road', 'NY', 2 union all
select 4, 'Office', 'ccc', 'oli Com', 'CL', 2 union all
select 5, 'Temp', 'eee', 'olkiu road', 'CL', 2 union all
select 6, 'Home', 'ttt', 'loik road', 'NY', 3

SELECT
1 AS Tag,
NULL AS Parent,
0 AS 'Agents!1!Sort!hide',
NULL AS 'Agents!1!',
NULL AS 'Agent!2!AgentID',
NULL AS 'Agent!2!Fname!Element',
NULL AS 'Agent!2!SSN!Element',
NULL AS 'AddressCollection!3!Element',
NULL AS 'Address!4!AddressType!Element',
NULL AS 'Address!4!Address1!Element',
NULL AS 'Address!4!Address2!Element',
NULL AS 'Address!4!City!Element'
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
AgentID * 100,
NULL, AgentID, Fname, SSN,
NULL,NULL, NULL, NULL, NULL
FROM @Agent
UNION ALL
SELECT
3 AS Tag,
2 AS Parent,
AgentID * 100 + 1,
NULL,NULL,NULL, NULL,
NULL, NULL, NULL, NULL, NULL
FROM @Agent
UNION ALL
SELECT
4 AS Tag,
3 AS Parent,
AgentID * 100 + 2,
NULL,NULL,NULL,NULL,NULL,
AddressType, Address1, Address2, City
FROM @Address
ORDER BY [Agents!1!Sort!hide]
FOR XML EXPLICIT

Note the usage of the "hide" directive on the column we generated for
sorting. Columns marked with "hide" will be ignored by the XML processor.
FOR XML EXPLICIT supports a few other interesting directives too. I will cover
them in a future post.
FOR XML PATH - How to remove the <row>
element from the output of a FOR XML PATH
query?
When you write a simple FOR XML query with PATH, you will see that a
<row> element will be generated for each row in the result set. For example:

DECLARE @t TABLE (Name VARCHAR(10))


INSERT INTO @t (Name) SELECT 'Jacob'
INSERT INTO @t (Name) SELECT 'Steve'

SELECT Name FROM @t


FOR XML PATH, ROOT ('Employees')

/*
<Employees>
<row>
<Name>Jacob</Name>
</row>
<row>
<Name>Steve</Name>
</row>
</Employees>
*/

Note that a <node> element is created for each row in the query result.
PATH is a very powerful operator that allows a great deal of flexibility. Most
of the operations previously possible only with EXPLICIT is now possible with
PATH. Let us see a few simple variations of the above query and see how we
could control the format of the output.

Let us first of all, remove the <node> element.

DECLARE @t TABLE (Name VARCHAR(10))


INSERT INTO @t (Name) SELECT 'Jacob'
INSERT INTO @t (Name) SELECT 'Steve'

SELECT Name FROM @t


FOR XML PATH(''), ROOT ('Employees')
/*
<Employees>
<Name>Jacob</Name>
<Name>Steve</Name>
</Employees>
*/
Now, Let us put each employee under an <Employee> Node.

DECLARE @t TABLE (Name VARCHAR(10))


INSERT INTO @t (Name) SELECT 'Jacob'
INSERT INTO @t (Name) SELECT 'Steve'

SELECT Name FROM @t


FOR XML PATH('Employee'), ROOT ('Employees')
/*
<Employees>
<Employee>
<Name>Jacob</Name>
</Employee>
<Employee>
<Name>Steve</Name>
</Employee>
</Employees>
*/

Finally, lets us change the <Name> element to an attribute.

DECLARE @t TABLE (Name VARCHAR(10))


INSERT INTO @t (Name) SELECT 'Jacob'
INSERT INTO @t (Name) SELECT 'Steve'

SELECT Name AS '@Name' FROM @t


FOR XML PATH('Employee'), ROOT ('Employees')
/*
<Employees>
<Employee Name="Jacob" />
<Employee Name="Steve" />
</Employees>
*/

FOR XML PATH - How to generate a Delimited


String using FOR XML PATH?
One of the two common string operations that we do often are 'parsing'
delimited strings and 'generating' delimited strings. This post explains how
to generate a delimited string using FOR XML.

I like XML and many of you might have noted the reflection of this likeness in
my posts. When I try to solve a problem, I usually look for an XML based
approach before trying any other method. This does not mean that the XML
approach is always superior. There are times when it is good and there are
times when an XML approach is not desirable.

I have experienced that TSQL loops are very expensive (usually). So most of
the times, if you can re-write a loop to a batch/set operation, you could get
performance benefits (well, most of the times. There are times when this
may not be true, but such cases are very rare).

There are two common string operations where I used to write a TSQL loop in
the SQL server 2000 era.

1. To split a delimited string and return a set


2. To generate a delimited string from a set

The XML enhancements added to SQL Server 2005 made both these
operations easier with XML. I think, most of the times these operations are
done in small pieces of data. Though you can do these operations on
extremely large data, I don't think it is advisable. There are other ways to
handle large chunks of data.

In this post, lets see how we could generate a delimited string using FOR
XML PATH. I have covered the other topic "How to split a delimited string" in
another post.

This post is inspired by a discussion with a colleague. Here is the details of


the specific requirement. Let us first see the source data.

DECLARE @companies Table(


CompanyID INT,
CompanyCode int
)

insert into @companies(CompanyID, CompanyCode) values(1,1)


insert into @companies(CompanyID, CompanyCode) values(1,2)
insert into @companies(CompanyID, CompanyCode) values(2,1)
insert into @companies(CompanyID, CompanyCode) values(2,2)
insert into @companies(CompanyID, CompanyCode) values(2,3)
insert into @companies(CompanyID, CompanyCode) values(2,4)
insert into @companies(CompanyID, CompanyCode) values(3,1)
insert into @companies(CompanyID, CompanyCode) values(3,2)

SELECT * FROM @companies


/*
CompanyID CompanyCode
----------- -----------
1 1
1 2
2 1
2 2
2 3
2 4
3 1
3 2
*/

This is the result that we need.

/*
CompanyID CompanyString
----------- -------------------------
1 1|2
2 1|2|3|4
3 1|2
*/

One option is to run a loop that constructs a delimited string for each
CompanyID. Another option is to create a function that returns a delimited
string for each company ID. I am presenting a third option using FOR XML
PATH.

SELECT CompanyID,
(SELECT
CompanyCode AS 'data()'
FROM @companies c2
WHERE c2.CompanyID = c1.CompanyID
FOR XML PATH('')) AS CompanyString
FROM @companies c1
GROUP BY CompanyID/*
CompanyID CompanyString
----------- ------------------------
1 12
2 1234
3 12
*/

The above query uses FOR XML PATH to return a SPACE delimited string
containing the company code of each row. But this is not the final result that
we need. We need a pipe separated list and hence we need to apply a
REPLACE() operation.

SELECT CompanyID,
REPLACE((SELECT
CompanyCode AS 'data()'
FROM @companies c2
WHERE c2.CompanyID = c1.CompanyID
FOR XML PATH('')), ' ', '|') AS CompanyString
FROM @companies c1
GROUP BY CompanyID

/*
CompanyID CompanyString
----------- -------------------------
1 1|2
2 1|2|3|4
3 1|2
*/

Another XML Shaping Example - using FOR


XML PATH and EXPLICIT
In many of the previous posts, we discussed about controlling the structure
of the XML being generated with FOR XML. SQL Server 2005 introduced the
PATH operator which provides a great extend of control over the shaping of
the XML being generated. With PATH, we could achieve most of the XML
shaping requirements previously possible only with FOR XML EXPLICIT. In my
XML Workshop Series at www.sqlservercentral.com, I have discussed many
of the XML shaping requirements and different ways to meet those
requirements.

I just helped some one to solve another XML shaping problem in the MSDN
forums and thought of sharing it here, because the problem seems to be
common. Position of elements is significant in XML. Hence, some times we
need to control the position of elements being generated. We might need to
have a certain element placed before another. Most of the times when we
write a FOR XML EXPLICIT query, we might need to have elements placed in
a given order. I have discussed it in the FOR XML EXPLICIT tutorial given
here.

Let us look into the problem and the solution.

Source data

/*
id NameA NameB NameC
----------- ---------- ---------- ----------
1 Value A Value B Value C
*/
Current Query

SELECT
1 AS Tag,
NULL AS Parent,
ID AS [Tab!1!ID],
NameA AS [Tab!1!NameA!Element],
NULL AS [Tab2!2!NameB!Element],
NameC AS [Tab!1!NameC!Element]
FROM tbl
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
ID,
NULL AS [Tab!1!NameA!Element],
NameB AS [Tab!1!NameB!Element],
NULL AS [Tab!1!NameC!Element]
FROM tbl
FOR XML EXPLICIT

Current XML Result

<Tab ID="1">
<NameA>Value A</NameA>
<NameC>Value C</NameC>
<Tab2>
<NameB>Value B</NameB>
</Tab2>
</Tab>

Expected XML Result

<Tab ID="1">
<NameA>Value A</NameA>
<Tab2>
<NameB>Value B</NameB>
</Tab2>
<NameC>Value C</NameC>
</Tab>

Corrected Solution

We have two options here. We could either go with FOR XML PATH or FOR
XML EXPLICIT (FOR XML PATH is available only on SQL server 2005 and
above). Here is the query that uses FOR XML EXPLICIT.
DECLARE @tmp TABLE (
id Int,
NameA VarChar(10),
NameB VarChar(10),
NameC VarChar(10)
)

INSERT INTO @tmp (id, NameA, NameB, NameC)


VALUES (1, 'Value A', 'Value B', 'Value C')

SELECT
1 AS Tag,
NULL AS Parent,
ID AS [Tab!1!ID],
NameA AS [Tab!1!NameA!Element],
NULL AS [Tab2!2!NameB!Element],
NULL AS [NameC!3]
FROM @tmp
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
ID, NULL,
NameB, NULL
FROM @tmp
UNION ALL
SELECT
3 AS Tag,
1 AS Parent,
NULL, NULL, NULL,
NameC
FROM @tmp
FOR XML EXPLICIT

/*
<Tab ID="1">
<NameA>Value A</NameA>
<Tab2>
<NameB>Value B</NameB>
</Tab2>
<NameC>Value C</NameC>
</Tab>
*/

And here is the version that uses FOR XML PATH.

DECLARE @tmp TABLE (


id Int,
NameA VarChar(10),
NameB VarChar(10),
NameC VarChar(10)
)

INSERT INTO @tmp (id, NameA, NameB, NameC)


VALUES (1, 'Value A', 'Value B', 'Value C')

SELECT
id AS '@ID',
NameA AS 'NameA',
NameB AS 'Tab2/NameB',
NameC AS 'NameC'
FROM @tmp
FOR XML PATH('Tab')

<Tab ID="1">
<NameA>Value A</NameA>
<Tab2>
<NameB>Value B</NameB>
</Tab2>
<NameC>Value C</NameC>
</Tab>

Note that the query that uses FOR XML PATH is much simpler than the
version using FOR XM EXPLICIT.

FOR XML PATH - Generating an element


having NULL value
When you generate an XML document using FOR XML, columns having NULL
value will be eliminated. Some times you might need to have an empty
element (even if there is no value) generated to make sure that the
application that processes the XML document does not complain. The
following query shows an example that uses XSINIL instruction which
generates empty elements for columns having NULL values.

DECLARE @t TABLE (
id INT, Name1 VARCHAR(20),
Value1 VARCHAR(20), Name2 VARCHAR(20),
Value2 VARCHAR(20))

INSERT INTO @t (id, name1, value1, name2, value2)


SELECT 1, 'PrimaryID', NULL, 'LastName', 'Abiola' UNION ALL
SELECT 2, 'PrimaryID', '200', 'LastName', 'Aboud'
SELECT
(
SELECT
name1 AS 'Parameter/Name',
value1 AS 'Parameter/Value'
FROM @t t2 WHERE t2.id = t.id
FOR XML PATH(''), ELEMENTS XSINIL, TYPE
),
(
SELECT
name2 AS 'Parameter/Name',
value2 AS 'Parameter/Value'
FROM @t t2 WHERE t2.id = t.id
FOR XML PATH(''), TYPE
)
FROM @t t
FOR XML PATH('T2Method')

<T2Method>
<Parameter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>PrimaryID</Name>
<Value xsi:nil="true" />
</Parameter>
<Parameter>
<Name>LastName</Name>
<Value>Abiola</Value>
</Parameter>
</T2Method>
<T2Method>
<Parameter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>PrimaryID</Name>
<Value>200</Value>
</Parameter>
<Parameter>
<Name>LastName</Name>
<Value>Aboud</Value>
</Parameter>
</T2Method>

Note the "value" element in the first "paremeter" element. The value is NULL
and still an element is generated. Note the addition of a special attribute
"xsi:nil" to indicate that the element is empty. Note also the new namespace
added without your consent :-)

FOR XML : Using "TYPE" prevents streaming


of FOR XML results
We discussed FOR XML in a number of previous posts. SQL Server 2005
added a few enhancements to the FOR XML clause. One of the
enhancements added in SQL Server 2005 is the PATH clause which helps to
do a great deal of customization/control over the structure XML result being
generated. Another keyword added to FOR XML in SQL Server 2005 is the
TYPE directive that produces the result as XML data type.

I wrote a series of articles on FOR XML at www.sqlservercentral.com. You


can find articles on AUTO and RAW here, PATH here and EXPLICIT here. In
addition, I have added links to several FOR XML posts I wrote previously.

In this article, I have explained how to access the results of a FOR XML query
from ADO.NET. SQL Server can stream the output of a FOR XML query. It
means that SQL Server need not wait till the query execution completes, to
start sending you the data. Instead, it will start sending you the data as a
stream. As soon as a chunk of data is available, it is sent to you and SQL
Server will continue to execute the query to fetch the rest of the data.

See that the query is still executing, and you started getting part of the XML
result. The downside of this is that, if something goes wrong, say the query
timed out, you will end up with an incomplete XML document.
SQL Server 2005 introduced TYPE directive that converts the results of a FOR
XML query to a well-formed XML. When you use the TYPE directive, SQL
Server will not stream the results. Instead, it will read all the needed data,
create the result as XML data type which performs the necessary validations
to make sure that the XML document is well formed. SQL Server will start
sending you the data only after performing all these. This could add some
overhead at the server side as well as some delay in getting the results at
the client side.

Summary: Use the TYPE directive only if you really need it. By avoiding the
TYPE directive you can get some performance benefits in most of the cases.

FOR XML PATH – Yet another shaping


example using FOR XML PATH
One of my readers recently contacted me with an XML shaping
requirement. He wanted to generate an XML document with a specific
structure from a result set that he has created. Here is the source data.

RootNode ParentNode Node Name Number Valid Value


--------------- ---------- ----- ----- ------ ----- ---------------
Reference Basic Book Book1 1 true AH.KL.LO
Reference App A App1 1 true AIK.LPO
Reference App A App2 2 true JUI.MKJ
Reference Sub B SubA 1 false LOP.MJH
Reference Sub B SubB 2 false GTY.JUI
Reference DI C NULL 1 NULL PLW.KJU

Here is the XML document that he wanted to generate from the above
source data.

<Reference>
<Basic>
<Book Name="Book1" number="1" Valid="true">AH.KL.LO</Book>
</Basic>
<App>
<A Name="App1" number="1" Valid="true">AIK.LPO</A>
<A Name="App2" number="2" Valid="true">JUI.MKJ</A>
</App>
<Sub>
<B Name="SubA" number="1" Valid="false">LOP.MJH</B>
<B Name="SubB" number="2" Valid="false">GTY.JUI</B>
</Sub>
<DI>
<C number="1">PLW.KJU</C>
</DI>
</Reference>

Let us go ahead and try to write a FOR XML query to generate the above XML
document. First of all, let us create a table variable and fill it with the sample
data. Here is the script to create the sample data.

DECLARE @t TABLE (
RootNode VARCHAR(15),
ParentNode VARCHAR(10),
Node VARCHAR(5),
Name VARCHAR(5),
Number VARCHAR(1),
Valid VARCHAR(5),
Value VARCHAR(15)
)

INSERT INTO @t
SELECT 'Reference','Basic','Book','Book1','1','true','AH.KL.LO' UNION ALL
SELECT 'Reference','App','A','App1','1','true','AIK.LPO' UNION ALL
SELECT 'Reference','App','A','App2','2','true','JUI.MKJ' UNION ALL
SELECT 'Reference','Sub','B','SubA','1','false','LOP.MJH' UNION ALL
SELECT 'Reference','Sub','B','SubB','2','false','GTY.JUI' UNION ALL
SELECT 'Reference','DI','C',NULL,'1',NULL,'PLW.KJU'

Let us start writing the query. Let us first generate the "Book" node with its 3
attributes.

SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid'
FROM @t
WHERE node = 'Book'
FOR XML PATH('Book')

The above code generates the following XML output.

<Book Name="Book1" number="1" valid="true" />

The "Book" node holds a text value too. Let us write the code to generate the text
value.

SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'Book'
FOR XML PATH('Book')

<Book Name="Book1" number="1" valid="true">AH.KL.LO</Book>

Now, let us add the parent nodes: "Basic" and "Reference".

SELECT
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'Book'
FOR XML PATH('Book'), TYPE
) AS Basic
FOR XML PATH(''), ROOT('Reference')

<Reference>
<Basic>
<Book Name="Book1" number="1" valid="true">AH.KL.LO</Book>
</Basic>
</Reference>

Let us add the "App" node to this version of the query.

SELECT
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'Book'
FOR XML PATH('Book'), TYPE
) AS Basic,
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'A'
FOR XML PATH('A'), TYPE
) AS App
FOR XML PATH(''), ROOT('Reference')

<Reference>
<Basic>
<Book Name="Book1" number="1" valid="true">AH.KL.LO</Book>
</Basic>
<App>
<A Name="App1" number="1" valid="true">AIK.LPO</A>
<A Name="App2" number="2" valid="true">JUI.MKJ</A>
</App>
</Reference>

Now it is time to add the "Sub" element.

SELECT
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'Book'
FOR XML PATH('Book'), TYPE
) AS Basic,
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'A'
FOR XML PATH('A'), TYPE
) AS App,
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'B'
FOR XML PATH('B'), TYPE
) AS Sub
FOR XML PATH(''), ROOT('Reference')

<Reference>
<Basic>
<Book Name="Book1" number="1" valid="true">AH.KL.LO</Book>
</Basic>
<App>
<A Name="App1" number="1" valid="true">AIK.LPO</A>
<A Name="App2" number="2" valid="true">JUI.MKJ</A>
</App>
<Sub>
<B Name="SubA" number="1" valid="false">LOP.MJH</B>
<B Name="SubB" number="2" valid="false">GTY.JUI</B>
</Sub>
</Reference>

Let us now add the "DI" node and write the final version of the query.

SELECT
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'Book'
FOR XML PATH('Book'), TYPE
) AS Basic,
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'A'
FOR XML PATH('A'), TYPE
) AS App,
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'B'
FOR XML PATH('B'), TYPE
) AS Sub,
(
SELECT
name AS '@Name',
number AS '@number',
valid AS '@valid',
value AS 'data()'
FROM @t
WHERE node = 'C'
FOR XML PATH('C'), TYPE
) AS DI
FOR XML PATH(''), ROOT('Reference')

<Reference>
<Basic>
<Book Name="Book1" number="1" valid="true">AH.KL.LO</Book>
</Basic>
<App>
<A Name="App1" number="1" valid="true">AIK.LPO</A>
<A Name="App2" number="2" valid="true">JUI.MKJ</A>
</App>
<Sub>
<B Name="SubA" number="1" valid="false">LOP.MJH</B>
<B Name="SubB" number="2" valid="false">GTY.JUI</B>
</Sub>
<DI>
<C number="1">PLW.KJU</C>
</DI>
</Reference>

Вам также может понравиться