Reputation: 85
I have a SQL table with an XML column. I would like to separate out the XML elements into their own columns within a view.
I am able to do this using .value, however I don't always know what the element names are. For example in the select below the c element is missing.
create table #temp (Id int, Name varchar(32), taskdata xml)
insert into #temp values
(1, 'Fred','<data><a>Red</a><b>Apple</b></data>'),
(2, 'Mary','<data><a>Blue</a><b>Ball</b></data>'),
(3, 'Paul','<data><a>Green</a><b>Tree</b></data>'),
(4, 'Lisa','<data><a>Yellow</a><b>Hat</b><c>House</c></data>')
select Id
,Name
,Taskdata.value('(/data/a)[1]', 'nvarchar(max)') AS a
,Taskdata.value('(/data/b)[1]', 'nvarchar(max)') AS b
from #temp
drop table #temp
I can get a list of all the elements names using:
select distinct T.N.value('local-name(.)','nvarchar(64)') ColNames
from #temp
cross apply Taskdata.nodes('//data/*') as T(N)
However I cant work out how to replace:
Taskdata.value('(/data/a)[1]', 'nvarchar(max)') AS a
For something more dynamic.
Upvotes: 3
Views: 1429
Reputation: 67311
If you need a fully generic approach you might try dynamic SQL:
DECLARE @cmd VARCHAR(1000)=
'select Id
,Name' +
(
SELECT DISTINCT',Taskdata.value(''(/data/' + TheNode.value('local-name(.)','nvarchar(64)') + ')[1]'', ''nvarchar(max)'') AS [' + TheNode.value('local-name(.)','nvarchar(64)') + '] '
FROM #temp AS innerT
CROSS APPLY innerT.taskdata.nodes('/data/*') AS ThisIs(TheNode)
FOR XML PATH('')
)
+
'from #temp;'
EXEC (@cmd);
Views cannot use temp tables, had to change your #temp to a normal table...
create table temp (Id int, Name varchar(32), taskdata xml)
insert into temp values
(1, 'Fred','<data><a>Red</a><b>Apple</b></data>'),
(2, 'Mary','<data><a>Blue</a><b>Ball</b></data>'),
(3, 'Paul','<data><a>Green</a><b>Tree</b></data>'),
(4, 'Lisa','<data><a>Yellow</a><b>Hat</b><c>House</c></data>')
DECLARE @cmd VARCHAR(1000)=
'CREATE VIEW dbo.SomeName AS select Id
,Name' +
(
SELECT DISTINCT',Taskdata.value(''(/data/' + TheNode.value('local-name(.)','nvarchar(64)') + ')[1]'', ''nvarchar(max)'') AS [' + TheNode.value('local-name(.)','nvarchar(64)') + '] '
FROM temp AS innerT
CROSS APPLY innerT.taskdata.nodes('/data/*') AS ThisIs(TheNode)
FOR XML PATH('')
)
+
'from temp;'
EXEC (@cmd);
GO
SELECT * FROM dbo.SomeName;
GO
drop view dbo.SomeName;
drop table temp;
The problem with XML is: You have to know the structure, at least some things your data has in common: Is there always a root element "data"? Are there always 1:n inner elements and nothing else? What is their maximum number? How would you know which element is missing, if you have a and c but no b?
This would be one approach:
select Id
,Name
,Taskdata.value('/data[1]/*[1]', 'nvarchar(max)') AS a
,Taskdata.value('/data[1]/*[2]', 'nvarchar(max)') AS b
,Taskdata.value('/data[1]/*[3]', 'nvarchar(max)') AS c
from #temp
You would get the same if you know the inner element's names by querying with
,Taskdata.value('(/data/c)[1]', 'nvarchar(max)') AS c
The result
Id Name a b c
1 Fred Red Apple NULL
2 Mary Blue Ball NULL
3 Paul Green Tree NULL
4 Lisa Yellow Hat House
Upvotes: 3