ESV
ESV

Reputation: 7730

SQL Server XML Query Advice

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

Answers (3)

Joe
Joe

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

ewbi
ewbi

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

Mitchel Sellers
Mitchel Sellers

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

Related Questions