Duncan
Duncan

Reputation: 17

Finding XML elements by value even when the parent element is different

I have a SQL Server database table with a XML column, and I want to extract some specific data from it. Unfortunately, the XML structure changes depending on what type of item is being used.

For example, the XML for a type one item would look like this:

<MainStuff>
    <TypeOneItem>
        <Name>Not this one</Name>
        <Value>Do not get this value</Value>
    </TypeOneItem>
    <TypeOneItem>
        <Name>Not this one either</Name>
        <Value>Do not get this value</Value>
    </TypeOneItem>
    <TypeOneItem>
        <Name>Look for this name</Name>
        <Value>Get this value</Value>
    </TypeOneItem>
</MainStuff>

The XML for a type two item would look like this:

<MainStuff>
    <TypeTwoItem>
        <Name>Not this one</Name>
        <Value>Do not get this value</Value>
    </TypeTwoItem>
    <TypeTwoItem>
        <Name>Not this one either</Name>
        <Value>Do not get this value</Value>
    </TypeTwoItem>
    <TypeTwoItem>
        <Name>Look for this name</Name>
        <Value>Get this value</Value>
    </TypeTwoItem>
</MainStuff>

In each case, I want to find the value for the record with a name of "Look for this name" - i.e. I will want to return "Get this value" for them both.

I can do it individually like this (where the column name is XmlData):

SELECT
    XmlData.value('(//TypeOneItem[Name = "Look for this name"]/Value/text())[1]', 'VarChar(100)')

SELECT 
    XmlData.value('(//TypeTwoItem[Name = "Look for this name"]/Value/text())[1]', 'VarChar(100)')

However, I want to be able to get a list of all of these values in one go, irrespective of whether they're Type One or Type Two:

SELECT
    XmlData.value('(//EveryItemType[Name = "Look for this name"]/Value/text())[1]', 'VarChar(100)')

There are actually far more than just Type One and Type Two, so a Coalesce would be impractical. And for what it's worth, I didn't come up with this structure and I can't change it.

I'm hoping there's a really easy answer to this, but I haven't been able to find anything useful by googling.

Upvotes: 0

Views: 55

Answers (1)

Charlieface
Charlieface

Reputation: 72297

You can use /* to descend to a node of any name.

You can either do this in a .nodes call

SELECT
  x1.item.value('(Value/text())[1]', 'varchar(100)')
FROM YourTable AS t
CROSS APPLY t.XmlData.nodes('MainStuff/*[Name/text() = "Look for this name"]') AS x1(item);

Or directly in a .values call

SELECT
  t.XmlData.value('(MainStuff/*[Name/text() = "Look for this name"]/Value/text())[1]', 'varchar(100)')
FROM YourTable AS t;

You can also do substring-ing on the local-name() function to identify nodes which begin Type

SELECT
  x1.item.value('(Value/text())[1]', 'varchar(100)')
FROM YourTable AS t
CROSS APPLY t.XmlData.nodes('MainStuff/*[substring(local-name(), 1, 4) = "Type"][Name/text() = "Look for this name"]') AS x1(item);

db<>fiddle

Upvotes: 1

Related Questions