Read multiple lines from xml column to table - sql

I'm struggling reading data from measurements stored as xml columns. My xml files looks like this:
<Data t0="2018-09-18T07:43:11.882+01:00" dt="2.003338853E-5">
<Measurement>
<Value>2.075</Value>
<Value>2.175</Value>
<Value>2.275</Value>
<Value>2.325</Value>
<Value>2.425</Value>
...
</Measurement>
</Data>
What I need as result is a table containing the values.
What I already tried is this:
SELECT CONVERT(varchar(max),c.query('data(Value)')) as XMLValues
FROM [dbo].[T_Measurement] as t1
CROSS APPLY Measurement.nodes('/Data/Measurement') x(c)
which creates a resulting xml field with space separated values. But how can I create a query resulting in a table?
regards, Daniel

You can use query() to extract the values and then value to cast them to a numeric type:
declare #tmp table (Measurement xml)
insert into #tmp
select
'<Data t0="2018-09-18T07:43:11.882+01:00" dt="2.003338853E-5">
<Measurement>
<Value>2.075</Value>
<Value>2.175</Value>
<Value>2.275</Value>
<Value>2.325</Value>
<Value>2.425</Value>
</Measurement>
</Data>'
SELECT
Tbl.Col.value('.','decimal(18,3)') as XMLValues
FROM #tmp as t1
CROSS APPLY Measurement.nodes('/Data/Measurement/Value') Tbl(Col)
Result:
If you have multiple measurements in a single file (for example with an attribute that identifies each Measurement):
DECLARE #tmp TABLE (Measurement XML)
INSERT INTO #tmp
SELECT '<Data t0="2018-09-18T07:43:11.882+01:00" dt="2.003338853E-5">
<Measurement setId="1">
<Value>1.075</Value>
<Value>1.175</Value>
<Value>1.275</Value>
<Value>1.325</Value>
<Value>1.425</Value>
</Measurement>
<Measurement setId="2">
<Value>2.076</Value>
<Value>2.176</Value>
<Value>2.276</Value>
<Value>2.326</Value>
<Value>2.426</Value>
</Measurement>
</Data>'
SELECT
Tbl.Col.value('../#setId', 'int') as setId,
Tbl.Col.value('.', 'decimal(18,3)') as XMLValues
FROM #tmp AS t1
CROSS APPLY Measurement.nodes('/Data/Measurement/Value') Tbl(Col)
Results:

Related

Extract comma separated values from comma separated GUIDs

I have a column in table T1 named Categories, which contains GUIDs in XML. I am able to extract the GUIDs in a comma-separated form using the below query.
SELECT
Row, ID, Name, City,
Category = STUFF(
(
SELECT ',' + t.c.value('.', 'nvarchar(max)')
FROM dbo.T1 t1
OUTER APPLY t1.Categories.nodes('root/string') as t(c)
WHERE t1.ID = t2.ID FOR XML PATH('')
), 1, 1, ''
)
FROM
dbo.T1 t2
I have another table T2, which contains the names of the Categories. I now want to use these comma-separated GUIDs to go and fetch their corresponding Name from T2.
What changes do I need to make in my SELECT statement to write a LEFT OUTER JOIN which takes this comma-separated GUIDs and returns comma-separated names from T2.
T2 looks something like this:
I would join the category name table before concatenating the values to avoid another iteration of splitting and concatenating.
Sample data
create table xmlData
(
id int,
data xml
);
insert into xmlData (id, data) values
(1,'
<root>
<guid>5d8547aa-e1e7-4f69-88a2-655879531582</guid>
<guid>78555c5d-e39f-48f3-a148-30161b0fb995</guid>
</root>
'),
(2,'
<root>
<guid>5d8547aa-e1e7-4f69-88a2-655879531582</guid>
<guid>f58177f6-63c8-4985-baa8-2db05248f13f</guid>
</root>
'),
(3,'
<root>
<guid>5d8547aa-e1e7-4f69-88a2-655879531582</guid>
<guid>d8f9b789-6d60-4688-9d91-c0f8b1df5319</guid>
</root>
');
create table categoryName
(
guid uniqueidentifier,
name nvarchar(20)
);
insert into categoryName (guid, name) values
('5d8547aa-e1e7-4f69-88a2-655879531582', 'Alpha'),
('78555c5d-e39f-48f3-a148-30161b0fb995', 'Beta'),
('f58177f6-63c8-4985-baa8-2db05248f13f', 'Gamma'),
('d8f9b789-6d60-4688-9d91-c0f8b1df5319', 'Delta');
Solution
Two versions because the SQL Server version is not specified in the question tags... The string_agg() function is available starting from SQL Server 2017.
With string_agg()
select xd.id,
string_agg(cn.name, ',') as 'category_names'
from xmlData xd
cross apply xd.data.nodes('root/guid') g(guid)
join categoryName cn
on cn.guid = g.guid.value('.', 'nvarchar(36)')
group by xd.id
order by xd.id;
Without string_agg()
select xd.id,
stuff( ( select ',' + cn.name
from xmlData xd2
cross apply xd.data.nodes('root/guid') g(guid)
join categoryName cn
on cn.guid = g.guid.value('.', 'nvarchar(36)')
where xd2.id = xd.id
for xml path('') ), 1, 1, '' ) as 'category_names'
from xmlData xd
order by xd.id;
Result
id category_names
-- --------------
1 Alpha,Beta
2 Alpha,Gamma
3 Alpha,Delta
Fiddle to see things in action.

xml.exist - sql:column to sequence

I try to create a query with xml.exist by using the sql:column-function where the value of the column should be transformed into a usable XQuery-sequence.
This is the query with a static sequence, which is working.
SELECT
FieldA
, FieldB
FROM
MyTable
WHERE
FieldC.exist('DSAuth/role[#id=("195", "267", "350")')
To get a dynamic sequence i would use a table function with returns a table with a column "IDsequence" that have all id's as a string. e.g. ' "195", "267", "350" '.
The table function should return only one row! With multiple rows it works, but i have to group the result at last, which is bad for performance.
SELECT
FieldA
, FieldB
FROM
MyTable
CROSS APPLY
dbo.MyFunc(0) AS f
WHERE
FieldC.exist('DSAuth/role[#id=sql:column("f.IDsequence")]')
Is there a way to get a usable sequence for XQuery from sql:column("f.IDsequence")?
Thanks for help.
Edit:
The problem in performance is that FieldB (and a few more fields) is a xml column, so i have to convert it to group by.
SELECT
FieldA
, CAST(CAST(FieldB AS nvarchar(max)) AS xml) AS FieldB
FROM
MyTable
CROSS APPLY
dbo.MyFunc(0) AS f
WHERE
FieldC.exist('DSAuth/role[#id=sql:column("f.IDsequence")]')
GROUP BY
FieldA
, CAST(FieldB AS nvarchar(max))
I don't know if this is the easiest approach, but you might try to include your list into the XML and use it in the predicate like here:
--A mockup-table to show the principles
DECLARE #tbl TABLE(TheXml XML)
INSERT INTO #tbl VALUES
(
N'<root>
<a>1</a>
<a>2</a>
</root>'
)
,(
N'<root>
<a>1</a>
</root>'
)
,(
N'<root>
<a>3</a>
<a>4</a>
</root>'
);
--This is easy: Just one value
SELECT * FROM #tbl WHERE TheXml.exist('/root/a[. cast as xs:int?=1]')=1;
--This is your sequence.
--But you cannot introduce the value list with sql:column() or sql:variable().
SELECT * FROM #tbl WHERE TheXml.exist('/root/a[. cast as xs:int?=(2,3)]')=1;
--But you can add the values to your XML before like in this cte
DECLARE #values TABLE(val INT);
INSERT INTO #values VALUES(2),(3);
WITH cte(NewXml) AS
(
SELECT (SELECT (SELECT val AS [#val] FROM #values FOR XML PATH('values'),TYPE)
,TheXml AS [*]
FOR XML PATH(''),TYPE
)
FROM #tbl t
)
SELECT NewXml.query('/root') TheXml
FROM cte
WHERE NewXml.exist('/root/a[. cast as xs:int?=/values/#val]')=1;
The intermediate XML looks like this:
<values val="2" />
<values val="3" />
<root>
<a>1</a>
<a>2</a>
</root>
The final .query('/root') will return the previous XML unchanged.
UPDATE
The same would work with an introduction on string base like here:
WITH cte(NewXml) AS
(
SELECT (SELECT CAST(N'<values val="2" /><values val="3" />' AS XML)
,TheXml AS [*]
FOR XML PATH(''),TYPE
)
FROM #tbl t
)
SELECT NewXml.query('/root') TheXml
FROM cte
WHERE NewXml.exist('/root/a[. cast as xs:int?=/values/#val]')=1

Generate XML of the Structure of a table when it's empty in SQL Server

I am trying to generate a table containing one row that contains the XML of each temporary table in a stored procedure.
The problem is when the table is empty the FOR XML PATH('RS'), root('OR_RS') returns null.
What I want is: when the table is empty, return the structure of this table
something like this :
<OR_RS>
<ContactCode></ContactCode>
<EmailPaper></EmailPaper>
<ShortEmail></ShortEmail>
<WebSite></WebSite>
<Providers></Providers>
</OR_RS>
I have tried to do this :
-- ,ISNULL( (SELECT * FROM #OR_RS FOR XML PATH('RS'), root('OR_RS')), (SELECT ISNULL(ContactCode,'') , ISNULL(EmailPaper,'') , ISNULL(ShortEmail,'') , ISNULL(WebSite,'') , ISNULL(Providers,'') FROM #OR_RS FOR XML RAW)) as OR_RS
but it returns always Null:
SELECT 1 as Id, (SELECT * FROM #OR_MK FOR XML PATH('MK'), root('OR_MK') ) as OR_MK
,(SELECT * FROM #OR_CA FOR XML PATH('CA'), root('OR_CA') ) as OR_CA
-- ,ISNULL( (SELECT * FROM #OR_RS FOR XML PATH('RS'), root('OR_RS')), (SELECT ISNULL(ContactCode,'') , ISNULL(EmailPaper,'') , ISNULL(ShortEmail,'') , ISNULL(WebSite,'') , ISNULL(Providers,'') FROM #OR_RS FOR XML AUTO)) as OR_RS
,(SELECT * FROM #OR_RS FOR XML PATH('RS'), root('OR_RS')) as OR_RS
,(SELECT * FROM #OR_DC FOR XML PATH('DC'), root('OR_DC') ) as OR_DC
,(SELECT * FROM #BENEFICIARY FOR XML PATH('BEN'), root('BENEFICIARY') ) as BENEFICIARY
,(SELECT * FROM #MK_REPORT for XML PATH('REP'), root('MK_REPORT')) as MK_REPORT
Whatever you try to do with this...
You might want to have a look at sp_describe_first_result_set, but, since this is a procedure, it will not be that easy to get its result into your query...
You can use a trick (a RIGHT JOIN) to enforce a resultset. And with ELEMENTS XSINIL you force the engine to include the column in any case. Otherwise XML takes missing elements as NULL, so your empty result would not be written into the result XML at all otherwise. Try this:
DECLARE #tbl TABLE(ID INT IDENTITY,SomeValue INT,SomeString VARCHAR(100));
SELECT *
FROM #tbl
RIGHT JOIN (SELECT 1 AS x) AS tbl ON 1=1
FOR XML RAW, ELEMENTS XSINIL
The result
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ID xsi:nil="true" />
<SomeValue xsi:nil="true" />
<SomeString xsi:nil="true" />
<x>1</x>
</row>
If you want to get more details, you might add ,XMLDATA or ,XMLSCHEMA to generate full meta data.

Selecting XML Data from SQL query - No values returned

Xml Column returns no rows.
Below created a table type and xml value inserted into that table. After executing the below mentioned two queries no value is returned.
Xml data is a valid data.
DECLARE #Test TABLE (Id INT IDENTITY (1,1), XMLDATA XML)
INSERT INTO #test
SELECT '
<TXLife xmlns="http://ACORD.org/Standards/Life/2" xmlns:tx="http://ACORD.org/Standards/Life/2" Version="2.20.00">
<TXLifeRequest PrimaryObjectID="Holding_1">
<CorrelationGUID>4b30545a-158b-441a-a37a-0b259f757059</CorrelationGUID>
</TXLifeRequest>
</TXLife>'
SELECT
Id
, XMLDATA.query('//CorrelationGUID') AS 'TransType'
, XMLDATA
FROM #test
SELECT C.value('./CorrelationGUID[1]', 'varchar(50)') AS 'TransType'
FROM #test
CROSS APPLY XMLDATA.nodes('/TXLife/TXLifeRequest') n (C)
Well, if I test your code with CREATE TABLE, here and here, the unfiltered select works just fine. However, the second filtered select returns no results.
If I alter the query to to specify the right namespace, like in #Devart's answer,
WITH XMLNAMESPACES (DEFAULT 'http://ACORD.org/Standards/Life/2')
SELECT
C.value('./CorrelationGUID[1]', 'varchar(50)') AS 'TransType'
FROM
Test
CROSS APPLY
XMLDATA.nodes('/TXLife/TXLifeRequest') n (C)
You can see, it works as expected.
Try this one -
DECLARE #XML XML
SELECT #XML = '
<TXLife xmlns="http://ACORD.org/Standards/Life/2" xmlns:tx="http://ACORD.org/Standards/Life/2" Version="2.20.00">
<TXLifeRequest PrimaryObjectID="Holding_1">
<CorrelationGUID>4b30545a-158b-441a-a37a-0b259f757059</CorrelationGUID>
</TXLifeRequest>
</TXLife>'
;WITH XMLNAMESPACES (DEFAULT 'http://ACORD.org/Standards/Life/2')
SELECT t.c.value('CorrelationGUID[1]', 'UNIQUEIDENTIFIER')
FROM #XML.nodes('//TXLifeRequest') t(c)
Output -
------------------------------------
4B30545A-158B-441A-A37A-0B259F757059
You need to declare the namespace. Try this:
SELECT
XMLDATA.value('declare namespace L="http://ACORD.org/Standards/Life/2";(//L:CorrelationGUID)[1]',
'UNIQUEIDENTIFIER') AS 'TransType'
FROM #test
UNION ALL
SELECT
C.value('declare namespace L="http://ACORD.org/Standards/Life/2";(./L:CorrelationGUID)[1]',
'UNIQUEIDENTIFIER') AS 'TransType'
FROM #test
CROSS APPLY
XMLDATA.nodes('declare namespace L="http://ACORD.org/Standards/Life/2";/L:TXLife/L:TXLifeRequest') N(C)
See it working here.

SQL: How can I get the value of an attribute in XML datatype?

I have the following xml in my database:
<email>
<account language="en" ... />
</email>
I am using something like this now: but still have to find the attribute value.
SELECT convert(xml,m.Body).query('/Email/Account')
FROM Mail
How can I get the value of the language attribute in my select statement with SQL?
Use XQuery:
declare #xml xml =
'<email>
<account language="en" />
</email>'
select #xml.value('(/email/account/#language)[1]', 'nvarchar(max)')
declare #t table (m xml)
insert #t values
('<email><account language="en" /></email>'),
('<email><account language="fr" /></email>')
select m.value('(/email/account/#language)[1]', 'nvarchar(max)')
from #t
Output:
en
fr
This should work:
DECLARE #xml XML
SET #xml = N'<email><account language="en" /></email>'
SELECT T.C.value('#language', 'nvarchar(100)')
FROM #xml.nodes('email/account') T(C)
It depends a lot on how you're querying the document. You can do this, though:
CREATE TABLE #example (
document NText
);
INSERT INTO #example (document)
SELECT N'<email><account language="en" /></email>';
WITH XmlExample AS (
SELECT CONVERT(XML, document) doc
FROM #example
)
SELECT
C.value('#language', 'VarChar(2)') lang
FROM XmlExample CROSS APPLY
XmlExample.doc.nodes('//account') X(C);
DROP TABLE #example;
EDIT after changes to your question.
if the xml data is stored in sql server as a string column then use cast
select cast(your_field as XML)
.value('(/email/account/#language)[1]', 'varchar(20)')
from your_table