Xml structure using For Xml in Sql Server - sql-server

I am trying to create a xml ouput for my table using this approach but it seems to duplicate multiple nodes for TopLevelItem .Ideally i was hoping the format would be like this :
<TopLevelItems>
<TopLevelItem field1="1">
<LowLevelItem fieldA="a" />
<LowLevelItem fieldA="b" />
<LowLevelItem fieldA="def" />
</TopLevelItem>
<TopLevelItem field1="2">
<LowLevelItem fieldA="c" />
<LowLevelItem fieldA="d" />
</TopLevelItem>
</TopLevelItems>
DECLARE #sites TABLE (username varchar(50), ID INT, Name VARCHAR(50) )
INSERT INTO #sites
VALUES ('a', 1, 'a' ),
('a', 1, 'b' ),
('a', 2, 'c' ),
('a', 2, 'd' ),
('b', 1, 'def' )
select
T.ID as '#field1',
((select
L.Name as '#fieldA'
from #sites as L
where T.ID = L.ID
for xml path('LowLevelItem'), type))
from #sites as T
for xml path('TopLevelItem'), root('TopLevelItems')
Let me know if i am missing anything in my query or the approach that i am using is not right.
Thanks in advance.

SELECT T.ID AS '#field1'
, ( (SELECT L.Name AS '#fieldA'
FROM #sites AS L
WHERE T.ID = L.ID
FOR
XML PATH('LowLevelItem')
, TYPE)
)
FROM (SELECT DISTINCT ID FROM #sites) AS T
FOR XML PATH('TopLevelItem')
, ROOT('TopLevelItems')

Related

SQL Synapse Compare data between Column(multiple values) and Column(multiple values)

I need to compare 2 columns from 2 tables.
table a:
ID|TEL
----------------
A1|1111,2222,3333
TABLE B:
ID|TEL
----------------
A1|2222,4444
Result should update in TABLE A
A1|1111,2222,3333,4444
As I know, maybe I should use select value from string_split (B.Tel,'|') to split it. However, I don't know how to loop to compare between A and B.
Please help.
Here is what I've tried but it's not working.
with split_tel as
(select id,
Value tel
from B
CROSS APPLY STRING_SPLIT(tel, ','))
,pre as (select sp.id
,sp.tel as split
,A.tel as target
from split_tel sp
inner join A
on sp.id = A.id)
select id,split,target
from pre
where split like '%' + target + '%' ;
First of all you should not really be storing your data like this, particularly if you are having to conduct set-based operations on it. However there is a simple solution using STRING_SPLIT and STRING_AGG if you are always effectively adding numbers, not taking away:
IF OBJECT_ID('tempdb..#tmpA') IS NOT NULL DROP TABLE #tmpA
IF OBJECT_ID('tempdb..#tmpB') IS NOT NULL DROP TABLE #tmpB
CREATE TABLE #tmpA (
id VARCHAR(5) NOT NULL,
tel VARCHAR(100) NOT NULL
);
CREATE TABLE #tmpB (
id VARCHAR(5) NOT NULL,
tel VARCHAR(100) NOT NULL
);
--INSERT INTO #tmpA VALUES ( 'A1', '1111,2222,3333' );
--INSERT INTO #tmpB VALUES ( 'A1', '2222,4444' );
INSERT INTO #tmpA
SELECT 'A1', '1111,2222,3333'
UNION ALL
SELECT 'B1', '2222'
INSERT INTO #tmpB
SELECT 'A1', '2222,4444'
UNION ALL
SELECT 'B1', '3333'
SELECT id, STRING_AGG( value, ',' ) tel
FROM
(
SELECT a.id, x.value
FROM #tmpA a
CROSS APPLY STRING_SPLIT( a.tel, ',' ) x
UNION
SELECT b.id, x.value
FROM #tmpB b
CROSS APPLY STRING_SPLIT( b.tel, ',' ) x
) y
GROUP BY id;

Unable to pass Table Column name in xml path in SQL Server

SELECT
CASE WHEN D.DocumentCode='SA' THEN D.Name END AS 'IDProofofsigningauthority',
CASE WHEN D.DocumentCode='GSTIN' THEN D.Name END AS 'GSTINRegistrationCopy',
CASE WHEN D.DocumentCode='SA_T' OR D.DocumentCode='SA_E' THEN D.Name END AS 'IDProofofsigningauthority',
CASE WHEN D.DocumentCode='PAN_T' THEN D.Name END AS 'PANCard',
CONCAT ('https://abc/xyz/',"Filename") AS KYCDocumentUrl,
CASE WHEN D.DocumentCode='GSTIN' THEN BPD.DocumentNumber END AS 'GSTINRegistrationCopyDocumentNumber',
CASE WHEN D.DocumentCode='PAN_T' THEN BPD.DocumentNumber END AS 'PANCardDocumentNumber'
FROM
[dbo].[Documents] D
INNER JOIN
[dbo].[BusinessPartyDcoument] BPD WITH (NOLOCK) ON D.Id = BPD.DocumentId
FOR XML PATH('Document')
This is my current output:
<Document>
<GSTINRegistrationCopy>GSTIN Number</GSTINRegistrationCopy>
<KYCDocumentUrl>https://abc/xyz/R1NUSU5fMTEy.pdf</KYCDocumentUrl>
<GSTINRegistrationCopyNumber>1111</GSTINRegistrationCopyNumber>
</Document>
<Document>
<PANCard>PAN Card</PANCard>
<KYCDocumentUrl>https://abc/xyz/UEFOX1RfNjFfOC8yLzIwMTkgN.pdf</KYCDocumentUrl>
<PANCardDocumentNumber>BBBBB1111V</PANCardDocumentNumber>
</Document>
<Document>
<IDProofauthority>ID Proof of signing authority</IDProofauthority>
<KYCDocumentUrl>https://abc/xyz/U0FfNjFfOC8yLzIwMTkgNjo1.pdf</KYCDocumentUrl>
</Document>
This is my desired output:
<GSTINRegistrationCopy>
<DocumentName>GSTIN Number</DocumentName>
<KYCDocumentUrl>https://abc/xyz/R1NUSU5fMTEy.pdf</KYCDocumentUrl>
<DocumentNumber>1111</DocumentNumber>
</GSTINRegistrationCopy>
<PANCard>
<DocumentName>PAN Card</DocumentName>
<KYCDocumentUrl>https://abc/xyz/UEFOX1RfNjFfOC8yLzIwMTkgN.pdf</KYCDocumentUrl>
<DocumentNumber>BBBBB1111V</DocumentNumber>
</PANCard>
<IDProofauthority>
<DocumentName>ID Proof of signing authority</DocumentName>
<KYCDocumentUrl>https://abc/xyz/U0FfNjFfOC8yLzIwMTkgNjo1.pdf</KYCDocumentUrl>
</IDProofauthority>
I need to get document name instead of <Document> tag as shown in the output using SQL Server query. I need to get expected XML output using my query.
Please suggest how to get this done.
Actually, you want nested xml for each "case" in you "case when".
You can use "for xml path" and "for xml path, type" in nested queries.
But this solution has obvious problems with performance, you can't use it for big amounts of data
declare #Document table
(
DocumentCode varchar(10),
Name varchar(100),
filename varchar(100),
DocumentNumber varchar(20)
)
insert into #document values
('PAN_T','PAN Card','UEFOX1RfNjFfOC8yLzIwMTkgN.pdf','BBBBB1111V'),
('GSTIN','GSTIN Number','R1NUSU5fMTEy.pdf','1111'),
('SA','D Proof of signing authority','U0FfNjFfOC8yLzIwMTkgNjo1.pdf',NULL);
SELECT
CASE WHEN D.DocumentCode='GSTIN' THEN
(
select D.Name 'DocumentName',
'https://abc/xyz/'+d.Filename 'KYCDocumentUrl',
D.DocumentNumber 'DocumentNumber'
for xml path('') ,type
) END GSTINRegistrationCopy
,CASE WHEN D.DocumentCode='PAN_T' THEN
(
select D.Name 'PANCard',
'https://abc/xyz/'+d.Filename 'KYCDocumentUrl',
D.DocumentNumber 'DocumentNumber'
for xml path('') ,type
) END PANCard
,CASE WHEN D.DocumentCode='SA' THEN
(
select D.Name 'DocumentName',
'https://abc/xyz/'+d.Filename 'KYCDocumentUrl'
for xml path('') ,type
) END IDProofauthority
FROM
#Document D
FOR XML PATH('')
(Similar to vitalygolub's answer...)
Given source data structured like the OPs:
if object_id('[dbo].[Documents]') is not null drop table [dbo].[Documents];
if object_id('[dbo].[BusinessPartyDcoument]') is not null drop table [dbo].[BusinessPartyDcoument];
select * into [dbo].[Documents]
from (values
(1, 'GSTIN', 'GSTIN Number'),
(2, 'PAN_T', 'PAN Card'),
(3, 'SA', 'ID Proof of signing authority')
) Src ([Id], [DocumentCode], [Name]);
select * into [dbo].[BusinessPartyDcoument]
from (values
(1, 'R1NUSU5fMTEy.pdf', '1111'),
(2, 'UEFOX1RfNjFfOC8yLzIwMTkgN.pdf', 'BBBBB1111V'),
(3, 'U0FfNjFfOC8yLzIwMTkgNjo1.pdf', null)
) Src ([DocumentId], [Filename], [DocumentNumber]);
The following SQL:
SELECT
(
SELECT
D.Name AS 'DocumentName',
DocumentUrl AS 'KYCDocumentUrl',
BPD.DocumentNumber AS 'DocumentNumber'
WHERE D.DocumentCode='GSTIN'
FOR XML PATH('GSTINRegistrationCopy'), TYPE
),
(
SELECT
D.Name AS 'DocumentName',
DocumentUrl AS 'KYCDocumentUrl',
BPD.DocumentNumber AS 'DocumentNumber'
WHERE D.DocumentCode='PAN_T'
FOR XML PATH('PANCard'), TYPE
),
(
SELECT
D.Name AS 'DocumentName',
DocumentUrl AS 'KYCDocumentUrl'
WHERE D.DocumentCode='SA'
FOR XML PATH('IDProofofsigningauthority'), TYPE
)
FROM
[dbo].[Documents] D
INNER JOIN
[dbo].[BusinessPartyDcoument] BPD WITH (NOLOCK) ON D.Id = BPD.DocumentId
OUTER APPLY (
SELECT DocumentUrl = CONCAT('https://abc/xyz/', "Filename")
) DU
FOR XML PATH('');
Outputs the following XML:
<GSTINRegistrationCopy>
<DocumentName>GSTIN Number</DocumentName>
<KYCDocumentUrl>https://abc/xyz/R1NUSU5fMTEy.pdf</KYCDocumentUrl>
<DocumentNumber>1111</DocumentNumber>
</GSTINRegistrationCopy>
<PANCard>
<DocumentName>PAN Card</DocumentName>
<KYCDocumentUrl>https://abc/xyz/UEFOX1RfNjFfOC8yLzIwMTkgN.pdf</KYCDocumentUrl>
<DocumentNumber>BBBBB1111V</DocumentNumber>
</PANCard>
<IDProofofsigningauthority>
<DocumentName>ID Proof of signing authority</DocumentName>
<KYCDocumentUrl>https://abc/xyz/U0FfNjFfOC8yLzIwMTkgNjo1.pdf</KYCDocumentUrl>
</IDProofofsigningauthority>

TSQL xml output with namespaces [duplicate]

UPDATE: I've discovered there is a Microsoft Connect item raised for this issue here
When using FOR XML PATH and WITH XMLNAMESPACES to declare a default namespace, I will get the namespace declaration duplicated in any top level nodes for nested queries that use FOR XML, I've stumbled across a few solutions on-line, but I'm not totally convinced...
Here's an Complete Example
/*
drop table t1
drop table t2
*/
create table t1 ( c1 int, c2 varchar(50))
create table t2 ( c1 int, c2 int, c3 varchar(50))
insert t1 values
(1, 'Mouse'),
(2, 'Chicken'),
(3, 'Snake');
insert t2 values
(1, 1, 'Front Right'),
(2, 1, 'Front Left'),
(3, 1, 'Back Right'),
(4, 1, 'Back Left'),
(5, 2, 'Right'),
(6, 2, 'Left')
;with XmlNamespaces( default 'uri:animal')
select
a.c2 as "#species"
, (select l.c3 as "text()"
from t2 l where l.c2 = a.c1
for xml path('leg'), type) as "legs"
from t1 a
for xml path('animal'), root('zoo')
What's the best solution?
After hours of desperation and hundreds of trials & errors, I've come up with the solution below.
I had the same issue, when I wanted just one xmlns attribute, on the root node only. But I also had a very difficult query with lot's of subqueries and FOR XML EXPLICIT method alone was just too cumbersome. So yes, I wanted the convenience of FOR XML PATH in the subqueries and also to set my own xmlns.
I kindly borrowed the code of 8kb's answer, because it was so nice. I tweaked it a bit for better understanding. Here is the code:
DECLARE #Order TABLE (OrderID INT, OrderDate DATETIME)
DECLARE #OrderDetail TABLE (OrderID INT, ItemID VARCHAR(1), Name VARCHAR(50), Qty INT)
INSERT #Order VALUES (1, '2010-01-01'), (2, '2010-01-02')
INSERT #OrderDetail VALUES (1, 'A', 'Drink', 5),
(1, 'B', 'Cup', 2),
(2, 'A', 'Drink', 2),
(2, 'C', 'Straw', 1),
(2, 'D', 'Napkin', 1)
-- Your ordinary FOR XML PATH query
DECLARE #xml XML = (SELECT OrderID AS "#OrderID",
(SELECT ItemID AS "#ItemID",
Name AS "data()"
FROM #OrderDetail
WHERE OrderID = o.OrderID
FOR XML PATH ('Item'), TYPE)
FROM #Order o
FOR XML PATH ('Order'), ROOT('dummyTag'), TYPE)
-- Magic happens here!
SELECT 1 AS Tag
,NULL AS Parent
,#xml AS [xml!1!!xmltext]
,'http://test.com/order' AS [xml!1!xmlns]
FOR XML EXPLICIT
Result:
<xml xmlns="http://test.com/order">
<Order OrderID="1">
<Item ItemID="A">Drink</Item>
<Item ItemID="B">Cup</Item>
</Order>
<Order OrderID="2">
<Item ItemID="A">Drink</Item>
<Item ItemID="C">Straw</Item>
<Item ItemID="D">Napkin</Item>
</Order>
</xml>
If you selected #xml alone, you would see that it contains root node dummyTag. We don't need it, so we remove it by using directive xmltext in FOR XML EXPLICIT query:
,#xml AS [xml!1!!xmltext]
Although the explanation in MSDN sounds more sophisticated, but practically it tells the parser to select the contents of XML root node.
Not sure how fast the query is, yet currently I am relaxing and drinking Scotch like a gent while peacefully looking at the code...
If I have understood correctly, you are referring to the behavior that you might see in a query like this:
DECLARE #Order TABLE (
OrderID INT,
OrderDate DATETIME)
DECLARE #OrderDetail TABLE (
OrderID INT,
ItemID VARCHAR(1),
ItemName VARCHAR(50),
Qty INT)
INSERT #Order
VALUES
(1, '2010-01-01'),
(2, '2010-01-02')
INSERT #OrderDetail
VALUES
(1, 'A', 'Drink', 5),
(1, 'B', 'Cup', 2),
(2, 'A', 'Drink', 2),
(2, 'C', 'Straw', 1),
(2, 'D', 'Napkin', 1)
;WITH XMLNAMESPACES('http://test.com/order' AS od)
SELECT
OrderID AS "#OrderID",
(SELECT
ItemID AS "#od:ItemID",
ItemName AS "data()"
FROM #OrderDetail
WHERE OrderID = o.OrderID
FOR XML PATH ('od.Item'), TYPE)
FROM #Order o
FOR XML PATH ('od.Order'), TYPE, ROOT('xml')
Which gives the following results:
<xml xmlns:od="http://test.com/order">
<od.Order OrderID="1">
<od.Item xmlns:od="http://test.com/order" od:ItemID="A">Drink</od.Item>
<od.Item xmlns:od="http://test.com/order" od:ItemID="B">Cup</od.Item>
</od.Order>
<od.Order OrderID="2">
<od.Item xmlns:od="http://test.com/order" od:ItemID="A">Drink</od.Item>
<od.Item xmlns:od="http://test.com/order" od:ItemID="C">Straw</od.Item>
<od.Item xmlns:od="http://test.com/order" od:ItemID="D">Napkin</od.Item>
</od.Order>
</xml>
As you said, the namespace is repeated in the results of the subqueries.
This behavior is a feature according to a conversation on devnetnewsgroup (website now defunct) although there is the option to vote on changing it.
My proposed solution is to revert back to FOR XML EXPLICIT:
SELECT
1 AS Tag,
NULL AS Parent,
'http://test.com/order' AS [xml!1!xmlns:od],
NULL AS [od:Order!2],
NULL AS [od:Order!2!OrderID],
NULL AS [od:Item!3],
NULL AS [od:Item!3!ItemID]
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
'http://test.com/order' AS [xml!1!xmlns:od],
NULL AS [od:Order!2],
OrderID AS [od:Order!2!OrderID],
NULL AS [od:Item!3],
NULL [od:Item!3!ItemID]
FROM #Order
UNION ALL
SELECT
3 AS Tag,
2 AS Parent,
'http://test.com/order' AS [xml!1!xmlns:od],
NULL AS [od:Order!2],
o.OrderID AS [od:Order!2!OrderID],
d.ItemName AS [od:Item!3],
d.ItemID AS [od:Item!3!ItemID]
FROM #Order o INNER JOIN #OrderDetail d ON o.OrderID = d.OrderID
ORDER BY [od:Order!2!OrderID], [od:Item!3!ItemID]
FOR XML EXPLICIT
And see these results:
<xml xmlns:od="http://test.com/order">
<od:Order OrderID="1">
<od:Item ItemID="A">Drink</od:Item>
<od:Item ItemID="B">Cup</od:Item>
</od:Order>
<od:Order OrderID="2">
<od:Item ItemID="A">Drink</od:Item>
<od:Item ItemID="C">Straw</od:Item>
<od:Item ItemID="D">Napkin</od:Item>
</od:Order>
</xml>
An alternative solution I've seen is to add the XMLNAMESPACES declaration after building the xml into a temporary variable:
declare #xml as xml;
select #xml = (
select
a.c2 as "#species"
, (select l.c3 as "text()"
from t2 l where l.c2 = a.c1
for xml path('leg'), type) as "legs"
from t1 a
for xml path('animal'))
;with XmlNamespaces( 'uri:animal' as an)
select #xml for xml path('') , root('zoo');
The problem here is compounded by the fact that you cannot directly declare the namespaces manually when using XML PATH. SQL Server will disallow any attribute names beginning with 'xmlns' and any tag names with colons in them.
Rather than having to resort to using the relatively unfriendly XML EXPLICIT, I got around the problem by first generating XML with 'cloaked' namespace definitions and references, then doing string replaces as follows ...
DECLARE #Order TABLE (
OrderID INT,
OrderDate DATETIME)
DECLARE #OrderDetail TABLE (
OrderID INT,
ItemID VARCHAR(1),
ItemName VARCHAR(50),
Qty INT)
INSERT #Order
VALUES
(1, '2010-01-01'),
(2, '2010-01-02')
INSERT #OrderDetail
VALUES
(1, 'A', 'Drink', 5),
(1, 'B', 'Cup', 2),
(2, 'A', 'Drink', 2),
(2, 'C', 'Straw', 1),
(2, 'D', 'Napkin', 1)
declare #xml xml
set #xml = (SELECT
'http://test.com/order' as "#xxmlns..od", -- 'Cloaked' namespace def
(SELECT OrderID AS "#OrderID",
(SELECT
ItemID AS "#od..ItemID",
ItemName AS "data()"
FROM #OrderDetail
WHERE OrderID = o.OrderID
FOR XML PATH ('od..Item'), TYPE)
FROM #Order o
FOR XML PATH ('od..Order'), TYPE)
FOR XML PATH('xml'))
set #xml = cast(replace(replace(cast(#xml as nvarchar(max)), 'xxmlns', 'xmlns'),'..',':') as xml)
select #xml
A few things to point out:
I'm using 'xxmlns' as my cloaked version of 'xmlns' and '..' to stand in for ':'. This might not work for you if you're likely to have '..' as part of text values - you can substitute this with something else as long as you pick something that makes a valid XML identifier.
Since we want the xmlns definition at the top level, we cannot use the 'ROOT' option to XML PATH - instead I needed to add an another outer level to the subselect structure to achieve this.
I'm bit confusing about all these explanation while declaring a "xmlns:animals" manually is doing the job :
Here an example i wrote to generate Open graph meta data
DECLARE #l_xml as XML;
SELECT #l_xml =
(
SELECT 'http://ogp.me/ns# fb: http://ogp.me/ns/fb# scanilike: http://ogp.me/ns/fb/scanilike#' as 'xmlns:og',
(SELECT
(SELECT 'og:title' as 'property', title as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:type' as 'property', OpenGraphWebMetadataTypes.name as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:image' as 'property', image as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:url' as 'property', url as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:description' as 'property', description as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:site_name' as 'property', siteName as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:appId' as 'property', appId as 'content' for xml raw('meta'), TYPE)
FROM OpenGraphWebMetaDatas INNER JOIN OpenGraphWebMetadataTypes ON OpenGraphWebMetaDatas.type = OpenGraphWebMetadataTypes.id WHERE THING_KEY = #p_index
for xml path('header'), TYPE),
(SELECT '' as 'body' for xml path(''), TYPE)
for xml raw('html'), TYPE
)
RETURN #l_xml
returning the expected result
<html xmlns:og="http://ogp.me/ns# fb: http://ogp.me/ns/fb# scanilike: http://ogp.me/ns/fb/scanilike#">
<header>
<meta property="og:title" content="The First object"/>
<meta property="og:type" content="scanilike:tag"/>
<meta property="og:image" content="http://www.mygeolive.com/images/facebook/facebook-logo.jpg"/>
<meta property="og:url" content="http://www.scanilike.com/opengraph?id=1"/>
<meta property="og:description" content="This is the very first object created using the IOThing & ScanILike software. We keep it in file for history purpose. "/>
<meta property="og:site_name" content="http://www.scanilike.com"/>
<meta property="og:appId" content="200270673369521"/>
</header>
<body/>
</html>
hope this will help people are searching the web for similar issue. ;-)
It would be really nice if FOR XML PATH actually worked more cleanly. Reworking your original example with #table variables:
declare #t1 table (c1 int, c2 varchar(50));
declare #t2 table (c1 int, c2 int, c3 varchar(50));
insert #t1 values
(1, 'Mouse'),
(2, 'Chicken'),
(3, 'Snake');
insert #t2 values
(1, 1, 'Front Right'),
(2, 1, 'Front Left'),
(3, 1, 'Back Right'),
(4, 1, 'Back Left'),
(5, 2, 'Right'),
(6, 2, 'Left');
;with xmlnamespaces( default 'uri:animal')
select a.c2 as "#species",
(
select l.c3 as "text()"
from #t2 l
where l.c2 = a.c1
for xml path('leg'), type
) as "legs"
from #t1 a
for xml path('animal'), root('zoo');
Yields the problem XML with repeated namespace declarations:
<zoo xmlns="uri:animal">
<animal species="Mouse">
<legs>
<leg xmlns="uri:animal">Front Right</leg>
<leg xmlns="uri:animal">Front Left</leg>
<leg xmlns="uri:animal">Back Right</leg>
<leg xmlns="uri:animal">Back Left</leg>
</legs>
</animal>
<animal species="Chicken">
<legs>
<leg xmlns="uri:animal">Right</leg>
<leg xmlns="uri:animal">Left</leg>
</legs>
</animal>
<animal species="Snake" />
</zoo>
You can migrate elements between namespaces using XQuery with wildcard namespace matching (that is, *:elementName), as below, but it can be quite cumbersome for complex XML:
;with xmlnamespaces( default 'http://tempuri.org/this/namespace/is/meaningless' )
select (
select a.c2 as "#species",
(
select l.c3 as "text()"
from #t2 l
where l.c2 = a.c1
for xml path('leg'), type
) as "legs"
from #t1 a
for xml path('animal'), root('zoo'), type
).query('declare default element namespace "uri:animal";
<zoo>
{ for $a in *:zoo/*:animal return
<animal>
{attribute species {$a/#species}}
{ for $l in $a/*:legs return
<legs>
{ for $m in $l/*:leg return
<leg>{ $m/text() }</leg>
}</legs>
}</animal>
}</zoo>');
Which yields your desired result:
<zoo xmlns="uri:animal">
<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>
<animal species="Chicken">
<legs>
<leg>Right</leg>
<leg>Left</leg>
</legs>
</animal>
<animal species="Snake" />
</zoo>

Convert columns to xml collection with attributes

I would like to have a row for each row in the table, but transform the columns to an xml collection as efficiently as possible. In the example below it is a flattened table - but in the real world the columns would require many joins to get - resulting in many reads.
For example:
declare #tbl table (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
)
insert into #tbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000)
select Id
,(select [#name] = 'PolicyNumber', [#type] = 'string', [text()] = PolicyNumber from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
,(select [#name] = 'InsuredName', [#type] = 'string', [text()] = [inner].InsuredName from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
,(select [#name] = 'EffectiveDate', [#type] = 'datetime', [text()] = [inner].EffectiveDate from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
,(select [#name] = 'Premium', [#type] = 'numeric', [text()] = [inner].Premium from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
from #tbl [outer]
Yields the individual columns in their own xml element, but I am after each row to have it's primary key and the structure:
<dts>
<dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
<dt name="InsuredName" type="string">Insured Number 1</dt>
<dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
<dt name="Premium" type="numeric">1000.0000000</dt>
</dts>
I understand this can be achieved with many sub-queries, but does anyone know of an easy way to have a single query that is smart enough to have the PK and all the individual columns transformed into an element in the dts collection?
If you know all metadata (column name and type) in advance this can be done very simply like here:
declare #tbl table (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
);
insert into #tbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);
SELECT 'PolicyNumber' AS [dt/#name]
,'string' AS [dt/#type]
,PolicyNumber AS [dt]
,''
,'InsuredName' AS [dt/#name]
,'string' AS [dt/#type]
,InsuredName AS [dt]
,''
,'EffectiveDate' AS [dt/#name]
,'datetime' AS [dt/#type]
,EffectiveDate AS [dt]
,''
,'Premium' AS [dt/#name]
,'numeric' AS [dt/#type]
,Premium AS [dt]
FROM #tbl
FOR XML PATH('dts'),ROOT('root')
The result
<root>
<dts>
<dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
<dt name="InsuredName" type="string">Insured Number 1</dt>
<dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
<dt name="Premium" type="numeric">1000.0000000</dt>
</dts>
<dts>
<dt name="PolicyNumber" type="string">2017A-BSDSDFWEF2</dt>
<dt name="InsuredName" type="string">Insured Number 2</dt>
<dt name="EffectiveDate" type="datetime">2017-06-01T00:00:00</dt>
<dt name="Premium" type="numeric">2000.0000000</dt>
</dts>
</root>
The trick is the nameless empty "column" between the <dt> elements. The engine is told: Look, there's a new element, close the one before and start a new one!
Otherwise you'd get an error...
UPDATE: generic approach
This will extract all meta data and construct the same statement as above, which is executed with EXEC:
CREATE TABLE tmpTbl (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
);
insert into tmpTbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);
DECLARE #cmd NVARCHAR(MAX)='SELECT ' +
STUFF(
(
SELECT ',''' + c.COLUMN_NAME + ''' AS [dt/#name]' +
',''' + c.DATA_TYPE + ''' AS [dt/#type]' +
',' + QUOTENAME(c.COLUMN_NAME) + ' AS [dt]' +
','''''
FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE TABLE_NAME='tmpTbl'
FOR XML PATH('')
),1,1,'') +
'FROM tmpTbl FOR XML PATH(''dts''),ROOT(''root'')';
EXEC( #cmd);
GO
--cleanup (careful with real data)
--DROP TABLE tmpTbl;
If you need for example "string" instead of "varchar" you'd need a mapping table or a CASE expression.
This can be solve using different techniques. Here is one of them using UNPIVOT to generate the type column:
WITH DataSource AS
(
SELECT [id]
,[column]
,[value]
,CASE [column]
WHEN 'PolicyNumber' THEN 'string'
WHEN 'InsuredName' THEN 'string'
WHEN 'EffectiveDate' THEN 'datetime'
WHEN 'Premium' THEN 'numeric'
END AS [type]
FROM
(
SELECT Id
,PolicyNumber
,InsuredName
,CAST(EffectiveDate AS VARCHAR(100)) AS EffectiveDate
,CAST(Premium AS VARCHAR(100)) AS Premium
FROM #tbl
) DS
UNPIVOT
(
[value] FOR [column] IN ([PolicyNumber], [InsuredName], [EffectiveDate], [Premium])
) UNPVT
)
SELECT DISTINCT [id]
,[Info]
FROM #tbl DS
CROSS APPLY
(
SELECT [column] "#name"
,[type] "#type"
,CASE WHEN [column] = 'EffectiveDate' THEN CONVERT(VARCHAR(32), CAST([value] AS DATETIME2), 126) ELSE [value] END "text()"
FROM DataSource Info
WHERE DS.[Id] = Info.[Id]
FOR XML PATH('dt'), ROOT('dts')
) DSInfo (Info);
It will give you XML like this for each row:
<dts>
<dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
<dt name="InsuredName" type="string">Insured Number 1</dt>
<dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
<dt name="Premium" type="numeric">1000.0000000</dt>
</dts>
There is no convenient way to produce this kind of output in SQL Server. One possible solution might be a FLWOR transformation, but I suspect it will be quite convoluted, indeed.
The other is by using UNPIVOT, as in the example below, though it is far from being easily expandable:
select (
select upt.ColumnName as [#name],
isnull(dt.ColumnType, 'string') as [#type],
upt.ColumnValue as [text()]
from (
select t.Id, t.PolicyNumber, t.InsuredName,
convert(varchar(100), t.EffectiveDate, 126) as [EffectiveDate],
cast(t.Premium as varchar(100)) as [Premium]
from #tbl t
) sq
unpivot (
ColumnValue for ColumnName in (
sq.PolicyNumber, sq.InsuredName, sq.EffectiveDate, sq.Premium
)
) upt
left join (values
('EffectiveDate', 'datetime'),
('Premium', 'numeric')
) dt (ColumnName, ColumnType) on upt.ColumnName = dt.ColumnName
where upt.Id = t.Id
for xml path('dt'), type
)
from #tbl t
for xml path('dts'), type;
First, you need to transpose column values into rows, so that your relational output will start resembling your required XML. In order to fit all your columns into the same ColumnValue, you have to cast them to the same data type.
Second, you have to provide the data for your type attribute. In the example above, I used an inline table constructor, because there is no way you can get data types from TV columns on the fly. If your actual data resides in a static table, you can try to join it with system metadata objects, such as INFORMATION_SCHEMA.COLUMNS. Although for your required values you will probably need an additional mapping table, as well (in order to substitute varchar with string, for example).
Last, in order to get a single /dts element for every original table row, I join the unpivotted data with the table again. This allows to generate the required nesting of XML elements, because root() clause is unsuitable for this.
Thanks for the suggestions! I decided flattening the query into a temp table is the best approach, then using the generic approach above plus the blank column trick satisfies the need.
if object_id('tempdb..#tmp') is not null
drop table #tmp
create table #tmp (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
);
insert into #tmp (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);
DECLARE #cmd NVARCHAR(MAX)='
select [outer].Id
,convert(xml, (SELECT ' +
STUFF(
(
SELECT ',[dt/#n] = ''' + c.name + '''' +
',[dt/#t] = ''' + case when t.name = 'bit' then 'b'
when t.name in ('date', 'smalldatetime', 'datetime2', 'datetime', 'datetimeoffset') then 'd'
when t.name = 'bigint' then 'g'
when t.name in ('tinyint', 'smallint', 'int', 'time', 'timestamp') then 'i'
when t.name in ('real', 'smallmoney', 'money', 'float', 'decimal', 'numeric') then 'n'
else 's'
end + '''' +
',[dt] = ' + QUOTENAME(c.name) +
','''''
FROM tempdb.sys.columns c
inner join sys.types t on c.system_type_id = t.system_type_id
where object_id = object_id('tempdb..#tmp')
FOR XML PATH('')
),1,1,'') + '
FROM #tmp [inner]
where [inner].Id = [outer].id
for xml path (''dts'')))
from #tmp [outer]'
EXEC( #cmd);

Is it possible to generate xml from SQL where I don't know the number of levels? [duplicate]

I wonder is there anyway to select hierarchy in SQL server 2005 and return xml format?
I have a database with a lot of data (about 2000 to 3000 records), and i am now using a function in SQL server 2005 to retrieve the data in hierarchy and return an XML but it seems not perfect because it's too slow when there is a lot of data
Here is my function
Database
ID Name Parent Order
Function
CREATE FUNCTION [dbo].[GetXMLTree]
(
#PARENT bigint
)
RETURNS XML
AS
BEGIN
RETURN /* value */
(SELECT [ID] AS "#ID",
[Name] AS "#Name",
[Parent] AS "#Parent",
[Order] AS "#Order",
dbo.GetXMLTree(Parent).query('/xml/item')
FROM MyDatabaseTable
WHERE [Parent]=#PARENT
ORDER BY [Order]
FOR XML PATH('item'),ROOT('xml'),TYPE)
END
I would like to use XML in hierarchy because with me there's alot of thing to do with it :)
Any best solutions plzzzzz
You can use a recursive CTE to build the hierarchy and loop over levels to build the XML.
-- Sample data
create table MyDatabaseTable(ID int, Name varchar(10), Parent int, [Order] int)
insert into MyDatabaseTable values
(1, 'N1', null, 1),
(2, 'N1_1', 1 , 1),
(3, 'N1_1_1', 2 , 1),
(4, 'N1_1_2', 2 , 2),
(5, 'N1_2', 1 , 2),
(6, 'N2', null, 1),
(7, 'N2_1', 6 , 1)
-- set #Root to whatever node should be root
declare #Root int = 1
-- Worktable that holds temp xml data and level
declare #Tree table(ID int, Parent int, [Order] int, [Level] int, XMLCol xml)
-- Recursive cte that builds #tree
;with Tree as
(
select
M.ID,
M.Parent,
M.[Order],
1 as [Level]
from MyDatabaseTable as M
where M.ID = #Root
union all
select
M.ID,
M.Parent,
M.[Order],
Tree.[Level]+1 as [Level]
from MyDatabaseTable as M
inner join Tree
on Tree.ID = M.Parent
)
insert into #Tree(ID, Parent, [Order], [Level])
select *
from Tree
declare #Level int
select #Level = max([Level]) from #Tree
-- Loop for each level
while #Level > 0
begin
update Tree set
XMLCol = (select
M.ID as '#ID',
M.Name as '#Name',
M.Parent as '#Parent',
M.[Order] as '#Order',
(select XMLCol as '*'
from #Tree as Tree2
where Tree2.Parent = M.ID
order by Tree2.[Order]
for xml path(''), type)
from MyDatabaseTable as M
where M.ID = Tree.ID
order by M.[Order]
for xml path('item'))
from #Tree as Tree
where Tree.[Level] = #Level
set #Level = #Level - 1
end
select XMLCol
from #Tree
where ID = #Root
Result
<item ID="1" Name="N1" Order="1">
<item ID="2" Name="N1_1" Parent="1" Order="1">
<item ID="3" Name="N1_1_1" Parent="2" Order="1" />
<item ID="4" Name="N1_1_2" Parent="2" Order="2" />
</item>
<item ID="5" Name="N1_2" Parent="1" Order="2" />
</item>
What benefit do you expect from using XML? I don't have a perfect solution for the case when you need XML by all means - but maybe you could also investigate alternatives??
With a recursive CTE (Common Table Expression), you could easily get your entire hierarchy in a single result set, and performance should be noticeably better than doing a recursive XML building function.
Check this CTE out:
;WITH Hierarchy AS
(
SELECT
ID, [Name], Parent, [Order], 1 AS 'Level'
FROM
dbo.YourDatabaseTable
WHERE
Parent IS NULL
UNION ALL
SELECT
t.ID, t.[Name], t.Parent, t.[Order], Level + 1 AS 'Level'
FROM
dbo.YourDatabaseTable t
INNER JOIN
Hierarchy h ON t.Parent = h.ID
)
SELECT *
FROM Hierarchy
ORDER BY [Level], [Order]
This gives you a single result set, where all rows are returned, ordered by level (1 for the root level, increasing 1 for each down level) and their [Order] column.
Could that be an alternative for you? Does it perform better??
I realise this answer is a bit late, but it might help some other unlucky person who is searching for answers to this problem. I have had similar performance problems using hierarchyid with XML:
It turned out for me that the simplest solution was actually just to call ToString() on the hierarchyid values before selecting as an XML column. In some cases this sped up my queries by a factor of ten!
Here's a snippet that exhibits the problem.
create table #X (id hierarchyid primary key, n int)
-- Generate 1000 random items
declare #n int = 1000
while #n > 0 begin
declare #parentID hierarchyID = null, #leftID hierarchyID = null, #rightID hierarchyID = null
select #parentID = id from #X order by newid()
if #parentID is not null select #leftID = id from #X where id.GetAncestor(1) = #parentID order by newid()
if #leftID is not null select #rightID = min(id) from #X where id.GetAncestor(1) = #parentID and id > #leftID
if #parentID is null set #parentID = '/'
declare #id hierarchyid = #parentID.GetDescendant(#leftID, #rightID)
insert #X (id, n) values (#id, #n)
set #n -= 1
end
-- select as XML using ToString() manually
select id.ToString() id, n from #X for xml path ('item'), root ('items')
-- select as XML without ToString() - about 10 times slower with SQL Server 2012
select id, n from #X for xml path ('item'), root ('items')
drop table #X

Resources