Combining fields at the same level in XML Path - sql

I am trying to produce the following XML as a column of a table:
<root>
<Address1>
<Addressline1>Test Road</Addressline1>
<Addressline2>Test Street</Addressline2>
<Addressline3>Test Town</Addressline3>
<Addressline4>Test State</Addressline4>
<Postcode>Test Postcode</Postcode>
</Address1>
<Address2>
<Addressline1>Test Road</Addressline1>
<Addressline2>Test Street</Addressline2>
<Addressline3>Test Town</Addressline3>
<Addressline4>Test State</Addressline4>
<Postcode>Test Postcode</Postcode>
</Address2>
</root>
I am almost there with the following SQL:
select (1Addressline1,
1Addressline2,
1Addressline3,
1Addressline4,
1Postcode for xml path('Address'), root('Addresses')) AS Address1,
(2Addressline1,
2Addressline2,
2Addressline3,
2Addressline4,
2Postcode for xml path('Address'), root('Addresses')) AS Address2,
ColumnA
ColumnB
ColumnC
From Addresses
Which produces the following XML:
<root>
<Address1>
<Addressline1>Test Road</Addressline1>
<Addressline2>Test Street</Addressline2>
<Addressline3>Test Town</Addressline3>
<Addressline4>Test State</Addressline4>
<Postcode>Test Postcode</Postcode>
</Address1>
</root>,
<root>
<Address2>
<Addressline1>Test Road</Addressline1>
<Addressline2>Test Street</Addressline2>
<Addressline3>Test Town</Addressline3>
<Addressline4>Test State</Addressline4>
<Postcode>Test Postcode</Postcode>
</Address2>
</root>,
ColumnA,
ColumnB,
ColumnC
It is not correct at is produces two separate XML columns. I now need to combine Address1 and Address 2 under the root node as above but cannot work out or find the correct syntax.
EDIT: As suggested I have modified my code to this which is giving a number of syntax errors:
SELECT ColumnA,
ColumnB,
ColumnC,
(
select
(
(
select RTRIM(Address1Line1),
RTRIM(Address1Line2),
RTRIM(Address1Line3),
RTRIM(Address1Line4),
RTRIM(Address1Line5)
for xml path('Address'), type
),
(
select RTRIM(Address2Line1),
RTRIM(Address2Line2),
RTRIM(Address2Line3),
RTRIM(Address2Line4),
RTRIM(Address2Line5)
for xml path('Address'), type
)
) FOR XML PATH(''), root('Addresses'),type
) AS Addresses
FROM TableA

DECLARE #Addresses TABLE
(AddressNumber INT, Addressline1 VARCHAR(50),Addressline2 VARCHAR(50)
,Addressline3 VARCHAR(50),Addressline4 VARCHAR(50),PostCode VARCHAR(50))
INSERT INTO #Addresses VALUES
(1,'Add 1 Addressline1', 'Add 1 Addressline2', 'Add 1 Addressline3', 'Add 1 Addressline4', 'ABCD132'),
(2,'Add 2 Addressline1', 'Add 2 Addressline2', 'Add 2 Addressline3', 'Add 2 Addressline4', 'JKLM132'),
(3,'Add 3 Addressline1', 'Add 3 Addressline2', 'Add 3 Addressline3', 'Add 3 Addressline4', 'RTPZ132'),
(4,'Add 4 Addressline1', 'Add 4 Addressline2', 'Add 4 Addressline3', 'Add 4 Addressline4', 'XMLO132')
select AddressNumber [#AddressID]
,Addressline1
,Addressline2
,Addressline3
,Addressline4
,PostCode
FROM #Addresses
for xml PATH('Address'),Elements , root('PatientAddress')
Result Set
<PatientAddress>
<Address AddressID="1">
<Addressline1>Add 1 Addressline1</Addressline1>
<Addressline2>Add 1 Addressline2</Addressline2>
<Addressline3>Add 1 Addressline3</Addressline3>
<Addressline4>Add 1 Addressline4</Addressline4>
<PostCode>ABCD132</PostCode>
</Address>
<Address AddressID="2">
<Addressline1>Add 2 Addressline1</Addressline1>
<Addressline2>Add 2 Addressline2</Addressline2>
<Addressline3>Add 2 Addressline3</Addressline3>
<Addressline4>Add 2 Addressline4</Addressline4>
<PostCode>JKLM132</PostCode>
</Address>
<Address AddressID="3">
<Addressline1>Add 3 Addressline1</Addressline1>
<Addressline2>Add 3 Addressline2</Addressline2>
<Addressline3>Add 3 Addressline3</Addressline3>
<Addressline4>Add 3 Addressline4</Addressline4>
<PostCode>RTPZ132</PostCode>
</Address>
<Address AddressID="4">
<Addressline1>Add 4 Addressline1</Addressline1>
<Addressline2>Add 4 Addressline2</Addressline2>
<Addressline3>Add 4 Addressline3</Addressline3>
<Addressline4>Add 4 Addressline4</Addressline4>
<PostCode>XMLO132</PostCode>
</Address>
</PatientAddress>

just replace the literals with the name of the columns in your table:
select
(SELECT '1' addressline1,'2' addressline2, '3' addressline3, '4' addressline4, 'PC' postcode for xml path('Address1'), type ),
(SELECT '21' addressline1,'22' addressline2, '23' addressline3, '24' addressline4, '2PC' postcode for xml path('Address2'), type )
FOR XML PATH(''), root('PatientAddress'),type

Related

SQL XML to Return All Element Values

This should be simple but cannot get it to work after trying multiple suggestions from things I researched. I have some basic XML and just want to return the values from all the elements in seperate rows in a query result.
<Root>
<QueryRequest>
<ReturnedReuslts>
<Rows>
<Row>"1","Value 1"</Row>
<Row>"2","Value 2"</Row>
<Row>"3","Value 3"</Row>
</Rows>
</ReturnedReuslts>
</QueryRequest>
</Root>
I want it to return
Column 1 Column 2
-------- --------
1 Value 1
2 Value 2
3 Value 3
If I can't get it into two different columns, I'll settle for:
Column 1
----------------
"1","Value 1"
"2","Value 2"
"3","Value 3"
In SQL Server:
declare #strxml xml;
set #strxml = '<Root>
<QueryRequest>
<ReturnedReuslts>
<Rows>
<Row>"1","Value 1"</Row>
<Row>"2","Value 2"</Row>
<Row>"3","Value 3"</Row>
</Rows>
</ReturnedReuslts>
</QueryRequest>
</Root>'
select
substring(t1.col1, 1, charindex(',', t1.col1) - 1) Column1,
substring(t1.col1, charindex(',', t1.col1) + 1, len(t1.col1)) Column2
from
(select
col1 = replace(Node.Data.value('.', 'varchar(200)'), '"', '')
from
#strxml.nodes('/Root/QueryRequest/ReturnedReuslts/Rows/Row') Node(Data)) t1

How to split string in sql?

I have two tables, in one I have data like this:
id description
2 12.07.13y 1000eur to bank account KZ21321o0002134
4 To bank account KZasd9093636 12 of May 2016y 200dusd
And I have a second table where I need to put filtered information from table first like:
id
data
bank_account
tranfered_money
First i need to split description,then i need to recognize ban_account which always started with "KZ",data and transfered_money
This is just awful but seems to be able to extract the ban_acount:
CREATE TABLE exp
(
column1 varchar(400)
);
Insert into exp (column1) values ('12.07.13y 1000eur to bank account KZ21321o0002134');
Insert into exp (column1) values ('To bank account KZasd9093636 12 of May 2016y 200dusd');
Select
CASE
WHEN CHARINDEX ( SPACE(1), SUBSTRING ( column1, CHARINDEX('KZ' , column1),LEN(column1))) = 0
THEN SUBSTRING ( column1, CHARINDEX('KZ' , column1),LEN(column1))
ELSE SUBSTRING ( SUBSTRING (column1, CHARINDEX('KZ' , column1),LEN(column1)), 0, CHARINDEX (SPACE(1), SUBSTRING(column1, CHARINDEX('KZ' , column1),LEN(column1))))
END result
From exp
At first convert your table to XML.
Then create table with month/weekdays names and digits from 1 to 3000 (or you can take 2016 as current year)
You will need a table with currency. I made one based on data from here.
DECLARE #x xml
;WITH YourTable AS ( --I use this CTE, you should use your table in scripts below
SELECT *
FROM (VALUES
(2, '12.07.13y 1000eur to bank account KZ21321o0002134'),
(4, 'To bank account KZasd9093636 12 of May 2016y 200dusd')
) as t(id, [description])
)
SELECT #x = ( --XML sample that we get you can see below after output
SELECT CAST(N'<row id="'+CAST(id as nvarchar(max))+'"><b>'+REPLACE([description],' ','</b><b>')+'</b></row>' as xml)
FROM YourTable
FOR XML PATH('')
)
;WITH CurrencyList AS ( --Currency table
SELECT *
FROM (VALUES
('AED', 'United Arab Emirates Dirham'),
('AFN', 'Afghanistan Afghani'),
('ALL', 'Albania Lek'),
('AMD', 'Armenia Dram'),
...
('ZAR', 'South Africa Rand'),
('ZMW', 'Zambia Kwacha'),
('ZWD', 'Zimbabwe Dollar')
) as t(code, countryname)
),cte AS ( --generate numbers 1 to 3000
SELECT 0 as d
UNION ALL
SELECT d+1
FROM cte
WHERE d < 3000
), datenames AS ( --generate datenames
SELECT d,
CASE WHEN d < 7 THEN DATENAME(weekday,DATEADD(day,d,'1970-01-01 00:00:00.000')) ELSE NULL END as weekday_name,
CASE WHEN d < 12 THEN DATENAME(month,DATEADD(month,d,'1970-01-01 00:00:00.000')) ELSE NULL END as mon_name
FROM cte
)
--Final query
SELECT t.c.value('../#id','int') as id,
t.c.value('.','nvarchar(max)') as str_part,
CASE WHEN t.c.value('.','nvarchar(max)') LIKE 'KZ%' THEN 'bank_account'
WHEN countryname IS NOT NULL THEN 'tranfered_money'
WHEN dn.d IS NOT NULL OR RIGHT(t.c.value('.','nvarchar(max)'),1) ='y' THEN 'datepart'
ELSE NULL END as what_is
FROM #x.nodes('/row/b') as t(c)
LEFT JOIN CurrencyList cl
ON RIGHT(t.c.value('.','nvarchar(max)'),3) = cl.code --check 3 last symbols of string with currency codes
LEFT JOIN datenames dn
ON dn.d = t.c.value('. cast as xs:int?','int') -- if it is a day/month/year number
OR t.c.value('.','nvarchar(max)') = dn.weekday_name -- or it is a week day name
OR t.c.value('.','nvarchar(max)') = dn.mon_name --or month name
OPTION (MAXRECURSION 0)
Will bring you:
id str_part what_is
2 12.07.13y datepart
2 1000eur tranfered_money
2 to NULL
2 bank NULL
2 account NULL
2 KZ21321o0002134 bank_account
4 To NULL
4 bank NULL
4 account NULL
4 KZasd9093636 bank_account
4 12 datepart
4 of NULL
4 May datepart
4 2016y datepart
4 200dusd tranfered_money
After that you need to bring dates in normal date form and that is all.
XML Sample:
<row id="2">
<b>12.07.13y</b>
<b>1000eur</b>
<b>to</b>
<b>bank</b>
<b>account</b>
<b>KZ21321o0002134</b>
</row>
<row id="4">
<b>To</b>
<b>bank</b>
<b>account</b>
<b>KZasd9093636</b>
<b>12</b>
<b>of</b>
<b>May</b>
<b>2016y</b>
<b>200dusd</b>
</row>

Incorrect structure of XML generated in SQL

Iv had another problem, i just change my UNION to UNION ALL and it work correctly but i want to add another XML path (at the beginning) - this will be a const for both querys.
(SELECT 1 AS "ns0:kindOfItem",
code AS "ns0:wholeCode",
REPLACE(weight, ',', '.') AS "ns0:weight",
1 AS "ns0:ammountOfNumbers",
(SELECT price AS "ns0:value",
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:sendedItems'), TYPE),
(SELECT
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:present'), TYPE)
FROM [PL].[dbo].[dk_documents] where id in (1,2,3)
UNION ALL
(SELECT 1 AS "ns0:kindOfItem",
code AS "ns0:wholeCode",
REPLACE(weight, ',', '.') AS "ns0:weight",
1 AS "ns0:ammountOfNumbers",
(SELECT price AS "ns0:value",
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:sendedItems'), TYPE),
(SELECT
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:present'), TYPE)
FROM [PL2].[dbo].[dk_documents] where id in (1,2,3)
FOR XML PATH('test'))
It work correctly but i want sth like this :
SELECT 1 as test,
(SELECT 1 AS "ns0:kindOfItem",
code AS "ns0:wholeCode",
REPLACE(weight, ',', '.') AS "ns0:weight",
1 AS "ns0:ammountOfNumbers",
(SELECT price AS "ns0:value",
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:sendedItems'), TYPE),
(SELECT
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:present'), TYPE)
FROM [PL].[dbo].[dk_documents] where id in (1,2,3)
UNION ALL
(SELECT 1 AS "ns0:kindOfItem",
code AS "ns0:wholeCode",
REPLACE(weight, ',', '.') AS "ns0:weight",
1 AS "ns0:ammountOfNumbers",
(SELECT price AS "ns0:value",
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:sendedItems'), TYPE),
(SELECT
'EUR' as "ns0:currency"
FOR XML PATH ('ns0:present'), TYPE)
FROM [PL2].[dbo].[dk_documents] where id in (1,2,3)
FOR XML PATH('test'))
FOR XML PATH('anotherPath')
i got this error:
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS.
Output should be like
<test>1</test>
<>All of tese columns from QUERY with union ALL</>
Just an example :
WITH XMLNAMESPACES(DEFAULT 'Dummy')
SELECT 1 as test,
2 as anotherOne,
(SELECT * FROM
(SELECT id, symbol from table1
WHERE id in (1,2,3)
UNION ALL
SELECT id, nrdok from table2
WHERE id in (4,5,6))as yolo
FOR XML PATH(''),TYPE)
FOR XML PATH('test')
It gives me an output :
<test xmlns="Dummy">
<test>1</test>
<anotherOne>2</anotherOne>
<id xmlns="Dummy">1</id>
<symbol xmlns="Dummy">test10</symbol>
<id xmlns="Dummy">2</id>
<symbol xmlns="Dummy">test10</symbol>
<id xmlns="Dummy">3</id>
<symbol xmlns="Dummy">test10</symbol>
<id xmlns="Dummy">4</id>
<symbol xmlns="Dummy">test11</symbol>
<id xmlns="Dummy">5</id>
<symbol xmlns="Dummy">test11</symbol>
<id xmlns="Dummy">6</id>
<symbol xmlns="Dummy">test11</symbol>
</test>
And i want :
<test xmlns="Dummy">
<test>1</test>
<anotherOne>2</anotherOne>
<id>1</id>
<symbol>test10</symbol>
<id>2</id>
<symbol>test10</symbol>
<id>3</id>
<symbol>test10</symbol>
<id>4</id>
<symbol>test11</symbol>
<id>5</id>
<symbol>test11</symbol>
<id>6</id>
<symbol>test11</symbol>
</test>
As pointed out at your previous question the repeated namespaces are not wrong, just annoying, and - if there are many and long URIs, they can blow up your XML to remarkable size...
There I placed a link to a related question already. The trick is to create the XML without the namespace and add the namespace in the finaly SELECT ... FOR XML PATH only:
But I must admit, that after a long while of trial and error I found, that there seem to be a bug if the DEFAULT namespace is involved. Any approach I tried led to either repeated namespace declarations or to repeated empty namespace declarations.
So the only solution I could find is this (I go to wash my fingers now :-) ):
DECLARE #table1 TABLE(id INT,symbol VARCHAR(100));
INSERT INTO #table1 VALUES
(1,'Test 1')
,(2,'Test 2')
,(3,'Test 3')
,(4,'Test 4');
DECLARE #nordic_table2 TABLE(id INT,nrdok VARCHAR(100));
INSERT INTO #nordic_table2 VALUES
(1,'Test 1')
,(2,'Test 2')
,(3,'Test 3')
,(4,'Test 4');
DECLARE #XmlWithoutNamespace XML=
(
SELECT 1 as test
,2 as anotherOne
,(
SELECT *
FROM
(
SELECT id, symbol from #table1
WHERE id in (1,2,3)
UNION ALL
SELECT id, nrdok from #nordic_table2
WHERE id in (4,5,6)
) AS yolo
FOR XML PATH(''),TYPE
)
FOR XML PATH('')
);
SELECT
CAST(
'<test xmlns="Dummy">'
+
CAST(#XmlWithoutNamespace AS NVARCHAR(MAX))
+
'</test>'
AS XML);
UPDATE
I strongly advise you to change your structure to this
SELECT id AS [#id]
,symbol AS [*]
FROM
(
SELECT id, symbol from #table1
WHERE id in (1,2,3)
UNION ALL
SELECT id, nrdok from #nordic_table2
WHERE id in (4,5,6)
) AS yolo
FOR XML PATH('symbol'),TYPE
The result would be this, which is much better to read and to query...
<test xmlns="Dummy">
<test>1</test>
<anotherOne>2</anotherOne>
<symbol id="1">Test 1</symbol>
<symbol id="2">Test 2</symbol>
<symbol id="3">Test 3</symbol>
<symbol id="4">Test 4</symbol>
</test>
UPDATE 2: More Namespaces...
It is actually really hard - almost impossible - to deal with namespaces properly. There is some highly developed logic within FOR XML PATH and in methods like .modify(). I tried several approaches but did not find a convincing one...
The only way I found is very ugly. The trick is, to create 1-level-XML only (no nested elements from sub-selects!) and store them as strings. But before you cut away the root with the namespace declarations. Doing so you'll get invalid XML fragments.
You concatenate them and CAST the whole lot in the last step back to XML.
--Just a container to collect the XML parts
DECLARE #CollectXML TABLE(ID INT IDENTITY,Content XML,CleanedAsString NVARCHAR(MAX));
--The final ",ROOT('xyz')" will force the namespace's declaration into the root node
WITH XMLNAMESPACES('SomeTestUri' AS abc)
INSERT INTO #CollectXML(Content)
SELECT
(
SELECT 1 as [abc:test]
,2 as anotherOne
FOR XML PATH(''),ROOT('xyz'),TYPE
);
--No we use ugly string manipulation to cut out the inner part without the namespace declaration
UPDATE #CollectXML SET CleanedAsString=
(
SELECT Rest
FROM #CollectXML
CROSS APPLY (SELECT CAST(Content AS NVARCHAR(MAX))) AS Casted(XmlAsString)
CROSS APPLY (SELECT REVERSE(SUBSTRING(XmlAsString,CHARINDEX('>',XmlAsString)+1,LEN(XmlAsString)))) AS Cut1(part1Reverse)
CROSS APPLY (SELECT REVERSE(SUBSTRING(part1Reverse,CHARINDEX('<',part1Reverse)+1,LEN(part1Reverse)))) AS Cut2(Rest)
WHERE ID=1
)
WHERE ID=1;
--The same with the second part
WITH XMLNAMESPACES('SomeTestUri' AS abc)
INSERT INTO #CollectXML(Content)
SELECT
(
SELECT id AS [#abc:id]
,symbol AS [*]
FROM
(
SELECT id, symbol from #table1
WHERE id in (1,2,3)
UNION ALL
SELECT id, nrdok from #nordic_table2
WHERE id in (4,5,6)
) AS yolo
FOR XML PATH('abc:symbol'),ROOT('xyz'),TYPE --the not needed root will take the namespace declaration out of the deeper elements
);
--and the ugly string manipulation
UPDATE #CollectXML SET CleanedAsString=
(
SELECT Rest
FROM #CollectXML
CROSS APPLY (SELECT CAST(Content AS NVARCHAR(MAX))) AS Casted(XmlAsString)
CROSS APPLY (SELECT REVERSE(SUBSTRING(XmlAsString,CHARINDEX('>',XmlAsString)+1,LEN(XmlAsString)))) AS Cut1(part1Reverse)
CROSS APPLY (SELECT REVERSE(SUBSTRING(part1Reverse,CHARINDEX('<',part1Reverse)+1,LEN(part1Reverse)))) AS Cut2(Rest)
WHERE ID=2
)
WHERE ID=2;
--The XML is put together and - as the very last step! - casted back to XML
SELECT
CAST(
'<test xmlns="Dummy" xmlns:abc="SomeTestUri">'
+
(SELECT CleanedAsString FROM #CollectXML WHERE ID=1)
+
(SELECT CleanedAsString FROM #CollectXML WHERE ID=2)
+
'</test>'
AS XML);
The result for this
<test xmlns="Dummy" xmlns:abc="SomeTestUri">
<abc:test>1</abc:test>
<anotherOne>2</anotherOne>
<abc:symbol abc:id="1">Test 1</abc:symbol>
<abc:symbol abc:id="2">Test 2</abc:symbol>
<abc:symbol abc:id="3">Test 3</abc:symbol>
<abc:symbol abc:id="4">Test 4</abc:symbol>
</test>
You can test below SQL Select statement.
I hope it helps,
declare #xml xml
declare #xml1 xml
set #xml1 = '<test>1</test>'
declare #xml2 xml
set #xml2 = ( select top 4 name from sys.databases FOR XML PATH('database') )
set #xml = (SELECT #xml1, #xml2 FOR XML PATH(''))
select #xml
Output will be as follows

SQL Server to produce XML data rows from JOINed select statement

I have a three tables in SQL Server 2008 which are setup as follows:
EMPLOYEE TABLE
empid(PK)
1
2
joined to EMPLOYEEATTRIBUTES
dataId(PK) | empId(FK) | attributeid | attributeVal
10 | 1 | A1 | somevalue1
20 | 1 | A2 | somevalue2
30 | 2 | A1 | somevalue3
40 | 2 | A3 | somevalue4
joined to ATTRIBUTES
attributeid | attributeName
A1 | attribute1
A2 | attribute2
A3 | attribute3
I need to get the xml data out into the following format
<rows>
<row empid="1">
<attribute1>somevalue1</attribute1>
<attribute2>somevalue2</attribute1>
</row>
<row empid="2">
<attribute1>somevalue3</attribute1>
<attribute3>somevalue4</attribute1>
</row>
</rows>
Anyone know how this can be done??
If you want to skip all of the gory details and just see an answer, look at the SQL query at the bottom of this posting.
The main challenge here is that the various SQL Server FOR XML options cannot generate the dynamic element names stipulated in the desired output. Therefore, my first answer is to consider simply returning a conventional SQL result set and having the client generate the XML. It is a very simple streaming transformation. However, this might not be an option for you, so we continue on the path of having SQL Server generate the XML.
My second thought was to use SQL Server's built-in XQuery functionality to perform the transformation, thus:
/* WARNING: the following SQL does not work */
SELECT
CAST((SELECT * FROM data FOR XML RAW) AS XML)
.query('
<rows>
{
for $empId in distinct-values(/row/#empId)
return
<row empid="{$empId}">
{
for $attr in /row[#empId = $empId]
return
attribute { "attribute" } { $attr/#attributeValue }
}
</row>
}
</rows>
')
Alas, this does not work. SQL Server complains:
Msg 9315, Level 16, State 1, Line 25
XQuery [query()]: Only constant expressions are supported for the name expression
of computed element and attribute constructors.
Apparently, the XQuery implementation suffers from the same limitation as the FOR XML features. So, my second answer is to suggest generating the XML on the client side :) But if you insist on generating the XML from SQL, then fasten your seatbelts...
The overall strategy is going to be to abandon SQL Server's native facilities for SQL generation. Instead, we are going to build up the XML document using string concatenation. If this approach is offensive, you can stop reading now :)
Let's start with generating a sample dataset to play with:
SELECT NULL AS empId INTO employee WHERE 1=0
UNION SELECT 1
UNION SELECT 2
SELECT NULL AS dataId, NULL AS empId, NULL AS attributeId, NULL AS attributeVal INTO employeeAttributes WHERE 1=0
UNION SELECT 10, 1, 'A1', 'someValue1'
UNION SELECT 20, 1, 'A2', 'someValue2'
UNION SELECT 30, 2, 'A1', 'someValue3'
UNION SELECT 40, 2, 'A3', 'someValue4 & <>!'
SELECT NULL AS attributeId, NULL AS attributeName INTO attributes WHERE 1=0
UNION SELECT 'A1', 'attribute1'
UNION SELECT 'A2', 'attribute2'
UNION SELECT 'A3', 'attribute3'
Note that I have changed the value of the last attribute in the provided example to include some XML-unfriendly characters.
Now, put together a basic SQL query to perform the necessary joins:
SELECT
e.empId
, a.attributeName
, ea.attributeVal
FROM employee AS e
INNER JOIN employeeAttributes AS ea
ON ea.empId = e.empId
INNER JOIN attributes AS a
ON a.attributeId = ea.attributeId
which gives this result:
empId attributeName attributeVal
1 attribute1 someValue1
1 attribute2 someValue2
2 attribute1 someValue3
2 attribute3 someValue4 & <>!
Those funny characters in the last attribute are going to give us trouble. Let's change the query to escape them.
; WITH
cruftyData AS (
SELECT
e.empId
, a.attributeName
, (SELECT ea.attributeVal AS x FOR XML RAW) AS attributeValXml
FROM employee AS e
INNER JOIN employeeAttributes AS ea
ON ea.empId = e.empId
INNER JOIN attributes AS a
ON a.attributeId = ea.attributeId
)
, data AS (
SELECT
empId
, attributeName
, SUBSTRING(attributeValXml, 9, LEN(attributeValXml)-11) AS attributeVal
FROM cruftyData
)
SELECT * FROM data
with results:
empId attributeName attributeValXml
1 attribute1 someValue1
1 attribute2 someValue2
2 attribute1 someValue3
2 attribute3 someValue4 & <>!
This ensures that attribute values can now be safely used in an XML document. What about attribute names? The rules for XML attribute names are more strict than those for element content. We will assume that the attributes names are valid XML identifiers. If this is not true, then some scheme will need to be devised to convert the names in the database to valid XML names. This is left as an exercise to the reader :)
The next challenge is to ensure that the attributes are grouped together for each employee, and we can tell when we are at the first or last value in a group. Here is the updated query:
; WITH
cruftyData AS (
SELECT
e.empId
, a.attributeName
, (SELECT ea.attributeVal AS x FOR XML RAW) AS attributeValXml
FROM employee AS e
INNER JOIN employeeAttributes AS ea
ON ea.empId = e.empId
INNER JOIN attributes AS a
ON a.attributeId = ea.attributeId
)
, data AS (
SELECT
empId
, attributeName
, SUBSTRING(attributeValXml, 9, LEN(attributeValXml)-11) AS attributeVal
, ROW_NUMBER() OVER (PARTITION BY empId ORDER BY attributeName DESC) AS down
, ROW_NUMBER() OVER (PARTITION BY empId ORDER BY attributeName) AS up
FROM cruftyData
)
SELECT * FROM data ORDER BY 1, 2
The only change is to add the down and up columns to the result set:
empId attributeName attributeVal down up
1 attribute1 someValue1 2 1
1 attribute2 someValue2 1 2
2 attribute1 someValue3 2 1
2 attribute3 someValue4 & <>! 1 2
We can now identify the first attribute for an employee because up will be 1. The last attribute can be identified in similar fashion using the down column.
Armed with all of this, we are now equipped to perform the nasty business of building up the XML result using string concatenation.
; WITH
cruftyData AS (
SELECT
e.empId
, a.attributeName
, (SELECT ea.attributeVal AS x FOR XML RAW) AS attributeValXml
FROM employee AS e
INNER JOIN employeeAttributes AS ea
ON ea.empId = e.empId
INNER JOIN attributes AS a
ON a.attributeId = ea.attributeId
)
, data AS (
SELECT
empId
, attributeName
, SUBSTRING(attributeValXml, 9, LEN(attributeValXml)-11) AS attributeVal
, ROW_NUMBER() OVER (PARTITION BY empId ORDER BY attributeName DESC) AS down
, ROW_NUMBER() OVER (PARTITION BY empId ORDER BY attributeName) AS up
FROM cruftyData
)
, xmlData AS (
SELECT
empId
, up
, CASE WHEN up <> 1 THEN '' ELSE '<row id="' + CAST (empId AS NVARCHAR) + '">' END AS xml1
, '<' + attributeName + '>' + attributeVal + '</' + attributeName + '>' AS xml2
, CASE WHEN down <> 1 THEN '' ELSE '</row>' END AS xml3
FROM data
)
SELECT xml1, xml2, xml3
--SELECT #result = #result + 'wombat' + xmlString
FROM xmlData
ORDER BY empId, up
with the result:
xml1 xml2 xml3
<row id="1"> <attribute1>someValue1</attribute1>
<attribute2>someValue2</attribute2> </row>
<row id="2"> <attribute1>someValue3</attribute1>
<attribute3>someValue4 & <>!</attribute3> </row>
All that remains is to concatenate all of the rows together, and to add the root rows tags. Since T-SQL does not (yet) have a string concatenation aggregate function, we will resort to using a variable as an accumulator. Here is the final query, in all its hacky glory:
DECLARE #result AS NVARCHAR(MAX)
SELECT #result = '<rows>'
; WITH
cruftyData AS (
SELECT
e.empId
, a.attributeName
, (SELECT ea.attributeVal AS x FOR XML RAW) AS attributeValXml
FROM employee AS e
INNER JOIN employeeAttributes AS ea
ON ea.empId = e.empId
INNER JOIN attributes AS a
ON a.attributeId = ea.attributeId
)
, data AS (
SELECT
empId
, attributeName
, SUBSTRING(attributeValXml, 9, LEN(attributeValXml)-11) AS attributeVal
, ROW_NUMBER() OVER (PARTITION BY empId ORDER BY attributeName DESC) AS down
, ROW_NUMBER() OVER (PARTITION BY empId ORDER BY attributeName) AS up
FROM cruftyData
)
, xmlData AS (
SELECT
empId
, up
, CASE WHEN up <> 1 THEN '' ELSE '<row id="' + CAST (empId AS NVARCHAR) + '">' END AS xml1
, '<' + attributeName + '>' + attributeVal + '</' + attributeName + '>' AS xml2
, CASE WHEN down <> 1 THEN '' ELSE '</row>' END AS xml3
FROM data
)
SELECT #result = #result + xml1 + xml2 + xml3
FROM xmlData
ORDER BY empId, up
SELECT #result = #result + '</rows>'
SELECT #result
The XML ends up in the #result variable. You can check that it is well-formed XML using:
SELECT CAST(#result AS XML)
The final XML looks like this:
<rows><row id="1"><attribute1>someValue1</attribute1><attribute2>someValue2</attribute2></row><row id="2"><attribute1>someValue3</attribute1><attribute3>someValue4 & <>!</attribute3></row></rows>
You can get close - but you can't get your desired output 100%.
Using this query:
SELECT
EmpID AS '#empid',
(
SELECT
a.AttributeName AS '#name',
ea.AttributeVal
FROM dbo.EmployeeAttributes ea
INNER JOIN dbo.Attributes a ON ea.AttributeId = a.AttributeId
WHERE ea.EmpID = e.EmpID
FOR XML PATH ('attribute'), TYPE
)
FROM dbo.Employee e
FOR XML PATH('row'), ROOT('rows')
you get this output:
<rows>
<row empid="1">
<attribute name="Attribute1">
<AttributeVal>SomeValue1</AttributeVal>
</attribute>
<attribute name="attribute2">
<AttributeVal>SomeValue2</AttributeVal>
</attribute>
</row>
<row empid="2">
<attribute name="Attribute1">
<AttributeVal>SomeValue3</AttributeVal>
</attribute>
<attribute name="attribute3">
<AttributeVal>SomeValue4</AttributeVal>
</attribute>
</row>
</rows>
What you cannot do is make the inner XML nodes have tag names that match the attribute name - you have to use some fixed tag name (like <attribute> in my sample), and then apply the values that are retrieved from your tables as either attributes on those XML tags (like the name= attribute in my sample) or as XML element values.
As far as I know, there is no way to use the AttributeValue as the XML tag name....
Here's an answer, but the PIVOT command limits you in that you have to know the name of your attributes in advance. With a little tweaking, you could probably do this dynamically (try searching for dynamic pivot in SQL Server 2005):
DECLARE #Employee TABLE ( empid INT )
DECLARE #EA TABLE
(
dataid INT
, empid INT
, attributeid CHAR(2)
, AttributeVal VARCHAR(100)
)
DECLARE #Attributes TABLE
(
AttributeID CHAR(2)
, AttributeName VARCHAR(100)
)
INSERT INTO #Employee
VALUES ( 1 ),
( 2 )
INSERT INTO #EA
( dataid, empid, attributeid, AttributeVal )
VALUES ( 10, 1, 'A1', 'somevalue1' )
, ( 20, 1, 'A2', 'somevalue2' )
, ( 30, 2, 'A1', 'somevalue3' )
, ( 40, 2, 'A3', 'somevalue4' )
INSERT INTO #Attributes
( AttributeID, AttributeName )
VALUES ( 'A1', 'attribute1' )
,
( 'A2', 'attribute2' )
,
( 'A3', 'attribute3' )
SELECT empID as '#empid'
, attribute1
, attribute2
, attribute3
, attribute4
FROM ( SELECT e.empid
, a.AttributeName
, ea.AttributeVal
FROM #Employee e
JOIN #EA ea ON e.empid = ea.empid
JOIN #Attributes a ON ea.attributeid = a.attributeid
) ps PIVOT
( MIN(AttributeVal) FOR AttributeName IN ( [attribute1], [attribute2], [attribute3], [attribute4] ) ) AS pvt
FOR XML PATH('row'), ROOT('rows')

Select XML nodes as rows

I am selecting from a table that has an XML column using T-SQL. I would like to select a certain type of node and have a row created for each one.
For instance, suppose I am selecting from a people table. This table has an XML column for addresses. The XML is formated similar to the following:
<address>
<street>Street 1</street>
<city>City 1</city>
<state>State 1</state>
<zipcode>Zip Code 1</zipcode>
</address>
<address>
<street>Street 2</street>
<city>City 2</city>
<state>State 2</state>
<zipcode>Zip Code 2</zipcode>
</address>
How can I get results like this:
Name City State
Joe Baker Seattle WA
Joe Baker Tacoma WA
Fred Jones Vancouver BC
Here is your solution:
/* TEST TABLE */
DECLARE #PEOPLE AS TABLE ([Name] VARCHAR(20), [Address] XML )
INSERT INTO #PEOPLE SELECT
'Joel',
'<address>
<street>Street 1</street>
<city>City 1</city>
<state>State 1</state>
<zipcode>Zip Code 1</zipcode>
</address>
<address>
<street>Street 2</street>
<city>City 2</city>
<state>State 2</state>
<zipcode>Zip Code 2</zipcode>
</address>'
UNION ALL SELECT
'Kim',
'<address>
<street>Street 3</street>
<city>City 3</city>
<state>State 3</state>
<zipcode>Zip Code 3</zipcode>
</address>'
SELECT * FROM #PEOPLE
-- BUILD XML
DECLARE #x XML
SELECT #x =
( SELECT
[Name]
, [Address].query('
for $a in //address
return <address
street="{$a/street}"
city="{$a/city}"
state="{$a/state}"
zipcode="{$a/zipcode}"
/>
')
FROM #PEOPLE AS people
FOR XML AUTO
)
-- RESULTS
SELECT [Name] = T.Item.value('../#Name', 'varchar(20)'),
street = T.Item.value('#street' , 'varchar(20)'),
city = T.Item.value('#city' , 'varchar(20)'),
state = T.Item.value('#state' , 'varchar(20)'),
zipcode = T.Item.value('#zipcode', 'varchar(20)')
FROM #x.nodes('//people/address') AS T(Item)
/* OUTPUT*/
Name | street | city | state | zipcode
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Joel | Street 1 | City 1 | State 1 | Zip Code 1
Joel | Street 2 | City 2 | State 2 | Zip Code 2
Kim | Street 3 | City 3 | State 3 | Zip Code 3
Here's how I do it generically:
I shred the source XML via a call such as
DECLARE #xmlEntityList xml
SET #xmlEntityList =
'
<ArbitrarilyNamedXmlListElement>
<ArbitrarilyNamedXmlItemElement><SomeVeryImportantInteger>1</SomeVeryImportantInteger></ArbitrarilyNamedXmlItemElement>
<ArbitrarilyNamedXmlItemElement><SomeVeryImportantInteger>2</SomeVeryImportantInteger></ArbitrarilyNamedXmlItemElement>
<ArbitrarilyNamedXmlItemElement><SomeVeryImportantInteger>3</SomeVeryImportantInteger></ArbitrarilyNamedXmlItemElement>
</ArbitrarilyNamedXmlListElement>
'
DECLARE #tblEntityList TABLE(
SomeVeryImportantInteger int
)
INSERT #tblEntityList(SomeVeryImportantInteger)
SELECT
XmlItem.query('//SomeVeryImportantInteger[1]').value('.','int') as SomeVeryImportantInteger
FROM
[dbo].[tvfShredGetOneColumnedTableOfXmlItems] (#xmlEntityList)
by utilizing the scalar-valued function
/* Example Inputs */
/*
DECLARE #xmlListFormat xml
SET #xmlListFormat =
'
<ArbitrarilyNamedXmlListElement>
<ArbitrarilyNamedXmlItemElement>004421UB7</ArbitrarilyNamedXmlItemElement>
<ArbitrarilyNamedXmlItemElement>59020UH24</ArbitrarilyNamedXmlItemElement>
<ArbitrarilyNamedXmlItemElement>542514NA8</ArbitrarilyNamedXmlItemElement>
</ArbitrarilyNamedXmlListElement>
'
declare #tblResults TABLE
(
XmlItem xml
)
*/
-- =============================================
-- Author: 6eorge Jetson
-- Create date: 01/02/3003
-- Description: Shreds a list of XML items conforming to
-- the expected generic #xmlListFormat
-- =============================================
CREATE FUNCTION [dbo].[tvfShredGetOneColumnedTableOfXmlItems]
(
-- Add the parameters for the function here
#xmlListFormat xml
)
RETURNS
#tblResults TABLE
(
-- Add the column definitions for the TABLE variable here
XmlItem xml
)
AS
BEGIN
-- Fill the table variable with the rows for your result set
INSERT #tblResults
SELECT
tblShredded.colXmlItem.query('.') as XmlItem
FROM
#xmlListFormat.nodes('/child::*/child::*') as tblShredded(colXmlItem)
RETURN
END
--SELECT * FROM #tblResults
In case this is useful to anyone else out there looking for a "generic" solution, I created a CLR procedure that can take an Xml fragment as above and "shred" it into a tabular resultset, without you providing any additional information about the names or types of the columns, or customizing your call in any way for the given Xml fragment:
http://architectshack.com/ClrXmlShredder.ashx
There are of course some restrictions (the xml must be "tabular" in nature like this sample, the first row needs to contain all the elements/columns that will be supported, etc) - but I do hope it's a few steps ahead of what's available built-in.
Here's an alternate solution:
;with cte as
(
select id, name, addresses, addresses.value('count(/address/city)','int') cnt
from #demo
)
, cte2 as
(
select id, name, addresses, addresses.value('((/address/city)[sql:column("cnt")])[1]','nvarchar(256)') city, cnt-1 idx
from cte
where cnt > 0
union all
select cte.id, cte.name, cte.addresses, cte.addresses.value('((/address/city)[sql:column("cte2.idx")])[1]','nvarchar(256)'), cte2.idx-1
from cte2
inner join cte on cte.id = cte2.id and cte2.idx > 0
)
select id, name, city
from cte2
order by id, city
FYI: I've posted another version of this SQL on the code review site here: https://codereview.stackexchange.com/questions/108805/select-field-in-an-xml-column-where-both-xml-and-table-contain-multiple-matches
If you can use it, the linq api is convenient for XML:
var addresses = dataContext.People.Addresses
.Elements("address")
.Select(address => new {
street = address.Element("street").Value,
city = address.Element("city").Value,
state = address.Element("state").Value,
zipcode = address.Element("zipcode").Value,
});