Iterate through XML variable in SQL Server - sql

I have a XML variable in a stored procedure (SQL Server 2008), its sample value is
<parent_node>
<category>Low</category>
<category>Medium</category>
<category>High</category>
</parent_node>
I have to take each category and insert into table as a separate record. How to iterate in XML and take individual node value?
If I want to call a stored procedure and send each category as input parameter, how we can do that? The stored procedure is legacy one, which accept only one category at at time. I am trying to do invoke procedure in this way.
loop fetch single category from xml variable.
invoke stored procedure with current category.
move to next category.
loop until list contain value.
Any help will be appreciated.

Something like this?
DECLARE #XmlVariable XML = '<parent_node>
<category>Low</category>
<category>Medium</category>
<category>High</category>
</parent_node>'
INSERT INTO dbo.YourTargetTable(CategoryColumn)
SELECT
XTbl.Cats.value('.', 'varchar(50)')
FROM
#XmlVariable.nodes('/parent_node/category') AS XTbl(Cats)
Update: if you must use the old legacy stored procedure and cannot change it (that would be my preferred way of doing this), then you would have to do the row-by-agonizing-row (RBAR) looping yourself, e.g. by using a table variable:
-- declare temporary work table
DECLARE #RbarTable TABLE (CategoryName VARCHAR(50))
-- insert values into temporary work table
INSERT INTO #RbarTable(CategoryName)
SELECT
XTbl.Cats.value('.', 'varchar(50)')
FROM
#XmlVariable.nodes('/parent_node/category') AS XTbl(Cats)
-- declare a single category
DECLARE #CategoryNameToBeInserted VARCHAR(50)
-- get the first category
SELECT TOP 1 #CategoryNameToBeInserted = CategoryName FROM #RbarTable
-- as long as we have data
WHILE #CategoryNameToBeInserted IS NOT NULL
BEGIN
-- execute your stored procedure here.....
EXEC sp_executesql N'dbo.YourStoredProcedure #CategoryName',
N'#CategoryName VARCHAR(50)',
#CategoryName = #CategoryNameToBeInserted
-- delete the category we just inserted from the temporary work table
DELETE FROM #RbarTable WHERE CategoryName = #CategoryNameToBeInserted
-- see if we still have more categories to insert
SET #CategoryNameToBeInserted = NULL
SELECT TOP 1 #CategoryNameToBeInserted = CategoryName FROM #RbarTable ORDER BY CategoryName
END

With XML in SQL Server there's always more than one way to do it. Depending on the size of your XML doc and the number of times you're querying it, you could be best off using sp_xml_preparedocument which parses the document, gives you a handle to reference it, and then you can query it as many times and ways as you want to. Here's how you do that:
declare #xml xml = '
<parent_node>
<category>Low</category>
<category>Medium</category>
<category>High</category>
</parent_node>'
declare #xml_handle int
exec sp_xml_preparedocument #xml_handle output, #xml
select value from openxml(#xml_handle, '/parent_node/category', 2) with (value varchar(100) 'text()') x
exec sp_xml_removedocument #xml_handle

Related

How can I pass xml data from one sproc to another?

I have 2 sprocs for an assignment, what I'm trying to do is pass the xml output from one sproc to another and put it in a variable, I know ex1.xml_sp1 is returning an int while calling it with EXEC and obviously when trying to select this it returns null because #x is xml data type.
What I want to do is retrieve and store the xml data from sproc 1 in to #x in sproc 2.
Is there any way to do this?
sproc 1:
ALTER PROC [ex1].[xml_sp1]
#careteamid int
as
select CareTeams.CareTeamID, Doctors.DoctorID, Doctors.DoctorName,
CareTeamDoctors.DateJoined, CareTeamDoctors.CurrentMember
from dbo.CareTeamTbl as CareTeams
inner join dbo.CareTeamDoctorTbl as CareTeamDoctors on
CareTeams.CareTeamID = CareTeamDoctors.CareTeamID
inner join dbo.DoctorTbl as Doctors on
CareTeamDoctors.DoctorID=CareTeamDoctors.DoctorID
where CareTeamDoctors.CareTeamID = #careteamid
and CareTeamDoctors.DoctorID = Doctors.DoctorID
for xml auto, root('thedata')
sproc 2:
ALTER PROC [ex1].[xml_sp2]
#careteamid int
as
declare #x xml
exec #x = ex1.xml_sp1
#careteamid = #careteamid
select #x as XMLData
I want to do is retrieve and store the xml data from sproc 1 in to #x in sproc 2.
You could achieve it very easily using OUTPUT parameters:
CREATE PROCEDURE [xml_sp1]
#careteamid INT,
#xml_output XML OUTPUT
AS
BEGIN
SET #xml_output = (SELECT * FROM ... FOR XML AUTO, root('thedata'));
END;
GO
CREATE PROCEDURE [xml_sp2]
#careteamid INT
AS
BEGIN
DECLARE #x XML;
EXEC [xml_sp1]
#careteamid,
#x OUTPUT;
SELECT #x AS XMLData;
END;
GO
And final call:
EXEC [xml_sp2] #careteamid = 1;
LiveDemo
Consider using BEGIN/END block and end each statement with ; to avoid possible nasty problems.
The full list of possible sharing data methods How to Share Data between Stored Procedures by Erland Sommarskog
For the return value (i.e. EXEC #ReturnValue = StoredProcName...;), INT is the only datatype allowed. If this needs to really stay as a Stored Procedure then you can either use an OUTPUT variable or create a temp table or table variable in the second Stored Procedure and do INSERT INTO ... EXEC StoredProc1;.
However, given that the first Stored Procedure is only doing a simple SELECT statement, you would be far better off converting this to be an Inline Table-Valued Function (iTVF) as follows:
CREATE FUNCTION dbo.GetData (#CareTeamID INT)
RETURNS TABLE
AS RETURN
SELECT tab.col AS [CareData]
FROM (
SELECT CareTeams.CareTeamID, Doctors.DoctorID, Doctors.DoctorName,
CareTeamDoctors.DateJoined, CareTeamDoctors.CurrentMember
FROM dbo.CareTeamTbl as CareTeams
INNER JOIN dbo.CareTeamDoctorTbl as CareTeamDoctors
ON CareTeams.CareTeamID = CareTeamDoctors.CareTeamID
INNER JOIN dbo.DoctorTbl as Doctors
ON CareTeamDoctors.DoctorID=CareTeamDoctors.DoctorID
WHERE CareTeamDoctors.CareTeamID = #CareTeamID
AND CareTeamDoctors.DoctorID = Doctors.DoctorID
FOR XML AUTO, ROOT('thedata'), TYPE -- "TYPE" causes result to be XML and not NVARCHAR
) tab(col);
Then just change the second stored procedure to be:
SELECT care.CareData AS [XMLData]
FROM dbo.GetData(#CareTeamID);
Or, if you actually need to make use of that XML data in the second Stored Procedure, do the following:
DECLARE #TempData XML;
SELECT #TempData = care.CareData
FROM dbo.GetData(#CareTeamID);
Finally, if you need the XML data to actually be of the XML datatype coming out of that iTVF (or even if you keep it as a Stored Procedure), then you need to add the TYPE option to the FOR XML clause, else what is returned is a string / NVARCHAR(MAX):
FOR XML AUTO, ROOT ('thedata'), TYPE
Why not try this instead of assigning output of sp1 directly
declare #t table
(
val xml
)
insert into #t
exec ex1.xml_sp1
#careteamid = #careteamid
select * from #t
Will this work?
declare #xml xml
set #xml = (select *
from tableABC
for xml auto, elements)
And then pass that variable to another sproc?
I go the answer from here
How to insert FOR AUTO XML result into table?.
Seems to me the idea is similar, or should work similar.

Get/Use the Select value from a Stored Procedure

I have to use a Stored Procedure - that I cannot change/modify. While it is a bit complicated, it can be simplified to be a SELECT statement i.e. with no RETURN or OUTPUT parameter. For the purpose of this discussion assume it to be something like:
SELECT [URL] as imgPath
FROM [mydatasource].[dbo].[DigitalContent]
I need to execute this Stored Procedure passing in the Row ID (SKU) of each row in a Table.
I use a cursor for this as below:
DECLARE #sku varchar(100)
DECLARE #imgPath varchar(500)
DECLARE c CURSOR FOR
SELECT [SKU]
FROM [mydatasource].[dbo].[PROD_TABLE]
OPEN c
FETCH NEXT FROM c INTO #sku
WHILE ##FETCH_STATUS = 0 BEGIN
EXEC #imgPath = [mydatasource].[dbo].[getImage] #sku
--UPDATE PROD_TABLE SET ImgPath=#imgPath WHERE SKU=#sku
SELECT #imgPath AS ImgPath
FETCH NEXT FROM c INTO #sku
END
CLOSE c
DEALLOCATE c
Unfortunately, the return value #imgPath comes back as 0 i.e. success. This results in 0s being inserted into my PROD_TABLE or dumped on the Console. However, as the getImage Stored Procedure executes, it dumps the correct values of imgPath to the console.
How do I get this correct value (i.e. the result of the SELECT in the Stored Procedure) in the Loop above, so that I can correctly update my PROD_TABLE?
Answer
Thanks to RBarryYoung suggestion, my final code looks like:
DECLARE #sku varchar(100)
DECLARE #imgPath varchar(500)
DECLARE c CURSOR FOR
SELECT [SKU]
FROM [mydatasource].[dbo].[PROD_TABLE]
OPEN c
FETCH NEXT FROM c INTO #sku
WHILE ##FETCH_STATUS = 0 BEGIN
CREATE TABLE #OutData ( imgPath varchar(500) )
INSERT INTO #OutData EXEC [mydatasource].[dbo].[getImage] #sku
--UPDATE PROD_TABLE SET ImgPath=(SELECT * FROM #OutData) WHERE SKU=#sku
SELECT * FROM #OutData
DROP TABLE #OutData
FETCH NEXT FROM c INTO #sku
END
CLOSE c
DEALLOCATE c
The performance may not be the best, but at least it works :-).
First, create a temp table (#OutData) whose definition matches the output dataset being returned by getImage.
Then, change your EXEC .. statement to this:
INSERT INTO #OutData EXEC [mydatasource].[dbo].[getImage] #sku
Response to the question: "Is it possible to insert the Key/Row ID into the Temp Table, that way I will not have to TRUNCATE it after each loop iteration?"
First, as a general rule you shouldn't use TRUNCATE on #temp tables as there are some obscure locking problems with that. If you need to do that, just DROP and CREATE them again (they're optimized for that anyway).
Secondly, you cannot modify the dataset returned by a stored procedure in any way. Of course once its in the #temp table you can do what you want with it. So you could add a KeyId column to #OutData. Then inside the loop make a second #temp table (#TmpData), and use INSERT..EXEC to dump into that table instead. Then INSERT..SELECT into #OutData by selecting from #TmpData, adding your KeyID column. Finally, DROP TABLE #TmpData as the last statement in your loop.
This should perform fairly well.
Sometimes executing code entirely inside SQL Server can be more difficult than doing so directly client-side, sending multiple queries calling the SProc (ideally batched in a single round-trip) and processing the results there directly.
Otherwise, the INSERT-EXEC method seems the easier if you absolutely can't modify the called procedure. There are a few alternative methods, all with some additional problems, shown here: http://www.sommarskog.se/share_data.html

Execute Stored Procedure for List of Parameters in SQL

I have an Stored Procedure that have an argument named Id:
CREATE PROCEDURE [TargetSp](
#Id [bigint]
)
AS
BEGIN
Update [ATable]
SET [AColumn] =
(
Select [ACalculatedValue] From [AnotherTable]
)
Where [ATable].[Member_Id] = #Id
END
So I need to use it for a list of Id's not for one Id like :
Exec [TargetSp]
#Id IN (Select [M].[Id] From [Member] AS [M] Where [M].[Title] = 'Example');
First: How can I Execute it for a list?
Second: Is there any Performance difference between I execute the sp many times or rewrite it in target script?
You could use a table-valued parameter (see http://msdn.microsoft.com/en-us/library/bb510489.aspx). Generally, if you send only one request to the server instead of a list of requests you will see a shorter execution time.
I normally pass in the information like that as XML, then you can use it just like it's a table... selecting, inserting, updating as necessary
DECLARE #IDS NVARCHAR(MAX), #IDOC INT
SET #IDS = N'<ROOT><ID>1</ID><ID>2<ID></ROOT>'
EXEC sp_xml_preparedocument #IDOC OUTPUT, #IDS
SELECT [ID] FROM OPENXML (#IDOC, '/ROOT/ID', 2) WITH ([ID] INT '.') AS XMLDOC
EXEC sp_xml_removedocument #IDOC
Similar to freefaller's example, but using xml type instead and inserting into a table variable #ParsedIds
DECLARE #IdXml XML = N'<root><id value="1"/><id value="2"/></root>'
DECLARE #ParsedIds TABLE (parsedId int not null)
INSERT INTO #ParsedIds (parsedId)
SELECT v.parsedId.value('#value', 'int')
FROM #IdXml.nodes('/root/id') as v(parsedId)
SELECT * FROM #ParsedIds
Interestingly I've worked on an large scale system with 1000's of users and we found that using this method out performed the table-valued parameter approach for small lists of id's (no more than say 5 id's). The table-valued parameter approach was faster for larger lists of Id's.
EDIT following edited question:
Looking at your example it looks like you want to update ATable based on the Title parameter. If you can you'd benefit from rewriting your stored procedure to instead except the title parameter.
create procedure [TargetSP](
#title varchar(50)
)
as
begin
update [ATable]
set [AColumn] =
(
select [ACalculatedValue] from [AnotherTable]
)
where [ATable].[Member_Id] in (select [M].[Id] from [Member] as [M] where [M].[Title] = #title);
end
Since you only care about all the rows with a title of 'Example', you shouldn't need to determine the list first and then tell SQL Server the list you want to update, since you can already identify those with a query. So why not do this instead (I'm guessing at some data types here):
ALTER PROCEDURE dbo.TargetSP
#title VARCHAR(255)
AS
BEGIN
SET NOCOUNT ON;
-- only do this once instead of as a subquery:
DECLARE #v VARCHAR(255) = (SELECT [ACalculatedValue] From [AnotherTable]);
UPDATE a
SET AColumn = #v
FROM dbo.ATable AS a
INNER JOIN dbo.Member AS m
ON a.Member_Id = m.Id
WHERE m.Title = #title;
END
GO
Now call it as:
EXEC dbo.TargetSP #title = 'Example';
DECLARE #VId BIGINT;
DECLARE [My_Cursor] CURSOR FAST_FORWARD READ_ONLY FOR
Select [M].[Id] From [Member] AS [M] Where [M].[Title] = 'Example'
OPEN [My_Cursor]
FETCH NEXT FROM [My_Cursor] INTO #VId
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC [TargetSp]
#Id = #VId
FETCH NEXT FROM [My_Cursor] INTO #VId
END
CLOSE [My_Cursor]
DEALLOCATE [My_Cursor];
GO
if the parameter is integer, you can only pass one value at a time.
Your options are:
call the proc several times, one for each parameter
Change the proc to accept a structure where you can pass more than
one id like a varchar where you pass a coma separated list of values
(not so good) or a table-value parameter
About the performance question, it would be faster to re-write the proc to iterate through a list of ids than call it several times, once per id, BUT unless you are dealing with a HUGE list of ids, I dont think you will see much of a difference

Passing multiple values for one SQL parameter

I have a CheckBoxList where users can select multiple items from the list. I then need to be able to pass these values to my Stored Procedure so they can be used in a WHERE condition like:
WHERE ID IN (1,2,3)
I tried doing this so that its a nvarchar parameter and i pass the string 1,2,3 with:
WHERE ID IN (#IDs)
But this returned the following error:
Conversion failed when converting the nvarchar value '1,2,3' to data type int
Any help would be much appreciated!
There's a few ways of doing it.
You could pass in the parameter as an XML blob like this example:
CREATE PROCEDURE [dbo].[uspGetCustomersXML]
#CustomerIDs XML
AS
BEGIN
SELECT c.ID, c.Name
FROM [dbo].[Customer] c
JOIN #CustomerIDs.nodes('IDList/ID') AS x(Item) ON c.ID = Item.value('.', 'int' )
END
GO
--Example Use:
EXECUTE [dbo].[uspGetCustomersXML] '<IDList><ID>1</ID><ID>10</ID><ID>100</ID></IDList>'
Or pass in the values as CSV and use a split function to split the values out into a table variable (there's a lot of split functions out there, quick search will throw one up).
CREATE PROCEDURE [dbo].[uspGetCustomersCSV]
#CustomerIDs VARCHAR(8000)
AS
BEGIN
SELECT c.Id, c.Name
FROM [dbo].[Customer] c
JOIN dbo.fnSplit(#CustomerIDs, ',') t ON c.Id = t.item
END
GO
--Example Use:
EXECUTE [dbo].[uspGetCustomersCSV] '1,10,100'
If you were using SQL 2008 or later, you could have used Table Valued Parameters which allow you to pass a TABLE variable in as a parameter. I blogged about these 3 approaches a while back, with a quick performance comparison.
alter procedure c2
(#i varchar(5))
as
begin
declare #sq nvarchar(4000)
set #sq= 'select * from test where id in (<has_i>) '
SET #sq= REPLACE(#sq, '<has_i>', #i)
EXECUTE sp_executesql #sq
end
exec c2 '1,3'
I did find a solution for a similar problem.
It is used for a data driven subscription, but can be easily altered for use in a parameter.
check my blog post here with a detailed description
If you are having problem converting it to a stored procedure call, just let me know.

How do I execute a stored procedure once for each row returned by query?

I have a stored procedure that alters user data in a certain way. I pass it user_id and it does it's thing. I want to run a query on a table and then for each user_id I find run the stored procedure once on that user_id
How would I write query for this?
use a cursor
ADDENDUM: [MS SQL cursor example]
declare #field1 int
declare #field2 int
declare cur CURSOR LOCAL for
select field1, field2 from sometable where someotherfield is null
open cur
fetch next from cur into #field1, #field2
while ##FETCH_STATUS = 0 BEGIN
--execute your sproc on each row
exec uspYourSproc #field1, #field2
fetch next from cur into #field1, #field2
END
close cur
deallocate cur
in MS SQL, here's an example article
note that cursors are slower than set-based operations, but faster than manual while-loops; more details in this SO question
ADDENDUM 2: if you will be processing more than just a few records, pull them into a temp table first and run the cursor over the temp table; this will prevent SQL from escalating into table-locks and speed up operation
ADDENDUM 3: and of course, if you can inline whatever your stored procedure is doing to each user ID and run the whole thing as a single SQL update statement, that would be optimal
try to change your method if you need to loop!
within the parent stored procedure, create a #temp table that contains the data that you need to process. Call the child stored procedure, the #temp table will be visible and you can process it, hopefully working with the entire set of data and without a cursor or loop.
this really depends on what this child stored procedure is doing. If you are UPDATE-ing, you can "update from" joining in the #temp table and do all the work in one statement without a loop. The same can be done for INSERT and DELETEs. If you need to do multiple updates with IFs you can convert those to multiple UPDATE FROM with the #temp table and use CASE statements or WHERE conditions.
When working in a database try to lose the mindset of looping, it is a real performance drain, will cause locking/blocking and slow down the processing. If you loop everywhere, your system will not scale very well, and will be very hard to speed up when users start complaining about slow refreshes.
Post the content of this procedure you want call in a loop, and I'll bet 9 out of 10 times, you could write it to work on a set of rows.
You can do it with a dynamic query.
declare #cadena varchar(max) = ''
select #cadena = #cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(#cadena);
Something like this substitutions will be needed for your tables and field names.
Declare #TableUsers Table (User_ID, MyRowCount Int Identity(1,1)
Declare #i Int, #MaxI Int, #UserID nVarchar(50)
Insert into #TableUser
Select User_ID
From Users
Where (My Criteria)
Select #MaxI = ##RowCount, #i = 1
While #i <= #MaxI
Begin
Select #UserID = UserID from #TableUsers Where MyRowCount = #i
Exec prMyStoredProc #UserID
Select
#i = #i + 1, #UserID = null
End
Use a table variable or a temporary table.
As has been mentioned before, a cursor is a last resort. Mostly because it uses lots of resources, issues locks and might be a sign you're just not understanding how to use SQL properly.
Side note: I once came across a solution that used cursors to update
rows in a table. After some scrutiny, it turned out the whole thing
could be replaced with a single UPDATE command. However, in this case,
where a stored procedure should be executed, a single SQL-command
won't work.
Create a table variable like this (if you're working with lots of data or are short on memory, use a temporary table instead):
DECLARE #menus AS TABLE (
id INT IDENTITY(1,1),
parent NVARCHAR(128),
child NVARCHAR(128));
The id is important.
Replace parent and child with some good data, e.g. relevant identifiers or the whole set of data to be operated on.
Insert data in the table, e.g.:
INSERT INTO #menus (parent, child)
VALUES ('Some name', 'Child name');
...
INSERT INTO #menus (parent,child)
VALUES ('Some other name', 'Some other child name');
Declare some variables:
DECLARE #id INT = 1;
DECLARE #parentName NVARCHAR(128);
DECLARE #childName NVARCHAR(128);
And finally, create a while loop over the data in the table:
WHILE #id IS NOT NULL
BEGIN
SELECT #parentName = parent,
#childName = child
FROM #menus WHERE id = #id;
EXEC myProcedure #parent=#parentName, #child=#childName;
SELECT #id = MIN(id) FROM #menus WHERE id > #id;
END
The first select fetches data from the temporary table. The second select updates the #id. MIN returns null if no rows were selected.
An alternative approach is to loop while the table has rows, SELECT TOP 1 and remove the selected row from the temp table:
WHILE EXISTS(SELECT 1 FROM #menuIDs)
BEGIN
SELECT TOP 1 #menuID = menuID FROM #menuIDs;
EXEC myProcedure #menuID=#menuID;
DELETE FROM #menuIDs WHERE menuID = #menuID;
END;
Can this not be done with a user-defined function to replicate whatever your stored procedure is doing?
SELECT udfMyFunction(user_id), someOtherField, etc FROM MyTable WHERE WhateverCondition
where udfMyFunction is a function you make that takes in the user ID and does whatever you need to do with it.
See http://www.sqlteam.com/article/user-defined-functions for a bit more background
I agree that cursors really ought to be avoided where possible. And it usually is possible!
(of course, my answer presupposes that you're only interested in getting the output from the SP and that you're not changing the actual data. I find "alters user data in a certain way" a little ambiguous from the original question, so thought I'd offer this as a possible solution. Utterly depends on what you're doing!)
I like the dynamic query way of Dave Rincon as it does not use cursors and is small and easy. Thank you Dave for sharing.
But for my needs on Azure SQL and with a "distinct" in the query, i had to modify the code like this:
Declare #SQL nvarchar(max);
-- Set SQL Variable
-- Prepare exec command for each distinctive tenantid found in Machines
SELECT #SQL = (Select distinct 'exec dbo.sp_S2_Laser_to_cache ' +
convert(varchar(8),tenantid) + ';'
from Dim_Machine
where iscurrent = 1
FOR XML PATH(''))
--for debugging print the sql
print #SQL;
--execute the generated sql script
exec sp_executesql #SQL;
I hope this helps someone...