Order by XML attribute value - sql-server

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

Related

multiple nodes with same name as rows

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'

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;

SQL SERVER XQuery node processing problem

I have to process data from xml by date of operation, simulating day by day, during 4th months process in the data base with a XML file that looks like this:
<Operaciones_por_Dia>
<OperacionDia fecha="2020-01-30">
<PagoRecibo TipoRecibo="5" NumFinca="9782331"/>
<PagoRecibo TipoRecibo="5" NumFinca="6696849"/>
<TransConsumo id="1" LecturaM3="325" descripcion="Cobro Mensual" NumFinca="3336538"/>
<TransConsumo id="3" LecturaM3="40" descripcion="Lectura errĂ³nea" NumFinca="2425954"/>
</OperacionDia>
<OperacionDia fecha="2020-04-08">
<PagoRecibo TipoRecibo="7" NumFinca="1423800"/>
<PagoRecibo TipoRecibo="7" NumFinca="1393022"/>
<TransConsumo id="2" LecturaM3="22" descripcion="Reclamo de cliente" NumFinca="2101885"/>
</OperacionDia>
</Operaciones_por_Dia>
When I prepare the data, I store all the dates from XML into a variable table and then I iterate from the min date throught the max date (#FechaOperacion has stored that date), then I have to process the node data where the date match with the date from xml node .
I try it doing this, but #DocumentoXML.value('(/Operaciones_por_Dia/OperacionDia/#fecha)[1]', 'DATE')
don't match well, the correct data that I have to compare is Fecha with #FechaOperacion, but I don't know how can I get that value with Xquery.
INSERT #PagosHoy (NumFinca, TipoRecibo, Fecha)
select ph.value('#NumFinca', 'INT')
, ph.value('#TipoRecibo', 'INT')
, ph.value('../#fecha', 'DATE')
from #DocumentoXML.nodes('/Operaciones_por_Dia/OperacionDia/PagoRecibo') AS t(ph)
where #DocumentoXML.value('(/Operaciones_por_Dia/OperacionDia/#fecha)[1]', 'DATE') = #FechaOperacion
With the following code it work well, but I want to know how to do it like the other way.
INSERT INTO #PagosHoy(NumFinca,TipoRecibo,Fecha)
SELECT [NumFinca],[TipoRecibo],[fechaDeIngreso]
FROM OPENXML (#hdoc, 'Operaciones_por_Dia/OperacionDia/PagoRecibo',1)
WITH ( [NumFinca] VARCHAR(30) '#NumFinca',
[TipoRecibo] INT '#TipoRecibo',
[fechaDeIngreso] VARCHAR(100) '../#fecha')
WHERE [fechaDeIngreso] = #FechaOperacion
EXEC spProcesaPagos #PagosHoy
Microsoft proprietary OPENXML and its companions sp_xml_preparedocument and sp_xml_removedocument are kept just for backward compatibility with the obsolete SQL Server 2000. It is strongly recommended to re-write your SQL and switch it to XQuery.
Here is another method similar to what is proposed by #AlwaysLearning, but more simple.
It is using XPath predicate instead of WHERE clause.
SQL
-- DDL and sample data population, start
DECLARE #PagosHoy TABLE (ID INT IDENTITY PRIMARY KEY, NumFinca INT, TipoRecibo INT, Fecha DATE);
DECLARE #xml XML =
N'<Operaciones_por_Dia>
<OperacionDia fecha="2020-01-30">
<PagoRecibo TipoRecibo="5" NumFinca="9782331"/>
<PagoRecibo TipoRecibo="5" NumFinca="6696849"/>
<TransConsumo id="1" LecturaM3="325" descripcion="Cobro Mensual"
NumFinca="3336538"/>
<TransConsumo id="3" LecturaM3="40" descripcion="Lectura errĂ³nea"
NumFinca="2425954"/>
</OperacionDia>
<OperacionDia fecha="2020-04-08">
<PagoRecibo TipoRecibo="7" NumFinca="1423800"/>
<PagoRecibo TipoRecibo="7" NumFinca="1393022"/>
<TransConsumo id="2" LecturaM3="22" descripcion="Reclamo de cliente"
NumFinca="2101885"/>
</OperacionDia>
</Operaciones_por_Dia>';
-- DDL and sample data population, end
DECLARE #FechaOperacion DATE = '2020-01-30';
INSERT #PagosHoy (NumFinca, TipoRecibo, Fecha)
SELECT c.value('#NumFinca', 'INT')
, c.value('#TipoRecibo', 'INT')
, #FechaOperacion AS FechaOperacion
FROM #xml.nodes('/Operaciones_por_Dia/OperacionDia[#fecha eq sql:variable("#FechaOperacion")]/PagoRecibo') AS t(c)
-- test
SELECT * FROM #PagosHoy;
Output
+----+----------+------------+------------+
| ID | NumFinca | TipoRecibo | Fecha |
+----+----------+------------+------------+
| 1 | 9782331 | 5 | 2020-01-30 |
| 2 | 6696849 | 5 | 2020-01-30 |
+----+----------+------------+------------+
The where clause in your first example queries /Operaciones_por_Dia/OperacionDia/#fecha[1] independently of the nodes('/Operaciones_por_Dia/OperacionDia/PagoRecibo') query so will always be comparing #FechaOperacion against the first date attribute - which is 2020-01-30 in this document.
declare #FechaOperacion date = convert(date, '2020-01-30', 120);
select
ph.value('#NumFinca', 'INT'),
ph.value('#TipoRecibo', 'INT'),
ph.value('../#fecha', 'DATE')
from #DocumentoXML.nodes('/Operaciones_por_Dia/OperacionDia/PagoRecibo') AS t(ph)
where #DocumentoXML.value('(/Operaciones_por_Dia/OperacionDia/#fecha)[1]', 'DATE') = #FechaOperacion
Depending on the value of #FechaOperacion it will either return all rows in the document (when it matches) or now rows at all (when it doesn't)...
(No column name) (No column name) (No column name)
---------------- ---------------- ----------------
9782331 5 2020-01-30
6696849 5 2020-01-30
1423800 7 2020-04-08
1393022 7 2020-04-08
The solution is to query for nodes('/Operaciones_por_Dia/OperacionDia') and then use a cross apply to query the PagoRecibo child nodes.
declare #FechaOperacion date = convert(date, '2020-01-30', 120);
select
ph.value('#NumFinca', 'INT'),
ph.value('#TipoRecibo', 'INT'),
od.value('#fecha', 'DATE')
from #DocumentoXML.nodes('/Operaciones_por_Dia/OperacionDia') AS s(od)
cross apply s.od.nodes('PagoRecibo') AS t(ph)
where s.od.value('#fecha', 'DATE') = #FechaOperacion
Which returns...
(No column name) (No column name) (No column name)
---------------- ---------------- ----------------
9782331 5 2020-01-30
6696849 5 2020-01-30

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;

Query XML field with T-SQL

How can I query multiple nodes in XML data with T-SQL and have the result output to a single comma separated string?
For example, I'd like to get a list of all the destination names in the following XML to look like "Germany, France, UK, Italy, Spain, Portugal"
<Holidays>
<Summer>
<Regions>
<Destinations>
<Destination Name="Germany" />
<Destination Name="France" />
<Destination Name="UK" />
<Destination Name="Italy" />
<Destination Name="Spain" />
<Destination Name="Portugal" />
</Destinations>
<Regions>
</Summer>
</Holidays>
I was trying something like:
Countries = [xmlstring].value('/Holidays/Summer/Regions/Destinations/#Name', 'varchar')
First, to get a list of records from a source XML table, you need to use the .nodes function (DEMO):
select Destination.value('data(#Name)', 'varchar(50)') as name
from [xmlstring].nodes('/Holidays/Summer/Regions/Destinations/Destination')
D(Destination)
Sample output:
| NAME |
-------------
| Germany |
| France |
| UK |
| Italy |
| Spain |
| Portugal |
From here, you want to concatenate the destination values into a comma-separated list. Unfortunately, this is not directly supported by T-SQL, so you'll have to use some sort of workaround. If you're working with a source table using multiple rows, the simplest method is the FOR XML PATH('') trick. In this query I use a source table called Data, and split out the XML into separate records, which I then CROSS APPLY with FOR XML PATH('') to generate comma-separated rows. Finally, the final , is stripped from the result to create the list (DEMO):
;with Destinations as (
select id, name
from Data
cross apply (
select Destination.value('data(#Name)', 'varchar(50)') as name
from [xmlstring].nodes('/Holidays/Summer/Regions/Destinations/Destination') D(Destination)
) Destinations(Name)
)
select id, substring(NameList, 1, len(namelist) - 1)
from Destinations as parent
cross apply (
select name + ','
from Destinations as child
where parent.id = child.id
for xml path ('')
) DestList(NameList)
group by id, NameList
Sample Output (Note that I've added another XML fragment to the test data to make a more complex example):
| ID | COLUMN_1 |
-----------------------------------------------
| 1 | Germany,France,UK,Italy,Spain,Portugal |
| 2 | USA,Australia,Brazil |

Resources