Showing posts with label FOR XML PATH. Show all posts
Showing posts with label FOR XML PATH. Show all posts

Saturday, June 26, 2010

More Fun With Hyperlinks: DDL Code

In my last blog entry, I demonstrated some queries that will produce results with hyperlinks to T-SQL Code. For example, the following query will find all procedures, views, triggers, and functions in AdventureWorks that contain the string ‘ContactTypeID’. The hyperlinks are created via the processing-instruction() XPath function. You can get a detailed explanation of how it works in my previous blog entry.

use AdventureWorks
go
select
ObjType=type_desc
,ObjName=schema_name(schema_id)+'.'+name
,ObjDefLink
from sys.objects
cross apply (select ObjDef=object_definition(object_id)) F1
cross apply (select ObjDefLink=(select [processing-instruction(q)]=ObjDef
for xml path(''),type)) F2
where type in ('P' /* Procedures */
,'V' /* Views */
,'TR' /* Triggers */
,'FN','IF','TF' /* Functions */
)
and ObjDef like '%ContactTypeID%' /* String to search for */
order by charindex('F',type) desc /* Group the functions together */
,ObjType
,ObjName
This produces the output below in the Grid Results window. Clicking on any of the hyperlinks will bring up the code for that object in a new window.

Object Query With Hyperlinks

Piotr Rodak (who has a very nice blog, and he also has, by far, the most clever blog name in existence) left a comment on my last post saying, “It’s a pity that table definitions cannot be acquired in a similar way.”

Wow, what a great idea! Imagine yourself walking into a new client (or new job) with a database with hundreds of tables and no documentation anywhere. Yes, you could right-click on the database in Object Explorer and choose Tasks -> Generate Scripts… from the popup menu and go through all the dialogs, and then generate a single code window or a single file or (if you have SQL2008) separate files for each object.

But instead, how about a query that produces a list of tables in the database, along with a hyperlink to the DDL Code for the table (and all its indexes)?

Coo-ul.

I was up for the challenge, and so I put a (looonngg) query together to do just that. Using the Object Catalog Views (i.e. sys.tables, sys.columns, etc), it generates the vast majority of the DDL Code for a table… the only features it leaves out are anything that has to do with Data Compression, Sparse Columns, Column Sets, FileStream, and Partitioning. Some things I left out because of time… other things I left out because they were SQL2008-only features and I wanted the query to work in both SQL2005 and SQL2008.

Here is the output of the query for the AdventureWorks database:

Query with Hyperlinks to DDL Code

And, if we click on the hyperlink for the HumanResources.EmployeeDepartmentHistory table, for example, we get the following (in an XML window):

XML Window Opened By Hyperlink

And to get the syntax coloring in a new code window, we perform a couple of keystrokes: CTRL+A (Select All), CTRL+C (Copy), CTRL+F4 (Close Window), CTRL+N (New Query Window), CTRL+V (Paste), and then a few DELETE keystrokes to get rid of the XML delimiters at the beginning and the end, and there’s the code for the creation of the table. Note the columns, their defaults, the check constraints, primary key constraint, foreign key references, and the (non-primary-key) index definitions for the table.

Code Window created from the XML Window

The code for this query is too long to incorporate here in this blog article, but you can download it from my SkyDrive. It’s just a single query, so you can easily incorporate it into a stored procedure if you wish.

Thanks again to Piotr for his comment that acted as the catalyst for this idea. I hope you find it to be helpful.

Thursday, June 17, 2010

Hyperlinks To T-SQL Code

There are often questions on the MSDN T-SQL Forum regarding how you can find all stored procedures (and/or functions and/or triggers and/or views) that contain a particular string. Thankfully, the object_definition() function gives us the ability to acquire the T-SQL code of those objects and we can easily find a particular search string in that code.

For example, the following query will look through all the objects (sys.objects) in the AdventureWorks database, looking for procedures (type=’P’) and views (type=’V’) and triggers (type=’TR’) and functions (types ‘FN’, ‘IF’, ‘TF’) that contain the string ‘ContactTypeID’:

select ObjType=type_desc 
,ObjName=schema_name(schema_id)+'.'+name
,ObjDef
from sys.objects
cross apply (select ObjDef=object_definition(object_id)) F1
where type in ('P' /* Procedures */
,'V' /* Views */
,'TR' /* Triggers */
,'FN','IF','TF' /* Functions */
)
and ObjDef like '%ContactTypeID%' /* String to search for */
order by charindex('F',type) desc /* Group the functions together */
,ObjType
,ObjName
I use a CROSS APPLY to introduce a column called ObjDef, which contains the full object_definition() value (i.e. the T-SQL code) of the object. This way I can reference ObjDef in my WHERE clause and in the SELECT list. And if I want to search for a second string, I can simply add a AND ObjDef LIKE ‘%otherstring%’ predicate to the WHERE clause.

I also sort the output so that the rows are grouped by the type of object and then, within each type, the rows are sorted by the name.

And that gives us the following result:

Boring Object Query

This is very nice to get this all at a glance, but the ObjDef column is limited. I can widen the column in the grid, but only so far. And the contents don’t contain any of the newline characters… it’s just one looonnngggg string of text that I can’t read. I could copy/paste the contents into Excel, but again, it will just be a single line of text with no newline characters. And even so, SSMS will not output any more than 65536 characters in a column in a grid result window, so we may not get the full code anyway.

We could output to a text window, which will retain the newlines, but the maximum characters per column that we can output is 8192. Plus the output is ugly.

So what can we do, outside of a lot of searching and pointing-and-clicking in the Object Browser, to see the code for these objects?

Well, MVP Adam Machanic had what I thought was a brilliant idea in how to accomplish this in his sp_who_is_active procedure. The answer is XML. XML columns have two great features. First of all, you can bump up the maximum character output of XML to be unlimited if you wish:

Query Options Dialog

And second of all, XML columns are conveniently presented as hyperlinks in Grid Output.

An unfortunate side-effect of converting text to XML, though, is that XML will encode characters like less-than and greater-than and ampersand to < and > and & respectively. But Adam cleverly uses the processing-instruction() XPath function, which will bypass the encoding and, more importantly, will preserve all the newlines and indentions exactly as is.

So here is a revised copy of our query to find ‘ContactTypeID’ in AdventureWorks, with a new column called ObjDefLink created via the processing-instruction() XPath function in a second CROSS APPLY:

select ObjType=type_desc 
,ObjName=schema_name(schema_id)+'.'+name
,ObjDefLink
from sys.objects
cross apply (select ObjDef=object_definition(object_id)) F1
cross apply (select ObjDefLink=(select [processing-instruction(q)]=ObjDef
for xml path(''),type)) F2
where type in ('P' /* Procedures */
,'V' /* Views */
,'TR' /* Triggers */
,'FN','IF','TF' /* Functions */
)
and ObjDef like '%ContactTypeID%' /* String to search for */
order by charindex('F',type) desc /* Group the functions together */
,ObjType
,ObjName
The processing-instruction(q) will put our object definition code between <?q … ?> delimiters, but, as I mentioned, it’s all presented as a hyperlink, as you can see below:

Exciting Object Query with Hyperlinks!

Let’s click on the hyperlink in the second row to see the code of the Purchasing.vVendor view in a new window:

XML Window Opened by Hyperlink

Looks great! I can see all the code for that view, but it’s in a drab gray color, since that’s how an XML window colors any processing-instruction tag.

If you prefer to see the code with all the usual syntax coloring in a T-SQL window, it’s just a matter of a few keyboard shortcuts: CTRL+A (to Select All), CTRL+C (to copy to the Clipboard), CTRL+F4 (to close the window), CTRL+N (to open a new query window), and CTRL+V (to paste the contents into that window). And then remove the <?q … ?> delimiters from the beginning and the end, and voila… there you see the code in all its glory:

Code Window created from the XML Window

This method can come in handy in several ways.

For example, rather than showing individual rows for the objects whose code contains a certain string, let’s instead just create a single hyperlink to ALL the code that contains the string. Here’s how:

declare @Script nvarchar(max) 
select @Script=(select '
/*
'
+replicate('=',100)+'
'
+schema_name(schema_id)+'.'+name+' ('+type_desc+')
'
+replicate('=',100)+'
*/'
+ObjDef+'
GO
'
from sys.objects
cross apply (select ObjDef=object_definition(object_id)) F1
where type in ('P' /* Procedures */
,'V' /* Views */
,'TR' /* Triggers */
,'FN','IF','TF' /* Functions */
)
and ObjDef like '%ContactTypeID%' /* String to search for */
order by charindex('F',type) desc /* Group the functions together */
,type_desc
,schema_name(schema_id)+'.'+name
for xml path(''),type).value('.','nvarchar(max)')

select CodeLink=(select [processing-instruction(q)]=@Script
for xml path(''),type)
First, I populate a @Script variable, concatenating it with the code of each object, along with some comment header information I supply that contains the object’s name and its type, and I follow each code chunk with a GO command. (For an explanation of the FOR XML PATH and TYPE and .value() stuff in the code, please see my blog post entitled Making a List and Checking It Twice).

Then, the second query simply creates a single-row single-column processing-instruction XML link out of that variable. Here’s what the result looks like in the Grid Results window in SSMS:

Object Query to produce hyperlink to code of ALL objects

And when you click on that hyperlink, you get all the code (of all 3 objects… the function and the two views):

XML Window Opened by Hyperlink

And, again, with a quick CTRL+A, CTRL+C, CTRL+F4, CTRL+N, CTRL+V, and a couple DELETE keypresses, we get the code with syntax coloring, ready for examination and possible modification:

Code Window created from the XML Window

You can also incorporate these code hyperlinks into your DMV queries. For example, here is a query that I acquired from MVP Glenn Berry and tweaked a little bit to include a couple additional columns that I wanted, including the hyperlink column to the code. It uses DMV’s to look into the procedure cache and presents the top 50 queries in descending order of Average CPU time… in other words, the most expensive queries in terms of CPU:

select 
top 50 [Database]=coalesce(d.name,'AdHoc')
,CodeLink=(select [processing-instruction(q)]=qt.[text]
for xml path(''),type)
,TotWorkTimeMS=cast(qs.total_worker_time/1000.0
as decimal(12,2))
,AvgWorkTimeMS=cast(qs.total_worker_time/1000.0/qs.execution_count
as decimal(12,2))
,ExecCount=qs.execution_count
,[Calls/Second]=coalesce(qs.execution_count
/datediff(second,qs.creation_time,getdate())
,0)
,AvgElapsedTimeMS=cast(coalesce(qs.total_elapsed_time/1000.0/qs.execution_count,0)
as decimal(12,2))
,MaxLogReads=qs.max_logical_reads
,MaxLogWrites=qs.max_logical_writes
,CacheAgeMins=datediff(minute,qs.creation_time,getdate())
,QueryPlan=qp.query_plan
from sys.dm_exec_query_stats qs
cross apply sys.dm_exec_sql_text(qs.sql_handle) qt
cross apply sys.dm_exec_query_plan(qs.plan_handle) qp
left join sys.databases d on qt.dbid=d.database_id
order by AvgWorkTimeMS desc
And here is the result:

Most Expensive Queries

So the code that produced each of the high-CPU queries is just a click away.

I hope you find all this as useful as I do.

Update Jun26,2010: Check out my next blog entry, where I show how to provide hyperlinks to DDL (CREATE TABLE) code.

Friday, November 6, 2009

XML PATHs Of Glory

In a past blog post, I illustrated how you can use the FOR XML PATH clause to create comma-separated lists of items. In this entry, I’ll go into detail as to how FOR XML PATH can be used for what it was designed for: to shape actual XML output. Finally, I’ll use FOR XML PATH to create some HTML output as well.

The PATH option was introduced in SQL2005 to provide a flexible and easier approach to constructing XML output. I thank my lucky stars that I started with T-SQL at the SQL2005 level, because the SQL2000 method of using the EXPLICIT option looks like a complete nightmare. (If you’re into torture, take a look at Books Online for documentation on how to use the EXPLICIT option. When you're done screaming, then come back and read on).

Let's take a quick look at the output that results with the FOR XML PATH clause. If you pass no specific path name, then it assumes a path of ‘row’:

select ID=ContactID
,FirstName
,LastName
,Phone
from Person.Contact
where ContactID between 90 and 94
for xml path
/*
<row>
<ID>90</ID>
<FirstName>Andreas</FirstName>
<LastName>Berglund</LastName>
<Phone>795-555-0116</Phone>
</row>
<row>
<ID>91</ID>
<FirstName>Robert</FirstName>
<LastName>Bernacchi</LastName>
<Phone>449-555-0176</Phone>
</row>
<row>
<ID>92</ID>
<FirstName>Matthias</FirstName>
<LastName>Berndt</LastName>
<Phone>384-555-0169</Phone>
</row>
<row>
<ID>93</ID>
<FirstName>John</FirstName>
<LastName>Berry</LastName>
<Phone>471-555-0181</Phone>
</row>
<row>
<ID>94</ID>
<FirstName>Steven</FirstName>
<LastName>Brown</LastName>
<Phone>280-555-0124</Phone>
</row>
*/
For the query below, let's supply a specific path name of ‘Contact’. And it’s usually good practice to create XML with a root tag, and we can do that by adding the ROOT directive like so:

select ID=ContactID
,FirstName
,LastName
,Phone
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact>
<ID>90</ID>
<FirstName>Andreas</FirstName>
<LastName>Berglund</LastName>
<Phone>795-555-0116</Phone>
</Contact>
<Contact>
<ID>91</ID>
<FirstName>Robert</FirstName>
<LastName>Bernacchi</LastName>
<Phone>449-555-0176</Phone>
</Contact>
<Contact>
<ID>92</ID>
<FirstName>Matthias</FirstName>
<LastName>Berndt</LastName>
<Phone>384-555-0169</Phone>
</Contact>
<Contact>
<ID>93</ID>
<FirstName>John</FirstName>
<LastName>Berry</LastName>
<Phone>471-555-0181</Phone>
</Contact>
<Contact>
<ID>94</ID>
<FirstName>Steven</FirstName>
<LastName>Brown</LastName>
<Phone>280-555-0124</Phone>
</Contact>
</Contacts>
*/
You’ll note that the column names were used as the tags for each element in the XML. For example, I renamed the first column to be ID rather than ContactID and therefore the element tag <ID></ID> was created.

You have the ability to shape the XML in whatever ways you wish based on what names you give to your columns. For example, any column that starts with an at-sign (@) will create attributes rather than elements, as illustrated below:

select "@ID"=ContactID
,"@FirstName"=FirstName
,"@LastName"=LastName
,"@Phone"=Phone
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact ID="90" FirstName="Andreas" LastName="Berglund" Phone="795-555-0116" />
<Contact ID="91" FirstName="Robert" LastName="Bernacchi" Phone="449-555-0176" />
<Contact ID="92" FirstName="Matthias" LastName="Berndt" Phone="384-555-0169" />
<Contact ID="93" FirstName="John" LastName="Berry" Phone="471-555-0181" />
<Contact ID="94" FirstName="Steven" LastName="Brown" Phone="280-555-0124" />
</Contacts>
*/
You can mix attributes and elements together like so:

select "@ID"=ContactID
,FirstName
,LastName
,Phone
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact ID="90">
<FirstName>Andreas</FirstName>
<LastName>Berglund</LastName>
<Phone>795-555-0116</Phone>
</Contact>
<Contact ID="91">
<FirstName>Robert</FirstName>
<LastName>Bernacchi</LastName>
<Phone>449-555-0176</Phone>
</Contact>
<Contact ID="92">
<FirstName>Matthias</FirstName>
<LastName>Berndt</LastName>
<Phone>384-555-0169</Phone>
</Contact>
<Contact ID="93">
<FirstName>John</FirstName>
<LastName>Berry</LastName>
<Phone>471-555-0181</Phone>
</Contact>
<Contact ID="94">
<FirstName>Steven</FirstName>
<LastName>Brown</LastName>
<Phone>280-555-0124</Phone>
</Contact>
</Contacts>
*/
And you can create nested attributes and elements, as illustrated below:

select "@ID"=ContactID
,"Name/@Title"=Title
,"Name/@Suffix"=Suffix
,"Name/First"=FirstName
,"Name/Last"=LastName
from Person.Contact
where ContactID between 92 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact ID="92">
<Name Title="Mr.">
<First>Matthias</First>
<Last>Berndt</Last>
</Name>
<Phone>384-555-0169</Phone>
</Contact>
<Contact ID="93">
<Name>
<First>John</First>
<Last>Berry</Last>
</Name>
<Phone>471-555-0181</Phone>
</Contact>
<Contact ID="94">
<Name Title="Mr." Suffix="IV">
<First>Steven</First>
<Last>Brown</Last>
</Name>
<Phone>280-555-0124</Phone>
</Contact>
</Contacts>
*/
In the above query, I introduced a Name element with two attributes (Title and Suffix) and two sub-elements (First and Last). You can also see that some of the contacts had NULL for the Title and Suffix and therefore those attributes were not created for those contacts.

Note that attributes must be introduced first, before the elements. For example, if I tried to do the following, I would get an error:

select "@ID"=ContactID
,"Name/First"=FirstName
,"Name/Last"=LastName
,"Name/@Title"=Title
,"Name/@Suffix"=Suffix
,Phone
from Person.Contact
where ContactID between 92 and 94
for xml path('Contact'),root('Contacts')
/*
Msg 6852, Level 16, State 1, Line 1
Attribute-centric column 'Name/@Title' must not come after a
non-attribute-centric sibling in XML hierarchy in FOR XML PATH.
*/
When you have two adjacent columns with the same name, then their data will be concatenated together in one element, like so:

select Name=Title
,Name=FirstName
,Name=MiddleName
,Name=LastName
,Name=Suffix
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact><Name>AndreasBerglund</Name></Contact>
<Contact><Name>Mr.RobertM.Bernacchi</Name></Contact>
<Contact><Name>Mr.MatthiasBerndt</Name></Contact>
<Contact><Name>JohnBerry</Name></Contact>
<Contact><Name>Mr.StevenB.BrownIV</Name></Contact>
</Contacts>
*/
Note again that NULL column values are ignored in the concatenation.

If you wanted to construct a nice readable single element consisting of the contact’s full name (Title, FirstName, MiddleName, LastName, and Suffix), you could approach it like this:

select Name=coalesce(Title+' ','')
+FirstName+' '
+coalesce(MiddleName+' ','')
+LastName
+coalesce(' '+Suffix,'')
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact><Name>Andreas Berglund</Name></Contact>
<Contact><Name>Mr. Robert M. Bernacchi</Name></Contact>
<Contact><Name>Mr. Matthias Berndt</Name></Contact>
<Contact><Name>John Berry</Name></Contact>
<Contact><Name>Mr. Steven B. Brown IV</Name></Contact>
</Contacts>
*/
But look all the logic required to handle possible NULL values in the Title and MiddleName and Suffix columns. Well, good news! You can use the following trick. Incorporate data() into the column name as illustrated below, and it will take care of concatenating it all together with spaces between and eliminating all the NULL values automatically:

select "Name/data()"=Title
,"Name/data()"=FirstName
,"Name/data()"=MiddleName
,"Name/data()"=LastName
,"Name/data()"=Suffix
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact><Name>Andreas Berglund</Name></Contact>
<Contact><Name>Mr. Robert M. Bernacchi</Name></Contact>
<Contact><Name>Mr. Matthias Berndt</Name></Contact>
<Contact><Name>John Berry</Name></Contact>
<Contact><Name>Mr. Steven B. Brown IV</Name></Contact>
</Contacts>
*/
However, this approach will not work if you were trying to construct a Name attribute as opposed to a Name element:

select "@Name/data()"=Title
,"@Name/data()"=FirstName
,"@Name/data()"=MiddleName
,"@Name/data()"=LastName
,"@Name/data()"=Suffix
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
Msg 6850, Level 16, State 1, Line 1
Column name '@Name/data()' contains an invalid XML identifier as required by FOR XML;
'@'(0x0040) is the first character at fault.
*/
But you can handle that through a sub-query like so:

select "@Name"=(select "data()"=Title
,"data()"=FirstName
,"data()"=MiddleName
,"data()"=LastName
,"data()"=Suffix
from Person.Contact c2
where c2.ContactID=Contact.ContactID
for xml path(''))
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact Name="Andreas Berglund" />
<Contact Name="Mr. Robert M. Bernacchi" />
<Contact Name="Mr. Matthias Berndt" />
<Contact Name="John Berry" />
<Contact Name="Mr. Steven B. Brown IV" />
</Contacts>
*/
Besides data(), you can also incorporate text() or node() into a column name or give a column a wildcard name (*) and the data will be inserted directly as text. They are all interchangeable, as you can see in the following example:

select "text()"=Title
,"node()"=FirstName
,"*"=MiddleName
,"node()"=LastName
,"text()"=Suffix
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact>AndreasBerglund</Contact>
<Contact>Mr.RobertM.Bernacchi</Contact>
<Contact>Mr.MatthiasBerndt</Contact>
<Contact>JohnBerry</Contact>
<Contact>Mr.StevenB.BrownIV</Contact>
</Contacts>
*/
Remember, two adjacent columns with names that incorporate data() will be separated by a space, but, as you see above, those named with text() or node() or a wildcard are just concatenated directly with no intervening space.

You only really need to specify text() or node() or wildcard names if you want to insert a text element directly subordinate to the main path element, as we saw in the previous query. If, on the other hand, you are inserting text in a sub-element like so…:

select "Name/text()"=Title
,"Name/node()"=FirstName
,"Name/*"=MiddleName
,"Name/node()"=LastName
,"Name/text()"=Suffix
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact><Name>AndreasBerglund</Name></Contact>
<Contact><Name>Mr.RobertM.Bernacchi</Name></Contact>
<Contact><Name>Mr.MatthiasBerndt</Name></Contact>
<Contact><Name>JohnBerry</Name></Contact>
<Contact><Name>Mr.StevenB.BrownIV</Name></Contact>
</Contacts>
*/
…then you’ll see that they are really unnecessary, since the following query (which we looked at earlier) does the exact same thing:

select Name=Title
,Name=FirstName
,Name=MiddleName
,Name=LastName
,Name=Suffix
from Person.Contact
where ContactID between 90 and 94
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact><Name>AndreasBerglund</Name></Contact>
<Contact><Name>Mr.RobertM.Bernacchi</Name></Contact>
<Contact><Name>Mr.MatthiasBerndt</Name></Contact>
<Contact><Name>JohnBerry</Name></Contact>
<Contact><Name>Mr.StevenB.BrownIV</Name></Contact>
</Contacts>
*/
You can also incorporate comment() or processing-instruction() into the column names to create those kinds of elements, as illustrated below:

select "@ID"=ContactID
,"comment()"='Modified on '+convert(varchar(30),ModifiedDate,126)
,"comment()"=case when ContactID=92 then 'Here is Contact#92' end
,"processing-instruction(EmailPromo)"=EmailPromotion
,"Name/@First"=FirstName
,"Name/@Last"=LastName
,"Name"='This is inserted directly as text'
,"Name"='...And so is this'
,"*"='This is inserted as text in the main Contact path'
from Person.Contact
where ContactID between 90 and 92
for xml path('Contact'),root('Contacts')
/*
<Contacts>
<Contact ID="90">
<!--Modified on 2001-08-01T00:00:00-->
<?EmailPromo 0?>
<Name First="Andreas" Last="Berglund">
This is inserted directly as text...And so is this
</Name>
This is inserted as text in the main Contact path
</Contact>
<Contact ID="91">
<!--Modified on 2002-09-01T00:00:00-->
<?EmailPromo 1?>
<Name First="Robert" Last="Bernacchi">
This is inserted directly as text...And so is this
</Name>
This is inserted as text in the main Contact path
</Contact>
<Contact ID="92">
<!--Modified on 2002-08-01T00:00:00-->
<!--Here is Contact#92-->
<?EmailPromo 1?>
<Name First="Matthias" Last="Berndt">
This is inserted directly as text...And so is this
</Name>
This is inserted as text in the main Contact path
</Contact>
</Contacts>
*/
You’ll note above that the two adjacent columns named comment() do NOT concatenate together like other adjacent columns with the same name. They are always separate elements. The same is true for processing-instruction() columns.

You can also concatenate whole individual XML documents together, as illustrated below, where we use two scalar subqueries to construct XML data from the Sales.SalesPerson and Sales.SalesReason tables. Since we did not give actual column names to the two subqueries, they are inserted directly as is. (Note that we could have named each of them node() or a wildcard and it would have worked the same. However, it’s important to note that you may NOT use text() or data() in naming true XML datatype columns):

select (select "@ID"=SalesPersonID
,"@Quota"=SalesQuota
from Sales.SalesPerson
for xml path('Person'),root('SalesPeople'),type)
,(select "@ID"=SalesReasonID
,"@Name"=Name
,"@Type"=ReasonType
from Sales.SalesReason
for xml path('Reason'),root('SalesReasons'),type)
for xml path('MyData')
/*
<MyData>
<SalesPeople>
<Person ID="268" />
<Person ID="275" Quota="300000.0000" />
<Person ID="276" Quota="250000.0000" />
<Person ID="277" Quota="250000.0000" />
<Person ID="278" Quota="250000.0000" />
<Person ID="279" Quota="300000.0000" />
<Person ID="280" Quota="250000.0000" />
<Person ID="281" Quota="250000.0000" />
<Person ID="282" Quota="250000.0000" />
<Person ID="283" Quota="250000.0000" />
<Person ID="284" />
<Person ID="285" Quota="250000.0000" />
<Person ID="286" Quota="250000.0000" />
<Person ID="287" Quota="300000.0000" />
<Person ID="288" />
<Person ID="289" Quota="250000.0000" />
<Person ID="290" Quota="250000.0000" />
</SalesPeople>
<SalesReasons>
<Reason ID="1" Name="Price" Type="Other" />
<Reason ID="2" Name="On Promotion" Type="Promotion" />
<Reason ID="3" Name="Magazine Advertisement" Type="Marketing" />
<Reason ID="4" Name="Television Advertisement" Type="Marketing" />
<Reason ID="5" Name="Manufacturer" Type="Other" />
<Reason ID="6" Name="Review" Type="Other" />
<Reason ID="7" Name="Demo Event" Type="Marketing" />
<Reason ID="8" Name="Sponsorship" Type="Marketing" />
<Reason ID="9" Name="Quality" Type="Other" />
<Reason ID="10" Name="Other" Type="Other" />
</SalesReasons>
</MyData>
*/
Note that the ,TYPE directive was used to make sure that the XML subqueries came through as true XML datatypes. This is very important. If we had left off the ,TYPE directive, they would be processed as strings and then when they were incorporated into the main query, the main FOR XML PATH(‘MyData’) would encode all of the less-than and greater-than signs into this ugly mess:

select (select "@ID"=SalesPersonID
,"@Quota"=SalesQuota
from Sales.SalesPerson
for xml path('Person'),root('SalesPeople'))
,(select "@ID"=SalesReasonID
,"@Name"=Name
,"@Type"=ReasonType
from Sales.SalesReason
for xml path('Reason'),root('SalesReasons'))
for xml path('MyData')
/*
<MyData>
&lt;SalesPeople&gt;
&lt;Person ID="268" /&gt;
&lt;Person ID="275" Quota="300000.0000" /&gt;
&lt;Person ID="276" Quota="250000.0000" /&gt;
&lt;Person ID="277" Quota="250000.0000" /&gt;
&lt;Person ID="278" Quota="250000.0000" /&gt;
&lt;Person ID="279" Quota="300000.0000" /&gt;
&lt;Person ID="280" Quota="250000.0000" /&gt;
&lt;Person ID="281" Quota="250000.0000" /&gt;
&lt;Person ID="282" Quota="250000.0000" /&gt;
&lt;Person ID="283" Quota="250000.0000" /&gt;
&lt;Person ID="284" /&gt;
&lt;Person ID="285" Quota="250000.0000" /&gt;
&lt;Person ID="286" Quota="250000.0000" /&gt;
&lt;Person ID="287" Quota="300000.0000" /&gt;
&lt;Person ID="288" /&gt;
&lt;Person ID="289" Quota="250000.0000" /&gt;
&lt;Person ID="290" Quota="250000.0000" /&gt;
&lt;/SalesPeople&gt;
&lt;SalesReasons&gt;
&lt;Reason ID="1" Name="Price" Type="Other" /&gt;
&lt;Reason ID="2" Name="On Promotion" Type="Promotion" /&gt;
&lt;Reason ID="3" Name="Magazine Advertisement" Type="Marketing" /&gt;
&lt;Reason ID="4" Name="Television Advertisement" Type="Marketing" /&gt;
&lt;Reason ID="5" Name="Manufacturer" Type="Other" /&gt;
&lt;Reason ID="6" Name="Review" Type="Other" /&gt;
&lt;Reason ID="7" Name="Demo Event" Type="Marketing" /&gt;
&lt;Reason ID="8" Name="Sponsorship" Type="Marketing" /&gt;
&lt;Reason ID="9" Name="Quality" Type="Other" /&gt;
&lt;Reason ID="10" Name="Other" Type="Other" /&gt;
&lt;/SalesReasons&gt;
</MyData>
*/
Now that we’ve learned so much about FOR XML PATH, let’s put our knowledge to use. Let’s say that you want to construct a webpage or an e-mail that incorporates a table in HTML format. Using our knowledge of FOR XML PATH, we will construct all the HTML between the <table></table> tags. That can then be incorporated into the correct spot in the webpage or e-mail.

Note that this query below uses most of what we learned in this article. You’ll see the following:
  • We create ALIGN and VALIGN attributes to align the table headers correctly.
  • We put a <br /> tag into the Phone Number header to split it into two lines.
  • We use data() to construct the Full Name of the contact.
  • We create a hyperlink for the E-Mail Address
  • We subtly color the E-Mail Address in a pale yellow color if EmailPromotion is equal to 1.
  • We use the ,TYPE directive in our XML CTEs so that we can concatenate them in subsequent CTEs.

Here’s the query, which creates a single NVARCHAR(MAX) variable called @TableHTML:

declare @TableHTML nvarchar(max);

with HTMLTableHeader(HTMLContent) as
(
select "th/@align"='right'
,"th/@valign"='bottom'
,"th"='ContactID'
,"*"=''
,"th/@valign"='bottom'
,"th"='Full Name'
,"*"=''
,"th"='Phone'
,"th/br"=''
,"th"='Number'
,"*"=''
,"th/@valign"='bottom'
,"th"='Email Address'
for xml path('tr'),type
)
,
HTMLTableDetail(HTMLContent) as
(
select "td/@align"='right'
,"td"=ContactID
,"*"=''
,"td/data()"=Title
,"td/data()"=FirstName
,"td/data()"=MiddleName
,"td/data()"=LastName
,"td/data()"=Suffix
,"*"=''
,"td"=Phone
,"*"=''
,"td/@bgcolor"=case when EmailPromotion=1 then '#FFFF88' end
,"td/a/@href"='mailto:'+EmailAddress
,"td/a"=EmailAddress
from Person.Contact
where ContactID between 90 and 94
for xml path('tr'),type
)
,
HTMLTable(HTMLContent) as
(
select "@border"=1
,(select HTMLContent from HTMLTableHeader)
,(select HTMLContent from HTMLTableDetail)
for xml path('table') /*No TYPE because we want a string */
)
select @TableHTML=(select HTMLContent from HTMLTable);
Remember the rule that if two adjacent columns have the same name, their data will concatenated? I had to prevent that from happening with adjacent columns that I named th and td by inserting a blank column with a wildcard name between them to force them to come out as discrete elements.

And here are the contents of that variable as a result of that query:

/*
<table border="1">
<tr>
<th align="right" valign="bottom">ContactID</th>
<th valign="bottom">Full Name</th>
<th>Phone<br />Number</th>
<th valign="bottom">Email Address</th>
</tr>
<tr>
<td align="right">90</td>
<td>Andreas Berglund</td>
<td>795-555-0116</td>
<td>
<a href="mailto:andreas1@adventure-works.com">andreas1@adventure-works.com</a>
</td>
</tr>
<tr>
<td align="right">91</td>
<td>Mr. Robert M. Bernacchi</td>
<td>449-555-0176</td>
<td bgcolor="#FFFF88">
<a href="mailto:robert4@adventure-works.com">robert4@adventure-works.com</a>
</td>
</tr>
<tr>
<td align="right">92</td>
<td>Mr. Matthias Berndt</td>
<td>384-555-0169</td>
<td bgcolor="#FFFF88">
<a href="mailto:matthias1@adventure-works.com">matthias1@adventure-works.com</a>
</td>
</tr>
<tr>
<td align="right">93</td>
<td>John Berry</td>
<td>471-555-0181</td>
<td>
<a href="mailto:john11@adventure-works.com">john11@adventure-works.com</a>
</td>
</tr>
<tr>
<td align="right">94</td>
<td>Mr. Steven B. Brown IV</td>
<td>280-555-0124</td>
<td>
<a href="mailto:steven1@adventure-works.com">steven1@adventure-works.com</a>
</td>
</tr>
</table>
*/
Our webpage template looks like this, with a placeholder where we want to insert our table:

/*
<html>
<head>
<title>HTML Table constructed via FOR XML PATH</title>
</head>
<body style="font-family:Arial; font-size:small">
<span style="font-size:x-large">
<b>Selected Contacts:</b>
</span>
<!-- Insert Table Here -->
</body>
</html>
*/
And here is the final result, with the table data inserted in the placeholder position, when we look at the web page in Internet Explorer:

HTML Table constructed via FOR XML PATH

I hope this article gave you a tantalizing look at the possibilities of things you can accomplish with the FOR XML PATH clause. In future blog entries, I’ll explore some other aspects of XML.