Suppose I have a mapping table called tblMap that just maps an old attribute ID to a new attribute ID (oldID -> newID). NOTEWORTHY: newID is not contained in the list of oldID's.
I then have a table tblData which contains an xml string that has a number of attribute ids. I would like to replace all the current attribute ids with the newIDs that are found in tblMap. If an id mapping is not found in tblMap then it should stay as is. Any hints on how I can achieve this?
WHAT I TRIED:
I was trying to coerce something using XMLText.modify('replace value of ...') as described in: This StackOverflow Article but haven't been successful at getting it to work.
CREATE TABLE tblmap (
oldid INT,
newid INT
)
GO
INSERT INTO tblMap
VALUES
( 58, 1002),
( 85, 5002),
( 70, 3202),
(2, 2340),
(5, 7432)
GO
CREATE TABLE tblData ( [SourceID] int, [SourceRecID] bigint, [Value] xml )
GO
INSERT INTO tblData
VALUES
( 1, 0, N'<attributes><attribute id="58" value="0" /><attribute id="86" value="1" /><attribute id="85" value="1" /><attribute id="70" value="0" /><attribute id="38" value="0" /><attribute id="68" value="0" /><attribute id="42" value="1" /><attribute id="67" value="1" /><attribute id="62" value="1" /></attributes>' ),
( 1, 686, N'<attributes><attribute id="1" value="0.25" /><attribute id="4" value="1" /><attribute id="10" value="3" /><attribute id="11" value="1" /><attribute id="12" value="6" /></attributes>' ),
( 1, 687, N'<attributes><attribute id="1" value="2.00" /><attribute id="2" value="60.00" /><attribute id="3" value="-1" /><attribute id="5" value="252.00" /><attribute id="6" value="0" /><attribute id="7" value="1" /><attribute id="9" value="1" /><attribute id="10" value="1" /><attribute id="11" value="2" /><attribute id="12" value="10" /></attributes>' ),
( 1, 688, N'<attributes><attribute id="1" value="2.00" /><attribute id="2" value="60.00" /><attribute id="3" value="-1" /><attribute id="5" value="252.00" /><attribute id="6" value="0" /><attribute id="7" value="1" /><attribute id="11" value="2" /><attribute id="12" value="10" /></attributes>' )
SELECT *
FROM tblMap
GO
SELECT *
FROM tblData
GO
I have constructed all the schema/sample data here for your convenience:
https://rextester.com/MUMI61854
I would try to completely recreate the entire XML (or rather the /attributes node) and update the table with new value:
declare #tblmap table (oldid INT, newid INT);
INSERT INTO #tblMap
VALUES
( 58, 1002),
( 85, 5002),
( 70, 3202),
(2, 2340),
(5, 7432);
declare #tblData table ([SourceID] int, [SourceRecID] bigint, [Value] xml);
INSERT INTO #tblData
VALUES
( 1, 0, N'<attributes><attribute id="58" value="0" /><attribute id="86" value="1" /><attribute id="85" value="1" /><attribute id="70" value="0" /><attribute id="38" value="0" /><attribute id="68" value="0" /><attribute id="42" value="1" /><attribute id="67" value="1" /><attribute id="62" value="1" /></attributes>' ),
( 1, 686, N'<attributes><attribute id="1" value="0.25" /><attribute id="4" value="1" /><attribute id="10" value="3" /><attribute id="11" value="1" /><attribute id="12" value="6" /></attributes>' ),
( 1, 687, N'<attributes><attribute id="1" value="2.00" /><attribute id="2" value="60.00" /><attribute id="3" value="-1" /><attribute id="5" value="252.00" /><attribute id="6" value="0" /><attribute id="7" value="1" /><attribute id="9" value="1" /><attribute id="10" value="1" /><attribute id="11" value="2" /><attribute id="12" value="10" /></attributes>' ),
( 1, 688, N'<attributes><attribute id="1" value="2.00" /><attribute id="2" value="60.00" /><attribute id="3" value="-1" /><attribute id="5" value="252.00" /><attribute id="6" value="0" /><attribute id="7" value="1" /><attribute id="11" value="2" /><attribute id="12" value="10" /></attributes>' );
SELECT * FROM #tblMap;
SELECT * FROM #tblData;
-- Update table with new XML
with cte as (
select d.*, (
select isnull(m.newid, a.c.value('./#id', 'int')) as [#id], a.c.value('./#value', 'nvarchar(max)') as [#value]
from d.Value.nodes('/attributes[1]/attribute') a(c)
left join #tblmap m on m.oldid = a.c.value('./#id', 'int')
for xml path('attribute'), type, root('attributes')
) as [NewValue]
from #tblData d
)
update c set Value = NewValue
from cte c;
-- New version
select * from #tblData;
(I have turned your tables into table variables, as it leaves zero footprint on the instance. Everything else is the same.)
Unfortunately, this approach can become prohibitively difficult to implement if your actual XML schema is more complex than your example shows, and involves additional unpredictable elements and / or attributes under the /attributes node. In that case, I would recommend either a FLWOR (which is slow and quite difficult to write, at least for me) or cursored update.
To DEBUG:
-- Update table with new XML
with cte as (
select d.*, (
select isnull(m.newid, a.c.value('./#id', 'int')) as [#id], a.c.value('./#value', 'nvarchar(max)') as [#value]
from d.Value.nodes('/attributes[1]/attribute') a(c)
left join #tblmap m on m.oldid = a.c.value('./#id', 'int')
for xml path('attribute'), type, root('attributes')
) as [NewValue]
from #tblData d
)
SELECT c.SourceID,
c.SourceRecID,
c.Value,
c.NewValue
from cte c;
My suggestion calls XQuery to the rescue (txh Roger Wolf for the declared table variables, used them too...):
declare #tblmap table (oldid INT, newid INT);
INSERT INTO #tblMap
VALUES
( 58, 1002),
( 85, 5002),
( 70, 3202),
(2, 2340),
(5, 7432);
declare #tblData table ([SourceID] int, [SourceRecID] bigint, [Value] xml);
INSERT INTO #tblData
VALUES
( 1, 0, N'<attributes><attribute id="58" value="0" /><attribute id="86" value="1" /><attribute id="85" value="1" /><attribute id="70" value="0" /><attribute id="38" value="0" /><attribute id="68" value="0" /><attribute id="42" value="1" /><attribute id="67" value="1" /><attribute id="62" value="1" /></attributes>' ),
( 1, 686, N'<attributes><attribute id="1" value="0.25" /><attribute id="4" value="1" /><attribute id="10" value="3" /><attribute id="11" value="1" /><attribute id="12" value="6" /></attributes>' ),
( 1, 687, N'<attributes><attribute id="1" value="2.00" /><attribute id="2" value="60.00" /><attribute id="3" value="-1" /><attribute id="5" value="252.00" /><attribute id="6" value="0" /><attribute id="7" value="1" /><attribute id="9" value="1" /><attribute id="10" value="1" /><attribute id="11" value="2" /><attribute id="12" value="10" /></attributes>' ),
( 1, 688, N'<attributes><attribute id="1" value="2.00" /><attribute id="2" value="60.00" /><attribute id="3" value="-1" /><attribute id="5" value="252.00" /><attribute id="6" value="0" /><attribute id="7" value="1" /><attribute id="11" value="2" /><attribute id="12" value="10" /></attributes>' );
--The query will do the whole process in one single pass
WITH CombineThem AS
(
SELECT d.SourceID
,d.SourceRecID
,d.[Value]
,(SELECT
(SELECT *
FROM #tblMap
FOR XML PATH('map'),ROOT('maps'),TYPE)
,[Value] AS [*]
FOR XML PATH('Combined'),TYPE) AS Combined
FROM #tblData d
)
,updateableCTE AS
(
SELECT ct.[Value]
,ct.Combined
.query('<attributes>
{
for $attr in /Combined/attributes/attribute
return <attribute id="{
(
/Combined/maps/map[oldid[1]=$attr/#id]/newid
,$attr/#id
)[1]
}"
value="{$attr/#value}"/>
}
</attributes>') NewValue
FROM CombineThem ct
)
UPDATE updateableCTE SET [Value]=NewValue;
--check the result
SELECT * FROM #tblData;
Some Explanation
In order to use the mapping and the data in XQuery I create a combined XML in the first CTE. This will include the full <attributes> element together with a <maps> element.
The .query() will run through the attributes and search the <maps> for a fitting re-mapping. The magic happens in (val1,val2)[1]. This behaves like COALESCE(). It will pick the first non-null-value, which is either the fitting new id or the existing value.
Instead of updating the XML using .modify() the final step will set the [Value] column to the newly created XML in one go.
Honestly, I'm not 100% the reliability on ORDER BY (SELECT NULL) here, however, I don't have many options other than to hope the order is that of the nodes.
Anyway, the solution involves dynamic SQL; there may be a "better" way of doing this, but if there is I don't know it. I suggest doing some decent testing first, however, this appears to get the results you're after:
DECLARE #SQL nvarchar(MAX);
SET #SQL = STUFF((SELECT NCHAR(10) +
N'UPDATE tblData' + NCHAR(10) +
N'SET [Value].modify(''replace value of (/attributes/attribute/#id)[' + CONVERT(varchar(4),ROW_NUMBER() OVER (PARTITION BY D.SourceID, D.SourceRecID ORDER BY (SELECT NULL))) + N'] with "' + CONVERT(varchar(4),ISNULL(M.newid,V.AA.value('#id','int'))) + N'"'')' + NCHAR(10) +
N'WHERE SourceID = ' + CONVERT(varchar(4),D.SourceID) + NCHAR(10) +
N' AND SourceRecID = ' + CONVERT(varchar(4),D.SourceRecID) + N';'
FROM tblData D
CROSS APPLY D.[Value].nodes('attributes/attribute') V(AA)
LEFT JOIN tblmap M ON V.AA.value('#id','int') = M.oldid
FOR XML PATH(N'')),1,1,N'');
EXEC sp_executesql #SQL;
This question already has answers here:
Updating SQL Server XML node
(2 answers)
Closed 4 years ago.
How do I replace the variable value of xml in sql? I need to change the values of ID and Text.
Sample XML
<Values>
<ValueList>
<Entry key="Num" type="Values">
<value ID="1" Text="One" />
</Entry>
<Entry key="Name" type="Values">
<value ID="2" Text="two" />
</Entry>
</ValueList>
</Values>
You can modified the ID value like following. Here I am updating ID="1" to "111", similarly you can change the text for a specific text using modify.
DECLARE #XML XML
SET #XML= '<Values> <ValueList> <Entry key="Num" type="Values"> <value ID="1" Text="One" /> </Entry> <Entry key="Name" type="Values"> <value ID="2" Text="two" /> </Entry> </ValueList> </Values>'
SET #XML.modify('replace value of (Values/ValueList/Entry/value[#ID eq ("1")]/#ID)[1] with ("1111")')
select #XML
If you want to use variables, it can be achieved like following.
DECLARE #XML XML
Declare #IDTOReplace VARCHAR(5)='1'
DECLARE #IDWithReplace VARCHAR(5) = '111'
SET #XML= '<Values> <ValueList> <Entry key="Num" type="Values"> <value ID="1" Text="One" /> </Entry> <Entry key="Name" type="Values"> <value ID="2" Text="two" /> </Entry> </ValueList> </Values>'
SET #XML.modify('replace value of (Values/ValueList/Entry/value[#ID eq sql:variable("#IDTOReplace")]/#ID)[1] with sql:variable("#IDWithReplace")')
select #XML
If you want to change the Text based on some Id, it can be achieved like following.
DECLARE #XML XML
Declare #IDTOReplace VARCHAR(5)='1'
DECLARE #TextToReplace VARCHAR(100) = 'NewText'
SET #XML= '<Values> <ValueList> <Entry key="Num" type="Values"> <value ID="1" Text="One" /> </Entry> <Entry key="Name" type="Values"> <value ID="2" Text="two" /> </Entry> </ValueList> </Values>'
SET #XML.modify('replace value of (Values/ValueList/Entry/value[#ID eq sql:variable("#IDTOReplace")]/#Text)[1] with sql:variable("#TextToReplace")')
select #XML
I have successfully obtained a count of sub elements using this query.
SELECT
raa.ApplicationXML.value('count(Root/ThirdParty/*)', 'Int') as [Bureau Count]
FROM
Table1 raa
However, I want to group this count by the actual name of the sub element.
My XML looks something like this:
Record 1:
<Root>
<ThirdParty>
<BureauRed />
<BureauGreen />
<BureauBlue />
</ThirdParty>
</Root>
Record 2:
<Root>
<ThirdParty>
<BureauRed />
<BureauPurple />
<BureauBlue />
</ThirdParty>
</Root>
Record 3:
<Root>
<ThirdParty>
<BureauGreen />
<BureauRed />
<BureauPurple />
<BureauOrange />
<BureauBlue />
</ThirdParty>
</Root>
Not only do I need to pull the count of each bureau, but I need to get the name of the bureau (BureauRed, BureauGreen, BureauBlue, etc.) When I try to group by count I get the error:
XML methods are not allowed in a GROUP BY clause.
Using this query:
SELECT
raa.ApplicationXML.value('count(Root/ThirdParty/*)', 'Int') as [Bureau Count]
FROM
Table1 raa
GROUP BY
raa.ApplicationXML.value('count(Root/ThirdParty/*)', 'Int')
How do I get the count and the name of the bureau?
I'm looking for this result:
Bureau Count
============ =====
BureauRed 3
BureauGreen 2
BureauBlue 3
BureauPurple 2
BureauOrange 1
You can group after recieving data via XQuery:
DECLARE #xml1 xml =
'<Root>
<ThirdParty>
<BureauRed />
<BureauGreen />
<BureauBlue />
</ThirdParty>
</Root>';
DECLARE #xml2 xml =
'<Root>
<ThirdParty>
<BureauRed />
<BureauPurple />
<BureauBlue />
</ThirdParty>
</Root>';
DECLARE #xml3 xml =
'<Root>
<ThirdParty>
<BureauGreen />
<BureauRed />
<BureauPurple />
<BureauOrange />
<BureauBlue />
</ThirdParty>
</Root>';
DECLARE #temp TABLE (XmlData xml);
INSERT #temp VALUES (#xml1), (#xml2), (#xml3);
WITH cte AS
(
SELECT n.value('local-name(.)', 'varchar(50)') nodeName
FROM #temp t
CROSS APPLY t.XmlData.nodes('/Root/ThirdParty/*') x(n)
)
SELECT nodeName, count(*) cnt
FROM cte
GROUP BY nodeName;
Output:
nodeName cnt
-------------------------------------------------- -----------
BureauBlue 3
BureauGreen 2
BureauOrange 1
BureauPurple 2
BureauRed 3
You need to extract the elements that are children of Root/ThirdParty first and use that derived table to do the counting.
DECLARE #tv TABLE(x XML);
INSERT INTO #tv(x)VALUES(
N'<Root>
<ThirdParty>
<BureauRed />
<BureauGreen />
<BureauBlue />
</ThirdParty>
</Root>'),(
N'<Root>
<ThirdParty>
<BureauRed />
<BureauPurple />
<BureauBlue />
</ThirdParty>
</Root>'),(
N'<Root>
<ThirdParty>
<BureauGreen />
<BureauRed />
<BureauPurple />
<BureauOrange />
<BureauBlue />
</ThirdParty>
</Root>');
WITH bureaus AS (
SELECT
bureau=n.v.value('local-name(.)','NVARCHAR(256)')
FROM
#tv
CROSS APPLY x.nodes('/Root/ThirdParty/*') AS n(v)
)
SELECT
bureau,
[count]=COUNT(*)
FROM
bureaus
GROUP BY
bureau
ORDER BY
bureau;
whats the SQL for selecting the values from this XML chunk like done in the sample below?
<RWFCriteria reportType="OPRAProject">
<item id="88" name="" value="" type="Project" />
<item id="112" name="" value="12" type="Milestone" />
<item id="43" name="" value="11" type="Milestone" />
</RWFCriteria>
i want to select out similar to this but with the above XML data
DECLARE #Param XML
SET #Param = '<data>
<release id="1"><milestone id="1" /><milestone id="2" /></release>
<release id="3"><milestone id="1" /><milestone id="27"/></release>
</data>'
SELECT c.value('../#id', 'INT') AS ReleaseId, c.value('#id', 'INT') AS MilestoneId
FROM #Param.nodes('/data/release/milestone') AS T(c)
I want only the data in the nodes where type="Milestone"
Something like this:
DECLARE #Param XML
SET #Param = '<RWFCriteria reportType="OPRAProject">
<item id="88" name="" value="" type="Project" />
<item id="112" name="" value="12" type="Milestone" />
<item id="43" name="" value="11" type="Milestone" />
</RWFCriteria>'
SELECT
RWF.item.value('#id', 'INT') AS 'Id',
RWF.item.value('#name', 'VARCHAR(100)') AS 'Name',
RWF.item.value('#value', 'INT') AS 'Value',
RWF.item.value('#type', 'VARCHAR(100)') AS 'Type'
FROM
#Param.nodes('/RWFCriteria/item') AS RWF(item)
WHERE
RWF.item.value('#type', 'VARCHAR(100)') = 'Milestone'
Resulting output:
Id Name Value Type
112 12 Milestone
43 11 Milestone
I am trying to figure out how I can load my table variable with data from XML using dynamic xquery? I am getting a result set of nodes from the query and defining the value type of those nodes. It seems that it is the value definition of the nodes that it is blowing up on.
Here is an example of the script that works, but is not dynamic.
Script:
DECLARE #XML XML = '<root>
<data>
<list id="organization" label="Organization">
<options>
<item value="1" label="Organization1" selected="false" />
<item value="2" label="Organization2" selected="false" />
<item value="3" label="Organization3" selected="false" />
<item value="4" label="Organization4" selected="true" />
<item value="5" label="Organization5" selected="true" />
</options>
</list>
</data>
</root>';
DECLARE #Orgs TABLE (ID INT);
Insert Into #Orgs(ID) Select OrgNameIdNodes.ID.value('#value','int') from #xml.nodes('//*[#id="organization"]//item[#selected="true"]') as OrgNameIdNodes(ID);
Select *
from #orgs
What I would like to be able to do is pass in parameters for both value and the #xml.nodes sections so I would have something like:
Insert Into #Orgs(ID) Select OrgNameIdNodes.ID.value(#Value) from #xml.nodes(#Nodes) as OrgNameIdNodes(ID);
Is this possible?
How about using sp_executesql with dynamic sql. Something like:
DECLARE #XML XML = '<root>
<data>
<list id="organization" label="Organization">
<options>
<item value="1" label="Organization1" selected="false" />
<item value="2" label="Organization2" selected="false" />
<item value="3" label="Organization3" selected="false" />
<item value="4" label="Organization4" selected="true" />
<item value="5" label="Organization5" selected="true" />
</options>
</list>
</data>
</root>';
declare #orgs table(ID int);
declare #nodes nvarchar(4000),
#value nvarchar(4000),
#query nvarchar(4000)
select #value = '''#value'',''int'''
select #nodes = '//*[#id="organization"]//item[#selected="true"]'
select #query = 'Select OrgNameIdNodes.ID.value( ' + #value + ') ' +
'from #xml.nodes(''' + #nodes + ''') as OrgNameIdNodes(ID)'
insert into #Orgs(ID) EXEC sp_executesql #query, N'#xml xml', #xml = #xml
Select *
from #orgs