CTE and FOR XML to generate nested XML - sql-server

I have an adjacency list in the DB and want to deliver the data in XML format to the client through a SQL SP. I'm trying to use CTE and FOR XML but I am not getting the XML nodes to nest.
FYI, this will represent a site map.
The Table structure:
CREATE TABLE [dbo].[PageHierarchy](
[ModuleId] [int] NOT NULL,
[PageId] [int] IDENTITY(1,1) NOT NULL,
[ParentPageId] [int] NULL,
[PageUrl] [nvarchar](100) NULL,
[PageTitle] [nvarchar](50) NOT NULL,
[PageOrder] [int] NULL)
and the beginnings of the CTE:
;WITH cte AS
(
select * from PageHierarchy where ParentPageId is null
union all
select child.* from PageHierarchy child inner join cte parent on parent.PageId = child.ParentPageId
)
SELECT ModuleId, PageId, ParentPageId, PageUrl, PageTitle, PageOrder FROM cte
group by ModuleId, PageId, ParentPageId, PageUrl, PageTitle, PageOrder
order by PageOrder
for xml auto, root ('bob')
yields XML that looks like this:
<bob>
<cte ModuleId="1" PageId="1" PageUrl="~/Admin/" PageTitle="Administration" PageOrder="1000" />
<cte ModuleId="1" PageId="4" ParentPageId="1" PageTitle="Manage Users" PageOrder="1030" />
<cte ModuleId="1" PageId="5" ParentPageId="4" PageUrl="~/Admin/AddUser" PageTitle="Add Users" PageOrder="1040" />
<cte ModuleId="1" PageId="8" ParentPageId="4" PageUrl="~/Admin/EditUser" PageTitle="Edit/Search User" PageOrder="1070" />
</bob>
when what I want is XML that looks like this:
<bob>
<cte ModuleId="1" PageId="1" PageUrl="~/Admin/" PageTitle="Administration" PageOrder="1000" />
<cte ModuleId="1" PageId="4" ParentPageId="1" PageTitle="Manage Users" PageOrder="1030" >
<cte ModuleId="1" PageId="5" ParentPageId="4" PageUrl="~/Admin/AddUser" PageTitle="Add Users" PageOrder="1040" />
<cte ModuleId="1" PageId="8" ParentPageId="4" PageUrl="~/Admin/EditUser" PageTitle="Edit/Search User" PageOrder="1070" />
</cte>
</bob>
I'm guessing the issue is not with the CTE but with the select, but I don't know where to start to fix it. Also, I don't know how deep the nesting will go, so I'm assuming I'll need it to support at least 10 levels deep.
Edit 1:
I think I'm getting closer... in looking at this page, I created a UDF but still there are some issues:
CREATE FUNCTION PageHierarchyNode(#PageId int)
RETURNS XML
WITH RETURNS NULL ON NULL INPUT
BEGIN RETURN
(SELECT ModuleId AS "#ModuleId", PageId AS "#PageId",
ParentPageId AS "#ParentPageId", PageUrl AS "#PageUrl",
PageTitle AS "#PageTitle", PageOrder AS "#PageOrder",
CASE WHEN ParentPageId=#PageId
THEN dbo.PageHierarchyNode(PageId)
END
FROM dbo.PageHierarchy WHERE ParentPageId=#PageId
FOR XML PATH('Page'), TYPE)
END
and the SQL that calls the UDF
SELECT ModuleId AS "#ModuleId", PageId AS "#PageId",
ParentPageId AS "#ParentPageId", PageUrl AS "#PageUrl",
PageTitle AS "#PageTitle", PageOrder AS "#PageOrder",
dbo.PageHierarchyNode(PageId)
FROM PageHierarchy
FOR XML PATH('Page'), ROOT('SiteMap'), TYPE
this will nest the XML for me but it's duplicating nodes which is not what I want..
Edit 2:
I just needed to add a WHERE clause to the SELECT that calls the UDF:
...
WHERE ParentPageId IS NULL

Turns out I didn't want the CTE at all, just a UDF that I call recursively
CREATE FUNCTION PageHierarchyNode(#PageId int)
RETURNS XML
WITH RETURNS NULL ON NULL INPUT
BEGIN RETURN
(SELECT ModuleId AS "#ModuleId", PageId AS "#PageId",
ParentPageId AS "#ParentPageId", PageUrl AS "#PageUrl",
PageTitle AS "#PageTitle", PageOrder AS "#PageOrder",
CASE WHEN ParentPageId=#PageId
THEN dbo.PageHierarchyNode(PageId)
END
FROM dbo.PageHierarchy WHERE ParentPageId=#PageId
FOR XML PATH('Page'), TYPE)
END
with the SQL that calls the UDF as
SELECT ModuleId AS "#ModuleId", PageId AS "#PageId",
ParentPageId AS "#ParentPageId", PageUrl AS "#PageUrl",
PageTitle AS "#PageTitle", PageOrder AS "#PageOrder",
dbo.PageHierarchyNode(PageId)
FROM PageHierarchy
WHERE ParentPageId IS NULL
FOR XML PATH('Page'), ROOT('SiteMap'), TYPE

The question as well as the OP's answer helped me a lot. It took me a bit to grasp the answer as I was missing some context. So here's a seperate answer with a more generic explanation (I've tried to remove every bit of code that didn't relate directly to getting hierarchical data in XML output).
Suppose the following typical table for hierarchical data:
CREATE TABLE Employee (Id INT, BossId INT, Name NVARCHAR(50));
Suppose it has the following data:
INSERT INTO Employee (Id, BossId, Name) VALUES
(1, NULL, 'Boss Pancone'),
(2, 1, 'Capioregime Luciano'),
(3, 1, 'Capioregime Bruno'),
(4, 2, 'Johnny'),
(5, 2, 'Luca'),
(6, 2, 'Luciano jr.'),
(7, 3, 'Marco'),
(8, 3, 'Mario'),
(9, 3, 'Giacomo');
To get hierarchical XML data we could use the following function:
ALTER FUNCTION dbo.fn_EmployeeHierarchyNode (#BossId INT) RETURNS XML
BEGIN RETURN
(SELECT Id,
BossId,
Name,
dbo.fn_EmployeeHierarchyNode(Id)
FROM Employee
WHERE BossId = #BossId
FOR XML AUTO)
END;
Which can be called like this:
SELECT dbo.fn_EmployeeHierarchyNode(1)
Or, if you want the root node as well, like this:
SELECT Id,
BossId,
Name,
dbo.fn_EmployeeHierarchyNode(Id)
FROM Employee
WHERE BossId IS NULL
FOR XML AUTO
Which would produce:
<Employee Id="1" Name="Boss Pancone">
<Employee Id="2" BossId="1" Name="Capioregime Luciano">
<Employee Id="4" BossId="2" Name="Johnny" />
<Employee Id="5" BossId="2" Name="Luca" />
<Employee Id="6" BossId="2" Name="Luciano jr." />
</Employee>
<Employee Id="3" BossId="1" Name="Capioregime Bruno">
<Employee Id="7" BossId="3" Name="Marco" />
<Employee Id="8" BossId="3" Name="Mario" />
<Employee Id="9" BossId="3" Name="Giacomo" />
</Employee>
</Employee>

Recursive CTEs are not recursive as in "nested", they work differently and what you're trying to do doesn't work with CTEs. (Think of them as being always tail-recursive.)
The only way I have found to build recursive XML in SQL Server is by creating a scalar function which renders the nodes recursively; functions can make recursive calls so that this works as expected.

Related

How to query on XML column in SQL Server?

I have a table named "Table1" and in the table there are columns titled "Name" and "XMLDefinition".
Name XMLDefinition
--------------------------
Name1 xmlLink1
Name2 xmlLink2
Inside each XML an example would look similar to this below:
<Query xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<QueryView>
<QueryKey />
</QueryView>
<Description>
</Description>
<QueryFields>
<f />
<f />
</QueryFields>
<FilterFields>
<f ObjectName="TABLE5" ColumnName="ID">
<DateFilterTypes />
<FuzzyDateFilterTypes />
<MonthDayFilterTypes />
<Values>
<v>0</v>
</Values>
<TranslatedValues>
<v>No</v>
</TranslatedValues>
<DataType>Boolean</DataType>
</f>
<f ObjectName="TABLE2" ColumnName="USERID">
<DateFilterTypes />
<FuzzyDateFilterTypes />
<MonthDayFilterTypes />
<Values>
<v>B80055</v>
</Values>
<TranslatedValues>
<v>B80055</v>
</TranslatedValues>
<DataType>String</DataType>
</f>
</FilterFields>
</Query>
I'd like to return Name from TABLE1 as long as in the XML content contains where ObjectName = "TABLE2" AND ColumnName = "USERID".
I have tried the below and while it doesn't error out, it returns 0 records:
SELECT
a.Name,
X.Y.value('(f)[1]', 'VARCHAR(MAX)') as Object
FROM TABLE1 a
OUTER APPLY a.XMLDefinition.nodes('Query/FilterFields/f') as X(Y)
WHERE X.Y.value('(ObjectName)[1]', 'VARCHAR(MAX)') = 'TABLE2'
AND X.Y.value('(ColumnName)[1]', 'VARCHAR(MAX)') = 'USERID'
I'm not sure what I am missing as it seems I am drilling down from Query > FilterFields > f and I assume I'd be able to then filter based on the ObjectName and ColumnName here.
Attempt 2 Update:
SELECT Name from TABLE1
WHERE XMLDefinition.value('(/Query/QueryView/Description/QueryFields/FilterFields/f/#ObjectName) [1] ',' varchar(max)') = 'TABLE2'
AND XMLDefinition.value('(/Query/QueryView/Description/QueryFields/FilterFields/f/#ColumnName) [1] ',' varchar(max)') = 'USERID'
After trying this attempt by drilling through each tag, it is still giving me 0 results.
Attempt 3 Update:
select
a.Name,
X.Y.query(N'.') as [Object] --this returns the XML of the <f> element
from dbo.Table1 a
cross apply a.XMLDefinition.nodes('//*:f[#ObjectName="TABLE2"][#ColumnName="USERID"][1]') as X(Y);
I'm not sure why, but I tried this and now it worked and returned the results that I was looking for. I'm new to XML, but I assume this worked because it ignored all the namespaces and prior tags before the f tag?
The code below probably does what you're looking for. Note that cross apply will only return dbo.Table rows that matched the XPath query, as opposed to outer apply which will return all dbo.Table rows but only XML-derived values for those rows that matched the XPath query:
create table dbo.Table1 (
Name nvarchar(10),
XMLDefinition xml
);
insert dbo.Table1 (Name, XMLDefinition) values
(N'Name1', N'<xmlLink1 />'),
(N'Name2', N'<Query xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<FilterFields>
<f ObjectName="TABLE2" ColumnName="USERID" ParentPath="TABLE2" DisplayPath="TABLE2" CompareType="Or" UseLeftParenthesis="true" LeftParenthesisCount="1" IncludeCurrentNode="true">
<DateFilterTypes />
<FuzzyDateFilterTypes />
<MonthDayFilterTypes />
<Values>
<v>B80055</v>
</Values>
<TranslatedValues>
<v>B80055</v>
</TranslatedValues>
<DataType>String</DataType>
</f>
</FilterFields>
</Query>');
select
a.Name,
X.Y.query(N'.') as [Object] --this returns the XML of the <f> element
from dbo.Table1 a
cross apply a.XMLDefinition.nodes(N'/Query/FilterFields/f[#ObjectName="TABLE2"][#ColumnName="USERID"][1]') as X(Y);

Create where condition from XML value with multiple namespaces

I'm looking to return value for my where condition from an XML.
I'd like to return a value from messages table's Request column. Which is in XML data format. Unfortunately all I could achieve is retrieving nothing.
Then I tried to put the value as a column but I always get nulls for the values
Here's an XML from Request column:
<InvoiceRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/InternalReuqests">
<ActiveUserID xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">0</ActiveUserID>
<LinqConfigId xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">0</LinqConfigId>
<RequestHeaderInfo xmlns:d2p1="Fix.Services" xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">
<d2p1:MapArchive i:nil="true" />
<d2p1:HandledSuccessCategory>rscNone</d2p1:HandledSuccessCategory>
</RequestHeaderInfo>
<Username xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase" i:nil="true" />
<SSID>S-1-6-25-123456789-123456789-123456789-12345</SSID>
<miscdata xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:string>date:2020.02.26 08:27:00</d2p1:string>
<d2p1:string>hours:0</d2p1:string>
<d2p1:string>Ready:True</d2p1:string>
<d2p1:string>disct:False</d2p1:string>
<d2p1:string>extdisct:False</d2p1:string>
<d2p1:string>Matmove:False</d2p1:string>
<d2p1:string>Matlim:0</d2p1:string>
<d2p1:string>Comments:</d2p1:string>
</miscdata>
<ffreeID>468545</ffreeID>
</InvoiceRequest>
here's my sql query:
select id, Request.value('(/*:InvoiceRequest/*:ffreeID)[1]','varchar(max)')
from messages
I thought I should get in the first column the id from the database, and next to it the value of the ffreeID, but the Request.value is always null.
Could anyone look into it what am I missing?
You need for declare the default namespace, which for your xml is http://schemas.datacontract.org/2004/07/InternalReuqests:
--Sample XML
DECLARE #xml xml = '<InvoiceRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/InternalReuqests">
<ActiveUserID xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">0</ActiveUserID>
<LinqConfigId xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">0</LinqConfigId>
<RequestHeaderInfo xmlns:d2p1="Fix.Services" xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">
<d2p1:MapArchive i:nil="true" />
<d2p1:HandledSuccessCategory>rscNone</d2p1:HandledSuccessCategory>
</RequestHeaderInfo>
<Username xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase" i:nil="true" />
<SSID>S-1-6-25-123456789-123456789-123456789-12345</SSID>
<miscdata xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:string>date:2020.02.26 08:27:00</d2p1:string>
<d2p1:string>hours:0</d2p1:string>
<d2p1:string>Ready:True</d2p1:string>
<d2p1:string>disct:False</d2p1:string>
<d2p1:string>extdisct:False</d2p1:string>
<d2p1:string>Matmove:False</d2p1:string>
<d2p1:string>Matlim:0</d2p1:string>
<d2p1:string>Comments:</d2p1:string>
</miscdata>
<ffreeID>468545</ffreeID>
</InvoiceRequest>'; --Assumed this should be </InvoiceRequest>, not <InvoiceRequest>.
--Get value
WITH XMLNAMESPACES(DEFAULT 'http://schemas.datacontract.org/2004/07/InternalReuqests')
SELECT X.Request.value('(/InvoiceRequest/ffreeID/text())[1]','int')
FROM (VALUES(#XML))X(Request);
Here is another way by simulating a mock table. Everything else resembles #Larnu's solution. All credit goes to #Larnu.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, Request XML);
INSERT INTO #tbl (Request)
VALUES
(N'<InvoiceRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.datacontract.org/2004/07/InternalReuqests">
<ActiveUserID xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">0</ActiveUserID>
<LinqConfigId xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">0</LinqConfigId>
<RequestHeaderInfo xmlns:d2p1="Fix.Services"
xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase">
<d2p1:MapArchive i:nil="true"/>
<d2p1:HandledSuccessCategory>rscNone</d2p1:HandledSuccessCategory>
</RequestHeaderInfo>
<Username xmlns="http://schemas.datacontract.org/2004/07/Fix.ServiceBase" i:nil="true"/>
<SSID>S-1-6-25-123456789-123456789-123456789-12345</SSID>
<miscdata xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:string>date:2020.02.26 08:27:00</d2p1:string>
<d2p1:string>hours:0</d2p1:string>
<d2p1:string>Ready:True</d2p1:string>
<d2p1:string>disct:False</d2p1:string>
<d2p1:string>extdisct:False</d2p1:string>
<d2p1:string>Matmove:False</d2p1:string>
<d2p1:string>Matlim:0</d2p1:string>
<d2p1:string>Comments:</d2p1:string>
</miscdata>
<ffreeID>468545</ffreeID>
</InvoiceRequest>');
-- DDL and sample data population, end
WITH XMLNAMESPACES(DEFAULT 'http://schemas.datacontract.org/2004/07/InternalReuqests')
SELECT ID
, c.value('(ffreeID/text())[1]','INT') AS ffreeID
FROM #tbl AS tbl
CROSS APPLY tbl.Request.nodes('/InvoiceRequest') AS t(c);
Output
+----+---------+
| ID | ffreeID |
+----+---------+
| 1 | 468545 |
+----+---------+

SQL Server FOR XML with nested sub-select

The following sql code
SELECT
b.GivenName [Individual/Name/FirstName] ,
b.FamilyName [Individual/Name/LastName] ,
b.Address_Country [Individual/Address/CountryCode] ,
b.AddressFree [Individual/Address/AddressFree]
FROM dbo.stage_Clients_Merge b
WHERE b.DWHClientNo = #dwhclientNo
FOR XML PATH('Owner'), TYPE
will generate an xml fragment
<Owner>
<Individual>
<Name>
<FirstName>Bob</FirstName>
<LastName>Smith</LastName>
</Name>
<Address>
<CountryCode>US</CountryCode>
<AddressFree>123,Utah</AddressFree>
</Address>
</Individual>
</Owner>
If I extend this query to include a sub-query to return, for example, all the orders associated with a individual using the following:
(SELECT
c.OrderIdentificationNumber [Individual/OrderNumber]
FROM
dbo.stage_Customer_Orders c
WHERE
c.CustomerNo = b.masterClient
FOR
XML PATH(''), TYPE)
I will get the individual section twice. How can I combine the two to return the following?
<Owner>
<Individual>
<OrderNo>12345</OrderNo>
<OrderNo>23456</OrderNo>
<Name>
<FirstName>Bob</FirstName>
<LastName>Smith</LastName>
</Name>
<Address>
<CountryCode>US</CountryCode>
<AddressFree>123,Utah</AddressFree>
</Address>
</Individual>
</Owner>
Try it like this
I set up a mock-up scenario for stand-alone solutions. Next time you have a question, you might try this on your own. This makes it much easier to understand your issue and create a fully working and testable solution for it.
DECLARE #stage_Clients_Merge TABLE(ID INT,FirstName VARCHAR(100),LastName VARCHAR(100),CountryCode VARCHAR(100),AdressFree VARCHAR(100));
INSERT INTO #stage_Clients_Merge VALUES
(1,'Bob','Smith','US','123/Utah')
,(2,'Jim','Doh','US','123/Maryland');
DECLARE #orders TABLE(CustomerID INT, OrderNo INT)
INSERT INTO #orders VALUES
(1,11111),(1,12222)
,(2,21111),(2,22222);
SELECT (SELECT OrderNo FROM #orders WHERE CustomerID=b.ID FOR XML PATH(''),TYPE) AS Individual
,FirstName AS [Individual/Name/FirstName]
,LastName AS [Individual/Name/LastName]
,CountryCode AS [Individual/Address/CountryCode]
,AdressFree AS [Individual/Address/AdressFree]
FROM #stage_Clients_Merge AS b
FOR XML PATH('Owner');
You can use a subquery and [data()] to just get the column values with no additional nesting - then use path as before:
SELECT
(SELECT c.OrderIdentificationNumber as [data()]
FROM dbo.stage_Customer_Orders c
WHERE c.CustomerNo = b.masterClient
For XML Path('OrderNo'), type) as [Individual],
b.GivenName [Individual/Name/FirstName] ,
b.FamilyName [Individual/Name/LastName] ,
b.Address_Country [Individual/Address/CountryCode] ,
b.AddressFree [Individual/Address/AddressFree]
FROM dbo.stage_Clients_Merge b
WHERE b.DWHClientNo = #dwhclientNo
FOR XML PATH('Owner'), TYPE

SQL Server WHERE clause XPath with multiple nodes and conditions

I have this XML in a table column in SQL Server:
<root>
<Request>
<RequestData>
<VendorLeadList>
<VendorLeadItem>
<CampaignOfferTypeID>REN</CampaignOfferTypeID>
<LeadDispositionID>Lead</LeadDispositionID>
<Jurisdiction>NY</Jurisdiction>
<FirstName>Shikasta</FirstName>
<LastName>Kashti</LastName>
<MessageId>1347_1483825159115_c8273</MessageId>
</VendorLeadItem>
</VendorLeadList>
</RequestData>
<DualMessageID/>
<AzureBlobFile/>
<AzureBlobImageList/>
</Request>
</root>
I want to query all the records where it matches some nodes with specific values. For example I want all records where LeadDispositionID=Lead and Jurisdiction=NY and CampaignOfferTypeID=REN and a MessageId element exists (doesn't matter what value.)
I tried this but it doesn't work (no errors but the conditions doesn't match and it returns other records):
SELECT TOP 10 *
FROM [Messages]
WHERE PayLoadXml.exist('//LeadDispositionID[.="Lead"] and //CampaignOfferTypeID[.="REN"] and //Jurisdiction[.="NY"] and //MessageId') = 1
ORDER BY ID DESC
Any idea as to what I'm doing wrong?
You cannot combine nodes within .exist() simply with and. Your own example would work like this:
SELECT TOP 10 *
FROM #Messages
WHERE PayLoadXml.exist('//VendorLeadItem[LeadDispositionID[.="Lead"] and CampaignOfferTypeID[.="REN"] and Jurisdiction[.="NY"] and MessageId/text()]') = 1
Try it like this:
First a declared table to mock-up your Messages table. Insert 3 cases:
DECLARE #messages TABLE(SomeDescription VARCHAR(100),PayLoadXml XML);
INSERT INTO #messages VALUES
('Your example'
,'<root>
<Request>
<RequestData>
<VendorLeadList>
<VendorLeadItem>
<CampaignOfferTypeID>REN</CampaignOfferTypeID>
<LeadDispositionID>Lead</LeadDispositionID>
<Jurisdiction>NY</Jurisdiction>
<FirstName>Shikasta</FirstName>
<LastName>Kashti</LastName>
<MessageId>1347_1483825159115_c8273</MessageId>
</VendorLeadItem>
</VendorLeadList>
</RequestData>
<DualMessageID/>
<AzureBlobFile/>
<AzureBlobImageList/>
</Request>
</root>'
)
,('LeadDispositionID=Slave'
,'<root>
<Request>
<RequestData>
<VendorLeadList>
<VendorLeadItem>
<CampaignOfferTypeID>REN</CampaignOfferTypeID>
<LeadDispositionID>Slave</LeadDispositionID>
<Jurisdiction>NY</Jurisdiction>
<FirstName>Bruno</FirstName>
<LastName>Kashti</LastName>
<MessageId>1347_1483825159115_c8273</MessageId>
</VendorLeadItem>
</VendorLeadList>
</RequestData>
<DualMessageID/>
<AzureBlobFile/>
<AzureBlobImageList/>
</Request>
</root>'
)
,('LeadDispositionID=Lead but No MessageId'
,'<root>
<Request>
<RequestData>
<VendorLeadList>
<VendorLeadItem>
<CampaignOfferTypeID>REN</CampaignOfferTypeID>
<LeadDispositionID>Lead</LeadDispositionID>
<Jurisdiction>NY</Jurisdiction>
<FirstName>Bruno</FirstName>
<LastName>Kashti</LastName>
<MessageId></MessageId>
</VendorLeadItem>
</VendorLeadList>
</RequestData>
<DualMessageID/>
<AzureBlobFile/>
<AzureBlobImageList/>
</Request>
</root>'
);
This is the query:
The CROSS APPLY will ensure, that only nodes with a MessageId are taken into account. The WHERE will apply an additional filter
SELECT m.*
FROM #messages AS m
CROSS APPLY m.PayLoadXml.nodes(N'/root/Request/RequestData/VendorLeadList/VendorLeadItem[not(empty(MessageId/text()))]') AS A(itm)
WHERE itm.exist(N'LeadDispositionID[text()="Lead"]')=1
If you need to check more than one condition you might use this:
WHERE itm.exist(N'.[LeadDispositionID="Slave" and FirstName="Bruno"]')=1

May I retrieve a hierarchical data structure from a SQL Server stored procedure?

I have a Web Service one of whose methods returns a list of items, each of which possesses another list of items:
<TopLevelItems>
<TopLevelItem field1="a" field2="b" ...>
<LowLevelItem fieldA="1" fieldB="2" .../>
<LowLevelItem fieldA="3" fieldB="4" .../>
</TopLevelItem>
</TopLevelItems>
These lists are retrieved from a SQL Server database using simple queries (both TopLevelItem and LowLevelItem correspond to related tables in the database).
Until now, to retrieve all this data, I needed two queries: one to retrieve top level items, which was executed once; and another to retrieve low level items, which was executed once per top level item.
However, this seems to be highly inefficient. I would like to define a single stored procedure that performs all the necessary queries and retrieves the result as a hierarchical data structure. Is it possible? If so, how?
Hierarchical data in SQL server can be obtained using FOR XML. In this case, you would just need to write a query to join tables, then parent-child relationships will manifest as nested XML elements:
DECLARE #sites TABLE ( ID INT, Name VARCHAR(50) )
INSERT INTO #sites
VALUES ( 1, 'abc' ),
( 2, 'def' )
DECLARE #siteEnergy TABLE
(
SiteFK INT,
Month INT,
Energy INT
)
INSERT INTO #siteEnergy
VALUES ( 1, 1, 50 ),
( 1, 2, 49 ),
( 1, 3, 50 ),
( 2, 1, 33 ),
( 2, 2, 34 ),
( 2, 3, 50 )
SELECT *
FROM #sites site
JOIN #siteEnergy siteEnergy ON site.id = siteEnergy.sitefk
FOR XML AUTO, ROOT('SiteInformation')
Results:
<SiteInformation>
<site ID="1" Name="abc">
<siteEnergy SiteFK="1" Month="1" Energy="50" />
<siteEnergy SiteFK="1" Month="2" Energy="49" />
<siteEnergy SiteFK="1" Month="3" Energy="50" />
</site>
<site ID="2" Name="def">
<siteEnergy SiteFK="2" Month="1" Energy="33" />
<siteEnergy SiteFK="2" Month="2" Energy="34" />
<siteEnergy SiteFK="2" Month="3" Energy="50" />
</site>
</SiteInformation>
Use can create the XML in the SP directly
Example
declare #TopLevelItem table (TopID int, field1 varchar(50), field2 varchar(50))
declare #LowLevelItem table (TopID int, fieldA int, fieldB int)
insert into #TopLevelItem values (1, 'a', 'b')
insert into #LowLevelItem values (1, 1, 2)
insert into #LowLevelItem values (1, 3, 4)
select
T.field1 as '#field1',
T.field2 as '#field2',
((select
L.fieldA as '#fieldA',
L.fieldB as '#fieldB'
from #LowLevelItem as L
where T.TopID = L.TopID
for xml path('LowLevelItem'), type))
from #TopLevelItem as T
for xml path('TopLevelItem'), root('TopLevelItems')
Result
<TopLevelItems>
<TopLevelItem field1="a" field2="b">
<LowLevelItem fieldA="1" fieldB="2" />
<LowLevelItem fieldA="3" fieldB="4" />
</TopLevelItem>
</TopLevelItems>

Resources