Reputation: 7730
I'm writing a user-defined function to extract values from an XML column in SQL Server which represents a simple dictionary of string key-value pairs. The only way I've made it work so far seems overly complex. Do you have any simplifying suggestions or tips for the DictValue
function below?
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[DictValue]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION [dbo].[DictValue]
go
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[TableWithXmlColumn]') AND type in (N'U'))
DROP TABLE [dbo].[TableWithXmlColumn]
go
create table TableWithXmlColumn (
Id int identity primary key
,Dict xml
)
go
create function DictValue(
@id int
,@key nvarchar(max)
) returns nvarchar(max) as begin
declare @d xml -- string Dictionary
select @d = Dict from TableWithXmlColumn where Id = @id
declare @value xml
select
@value = d.Pair.value('data(.)', 'nvarchar(max)')
from
@d.nodes('/StringDictionary/Pair') as d(Pair)
where
@key = d.Pair.value('./@Key', 'nvarchar(max)')
return convert(nvarchar(max), @value)
end
go
declare @xmlId int
insert TableWithXmlColumn (Dict) values (
N'<?xml version="1.0" encoding="utf-16"?>
<StringDictionary>
<Pair Key="color">red</Pair>
<Pair Key="count">123</Pair>
</StringDictionary>')
set @xmlId = scope_identity()
select
dbo.DictValue(@xmlId, 'color') as color
,dbo.DictValue(@xmlId, 'count') as [count]
Upvotes: 1
Views: 2762
Reputation: 1669
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[XMLTable](@x XML)
RETURNS TABLE
AS RETURN
WITH cte AS (
SELECT
1 AS lvl,
x.value('local-name(.)','NVARCHAR(MAX)') AS Name,
CAST(NULL AS NVARCHAR(MAX)) AS ParentName,
CAST(1 AS INT) AS ParentPosition,
CAST(N'Element' AS NVARCHAR(20)) AS NodeType,
x.value('local-name(.)','NVARCHAR(MAX)') AS FullPath,
x.value('local-name(.)','NVARCHAR(MAX)')
+ N'['
+ CAST(ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS NVARCHAR)
+ N']' AS XPath,
ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS Position,
x.value('local-name(.)','NVARCHAR(MAX)') AS Tree,
x.value('text()[1]','NVARCHAR(MAX)') AS Value,
x.query('.') AS this,
x.query('*') AS t,
CAST(CAST(1 AS VARBINARY(4)) AS VARBINARY(MAX)) AS Sort,
CAST(1 AS INT) AS ID
FROM @x.nodes('/*') a(x)
UNION ALL
SELECT
p.lvl + 1 AS lvl,
c.value('local-name(.)','NVARCHAR(MAX)') AS Name,
CAST(p.Name AS NVARCHAR(MAX)) AS ParentName,
CAST(p.Position AS INT) AS ParentPosition,
CAST(N'Element' AS NVARCHAR(20)) AS NodeType,
CAST(p.FullPath + N'/' + c.value('local-name(.)','NVARCHAR(MAX)') AS NVARCHAR(MAX)) AS FullPath,
CAST(p.XPath + N'/'+ c.value('local-name(.)','NVARCHAR(MAX)')+ N'['+ CAST(ROW_NUMBER() OVER(PARTITION BY c.value('local-name(.)','NVARCHAR(MAX)')
ORDER BY (SELECT 1)) AS NVARCHAR)+ N']' AS NVARCHAR(MAX)) AS XPath,
ROW_NUMBER() OVER(PARTITION BY c.value('local-name(.)','NVARCHAR(MAX)')
ORDER BY (SELECT 1)) AS Position,
CAST( SPACE(2 * p.lvl - 1) + N'|' + REPLICATE(N'-', 1) + c.value('local-name(.)','NVARCHAR(MAX)') AS NVARCHAR(MAX)) AS Tree,
CAST( c.value('text()[1]','NVARCHAR(MAX)') AS NVARCHAR(MAX) ) AS Value, c.query('.') AS this,
c.query('*') AS t,
CAST(p.Sort + CAST( (lvl + 1) * 1024 + (ROW_NUMBER() OVER(ORDER BY (SELECT 1)) * 2) AS VARBINARY(4)) AS VARBINARY(MAX) ) AS Sort,
CAST((lvl + 1) * 1024 + (ROW_NUMBER() OVER(ORDER BY (SELECT 1)) * 2) AS INT)
FROM cte p
CROSS APPLY p.t.nodes('*') b(c)), cte2 AS (
SELECT
lvl AS Depth,
Name AS NodeName,
ParentName,
ParentPosition,
NodeType,
FullPath,
XPath,
Position,
Tree AS TreeView,
Value,
this AS XMLData,
Sort, ID
FROM cte
UNION ALL
SELECT
p.lvl,
x.value('local-name(.)','NVARCHAR(MAX)'),
p.Name,
p.Position,
CAST(N'Attribute' AS NVARCHAR(20)),
p.FullPath + N'/@' + x.value('local-name(.)','NVARCHAR(MAX)'),
p.XPath + N'/@' + x.value('local-name(.)','NVARCHAR(MAX)'),
1,
SPACE(2 * p.lvl - 1) + N'|' + REPLICATE('-', 1)
+ N'@' + x.value('local-name(.)','NVARCHAR(MAX)'),
x.value('.','NVARCHAR(MAX)'),
NULL,
p.Sort,
p.ID + 1
FROM cte p
CROSS APPLY this.nodes('/*/@*') a(x)
)
SELECT
ROW_NUMBER() OVER(ORDER BY Sort, ID) AS ID,
ParentName, ParentPosition,Depth, NodeName, Position,
NodeType, FullPath, XPath, TreeView, Value, XMLData
FROM cte2
Upvotes: 0
Reputation: 1833
I find the following variable-bound XQuery approach easier to understand:
create function DictValue(
@id int,
@key nvarchar(max)
)
returns nvarchar(max) as
begin
declare @value nvarchar(max)
select
@value = Dict.value(
'(StringDictionary/Pair[@Key=sql:variable("@key")])[1]',
'nvarchar(max)'
)
from TableWithXmlColumn
where Id = @id
return @value
end
I didn't verify it, but this might perform a bit better, too, as it avoids context switching on the T-SQL and XQuery engines and requires only one XQuery.
I just realized that you didn't specify whether the Dict XML for one Id might contain multiple Pair elements with the same Key:
insert TableWithXmlColumn (Dict) values (
N'<?xml version="1.0" encoding="utf-16"?>
<StringDictionary>
<Pair Key="color">red</Pair>
<Pair Key="color">blue</Pair>
<Pair Key="count">123</Pair>
</StringDictionary>')
If that's the case then consider this slightly modified function, which uses a FLWOR XQuery to enumerate and combine multiple values, if they exist. Note, though, that in this case the function will only return NULL when an @id is not found, not when there's no matching @key Pair element.
create function DictValue(
@id int,
@key nvarchar(max)
)
returns nvarchar(max) as
begin
declare @value nvarchar(max)
select
@value = Cast(
Dict.query('
for $i in StringDictionary/Pair[@Key=sql:variable("@key")]
return string($i)
') as nvarchar(max))
from TableWithXmlColumn
where Id = @id
return @value
end
Good luck!
Upvotes: 1
Reputation: 63126
Personally I don't see much that you can do, the code that you have is structured in a way that is very readable, and you are querying the XML to get the result set first, then grabbing the values.
You might be able to get around the use of the @d variable, however, I believe the readability of the code will suffer greatly.
Upvotes: 0