multiple nodes with same name as rows - sql-server

This may look like a duplicate, but what I can find are getting multiple rows from nodes with elements inside, like
<products>
<product>
<image>url1</image>
</product>
<product>
<image>url1</image>
</product>
</products>
What I have is an XML-field in a table (with PLU as an integer)
<product>
<images>
<image>url1</image>
<image>url2</image>
<image>url3</image>
</images>
</product>
I want
image
-----
url1
url2
url3
I tried
select a.image.value('image','nvarchar(max)') as image
from products r
cross apply r.xml.nodes('/product/images') a(image) where PLU='8019'
but that gives
XQuery [products.xml.value()]: 'value()' requires a singleton (or empty sequence),
found operand of type 'xdt:untypedAtomic *'
As I want the value of each node, not of subnodes, I tried
select a.image.value('.','nvarchar(max)') ...
but that gave me only one row with url1url2url3 all urls concatenated.
select a.image.value('image[1]','nvarchar(max)')
gives only url1

PLease try the following solution.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata XML);
INSERT #tbl (xmldata) VALUES
(N'<product>
<images>
<image>url1</image>
<image>url2</image>
<image>url3</image>
</images>
</product>');
-- DDL and sample data population, end
SELECT ID
, c.value('text()[1]','nvarchar(max)') AS image_url
FROM #tbl
CROSS APPLY xmldata.nodes('/product/images/image') AS t(c);
Output
+----+-----------+
| ID | image_url |
+----+-----------+
| 1 | url1 |
| 1 | url2 |
| 1 | url3 |
+----+-----------+

A shorter solution than Yitzhak Khabhinsky is this by Martin Boje Carpentier elsewhere, but I'll award the points to Yitzhak
select a.image.value('.','nvarchar(max)') as image
from products r
cross apply r.xml.nodes('/product/images/*') a(image) where PLU='8019'

Related

Converting XML to SQL - Multiple elements in the same node problem with repeating element names

I got a really simple XML file that I want to convert to a table.
XML structure:
<ROOT>
<ID>ID-20</ID> (ONLY 1 ID per file, this will be the first column)
<ProductList>
<ProductID>A-1235</ProductID>
<Quantity>100</Quantity>
<Price>300</Price>
<ProductID>A-12356</ProductID>
<Quantity>110</Quantity>
<Price>310</Price>
<ProductID>A-123567</ProductID>
<Quantity>120</Quantity>
<Price>320</Price>
...
</ProductList>
</ROOT>
The second column would be ProductID, the 3rd Quantity, the 4th Price.
I could make each ProductID appear in separate rows with the first column but I can't make the respective Quantity and Price show next to the ProductID.
My code so far:
SELECT T.C.value('../../../ID[1]', 'nvarchar(20)') AS ID,
C.value('.', 'nvarchar(20)') AS ProductID,
C2.value('(text())[1]', 'nvarchar(20)') AS Quantity--,COMMENTED PRICE OUT FOR NOW
--C2.value('(../Price/text())[1]', 'nvarchar(20)') AS Price
FROM #Xml.nodes('/ROOT/ProductList/ProductID') AS T(C)
cross apply C.nodes('../Quantity') AS T2(C2)
The Cross Apply part causes every Quantity to appear next to every ProductID.
I can't figure out the correct way to align these columns.
I found some similar questions here but I just couldn't figure this out for my case as the XML structure is a bit different.
Could someone please help me with this?
I'd appreciate it very much :)
Problem SOLVED!
Many thanks to all who contributed!
I completely agree with #marc_s, the XML structure is very fragile.
In any case, here is a solution for the current scenario.
#Shnugo recently came up with this approach here: How to extract value form XML?
All credit goes to him.
SQL
DECLARE #xml XML =
N'<ROOT>
<ID>ID-20</ID>
<ProductList>
<ProductID>A-1235</ProductID>
<Quantity>100</Quantity>
<Price>300</Price>
<ProductID>A-12356</ProductID>
<Quantity>110</Quantity>
<Price>310</Price>
<ProductID>A-123567</ProductID>
<Quantity>120</Quantity>
<Price>320</Price>...</ProductList>
</ROOT>';
WITH tally(Nmbr) AS
(
SELECT TOP(#xml.value('count(/ROOT/ProductList/ProductID)','INT'))
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values
)
SELECT tally.Nmbr
,#xml.value('(/ROOT/ID/text())[1]','NVARCHAR(20)') AS ID
,#xml.value('(/ROOT/ProductList/ProductID[sql:column("tally.Nmbr")]/text())[1]','NVARCHAR(200)') AS ProductID
,#xml.value('(/ROOT/ProductList/Quantity[sql:column("tally.Nmbr")]/text())[1]','INT') AS Quantity
,#xml.value('(/ROOT/ProductList/Price[sql:column("tally.Nmbr")]/text())[1]','INT') AS Price
FROM tally;
Output
+------+-------+-----------+----------+-------+
| Nmbr | ID | ProductID | Quantity | Price |
+------+-------+-----------+----------+-------+
| 1 | ID-20 | A-1235 | 100 | 300 |
| 2 | ID-20 | A-12356 | 110 | 310 |
| 3 | ID-20 | A-123567 | 120 | 320 |
+------+-------+-----------+----------+-------+
Your current XML structure is rather flawed...
What you should have (and that would easily allow to know what bits of information belong together) is an element per product - something like:
<ProductList>
<Product>
<ID>A-1235</ID>
<Quantity>100</Quantity>
<Price>300</Price>
</Product>
<Product>
<ID>A-12356</ID>
<Quantity>110</Quantity>
<Price>310</Price>
</Product>
</ProductList>
because with the current structure you have, there's no proper and reliable way to know which ProductId, Quantity and Price belong together .... you won't be able to reliably get this information as you have it right now .....
With this structure, your query would be:
SELECT
C.value('(ID)[1]', 'nvarchar(20)') AS ID,
C.value('(Quantity)[1]', 'int') AS Quantity,
C.value('(Price)[1]', 'decimal(16,2)') AS Price
FROM
#Xml.nodes('/ROOT/ProductList/Product') AS T(C)
..jff..
declare #x xml = N'
<ROOT>
<ID>ID-20</ID> (ONLY 1 ID per file, this will be the first column)
<ProductList>
<ProductID>A-1235</ProductID>
<Quantity>100</Quantity>
<Price>300</Price>
<ProductID>A-12356</ProductID>
<Quantity>110</Quantity>
<Price>310</Price>
<ProductID>A-123567</ProductID>
<Quantity>120</Quantity>
<Price>320</Price>
...........................
</ProductList>
</ROOT>';
select
[1],[2],[0],
cast([2] as int) as Quantity
from
(
select
x.n.value('.', 'varchar(20)') as val,
(row_number() over(order by x.n)-1) / 3 as grpid,
row_number() over(order by x.n) % 3 as rowid
from #x.nodes('/ROOT/ProductList/*') as x(n)
) as src
pivot
(
max(val) for rowid in ([1],[2],[0])
) as pvt;

T-SQL Regex range in XML method nodes()

I have a table T with two columns. Column A is a varchar column and Column B is a XML column.
Somewhere inside Column B there is always the following parent tag: <Documents> ... </Documents>. Inside there are some <Document>...</Document> children.
I would like to get a result set with two columns:
Column 1 should contain the same values of Column A;
Column 2 should contain the content of one <Document>...</Document> only.
Also, in column 2 there should only be <Document>s with attribute equal to 1, 2 or 3.
E.g. Starting table T:
Column A | Column B
--------------------------------------------------------------------------
abc | <Documents><Document ID="1">Doc Foo</Document><Document ID="4">Doc Bar</Document></Documents>
def | <Documents><Document ID="2">Doc Foo2</Document><Document ID="3">Doc Bar2</Document></Documents>
Expected result:
Column 1 | Column 2
-------------------------------------
abc |<Document ID="1">Doc Foo</Document>
def |<Document ID="2">Doc Foo2</Document>
def |<Document ID="3">Doc Bar2</Document>
I can get the expected result like this:
SELECT
[Column A] AS [Column 1]
,T2.c.query('.') AS [Column 2]
FROM T AS tbl
CROSS APPLY T.nodes('*/Documents/Document[#ID="1" or #ID="2" or #ID="3"]') AS T2(c)
But when filter conditions are more complex it can get out of control without regex.
This does not work:
SELECT
[Column A] AS [Column 1]
,T2.c.query('.') AS [Column 2]
FROM T AS tbl
CROSS APPLY T.nodes('*/Documents/Document[#ID="[1-3]"]') AS T2(c)
How to make it work?
To expand on what #JeroenMostert suggests, and if he's correct, then perhaps what you are after is:
WITH YourTable AS(
SELECT *
FROM (VALUES('abc',CONVERT(xml,'<Documents><Document ID="1">Doc Foo</Document><Document ID="4">Doc Bar</Document></Documents>')),
('def',CONVERT(xml,'<Documents><Document ID="2">Doc Foo2</Document><Document ID="3">Doc Bar2</Document></Documents>')))V(Col1,Col2))
SELECT YT.Col1,
Ds.D.query('.')
FROM YourTable YT
CROSS APPLY YT.Col2.nodes('Documents/Document') Ds(D)
WHERE Ds.D.value('#ID','int') IN (1,2,3);
You could filter on the attribute to appear in a sequence of values:
Document[#ID = (1,2,3)]
You were very close. XQuery is based on the notion of sequences. So you need to check if the #ID attribute value is a member of the sequence.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID CHAR(3), xmldata XML);;
INSERT INTO #tbl (ID, xmldata)
VALUES
('abc', '<Documents><Document ID="1">Doc Foo</Document><Document ID="4">Doc Bar</Document></Documents>')
, ('def', '<Documents><Document ID="2">Doc Foo2</Document><Document ID="3">Doc Bar2</Document></Documents>');
-- DDL and sample data population, end
SELECT ID
, c.query('.') AS [Column 2]
FROM #tbl AS tbl
CROSS APPLY tbl.xmldata.nodes('/Documents/Document[#ID=("1","2","3")]') AS t(c);
Output
+-----+--------------------------------------+
| ID | Column 2 |
+-----+--------------------------------------+
| abc | <Document ID="1">Doc Foo</Document> |
| def | <Document ID="2">Doc Foo2</Document> |
| def | <Document ID="3">Doc Bar2</Document> |
+-----+--------------------------------------+

How to do xml parsing in store procedure

I have a table which contains xml format request.
For eg:
Api_id xmlRequest Sent_Time
1 ........ 07-04-2016 10:07:12:345
1 ........ 08-04-2016 10:03:12:345
2 ........ 09-04-2016 10:08:12:345
2 ........ 09-04-2016 10:09:12:345
For Api_id, we can have multiple request.
XML request schema is same, but has different values.
Xml request is as :
<?xml version="1.0"?>
<!DOCTYPE PARTS SYSTEM "parts.dtd">
<PARTS>
<TITLE>Computer Parts</TITLE>
<PART>
<ITEM>Motherboard</ITEM>
<MANUFACTURER>ASUS</MANUFACTURER>
<MODEL>P3B-F</MODEL>
<COST> 123.00</COST>
</PART>
</PARTS>
I need store procedure, so I can send API_id, and value(which i can search in xml request ) and get xml requests based on Item value.
CREATE PROCEDURE getxmlRequest(
#Api_Id INT
#value
,#xmlRequest VARCHAR(max) out)
AS
BEGIN
Set #xmlRequest = SELECT xmlRequest FROM Api_request
WHERE Api_id = #Api_id
/* here need to iterate over #xmlRequest */
Set #Xmlvalue = SELECT X.R.value ('.','nvarchar (150)')
FROM #xmlRequest.nodes(XPATH) X(R)
if(#XmlValue = #value)
/*Add to result so i can return
/*I want to return all #xmlRequest if we has value from xpath*/
END;
So my question If
Set #xmlRequest = SELECT xmlRequest FROM Api_request
WHERE Api_id = #Api_id
If we will get multiple result : does it possible to iterate? If yes how efficient i can ?
How to return multiple #xmlRequest as Api_id is same?
Does any one work on such kind of scenario? Please help me.
Try this query
SELECT *,CONVERT(XML,xmlRequest,2)
FROM Api_request
WHERE Api_id = #Api_id
AND CONVERT(XML,xmlRequest,2).value('(/PARTS/PART/ITEM)[1]','nvarchar(max)')
LIKE '%'+#value+'%'
It will return all the xmlRequest where contains your value
Your question is quite unclear, but please have look on this:
CREATE TABLE #testTbl(Api_id INT, xmlRequest VARCHAR(MAX), SentTime DATETIME);
INSERT INTO #testTbl VALUES
(1,'<?xml version="1.0"?>
<!DOCTYPE PARTS SYSTEM "parts.dtd">
<PARTS>
<TITLE>Computer Parts</TITLE>
<PART>
<ITEM>Motherboard</ITEM>
<MANUFACTURER>ASUS</MANUFACTURER>
<MODEL>P3B-F</MODEL>
<COST> 123.00</COST>
</PART>
</PARTS>',GETDATE())
,(1,'<?xml version="1.0"?>
<!DOCTYPE PARTS SYSTEM "parts.dtd">
<PARTS>
<TITLE>Computer Parts</TITLE>
<PART>
<ITEM>CPU</ITEM>
<MANUFACTURER>INTEL</MANUFACTURER>
<MODEL>CPUModelXY</MODEL>
<COST>345.00</COST>
</PART>
</PARTS>',GETDATE())
,(2,'<?xml version="1.0"?>
<!DOCTYPE PARTS SYSTEM "parts.dtd">
<PARTS>
<TITLE>Car Parts</TITLE>
<PART>
<ITEM>Wheel</ITEM>
<MANUFACTURER>Pirelli</MANUFACTURER>
<MODEL>WheelModelXY</MODEL>
<COST>100.00</COST>
</PART>
</PARTS>',GETDATE());
This will rerturn all rows where the Api_id=1
SELECT Api_id
,CONVERT(XML,xmlRequest,2) AS xmlRequest
,SentTime
FROM #testTbl
WHERE Api_id=1;
This will return table-like data. You can use "normal" SQL (WHERE, GROUP BY, ...) to continue
DECLARE #Api_Id INT=NULL;
WITH MyRequests AS
(
SELECT Api_id
,RealXML.value('(/PARTS/TITLE)[1]','varchar(max)') AS Title
,part.value('ITEM[1]','varchar(max)') AS Item
,part.value('MANUFACTURER[1]','varchar(max)') AS Manufacturer
,part.value('MODEL[1]','varchar(max)') AS Model
,part.value('COST[1]','decimal(12,4)') AS Cost
,SentTime
FROM #testTbl
CROSS APPLY(SELECT CONVERT(XML,xmlRequest,2) AS RealXML) AS ConvertedToXML
CROSS APPLY RealXML.nodes('/PARTS/PART') AS A(part)
WHERE #ApiId IS NULL OR Api_Id=#Api_Id
)
SELECT *
FROM MyRequests
--WHERE ...
--GROUP BY ...
--ORDER ...
;
The result
+---+----------------+-------------+---------+--------------+----------+-------------------------+
| 1 | Computer Parts | Motherboard | ASUS | P3B-F | 123.0000 | 2016-04-07 11:54:08.980 |
+---+----------------+-------------+---------+--------------+----------+-------------------------+
| 1 | Computer Parts | CPU | INTEL | CPUModelXY | 345.0000 | 2016-04-07 11:54:08.980 |
+---+----------------+-------------+---------+--------------+----------+-------------------------+
| 2 | Car Parts | Wheel | Pirelli | WheelModelXY | 100.0000 | 2016-04-07 11:54:08.980 |
+---+----------------+-------------+---------+--------------+----------+-------------------------+
Clean Up
GO
DROP TABLE #testTbl;

Order by XML attribute value

I have the following table
EntityID | Data
-----------+-----------
1 | <xml data>
2 | <xml data>
3 | <xml data>
Where XML DATA looks like this:
<Qualifications>
<Qualification QualificationName="Has Dr's degree" Remark="Yes" />
<Qualification QualificationName="ASP.NET Experience" Remark="1 Year" />
<Qualification QualificationName="Sex" Remark="M" />
</Qualifications>
I'd like to have the ability to order by remark for a particular QualificationName
SELECT * FROM Table
....
ORDER BY 'ASP.NET Experience'
P.S.
Potencially I can change XML to something like this to make things simplier
<Qualifications>
<Has Dr's degree>Yes</Has Dr's degree>
<ASP.NET Experience>1 Year</ASP.NET Experience>
<Sex>M</Sex>
</Qualifications>
UPD1: For case when user wants to order by 'ASP.NET Experience' qualification The expected result would be like this:
EntityID | Data
-----------+-----------
3 | <xml data>
1 | <xml data>
2 | <xml data>
Because EntityID 3 has Remark '1 year' EntityID 1 has remark '2 years' and EntityID 2 has remark '3 years' inside XML column for 'ASP.NET Experience' qualification
Assuming #QualificationName identifies the particular node you want to order by this will give you the value of the Remark.
declare #xml xml; set #xml = '<Qualifications>
<Qualification QualificationName="Has Dr%quot;s degree" Remark="Yes" />
<Qualification QualificationName="ASP.NET Experience" Remark="1 Year" />
<Qualification QualificationName="Sex" Remark="M" />
</Qualifications>'
declare #Qualification nvarchar(100); set #Qualification = 'ASP.NET Experience'
select #xml.value('(//Qualifications/Qualification[#QualificationName=sql:variable("#Qualification")]/#Remark)[1]', 'varchar(10)')
I assumed your xml as:
<Qualifications>
<HasDrsdegree>Yes</HasDrsdegree>
<ASPNETExperience>2</ASPNETExperience>
<Sex>M</Sex>
</Qualifications>
I hope this query will resolve your problem, this query results one extra column which you can ignore but this will solve you problem for complete table in one go.
Select EntityID,Data,T2.Loc.value('.','int') as 'experience'
from [yourtablename]
cross apply Data.nodes('/Qualifications/ASPNETExperience') as T2(Loc)
order by experience

TSQL: Extract XML Nested Tags Into Columns

Using SQLServer2008R2
I currently have XML tags with data inside the XML tags (not between them), such as:
<zooid="1"><animals key="all" zebras="22" dogs="0" birds="4" /><animals key="all" workers="yes" vacation="occasion" /> ... *(more)*</zooid>
<zooid="2"><animals key="house" zebras="0" dogs="1" birds="2" /><animals key="house" workers="no" vacation="no" /> ... *(more)*</zoodid>
If I query the XML or use the value function against it, it returns blank values because it tries to read between tags - where no value exists. I need it to read inside of the tags, parse out the values before the equal sign as columns and the values between the quotations as values inside those columns (granted, I could create a function that could do this, but this would be quite meticulous, and I'm curious if something like this already exists). What it should look like this in columns:
Key | Zebras | Dogs | Birds | Key | Workers | Vacation | ... *(more)*
... and this in rows of data:
all | 22 | 0 | 4 | all | yes | occasion | ... *(more)*
house | 0 | 1 | 2 | house | no | no | ... *(more)*
So the final output (just using the two XML rows from the beginning for now), would look like the below data in table form:
Key | Zebras | Dogs | Birds | Key | Workers | Vacation | ... *(more)*
================================================================
all | 22 | 0 | 4 | all | yes | occasion | ... *(more)*
house | 0 | 1 | 2 | house | no | no | ... *(more)*
Other than querying against XML, using the .query tool and even trying the .node tool (using CROSS APPLY see this thread), I haven't been able to generate this.
Try this one -
DECLARE #YourXML NVARCHAR(MAX)
SELECT #YourXML = '
<zooid="1">
<animals key="all" zebras="22" dogs="0" birds="4" />
<animals key="all" workers="yes" vacation="occasion" />
</zooid>
<zooid="2">
<animals key="house" zebras="0" dogs="1" birds="2" />
<animals key="house" workers="no" vacation="no" />
</zoodid>'
DECLARE #XML XML
SELECT #XML =
REPLACE(
REPLACE(#YourXML, 'zooid=', 'zooid id=')
, '</zoodid>'
, '</zooid>')
SELECT
d.[Key]
, Dogs = MAX(d.Dogs)
, Zebras = MAX(d.Zebras)
, Birds = MAX(d.Birds)
, Workers = MAX(d.Workers)
, Vacation = MAX(d.Vacation)
FROM (
SELECT
[Key] = t.p.value('./#key', 'NVARCHAR(50)')
, Zebras = t.p.value('./#zebras', 'INT')
, Dogs = t.p.value('./#dogs', 'INT')
, Birds = t.p.value('./#birds', 'INT')
, Workers = t.p.value('./#workers', 'NVARCHAR(20)')
, Vacation = t.p.value('./#vacation', 'NVARCHAR(20)')
FROM #XML.nodes('/zooid/animals') t(p)
) d
GROUP BY d.[Key]
Your xml appears invalid. How are you able to specify an element like this: ? Generally xml structure is <(elementName) (Attribute)="(Value)"/>. Unless I am mistaken if you are casting text to xml the way it is it will fail. Saying that I can show a working example for proper xml in a self extracting example that will run in SQL Managment Studio as is.
declare #text1 varchar(max) = '<zooid="1"><animals="all" zebras="22" dogs="0" birds="4" /><animals="all" workers="yes" vacation="occasion" /></zooid>'
, #text2 varchar(max) = '<a zooid="1"><b animals="all" zebras="22" dogs="0" birds="4" /><b animals="all" workers="yes" vacation="occasion" /></a>'
, #xml xml
;
begin try
set #xml = cast(#text1 as xml)
end try
begin catch
set #xml = '<ElementName Attribute="BadData Elements are not named" />'
end catch
select #xml
begin try
set #xml = cast(#text2 as xml)
end try
begin catch
set #xml = '<ElementName Attribute="BadData" />'
end catch
select
#xml.value('(/a/b/#animals)[1]', 'varchar(20)') as AnimalsValue
, #xml.value('(/a/b/#zebras)[1]', 'int') as ZebrasValue
, #xml.value('(/a/b/#dogs)[1]', 'int') as DogsValue
, #xml.value('(/a/b/#birds)[1]', 'int') as BirdsValue
, #xml.value('(/a/b/#workers)[1]', 'varchar(16)') as Workers
, #xml.value('(/a/b/#vacation)[1]', 'varchar(16)') as Vacation
The '.value' method is a syntax for querying xml in SQL. I am basically finding the elements(I did generics of a that contained b). Then once at the level I want '#animals' stands for 'attribute of name animals'. The [1] is a position since I can only return one thing at a time, so I chose the first position. Then it needs to a datatype to return. Text is varchar and numbers are ints.
XML query methods: http://msdn.microsoft.com/en-us/library/ms190798.aspx

Resources