Reading XML attribute values using OPENXML - sql-server

For the following .nodes() approach, I need an equivalent OPENXML approach. The Attributes will be different and can not be hard-coded.
DECLARE #Xml XML='<row>
<DeletedVal>
<row attribute1="value1" attribute2="value2"/>
</DeletedVal>
</row>';
SELECT x1.y.value('local-name(.)', 'VARCHAR(30)') AS [Key]
, x1.y.value('.', 'VARCHAR(MAX)') AS [Value]
FROM #Xml.nodes('/row/DeletedVal//#*') x1(y)
Output:
Key Value
------------------------------ ------
attribute1 value1
attribute2 value2
The following OPENXML approach needs fixing, where I am not sure how to get the attributes.
DECLARE #DocHandle INT
EXEC sp_xml_preparedocument
#DocHandle OUTPUT
, #Xml;
SELECT *
FROM OPENXML (#docHandle, N'/row/DeletedVal//#*')
WITH ([Key] VARCHAR(10) 'key' --- This line needs editing
, [Value] VARCHAR(10) '.')
EXEC Sp_xml_removedocument
#DocHandle;
Output:
Key Value
---------- ----------
NULL value1
NULL value2

As #Larnu correctly pointed out, 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. It is available in MS SQL Server starting from 2005 onwards.
I made some performance improvements to your T-SQL by removing the //#* (search for descendants everywhere down) from the .nodes() XQuery method.
DECLARE #Xml XML =
N'<row>
<DeletedVal>
<row attribute1="value1" attribute2="value2"/>
</DeletedVal>
</row>';
SELECT c.value('local-name(.)', 'VARCHAR(30)') AS [Key]
, c.value('.', 'VARCHAR(MAX)') AS [Value]
FROM #Xml.nodes('/row/DeletedVal/row/#*') AS t(c);
Output
+------------+--------+
| Key | Value |
+------------+--------+
| attribute1 | value1 |
| attribute2 | value2 |
+------------+--------+

Referring this link, I found the solution as below:
DECLARE #Xml XML='<row><DeletedVal><row attribute1="value1" attribute2="value2"/></DeletedVal></row>';
DECLARE #DocHandle INT
EXEC sp_xml_preparedocument
#DocHandle OUTPUT
, #Xml;
SELECT *
FROM OPENXML (#docHandle, N'/row/DeletedVal//#*')
WITH ([Key] VARCHAR(10) '#mp:localname'
, [Value] VARCHAR(10) '.')
EXEC Sp_xml_removedocument
#DocHandle;

Related

SQL Server extract data from XML column without tag names

I have an XML string:
<XML>
<xml_line>
<col1>1</col1>
<col2>foo 1</col2>
</xml_line>
<xml_line>
<col1>2</col1>
<col2>foo 2</col2>
</xml_line>
</XML>
I am extracting data from that string (stored in #data_xml) by storing it in SQL Server table and parsing it:
-- create temp table, insert XML string
CREATE TABLE table1 (data_xml XML)
INSERT table1
SELECT #data_xml
-- parse XML string into temp table
SELECT
N.C.value('col1[1]', 'int') col1_name,
N.C.value('col2[1]', 'varchar(31)') col2_name,
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
I would like to know if there is a generic way to accomplish the same without specifying column names (i.e. col1[1], col2[1])
You can use something like:
SELECT
N.C.value('let $i := . return count(//xml_line[. << $i]) + 1', 'int') as LineNumber,
Item.Node.value('local-name(.)', 'varchar(max)') name,
Item.Node.value('.', 'varchar(max)') value
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
CROSS APPLY
N.C.nodes('*') Item(Node)
To get:
LineNumber
name
value
1
col1
1
1
col2
foo 1
2
col1
2
2
col2
foo 2
See this db<>fiddle.
However, to spread columns horizontally, you will need to generate dynamic SQL after querying for distinct element names.
ADDENDUM: Here is an updated db<>fiddle that also shows a dynamic SQL example.
The above maps all values as VARCHAR(MAX). If you have NVARCHAR data you can make the appropriate changes. If you have a need to map specific columns to specific types, you will need to explicitly define and populate a name-to-type mapping table and incorporate that into the dynamic SQL logic. The same may be necessary if you prefer that the result columns be in a specific order.
ADDENDUM 2: This updated db<>fiddle now includes column type and ordering logic.
--------------------------------------------------
-- Extract column names
--------------------------------------------------
DECLARE #Names TABLE (name VARCHAR(100))
INSERT #Names
SELECT DISTINCT Item.Node.value('local-name(.)', 'varchar(max)')
FROM table1
CROSS APPLY data_xml.nodes('//xml_line/*') Item(Node)
--SELECT * FROM #Names
--------------------------------------------------
-- Define column-to-type mapping
--------------------------------------------------
DECLARE #ColumnTypeMap TABLE ( ColumnName SYSNAME, ColumnType SYSNAME, ColumnOrder INT)
INSERT #ColumnTypeMap
VALUES
('col1', 'int', 1),
('col2', 'varchar(10)', 2)
DECLARE #ColumnTypeDefault SYSNAME = 'varchar(max)'
--------------------------------------------------
-- Define SQL Templates
--------------------------------------------------
DECLARE #SelectItemTemplate VARCHAR(MAX) =
' , N.C.value(<colpath>, <coltype>) <colname>
'
DECLARE #SqlTemplate VARCHAR(MAX) =
'SELECT
N.C.value(''let $i := . return count(//xml_line[. << $i]) + 1'', ''int'') as LineNumber
<SelectItems>
FROM
table1
CROSS APPLY
data_xml.nodes(''//xml_line'') N(C)
'
--------------------------------------------------
-- Expand SQL templates into SQL
--------------------------------------------------
DECLARE #SelectItems VARCHAR(MAX) = (
SELECT STRING_AGG(SI.SelectItem, '')
WITHIN GROUP(ORDER BY ISNULL(T.ColumnOrder, 999), N.Name)
FROM #Names N
LEFT JOIN #ColumnTypeMap T ON T.ColumnName = N.name
CROSS APPLY (
SELECT SelectItem = REPLACE(REPLACE(REPLACE(
#SelectItemTemplate
, '<colpath>', QUOTENAME(N.name + '[1]', ''''))
, '<colname>', QUOTENAME(N.name))
, '<coltype>', QUOTENAME(ISNULL(T.ColumnType, #ColumnTypeDefault), ''''))
) SI(SelectItem)
)
DECLARE #Sql VARCHAR(MAX) = REPLACE(#SqlTemplate, '<SelectItems>', #SelectItems)
--------------------------------------------------
-- Execute
--------------------------------------------------
SELECT DynamicSql = #Sql
EXEC (#Sql)
Result (with some additional data):
LineNumber
col1
col2
bar
foo
1
1
foo 1
null
More
2
2
foo 2
Stuff
null

SQL Server : select merge all values with same keys

I have a table in SQL Server that looks like this:
id: int
key: nvarchar(max)
value: nvarchar(max)
I want to select distinct keys as first column and all values with same key joined with '<br/>' as my second column. The key values are dynamic and is not predefined - and the performance really matters - I don't want to use Linq or any UDF!
id key value
---------------------------------------
1 color red<br/>white<br/>black
4 size 15"
PS: I have searched a lot sorry if it's duplicated, currently running on SQL Server 2014 but I can move to 2019
Please try the following solution. It will work starting from SQL Server 2008 onwards.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, [key] NVARCHAR(MAX), [value] NVARCHAR(MAX));
INSERT INTO #tbl ([key], [value]) VALUES
(N'color', N'red'),
(N'color', N'white'),
(N'color', N'black'),
(N'size', N'15"');
-- DDL and sample data population, end
DECLARE #separator CHAR(5) = '<br/>'
, #encoded VARCHAR(20) = '<br/>';
SELECT c.[key]
, STUFF(REPLACE(
(SELECT #separator + CAST([value] AS NVARCHAR(MAX)) AS [text()]
FROM #tbl AS O
WHERE O.[key] = C.[key]
FOR XML PATH('')
), #encoded, #separator)
, 1, LEN(#separator), NULL) AS valueList
FROM #tbl AS c
GROUP BY c.[key];
Output
+-------+-------------------------+
| key | valueList |
+-------+-------------------------+
| color | red<br/>white<br/>black |
| size | 15" |
+-------+-------------------------+

Multiple XML tag value into single column with comma separator

I have an XML where the XML have multiple similar tag and I want this value need to show in one column with comma separator and insert into table.
For example:
<test xmlns="http://www.google.com">
<code>a</code>
<code>b</code>
<code>c</code>
</test>
Since XML is too large and I am using OPENXML to perform operation and insert that value into particular table.
I am performing like
insert into table A
(
code
)
select Code from OPENXML(sometag)
with (
code varchar(100) 'tagvalue'
)
for XQUERY I am using something like this: 'for $i in x:Code return concat($i/text()[1], ";")' and I want same with OPENXML.
Output: I want code tag value into one column like a,b,c or a/b/c.
Since you're on SQL Server 2017 you could use STRING_AGG (Transact-SQL) to concatenate your code values, e.g.:
create table dbo.Test (
someTag xml
);
insert dbo.Test (someTag) values
('<test><code>a</code><code>b</code><code>c</code></test>'),
('<test><code>d</code><code>e</code><code>f</code></test>');
select [Code], [someTag]
from dbo.Test
outer apply (
select [Code] = string_agg([value], N',')
from (
select n1.c1.value('.', 'nvarchar(100)')
from someTag.nodes(N'/test/code') n1(c1)
) src (value)
) a1;
Which yields...
Code someTag
a,b,c <test><code>a</code><code>b</code><code>c</code></test>
d,e,f <test><code>d</code><code>e</code><code>f</code></test>
Just a small tweak to AlwaysLearning (+1)
Example
Declare #YourTable table (ID int,XMLData xml)
insert Into #YourTable values
(1,'<test><code>a</code><code>b</code><code>c</code></test>')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select DelimString = string_agg(xAttr.value('.','varchar(max)'),',')
From A.XMLData.nodes('/test/*') xNode(xAttr)
) B
Returns
ID DelimString
1 a,b,c
And just for completeness, here is method #3 via pure XQuery and FLWOR expression.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata xml);
INSERT #tbl (xmldata) VALUES
('<test xmlns="http://www.google.com"><code>a</code><code>b</code><code>c</code></test>'),
('<test xmlns="http://www.google.com"><code>d</code><code>e</code><code>f</code></test>');
-- DDL and sample data population, end
DECLARE #separator CHAR(1) = ',';
-- Method #3
-- SQL Server 2005 onwards
;WITH XMLNAMESPACES (DEFAULT 'http://www.google.com')
SELECT ID
, xmldata.query('for $i in /test/code
return if ($i is (/test/code[last()])[1]) then string($i)
else concat($i, sql:variable("#separator"))')
.value('.', 'NVARCHAR(MAX)') AS [Comma_separated_list]
FROM #tbl;
Output
+----+----------------------+
| ID | Comma_separated_list |
+----+----------------------+
| 1 | a, b, c |
| 2 | d, e, f |
+----+----------------------+

Open XML select values with XML name space

I am trying to select some values using open xml in sql server 2012. This works when I don't have any xml name space. But whenever the below prefix get added with root element, I am not able to select values. Any suggestion how I can select values with xmlns:
xmlns="somenamspace/2006-10-31" order-no="00000001"
USE grails
GO
DECLARE #XML AS XML, #hDoc AS INT, #SQL NVARCHAR (MAX), #rootxmlns varchar(100)
SELECT #XML = N'<order xmlns="somenamspace/2006-10-31" order-no="00000001">
<order-date>2017-07-24T20:48:57.000Z</order-date>
<original-order-no>00000001</original-order-no>
<customer>
<customer-name>abcd abcd</customer-name>
<customer-email>jjj#gmail.com</customer-email>
</customer>
<current-order-no>00000001</current-order-no>
<payments>
<payment>
<credit-card>
<card-type>VISA</card-type>
<card-number>XXXX-XXXX-XXXX-1111</card-number>
<card-holder>abcd</card-holder>
<expiration-month>1</expiration-month>
<expiration-year>2021</expiration-year>
</credit-card>
<amount>325.48</amount>
</payment>
</payments>
</order>';
SET #rootxmlns = '<root xmlns:ns1="somenamspace/2006-10-31"/>'
EXEC sp_xml_preparedocument #hDoc OUTPUT, #XML, #rootxmlns
SELECT orderNo
FROM OPENXML(#hDoc, 'ns1:order',2)
WITH
(
orderNo [varchar](50) 'original-order-no'
)
SELECT *
FROM OPENXML(#hDoc, 'ns1:order/customer',2)
WITH
(
customerName [varchar](50) 'customer-name',
customerEmail [varchar](100) 'customer-email'
)
SELECT cardType, cardNumber, cardHolder
FROM OPENXML(#hDoc, '/order/payments/payment/credit-card',2)
WITH
(
cardType [varchar](50) 'card-type',
cardNumber [varchar](100) 'card-number',
cardHolder [varchar](100) 'card-holder'
)
EXEC sp_xml_removedocument #hDoc
GO
Great, that you've found an answer yourself, but this can be solved better.
FROM OPENXML with the corresponding SPs to open and to remove a document is outdated and should not be used any more. Rather use the methods, the native XML type provides:
The following will give you at least some templates how to access the values within your XML:
DECLARE #XML AS XML=
N'<order xmlns="somenamspace/2006-10-31" order-no="00000001">
<order-date>2017-07-24T20:48:57.000Z</order-date>
<original-order-no>00000001</original-order-no>
<customer>
<customer-name>abcd abcd</customer-name>
<customer-email>jjj#gmail.com</customer-email>
</customer>
<current-order-no>00000001</current-order-no>
<payments>
<payment>
<credit-card>
<card-type>VISA</card-type>
<card-number>XXXX-XXXX-XXXX-1111</card-number>
<card-holder>abcd</card-holder>
<expiration-month>1</expiration-month>
<expiration-year>2021</expiration-year>
</credit-card>
<amount>325.48</amount>
</payment>
</payments>
</order>';
--The query
WITH XMLNAMESPACES(DEFAULT N'somenamspace/2006-10-31')
SELECT #xml.value(N'(/order/#order-no)[1]',N'int') AS OrderNumber
,#xml.value(N'(/order/order-date/text())[1]',N'datetime') AS OrderDate
,#xml.value(N'(/order/customer/customer-name/text())[1]',N'nvarchar(max)') AS CustomerName
,p.value(N'local-name(.)',N'nvarchar(max)') AS PaymentType
,p.value(N'(card-type/text())[1]','nvarchar(max)') AS CardType
,p.value(N'(../amount/text())[1]','decimal(10,4)') AS Amount
FROM #xml.nodes(N'/order/payments/payment/*[local-name()!="amount"]') AS A(p)
The result
Nr OrderDate CustomerName PaymentType CardType Amount
1 2017-07-24 20:48:57.000 abcd abcd credit-card VISA 325.4800
Explanation
Some data is taken directly via XPath out of #xml. The statement FROM #xml.nodes() will create a derived table of <payments><payment> nodes (as the wording suggests a 1:n relationship. The <amount> node is handled explicitly, the other node within <payment> is taken as payment details.
Got it,need to add name space in property level as well:
SET #rootxmlns = '<root xmlns:ns1="http://www.demandware.com/xml/impex/order/2006-10-31"/>'
EXEC sp_xml_preparedocument #hDoc OUTPUT, #XML, #rootxmlns
SELECT orderNo
FROM OPENXML(#hDoc, 'ns1:order',2)
WITH
(
orderNo [varchar](50) 'ns1:original-order-no'
)
SELECT *
FROM OPENXML(#hDoc, 'ns1:order/ns1:customer',2)
WITH
(
customerName [varchar](50) 'ns1:customer-name',
customerEmail [varchar](100) 'ns1:customer-email'
)
SELECT cardType, cardNumber, cardHolder
FROM OPENXML(#hDoc, 'ns1:order/ns1:payments/ns1:payment/ns1:credit-card',2)
WITH
(
cardType [varchar](50) 'ns1:card-type',
cardNumber [varchar](100) 'ns1:card-number',
cardHolder [varchar](100) 'ns1:card-holder'
)
EXEC sp_xml_removedocument #hDoc
GO

Get multiple values from XML field stored as Varchar(max)

I have this value in a varchar(max) column in SQL Server:
<VersionSeries><SeriesTypeIdList><int>3</int><int>4</int><int>2</int><int>29</int><int>31</int><int>32</int><int>39</int></SeriesTypeIdList></VersionSeries
I want to get the int values out into a table.
This is the code I have but it is only returning the first record it finds.
Declare #Version varchar(100)
Select #Version = '2016A Demo'
DECLARE #DataTable TABLE
(
Xml XML NOT NULL,
Code NVARCHAR(50) NULL
)
INSERT
INTO #DataTable(Xml)
SELECT
CONVERT(XML,CONVERT(NVARCHAR(max), Series))
FROM Version
where VersionName = #Version
Create table #SeriesCodes
(Code integer)
Insert Into #SeriesCodes
(Code)
SELECT
T.c.value('int[1]', 'nvarchar(50)') as Code
FROM #DataTable d
OUTER APPLY d.Xml.nodes('/VersionSeries/SeriesTypeIdList') T(c);
Select * from #SeriesCodes
This is a single statement to get what you want:
DECLARE #x2 XML = N'<VersionSeries><SeriesTypeIdList><int>3</int><int>4</int><int>2</int><int>29</int><int>31</int><int>32</int><int>39</int></SeriesTypeIdList></VersionSeries>';
SELECT t.c.query(N'.').value(N'(/*)[1]', N'int') AS [int_value]
FROM #X2.nodes(N'/VersionSeries/SeriesTypeIdList/*') AS [t]([c]);
Like this:
Declare #Version varchar(100)
Select #Version = '2016A Demo'
DECLARE #DataTable TABLE
(
Xml XML NOT NULL,
Code NVARCHAR(50) NULL
)
INSERT
INTO #DataTable(Xml)
values (N'<VersionSeries><SeriesTypeIdList><int>3</int><int>4</int><int>2</int><int>29</int><int>31</int><int>32</int><int>39</int></SeriesTypeIdList></VersionSeries>')
SELECT
T.c.value('.', 'nvarchar(50)') as Code
FROM #DataTable d
OUTER APPLY d.Xml.nodes('/VersionSeries/SeriesTypeIdList/int') T(c);

Resources