Convert XML into SQL server table - sql-server

Here is my XML Format:
<ROOT>
<Orders>
<OrderID>423</OrderID>
<ProductID>54</ProductID>
<ProductID>23</ProductID>
</Orders>
<Orders>
<OrderID>523</OrderID>
<ProductID>5</ProductID>
<ProductID>26</ProductID>
</Orders>
I want to have my output in the below format
OrderID ProductID
423 54
423 23
523 5
523 26
I found ways that help me read attribute from XML and convert it to SQL table. But didn't find ways or solution for my XML input.
Any help is appreciated. Thanks.

Try it like this
DECLARE #xml XML=
'<ROOT>
<Orders>
<OrderID>423</OrderID>
<ProductID>54</ProductID>
<ProductID>23</ProductID>
</Orders>
<Orders>
<OrderID>523</OrderID>
<ProductID>5</ProductID>
<ProductID>26</ProductID>
</Orders>
</ROOT>';
SELECT Ord.value('OrderID[1]','int') AS OrderID
,Prod.value('.','int') AS ProductID
FROM #xml.nodes('/ROOT/Orders') AS A(Ord)
CROSS APPLY A.Ord.nodes('ProductID') AS B(Prod)
As your leading value is the ProductID you need a nodes()-call for this. It was possible to use only one .nodes('/ROOT/Orders/ProductID') and find the corresponding OrderID with a backward move:
SELECT Prod.value('(../OrderID)[1]','int') AS OrderID
,Prod.value('.','int') AS ProductID
FROM #xml.nodes('/ROOT/Orders/ProductID') AS B(Prod)
But it is cleaner and better performing to move down the tree step by step...

Related

Pull multiple values from XML and return in one row

On SQL Server, I have an XML field on a record and it contains multiple nodes of a similar type, but with different IDs.
I want to pull a few IDs from that XML and return on one row.
Here is a simple example of the XML:
...
<Items>
<Item>
<ItemID>1</ItemID>
<ItemValue>A</ItemValue>
</Item>
<Item>
<ItemID>2</ItemID>
<ItemValue>B</ItemValue>
</Item>
</Items>
I want to output the values of the specific Items I'm searching for on one row.
Something like
select XML_Values
from the_table
where conditions_met = true
I have used cross apply and can get just the nodes of the required values returning, but the output is all on separate rows.
Ideally the output Id like would be something along the lines of:
| Id=1 | Id=2 | (Column headers)
| A | B |
I'd be super grateful for any help.
Please try the following solution.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata XML);
INSERT INTO #tbl (xmldata) VALUES
(N'<Items>
<Item>
<ItemID>1</ItemID>
<ItemValue>A</ItemValue>
</Item>
<Item>
<ItemID>2</ItemID>
<ItemValue>B</ItemValue>
</Item>
</Items>');
-- DDL and sample data population, end
SELECT ID
, c.value('(Item[1]/ItemValue/text())[1]','VARCHAR(30)') AS Item1
, c.value('(Item[2]/ItemValue/text())[1]','VARCHAR(30)') AS Item2
FROM #tbl
CROSS APPLY xmldata.nodes('/Items') AS t(c);
Output
+----+-------+-------+
| ID | Item1 | Item2 |
+----+-------+-------+
| 1 | A | B |
+----+-------+-------+

How to add a new line if there is a specific text appears in XML data?

I'm passing an XML to a stored procedure for inserting. XML contains some pieces of information like product specification, which is a string.
Here is a sample how the XML looks like:
<?xml version="1.0"?>
<Details>
<item Unit="PilotApp.DataAccessObject.DTO.Unit"
PSASysCommon=""
ProductModel="PilotApp.DataAccessObject.DTO.ProductModel"
Product="PilotSmithApp.DataAccessObject.DTO.Product"
SpecTag="62793f05-25ab-41b5-a081-f6c542f1f7cd"
Rate="100" UnitCode="1" Qty="1"
ProductSpec="Pilot Cone Blender Model No. Pilot PCB - 10 , volume of vessel -30 Ltr , handling capacity per batch by weight - 10 Kg and by volume - 20 Ltr. with motor - 0.25 HP/3 ph. Crompton make or equivalent , feeding door , discharge butterfly valve and safety guard .Material of construction of contact stainless steel (AISI) 304 and frame in carbon steel . Purpose : For blending dry powder and granules"
ProductModelID="10c0b51b-7799-4597-a4af-7c3fd431353b"
ProductID="15745d53-8219-431e-a0e3-0d319abf132d"
EnquiryID="00f9436c-ed2a-442c-b333-16348b0d8c33"
ID="e6812788-e67e-4874-bf80-87b39579a837"/>
</Details>
In this product specification section, there is Purpose section added. So, I want to insert it as a new line or display it as a new line and I want to do this using T-SQL
here is the insertion code of XML to a temp table
DECLARE #temp TABLE(
ID UNIQUEIDENTIFIER,
EnquiryID UNIQUEIDENTIFIER,
ProductID UNIQUEIDENTIFIER,
ProductModelID UNIQUEIDENTIFIER,
ProductSpec NVARCHAR(MAX),
Qty DECIMAL(18,2),
Rate DECIMAL(18,2),
UnitCode INT,
SpecTag UNIQUEIDENTIFIER,
IsProcessed bit,
tmpID UNIQUEIDENTIFIER
);
------------parse from xml to temptable ----
INSERT INTO #temp(ID,EnquiryID,ProductID,ProductModelID,
ProductSpec,Qty,Rate,UnitCode,SpecTag,IsProcessed,tmpID)
SELECT T.ID,T.EnquiryID,T.ProductID,T.ProductModelID,replace(replace(replace(replace(T.ProductSpec,'"','"'),'&','&'),'<','<'),'>','>') AS ProductSpec,
T.Qty,T.Rate,T.UnitCode,
-----modified on 14-May-2018 added field SpecTag in EnquiryDetail by Thomson
CASE WHEN T.SpecTag=CAST(CAST(0 AS BINARY) AS UNIQUEIDENTIFIER) THEN NEWID() ELSE T.SpecTag END,
T.IsProcessed,T.tmpID FROM
(SELECT [xmlData].[Col].value('./#ID', 'UNIQUEIDENTIFIER') as ID,
[xmlData].[Col].value('./#EnquiryID', 'UNIQUEIDENTIFIER') as EnquiryID,
[xmlData].[Col].value('./#ProductID', 'UNIQUEIDENTIFIER') as ProductID,
[xmlData].[Col].value('./#ProductModelID', 'UNIQUEIDENTIFIER') as ProductModelID,
[xmlData].[Col].value('./#ProductSpec', 'NVARCHAR(MAX)') as ProductSpec,
[xmlData].[Col].value('./#Qty','DECIMAL(18,2)')as Qty,
[xmlData].[Col].value('./#Rate','DECIMAL(18,2)')as Rate,
[xmlData].[Col].value('./#UnitCode','INT')as UnitCode,
[xmlData].[col].value('./#SpecTag','UNIQUEIDENTIFIER') AS SpecTag,
0 as IsProcessed,
newid() as tmpID
from #DetailXML.nodes('/Details/item') as [xmlData]([Col])) T
Here are the steps to solve this problem.
First get whole string from ProductSpec XML tag as column name "ProductSpec".
Get the sub-string from ProductSpec where sub string started from Purpose in new column as "ProductSpecPurpose".
Append char(10) or char(13) as per your need in string which you have extracted. E.g. char(10) + ProductSpecPurpose.
Merge the two columns which created in step 1 & 2.
Save it.
PS: I did not write solution directly so that at least you can try different sql functions and learn more. Because I believe in learning by ourselves rather spoon feeding. Give it try and if you are not able to figure it out. Do comment I will then write whole sql answer.
Try this to find how to extract the data nested within your XML:
DECLARE #xml XML=
'<?xml version="1.0"?>
<Details>
<item Unit="PilotApp.DataAccessObject.DTO.Unit"
PSASysCommon=""
ProductModel="PilotApp.DataAccessObject.DTO.ProductModel"
Product="PilotSmithApp.DataAccessObject.DTO.Product"
SpecTag="62793f05-25ab-41b5-a081-f6c542f1f7cd"
Rate="100" UnitCode="1" Qty="1"
ProductSpec="Pilot Cone Blender Model No. Pilot PCB - 10 , volume of vessel -30 Ltr , handling capacity per batch by weight - 10 Kg and by volume - 20 Ltr. with motor - 0.25 HP/3 ph. Crompton make or equivalent , feeding door , discharge butterfly valve and safety guard .Material of construction of contact stainless steel (AISI) 304 and frame in carbon steel . Purpose : For blending dry powder and granules"
ProductModelID="10c0b51b-7799-4597-a4af-7c3fd431353b"
ProductID="15745d53-8219-431e-a0e3-0d319abf132d"
EnquiryID="00f9436c-ed2a-442c-b333-16348b0d8c33"
ID="e6812788-e67e-4874-bf80-87b39579a837"/>
</Details>';
SELECT itm.value('#Unit','nvarchar(max)') AS Unit
,itm.value('#PSASysCommon','nvarchar(max)') AS PSASysCommon
,itm.value('#Product','nvarchar(max)') AS Product
,itm.value('#SpecTag','uniqueidentifier') AS SpecTag
,itm.value('#Rate','int') AS Rate
,itm.value('#UnitCode','int') AS UnitCode
,itm.value('#Qty','int') AS Qty
,itm.value('#ProductSpec','nvarchar(max)') AS ProductSpec
,itm.value('#ProductModelID','uniqueidentifier') AS ProductModelID
,itm.value('#ProductID','uniqueidentifier') AS ProductID
,itm.value('#ID','uniqueidentifier') AS ID
FROM #xml.nodes('/Details/item') A(itm);
My approach assumes, that there might be several <item> elements within <Details>.
Just some explanation: The <item> element is a self-closing element with all data placed within attributes. This is a very easy form to query. Good for you...
Btw: It would be best to avoid the <?xml blah?>-declaration at all. Within SQL-Server this declaration is useless and can disturb with encodings...
UPDATE
An enhanced query to parse the spec in lines and extract the Purpose:
SELECT itm.value('#Unit','nvarchar(max)') AS Unit
,itm.value('#PSASysCommon','nvarchar(max)') AS PSASysCommon
,itm.value('#Product','nvarchar(max)') AS Product
,itm.value('#SpecTag','uniqueidentifier') AS SpecTag
,itm.value('#Rate','int') AS Rate
,itm.value('#UnitCode','int') AS UnitCode
,itm.value('#Qty','int') AS Qty
,itm.value('#ProductSpec','nvarchar(max)') AS ProductSpec
,itm.value('#ProductModelID','uniqueidentifier') AS ProductModelID
,itm.value('#ProductID','uniqueidentifier') AS ProductID
,itm.value('#ID','uniqueidentifier') AS ID
,LTRIM(RTRIM(ProductSpecLine.value('text()[1]','nvarchar(max)'))) AS ProductSpecLine_Text
,Purpose
FROM #xml.nodes('/Details/item') A(itm)
OUTER APPLY(SELECT CAST('<x>' + REPLACE((SELECT itm.value('#ProductSpec','nvarchar(max)') AS [*] FOR XML PATH('')),',','</x><x>') + '</x>' AS XML)) B(x)
OUTER APPLY B.x.nodes('/x') C(ProductSpecLine)
OUTER APPLY (SELECT CASE WHEN CHARINDEX('Purpose : ',ProductSpecLine.value('text()[1]','nvarchar(max)'))>0
THEN SUBSTRING(ProductSpecLine.value('text()[1]','nvarchar(max)'),CHARINDEX('Purpose : ',ProductSpecLine.value('text()[1]','nvarchar(max)')),1000)
END) D(Purpose);

Avoid double XML INSERT to SQL

I need to import XML data into SQL Server 2012. The import works correctly, but I would want to avoid double import. I already tried with WHERE NOT EXISTS but it didn't work.
The import:
INSERT INTO dbo.tXMLImport(cText)
SELECT cast(CONVERT(XML,x.BulkColumn,2) AS varchar(max))
FROM OPENROWSET (BULK 'D:\XML\Data.xml', SINGLE_BLOB) AS x
EXL file content:
<?xml version="1.0" encoding="UTF-8"?>
<tOrder>
<cName>Name1</cName>
<cID>100</cID>
</tOrder>
Now, it should be checked if cID value 100 from XML file already exist in
dbo.tOrder row cOrderNumber
cOrderNumber
1 100
2 101
3 102
Following extention does not wokr:
WHERE NOT EXISTS(SELECT *
FROM dbo.tOrder
WHERE x.value('(/tOrder/cID)') = dbo.tOrder.CorderNumber)
If yes, no Import to be done. Maybe some one can support me with?
Thanks in advance.
I'm not sure if I really get this... If the same cOrderNumber exists already, wouldn't you try to update the existing row? Something like you'd do with MERGE?
But It might be something like this what you are looking for:
WHERE NOT EXISTS(SELECT 1 FROM dbo.tOrder
WHERE x.exist(N'/tOrder[cID/text()=sql:column("cOrderNumber")])')=1)
(Untested air code)
This looks if there is any record within tOrder where the XML column x has any occurance of a node <tOrder><CID> with a value like the current cOrderNumber's value.
T-SQL adds the sql:column() method to XQuery, which allows to use the value of a row within the query. There's sql:variable() too.
The xml's method .exist() checks the XML for any existance of a given condition and returns with 0 or 1.
UPDATE
After reading your question once again, I'm not sure if I got this correctly... Please check the following. If this doesn't help, please use my code to set up a stand-alone sample to reprodcue your issue:
A dummy table with some orders
DECLARE #YourTable TABLE(cOrderNumber INT, OrderName VARCHAR(100));
INSERT INTO #YourTable VALUES
(100,'Order 100')
,(200,'Order 200')
,(300,'Order 300')
--Try to insert an XML with the existing OrderNumber=100
DECLARE #xml100 XML=
'<tOrder>
<cName>Name1</cName>
<cID>100</cID>
</tOrder>';
INSERT INTO #YourTable(cOrderNumber,OrderName)
SELECT #xml100.value('(/tOrder/cID/text())[1]','int')
,#xml100.value('(/tOrder/cName/text())[1]','varchar(100)')
WHERE NOT EXISTS(SELECT 1 FROM #YourTable AS t2
WHERE t2.cOrderNumber=#xml100.value('(/tOrder/cID/text())[1]','int'));
--Same code as above, but the order number is now a not existing number
DECLARE #xml101 XML=
'<tOrder>
<cName>Name1</cName>
<cID>101</cID>
</tOrder>';
INSERT INTO #YourTable(cOrderNumber,OrderName)
SELECT #xml101.value('(/tOrder/cID/text())[1]','int')
,#xml101.value('(/tOrder/cName/text())[1]','varchar(100)')
WHERE NOT EXISTS(SELECT 1 FROM #YourTable AS t2
WHERE t2.cOrderNumber=#xml101.value('(/tOrder/cID/text())[1]','int'));
--check the result
SELECT *
FROM #YourTable;
nr name
-------------
100 Order 100
200 Order 200
300 Order 300
101 Name1

FOR XML SQL Server - Variable Element name in output XML

I'm quite new to FOR XML in SQL Server, I've searched considerable and I can't find an answer to this.
Can I have a variable element name using 'for xml' where the element name is not hard-coded and is instead take from a cell in each row? Take the following example...
Table ORDERS:
ID STATUS TIME AMOUNT
------------------------------------
1 COMPLETE 02:31 2355
2 ACCEPTED 02:39 6653
3 ACCEPTED 04:21 4102
4 RECEIVED 05:03 4225
FOR XML query:
select ID,
TIME as STATUS_TIME,
AMOUNT as CURRENT_AMOUNT
from ORDERS
for xml raw(' **STATUS NAME HERE** '),root('ORDERS'), elements
Required output:
<ORDERS>
<COMPLETE> <<<<--- Variable element name from STATUS in ORDERS
<ID>1</ID>
<STATUS_TIME>02:31</STATUS_TIME>
<CURRENT_AMOUNT>2355</CURRENT_AMOUNT>
</COMPLETE>
<ACCEPTED> <<<<--- Variable element name from STATUS in ORDERS
<ID>2</ID>
<STATUS_TIME>02:39</STATUS_TIME>
<CURRENT_AMOUNT>6653</CURRENT_AMOUNT>
</ACCEPTED>
<ACCEPTED> <<<<--- Variable element name from STATUS in ORDERS
<ID>3</ID>
<STATUS_TIME>04:21</STATUS_TIME>
<CURRENT_AMOUNT>4102</CURRENT_AMOUNT>
</ACCEPTED>
<RECEIVED> <<<<--- Variable element name from STATUS in ORDERS
<ID>4</ID>
<STATUS_TIME>05:03</STATUS_TIME>
<CURRENT_AMOUNT>4225</CURRENT_AMOUNT>
</RECEIVED>
</ORDERS>
I know I'm able to give attributes to the element names, and that I could give the individual ORDER in ORDERS and attribute of STATUS like below but unfortunately that's not what the people that will receive the XML document are looking for :(
select ID,
STATUS as '#STATUS'
TIME as STATUS_TIME,
AMOUNT as CURRENT_AMOUNT
from ORDERS
for xml raw('ORDER'),root('ORDERS'), elements
Output:
<ORDERS>
<ORDER STATUS='COMPLETE'> <<<<--- Attribute for STATUS but not what I want
<ID>1</ID>
<STATUS_TIME>02:31</STATUS_TIME>
<CURRENT_AMOUNT>2355</CURRENT_AMOUNT>
</ORDER>
<ORDER STATUS='ACCEPTED'> <<<<--- Attribute for STATUS but not what I want
<ID>2</ID>
<STATUS_TIME>02:39</STATUS_TIME>
<CURRENT_AMOUNT>6653</CURRENT_AMOUNT>
</ORDER>
....
I'd like to be able to do all this within SQL Server if possible. Many, many thanks if you can help me at all on this.
You can't specify column value in XML Raw(). So what you have to do is select required column from select query and cast result into XML, like this -
Schema
DECLARE #temp table (ID int, [STATUS] [varchar](100) NOT NULL, [TIME] [varchar](100), AMOUNT int);
INSERT #temp (ID, [STATUS], [TIME], AMOUNT) VALUES (1, 'COMPLETE', '02:31', 2355),(2, 'ACCEPTED', '02:41', 6653),(3, 'ACCEPTED', '02:31', 4102),(4, 'ACCEPTED', '02:31', 4225)
Query
SELECT
CAST('<' + STATUS + '>' +
'<ID>' + CAST(ID AS varchar) + '</ID>' +
'<TIME>' + TIME + '</TIME>' +
'<AMOUNT>' + CAST(AMOUNT AS varchar) + '</AMOUNT>' +
'</' + STATUS + '>' AS XML) from #temp
FOR XML PATH(''),root('ORDERS')
Output
<ORDERS>
<COMPLETE>
<ID>1</ID>
<TIME>02:31</TIME>
<AMOUNT>2355</AMOUNT>
</COMPLETE>
<ACCEPTED>
<ID>2</ID>
<TIME>02:41</TIME>
<AMOUNT>6653</AMOUNT>
</ACCEPTED>
<ACCEPTED>
<ID>3</ID>
<TIME>02:31</TIME>
<AMOUNT>4102</AMOUNT>
</ACCEPTED>
<ACCEPTED>
<ID>4</ID>
<TIME>02:31</TIME>
<AMOUNT>4225</AMOUNT>
</ACCEPTED>
</ORDERS>
In SQL Server, XML schema has to be static, so it is impossible to specify a variable element name (be it document or attribute).
If possible options for the STATUS field are limited and stable, you can mention them all explicitly, like in the example below:
select (
select t.ID, t.TIME as [STATUS_TIME], t.AMOUNT as [CURRENT_AMOUNT]
from #temp t
where t.STATUS = 'ACCEPTED'
for xml path('ACCEPTED'), type, elements
), (
select t.ID, t.TIME as [STATUS_TIME], t.AMOUNT as [CURRENT_AMOUNT]
from #temp t
where t.STATUS = 'COMPLETE'
for xml path('COMPLETE'), type, elements
)
for xml path('ORDERS'), type;
I think you have already noticed numerous possibilities for how this code can betray you, but frankly this approach is the only one available which does not include string manipulations (they will be detrimental to performance if the size of the XML output will be at least several Mb).
As a possible workaround, you can generate this query dynamically, including as many sections as there are distinct STATUS values in your table. Very ugly, but it will work.

SQL to populate values in xml list

In an SQL Server sproc I need to generate xml using data originating from two different tables. In my example below, the patient number for type EPI comes from one table and the patient number for type MRN comes from another table. To create the xml I am using a UNION to combine the records from two distinct select statements and then using 'FOR XML PATH'. Is there a different way - such as using two select sub-queries without using UNION?
<Patients>
<Patient>
<Number>1234</Number>
<NumberType>EPI</NumberType>
</Patient>
<Patient>
<Number>5678</Number>
<NumberType>MRN</NumberType>
</Patient>
</Patients>
Thanks in advance.
If I understood your answer to my question, you are not really joining the tables on PatientId, you are just creating a list of all the data from both tables, and you don't need to group the records by patient.
Yes, UNION is the easiest way to accomplish a single list.
However, since you want to output xml, there is an alternate that can be done without UNION, per your question:
Assuming you have two tables that might look something like this:
CREATE TABLE SrcA (PatientId int, NumberA int, TypeA varchar(16));
CREATE TABLE SrcB (PatientId int, NumberB int, TypeB varchar(16));
with sample values like this (note how each table has one record not in the other):
INSERT INTO SrcA VALUES(100, 1234, 'EPI'), (200, 2222, 'EPI'), (400, 4444, 'EPI');
INSERT INTO SrcB VALUES(100, 5678, 'MRN'), (200, 2121, 'MRN'), (300, 3131, 'MRN');
Then the following query:
SELECT
(SELECT SA.NumberA AS Number, SA.TypeA AS NumberType WHERE SA.NumberA IS NOT NULL FOR XML PATH('Patient'), TYPE),
(SELECT SB.NumberB AS Number, SB.TypeB AS NumberType WHERE SB.NumberB IS NOT NULL FOR XML PATH('Patient'), TYPE)
FROM SrcA SA
FULL OUTER JOIN SrcB SB ON SA.PatientId = SB.PatientId
FOR XML PATH(''), ROOT('Patients')
will produce:
<Patients>
<Patient>
<Number>1234</Number>
<NumberType>EPI</NumberType>
</Patient>
<Patient>
<Number>5678</Number>
<NumberType>MRN</NumberType>
</Patient>
<Patient>
<Number>2222</Number>
<NumberType>EPI</NumberType>
</Patient>
<Patient>
<Number>2121</Number>
<NumberType>MRN</NumberType>
</Patient>
<Patient>
<Number>4444</Number>
<NumberType>EPI</NumberType>
</Patient>
<Patient>
<Number>3131</Number>
<NumberType>MRN</NumberType>
</Patient>
</Patients>

Resources