1

I need to select all the elements under a specific node, all the elements have the same name.

Tables


Lets say I have 2 tables in my database;
  1. [Access_Groups], This table contains all the groups that can access my application

  2. [User_Accounts], This table contains user details and an XML file that contains their access groups

I want to see which users have access groups in their XML file, that match the groups in my [Access_Groups] table

XML File


The XML files in [User_Accounts] look like this:
<Profile>
    <Name>John Smith</Name>
    <Role>Developer</Role>
</Profile>
<Groups>
    <String>Group_1</String>
    <String>Group_2</String>
    <String>Group_3</String>
    <String>Group_4</String>
    <String>Group_5</String>
    <String>Group_6</String>
</Groups>

Query


If i run this following query:
SELECT [XML].value('(//Groups)[1]', 'varchar(max)') AS 'XML'
FROM [User_Accounts]

I will get the following result:

XML
Group_1Group_2Group_3Group_4Group_5Group_6

This is of no use to me as i cannot JOIN this table to [Access_Groups]

If I run this query instead:

SELECT 
[XML].value('(//Groups/*)[1]', 'varchar(max)') AS 'XMl_1',
[XML].value('(//Groups/*)[2]', 'varchar(max)') AS 'XMl_2',
[XML].value('(//Groups/*)[3]', 'varchar(max)') AS 'XMl_3',
[XML].value('(//Groups/*)[4]', 'varchar(max)') AS 'XMl_4',
[XML].value('(//Groups/*)[5]', 'varchar(max)') AS 'XMl_5',
[XML].value('(//Groups/*)[6]', 'varchar(max)') AS 'XMl_6'
FROM [User_Accounts]

I will get the following result:

XML_1 XML_2 XML_3 XML_4 XML_5 XML_6
Group_1 Group_2 Group_3 Group_4 Group_5 Group_6

I could JOIN this results set to [Access_Groups], However this is no use to me as i have to define every single column. Some users may have up to 100 groups.

Solution


Is there no way i can do something like this?:
SELECT [Name], 
[XML].value('(//Groups)[*]', 'varchar(max)') AS 'XML'
FROM [User_Accounts]

To get a result set like this:

Name XML
John Smith Group_1
John Smith Group_2
John Smith Group_3
John Smith Group_4
John Smith Group_5
John Smith Group_6

I could then SELECT FROM that results set WHERE IN (SELECT * FROM [ACCESS_GROUPS]) to determine if this user had access to my application

Obviously though:

SELECT [Name], 
[XML].value('(//Groups)[*]', 'varchar(max)') AS 'XML'
FROM [User_Accounts]

This does not work!

Has anyone got any idea of how i could compare all the 'String' nodes for each user to my [Access_Groups] table?

Thanks!

This is my first post, so it might be a bit rubbish

1 Answers1

1

This is possible, you need to use nodes() to expand your XML into rows, e.g.

DECLARE @User_Accounts TABLE ([XML] XML);
INSERT @User_Accounts ([XML]) VALUES('<Profile>
    <Name>John Smith</Name>
    <Role>Developer</Role>
</Profile>
<Groups>
    <String>Group_1</String>
    <String>Group_2</String>
    <String>Group_3</String>
    <String>Group_4</String>
    <String>Group_5</String>
    <String>Group_6</String>
</Groups>');


SELECT  Name = ua.[XML].value('(Profile/Name/text())[1]', 'VARCHAR(100)'),
        Role = ua.[XML].value('(Profile/Role/text())[1]', 'VARCHAR(100)'),
        GroupName = g.x.value('text()[1]', 'VARCHAR(100)')
FROM    @User_Accounts AS ua
CROSS APPLY ua.[XML].nodes('/Groups/String') AS g (x);

Example on db<>fiddle

GarethD
  • 68,045
  • 10
  • 83
  • 123
  • Thank you for the assistance. I don't think I am understanding this properly though. I am writing something like this: **SELECT ua.[XML].value('(//Profile/Name)[1]', 'VARCHAR(MAX)') AS 'Name', ua.[XML].value('(//Group)[1]', 'VARCHAR(MAX)') AS 'GroupName FROM [User_Accounts] ua CROSS APPLY ua.[XML].nodes('/Groups/String') g** Here CROSS APPLY doesn't seem to recognise the [XML] column from [User_Accounts] table and I'm not sure if .Nodes is valid in my version of t-sql as it doesn't highlight in blue as .Value does –  Jul 18 '22 at 13:36
  • What version of SQL Server are you using? I'd be surprised if it is not supported. If what you have posted is exactly what you are running then you are missing a bit at the end where you give your XML data an alias e.g. I have posted `AS g (x)` you only have ` AS g`. To try and make this easier for you I have updated the example in the question and in the db<>fiddle to match your table structure so you *should* now be able to copy and paste the code with minimal changes and get it running – GarethD Jul 18 '22 at 14:12
  • Aha, it took a little bit of playing around to understand what was happening but I've cracked it. Cannot thank you enough, as this has been bothering for a while! –  Jul 18 '22 at 14:35