Select from parent-children xml content - sql

I have been looking for solution for selecting parent-child relation from table [Group] which contains a xml column.
[Group] table has the following structure:
ID - int
Content - xml
There is xml data - parent-child relation in column Content
<root>
<person name="John">
<device name="notebook" />
<device name="xbox" />
</person>
<person name="Jane">
<device name="TV" />
</person>
<person name="Mark">
</person>
</root>
I would like to select data in the following format:
Group Id
PersonName
DeviceName
1
John
notebook
1
John
xbox
1
Jane
TV
Because Mark has no device assigned, there is no row for Mark in result.
Is it possible to achieve this result in a SELECT query?

As I mentioned, you can use XQuery for this. As you don't want any rows for Mark, I go straight to the device node, in the nodes method, as this means that no rows for Mark will be found. Then you can go back up one level to get the person's name:
SELECT V.ID AS GroupID,
p.d.value('../#name','nvarchar(50)') AS PersonName,
p.d.value('#name','nvarchar(50)') AS DeviceName
FROM(VALUES(1,CONVERT(xml,'<root>
<person name="John">
<device name="notebook" />
<device name="xbox" />
</person>
<person name="Jane">
<device name="TV" />
</person>
<person name="Mark">
</person>
</root>')))V(ID, Content)
CROSS APPLY V.Content.nodes('root/person/device') p(d);

Related

XML select statement to loop over all rows

I have
mytable consists of one XMLTYPE column called 'RS'. RS contains:
<test>
<mycol>
<name>a</name>
<number>1</number>
<number>2</number>
<number>50</number>
<number>60</number>
</mycol>
<mycol>
<name>b</name>
<number>5</number>
<number>820</number>
<number>601</number>
</mycol>
<mycol>
<name>c</name>
<number>6</number>
<number>8</number>
<number>62</number>
</mycol>
etc...
</test>
I'm looking to run a select statement that will display ALL names and up to 2 numbers from mytable.
something like this select statement but for all rows and without calling mycol[] several times.
select a.RS.extract('/test/mycol[1]/name[1]/text()').getstringval() as Names,
a.RS.extract('/test/mycol[1]/a[1]/text()').getstringval() ||''||
a.RS.extract('/test/mycol[1]/a[2]/text()').getstringval() ||''||
a.RS.extract('/test/mycol[1]/a[3]/text()').getstringval()
as num
from mytable a;/
output should be:
Names | num
a | 1 2
b | 5 820
c | 6 8
etc...
Thanks in advance.
Xml_table and string-join may be helpful

How to iterate through every potential XML subelement using SQL Server

I have a large XML file of over 45K contacts and I need to iterate through their subelement transactions into a SQL table. I've looked at several solutions to this, using value(), node(), etc..., but no examples appear to have an XML structure close to mine:
<Contacts>
<Contact>
<ContactID>1234</ContactID>
<ContactName>’John Doe’</ContactName>
<DOB>09031978</DOB>
<Address>’123 Main Street’</Address>
<Transactions>
<Transaction>
<TransactionID>4490</TransactionID>
<ProductName>’Recliner’</ProductName>
<Cost>123.00</Cost>
<PurchaseDate>07042020
</Transaction>
<Transaction>
<TransactionID>5678</TransactionID>
<ProductName>’Lamp’</ProductName>
<Cost>45.00</Cost>
<PurchaseDate>07042020
<Transaction>
</Transactions>
</Contact>
<Contact>
<ContactID>4567</ContactID>
<ContactName>’Jane Doe’</ContactName>
<DOB>05191984</DOB>
<Address>’567 Fake Street’</Address>
<Transactions>
<Transaction>
<TransactionID>4378</TransactionID>
<ProductName>’Coffee Table’</ProductName>
<Cost>225.00</Cost>
<PurchaseDate>07042018
</Transaction>
</Transactions>
</Contact>
</Contacts>
I need these data in a result like below:
ContactID
TransactionID
ProductName
Cost
PurchaseDate
1234
4490
Recliner
123.00
4 July 2020
1234
5678
Lamp
45.00
4 July 2020
4567
4378
Coffee Table
225.00
4 July 2018
I've tried a query using the following script:
EXEC sp_xml_preparedocument #idoc OUTPUT, #doc
-- Execute a SELECT stmt using OPENXML rowset provider.
SELECT *
FROM OPENXML (#idoc, '/Contacts/Contact/Transactions/Transaction',2)
WITH (ContactID int '../ContactID',
TransactionID int 'TransactionID',
ProductName nvarchar(50) 'ProductName',
Cost float 'Cost',
PurchaseDate date 'PurchaseDate')
But this will return either a null for ContactID; or return only one transaction for each ContactID. But I need it to iterate and get as many transactions as exist for a contact.
Any insights would be most welcome!
Try to avoid sp_xml_preparedocument because it uses large amounts of memory that can't be used by SQL Server until you remember to free it up by invoking sp_xml_removedocument.
What you're asking for can be easily achieved using nodes() with a cross apply, e.g. (after fixing your XML sample):
declare #doc xml = N'<Contacts>
<Contact>
<ContactID>1234</ContactID>
<ContactName>’John Doe’</ContactName>
<DOB>09031978</DOB>
<Address>’123 Main Street’</Address>
<Transactions>
<Transaction>
<TransactionID>4490</TransactionID>
<ProductName>’Recliner’</ProductName>
<Cost>123.00</Cost>
<PurchaseDate>07042020</PurchaseDate>
</Transaction>
<Transaction>
<TransactionID>5678</TransactionID>
<ProductName>’Lamp’</ProductName>
<Cost>45.00</Cost>
<PurchaseDate>07042020</PurchaseDate>
</Transaction>
</Transactions>
</Contact>
<Contact>
<ContactID>4567</ContactID>
<ContactName>’Jane Doe’</ContactName>
<DOB>05191984</DOB>
<Address>’567 Fake Street’</Address>
<Transactions>
<Transaction>
<TransactionID>4378</TransactionID>
<ProductName>’Coffee Table’</ProductName>
<Cost>225.00</Cost>
<PurchaseDate>07042018</PurchaseDate>
</Transaction>
</Transactions>
</Contact>
</Contacts>';
select
Cont.value('ContactID[1]', 'int') as ContactID
,Trans.value('TransactionID[1]', 'int') as TransactionID
,Trans.value('ProductName[1]', 'nvarchar(50)') as ProductName
,Trans.value('Cost[1]', 'float') as Cost
,convert(date, concat(substring(purDate,1,2), '/', substring(purDate,3,2), '/', substring(purDate,5,4)), 101) as PurchaseDate
from #doc.nodes('//Contact') nodes1(Cont)
cross apply nodes1.Cont.nodes('Transactions/Transaction') nodes2(Trans)
outer apply (
select purDate = Trans.value('PurchaseDate[1]', 'nvarchar(8)')
) temp;
Which yields:
ContactID
TransactionID
ProductName
Cost
PurchaseDate
1234
4490
’Recliner’
123
2020-07-04
1234
5678
’Lamp’
45
2020-07-04
4567
4378
’Coffee Table’
225
2018-07-04

How to Select distinct values in FOR XML PATH?

Given the following tables where T_DATA.ID = PARENT_ID or CHILD.ID
Name: T_DATA
+----+------+--------+
| ID | CODE | VALUE |
+----+------+--------+
| 1 | 3186 | value1 |
| 2 | 3186 | value2 |
| 3 | 3189 | value3 |
| 4 | 3189 | value4 |
| 5 | 3190 | value5 |
+----+------+--------+
Name: T_DATA_LINK
+-----------+----------+
| PARENT_ID | CHILD_ID |
+-----------+----------+
| 1 | 3 |
| 1 | 4 |
+-----------+----------+
I want to return an xml structure like this:
<ITEM_LIST>
<ITEM>
<CODE>3186</CODE>
<ROWS>
<ROW>
<ID>1</ID>
<ROW_INDEX>0</ROW_INDEX>
<VALUE>value1</VALUE>
</ROW>
<ROW>
<ID>2</ID>
<ROW_INDEX>1</ROW_INDEX>
<VALUE>value2</VALUE>
</ROW>
</ROWS>
</ITEM>
<ITEM>
<CODE>3189</CODE>
<ROWS>
<ROW>
<ID>3</ID>
<ROW_INDEX>0</ROW_INDEX>
<VALUE>value3</VALUE>
</ROW>
<ROW>
<ID>4</ID>
<ROW_INDEX>1</ROW_INDEX>
<VALUE>value4</VALUE>
</ROW>
</ROWS>
</ITEM>
<ITEM>
<CODE>3190</CODE>
<VALUE>value5</VALUE>
</ITEM>
</ITEM_LIST>
The ROW_INDEX is incremented by 1 for every ROW.
I need the T_DATA_LINK table to know whether an ITEM has a parent or not.
If it has a parent it means that there is more than one record with that CODE value and they need to be displayed as ROWS, otherwise it has to be displayed as a single ITEM.
UPDATE
I actually need to check the T_DATA_LINK table since there may be cases where an ITEM has a parent and only one record, but it still need to be displayed as a ROW.
#Shnugo I tried your solution, but even if now I get the correct values inside the ROWS, I get duplicates for each ITEM that has more than one record.
This is probably because I had to add to the GROUP BY the other fields I need to return with the SELECT which I didn't add to the example in order to keep it simpler.
For example, the ID need to be displayed at the ITEM level for the items which don't have any ROWS.
UPDATE 2
#Shnugo you are correct. Items 3 and 4 are the children of Item 1, but you don't see the relationship in the xml.
All the items are unique, always.
The items that are referenced in T_DATA_LINK are still unique, but are linked to each other in my application where they are displayed inside a table.
Basically the PARENT is the first column of the table and the children are the others columns.
This is the updated output I want to get.
ID should be always -1 for the items that have rows.
PARENT_CODE should be the CODE of the parent (if the item is a parent then it is equal to the CODE)
<ITEM_LIST>
<ITEM>
<ID>-1</ID>
<CODE>3186</CODE>
<PARENT_CODE>3186</PARENT_CODE>
<ROWS>
<ROW>
<ID>1</ID>
<ROW_INDEX>0</ROW_INDEX>
<VALUE>value1</VALUE>
</ROW>
<ROW>
<ID>2</ID>
<ROW_INDEX>1</ROW_INDEX>
<VALUE>value2</VALUE>
</ROW>
</ROWS>
</ITEM>
<ITEM>
<ID>-1</ID>
<CODE>3189</CODE>
<PARENT_CODE>3186</PARENT_CODE>
<ROWS>
<ROW>
<ID>3</ID>
<ROW_INDEX>0</ROW_INDEX>
<VALUE>value3</VALUE>
</ROW>
<ROW>
<ID>4</ID>
<ROW_INDEX>1</ROW_INDEX>
<VALUE>value4</VALUE>
</ROW>
</ROWS>
</ITEM>
<ITEM>
<ID>5</ID>
<CODE>3190</CODE>
<VALUE>value5</VALUE>
</ITEM>
</ITEM_LIST>
This is a new answer... Please try to put all needed information into the initial question...
DECLARE #t_data TABLE(ID INT,CODE INT,VALUE VARCHAR(100));
INSERT INTO #t_data VALUES
(1,3186,'value1')
,(2,3186,'value2')
,(3,3189,'value3')
,(4,3189,'value4')
,(5,3190,'value5');
DECLARE #t_data_link TABLE(PARENT_ID INT, CHILD_ID INT)
INSERT INTO #t_data_link VALUES
(1,3)
,(1,4);
--The CTE links the two tables and allows to handle them as one derived table
WITH Combined AS
(
SELECT d.*
,d2.CODE AS PARENT_CODE
,COUNT(*) OVER(PARTITION BY d.CODE) AS CountRows
FROM #t_data AS d
LEFT JOIN #t_data_link AS dl ON d.ID=dl.CHILD_ID
LEFT JOIN #t_data AS d2 ON dl.PARENT_ID=d2.ID
)
SELECT CASE WHEN c.CountRows>1 THEN -1 END AS ID
,CASE WHEN c.CountRows>1 THEN c.CODE END AS CODE
,CASE WHEN c.CountRows>1 THEN ISNULL(c.PARENT_CODE,c.CODE) END AS PARENT_CODE
--This part for elements with just one row per code
,(
SELECT d2.ID
,d2.CODE
,d2.VALUE
FROM #t_data AS d2
WHERE c.CODE=d2.CODE
AND c.CountRows=1
FOR XML PATH(''),TYPE
)
--This part for elements with more rows per code
,(
SELECT d2.ID
,ROW_NUMBER() OVER(ORDER BY (SELECT NULL))-1 AS ROW_INDEX
,d2.VALUE
FROM #t_data AS d2
WHERE c.CODE=d2.CODE
AND c.CountRows>1
FOR XML PATH('ROW'),ROOT('ROWS'),TYPE
)
FROM Combined AS c
GROUP BY c.CODE,c.CountRows,c.PARENT_CODE
FOR XML PATH('ITEM'),ROOT('ITEM_LIST');
The result
<ITEM_LIST>
<ITEM>
<ID>-1</ID>
<CODE>3186</CODE>
<PARENT_CODE>3186</PARENT_CODE>
<ROWS>
<ROW>
<ID>1</ID>
<ROW_INDEX>0</ROW_INDEX>
<VALUE>value1</VALUE>
</ROW>
<ROW>
<ID>2</ID>
<ROW_INDEX>1</ROW_INDEX>
<VALUE>value2</VALUE>
</ROW>
</ROWS>
</ITEM>
<ITEM>
<ID>-1</ID>
<CODE>3189</CODE>
<PARENT_CODE>3186</PARENT_CODE>
<ROWS>
<ROW>
<ID>3</ID>
<ROW_INDEX>0</ROW_INDEX>
<VALUE>value3</VALUE>
</ROW>
<ROW>
<ID>4</ID>
<ROW_INDEX>1</ROW_INDEX>
<VALUE>value4</VALUE>
</ROW>
</ROWS>
</ITEM>
<ITEM>
<ID>5</ID>
<CODE>3190</CODE>
<VALUE>value5</VALUE>
</ITEM>
</ITEM_LIST>
XML will omit any NULL value. The WHERE clause in the subselects will return with NULL if there's nothing found...

Querying SQL for grouped data and receiving it in BizTalk?

I have a query that returns data organized by group. I am wanting to have them come out in a grouped XML similar to the format below. I am planning to pass it in as a XML message into BizTalk using the WCF-SQL port adapter.
Data:
ID GroupID Item ID FileName
1 1 1 File001.txt
2 1 2 File001.txt
3 2 3 File002.txt
4 2 4 File002.txt
5 2 5 File002.txt
6 3 6 File003.txt
7 3 7 File003.txt
8 null 8 File004.txt
9 null 9 File005.txt
XML Ouput (input to BizTalk)
<GroupInfo ID=1 FileName=File001.txt>
<Items>
<Item ID=1 />
<Item ID=2 />
</Items>
</GroupInfo>
<GroupInfo ID=2 FileName=File002.txt>
<Items>
<Item ID=3 />
<Item ID=4 />
<Item ID=5 />
</Items>
</GroupInfo>
<GroupInfo ID=3 FileName=File003.txt>
<Items>
<Item ID=6 />
<Item ID=7 />
</Items>
</GroupInfo>
<GroupInfo FileName=File004.txt>
<Items>
<Item ID=8 />
</Items>
</GroupInfo>
<GroupInfo FileName=File005.txt>
<Items>
<Item ID=9 />
</Items>
</GroupInfo>
I'm not sure what to do to get the output in the required format. Please help.
To accomplish this, you would have to use FOR XML results from the Stored Procedure.
All documented here: http://technet.microsoft.com/en-us/library/ms178107.aspx
In this case, you might find it easier to join the table to itself on GroupID as FOR XML AUTO will automatically create child records for the joined rows.

How to convert nested hierarchy of xml to sql table

Using MSSQL 2008 and XQUERY
Consider the following XML stored in a table:
<ROOT>
<WrapperElement>
<ParentElement ID=1>
<Title>parent1</Title>
<Description />
<ChildElement ID="6">
<Title>Child 4</Title>
<Description />
<StartDate>2010-01-25T00:00:00</StartDate>
<EndDate>2010-01-25T00:00:00</EndDate>
</ChildElement>
<ChildElement ID="0">
<Title>Child1</Title>
<Description />
<StartDate>2010-01-25T00:00:00</StartDate>
<EndDate>2010-01-25T00:00:00</EndDate>
</ChildElement>
<ChildElement ID="8">
<Title>Child6</Title>
<Description />
<StartDate>2010-01-25T00:00:00</StartDate>
<EndDate>2010-01-25T00:00:00</EndDate>
</ChildElement>
</ParentElement>
</WrapperElement>
</Root>
I want to decompose this xml into something like
PE!ID | PE!Title | PE!Description | CE!ID | CE!Title | CE!StartDate |...
1 | parent1 | | 6 | child 4 | 2010-... |
1 | parent1 | | 0 | child1 | 2010-... |
etc.
Note: there may be many ChildElements per ParentElement, in this example.
I've been experimenting with xquery however i've not been able to navigate through complex elements as such.
Basically, i'm trying to do the exact opposite of what FOR XML does to a table, only with a much more simplistic set of data to work with.
Any ideas on where to go next or how to accomplish this?
Thanks
How about this (I declared #input to be a XML datatype variable with your XML content - replace accordingly):
SELECT
Parent.Elm.value('(#ID)[1]', 'int') AS 'ID',
Parent.Elm.value('(Title)[1]', 'varchar(100)') AS 'Title',
Parent.Elm.value('(Description)[1]', 'varchar(100)') AS 'Description',
Child.Elm.value('(#ID)[1]', 'int') AS 'ChildID',
Child.Elm.value('(Title)[1]', 'varchar(100)') AS 'ChildTitle',
Child.Elm.value('(StartDate)[1]', 'DATETIME') AS 'StartDate',
Child.Elm.value('(EndDate)[1]', 'DATETIME') AS 'EndDate'
FROM
#input.nodes('/ROOT/WrapperElement/ParentElement') AS Parent(Elm)
CROSS APPLY
Parent.Elm.nodes('ChildElement') AS Child(Elm)
You basically iterate over all the /ROOT/WrapperElement/ParentElemet nodes (as Parent(Elm) pseudo table), and for each of those entries, you then do a CROSS APPLY for the child elements contained inside that ParentElement and pluck out the necessary information.
Should work - I hope!