I have a stored procedure (sp_A) that receives data through a table-valued parameter (tvp), inserts all records into a table (table_A), and then makes a call to another stored procedure (sp_B). sp_B receives input parameters from some of the columns in the tvp, and has one output parameter #return of type nvarchar. Executing sp_B with a single row of input parameters works just fine. The problem is in calling sp_B from sp_A with a tvp, because then sp_B returns more than one value, which I'd like to handle, but can't find a suitable data structure to hold all the return values.
I have here a semblance of the problematic query
CREATE PROCEDURE [dbo].[sp_A]
#return varchar(50) output,
#inputData inputTable readonly
AS
BEGIN TRANSACTION
SET NOCOUNT OFF;
declare #typeid int, #insertcount int
set #typeid = (select typeid from #inputData )
--if typeid is not provided, insert into table_A and exit stored proc
if(#typeid IS NULL OR #typeid = 0)
BEGIN
INSERT INTO [dbo].[Table_A]
[cola1], [cola2], [cola3])
select _cola1, _cola2, _cola3
from #inputData
set #insertcount = ##ROWCOUNT
set #return = 'successful'
END
--else if typeid is provided, insert into table_A, and call sp_B, passing input parameters from #inputData
ELSE
BEGIN
declare #colb1 varchar, #colb2 varchar, #colb3 varchar
INSERT INTO [dbo].[Table_A]
([cola1], [cola2], [cola3], [typeid ])
select _cola1, _cola2, _cola3, _typeid
from #inputData
select #colb1 = _colb1, #colb2 = _colb2, #colb3 = _colb3
from #moTable
exec #insertcount = sp_B
#colb1, #colb2, #colb3, #return output
END
COMMIT TRANSACTION
select #insertcount
RETURN
I'm getting a "subquery returns more than 1 value..." error, understandably so... but I need that full value set for further processing in the application. What would be the best way to return, handle and propagate the return value set to the calling program (C# in this case)?
Related
When exactly do we use stored procedures with output parameters and when do we use stored procedures without parameters?
I base my question on an example:
Stored procedure with output parameter
CREATE PROCEDURE uspGetContactsCountByCity
#City nvarchar(60),
#ContactsCount int OUT
AS
BEGIN
SELECT #ContactsCount = COUNT(ContactID)
FROM Contacts
WHERE City = #City
END
Stored procedure executing
DECLARE #ContactsTotal INT
EXEC uspGetContactsCountByCity #ContactsCount = #ContactsTotal OUT, #city = 'Berlin'
SELECT #ContactsTotal
Results: 2
Stored procedure without output parameter
CREATE PROCEDURE uspGetContactsCountByCity2
#City nvarchar(60)
AS
BEGIN
SELECT COUNT(ContactID)
FROM Contacts
WHERE City = #City
END
Stored procedure executing:
EXEC uspGetContactsCountByCity2 #city = 'Berlin'
Results: 2
Both procedures return the same result, in same form, so what's the difference?
Basically, the result you're seeing is actually the result of your SELECT at the end of the procedure, which is doing the same thing.
Please take a look at this documentation:
If you specify the OUTPUT keyword for a parameter in the procedure definition, the stored procedure can return the current value of the parameter to the calling program when the stored procedure exits. To save the value of the parameter in a variable that can be used in the calling program, the calling program must use the OUTPUT keyword when executing the stored procedure.
So basically if you would like your stored procedure to just return just a value instead of a data set, you could use the output parameter. For example, let's take the procedures you have given as an example. They both do the same thing, this is why you got the same result. But what about changing a little bit in the first procedure that has the output parameter.
Here's an example:
create table OutputParameter (
ParaName varchar(100)
)
insert into OutputParameter values ('one'), ('two'),('three'),('one')
CREATE PROCEDURE AllDataAndCountWhereOne
#name nvarchar(60),
#count int OUT
as
Begin
SELECT #count = COUNT(*) from OutputParameter
Where ParaName = #name
select Distinct(ParaName) from OutputParameter
End
Declare #TotalCount int
Exec AllDataAndCountWhereOne #count = #TotalCount OUT, #name = 'One'
Select #TotalCount
With this example, you are getting all the distinct stored data in the table, plus getting the count of a given name.
ParaName
--------------------
one
three
two
(3 row(s) affected)
-----------
2
(1 row(s) affected)
This is one way of using the output parameter. You got both the distinct data and the count you wanted without doing extra query after getting the initial data set.
At the end, to answer your question:
Both procedures gives us the same result, in same form, so what's the difference?
You didn't make a difference in your own results, this is why you didn't really notice the difference.
Other Examples:
You could use the OUT parameter in other kinds of procedures. Let's assume that your stored procedure doesn't return anything, it's more like a command to the DB, but you still want a kind of message back, or more specifically a value. Take these two examples:
CREATE PROCEDURE InsertDbAndGetLastInsertedId
--This procedure will insert your name in the database, and return as output parameter the last inserted ID.
#name nvarchar(60),
#LastId int OUT
as
Begin
insert into OutputParameterWithId values (#name);
SELECT #LastId = SCOPE_IDENTITY()
End
or:
CREATE PROCEDURE InsertIntoDbUnlessSomeLogicFails
--This procedure will only insert into the db if name does exist, but there's no more than 5 of it
#name nvarchar(60),
#ErrorMessage varchar(100) OUT
as
Begin
set #ErrorMessage = ''
if ((select count(*) from OutputParameterWithId) = 0)
begin
set #ErrorMessage = 'Name Does Not Exist'
return
end
if ((select count(*) from OutputParameterWithId) = 5)
begin
set #ErrorMessage = 'Already have five'
return
end
insert into OutputParameterWithId values (#name);
End
These are just dummy examples, but just to make the idea more clear.
An example, based on yours would be if you introduced paging to the query.
So the result set is constrained to 10 items, and you use a total count out parameter to drive paging on a grid on screen.
Answer from ozz regarding paging does not make sense because there is no input param that implements a contraint on the number of records returned.
However, to answer the question... the results returned by these stored procedures are not the same. The first returns the record count of contacts in given city in the out param ContactsCount. While the count may also be recieved in the second implement through examining the reader.Rows.Count, the actual records are also made a available. In the first, no records are returned - only the count.
I am sure the solution is something super simple that I am missing but I keep getting a
SqlException: Procedure or function expects parameter which was not supplied
error. I am not a SQL wizard but to me the parameter looks okay. I did change the parameter and was not receiving this error but then when I consistently started receiving it I restored the stored procedure to the original version that I knew for a fact was fine but still receive it.
I tried executing the stored procedure like this
EXECUTE [dbo].[BHS_CloseCnt_Print_PackList] #palletid = '562992'
with a variable filled in. This stored procedure calls a function that determines the status of an order, if the variable I plug in and check with this method meets the criteria for the function I get an expected return.
If the container does not yet meet the function criteria, I get a null which I believe is okay.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER procedure [dbo].[BHS_CloseCnt_Print_PackList]
(#PalletId numeric)
AS
BEGIN
Declare #PO as nvarchar(50)
Declare #Internal_Shipment_Num as numeric
Declare #Internal_Shipment_Line_Num as numeric
select top 1
#Internal_Shipment_Num = sc.INTERNAL_SHIPMENT_NUM,
#Internal_Shipment_Line_Num = sc.INTERNAL_SHIPMENT_LINE_NUM
from
SHIPPING_CONTAINER sc
where
INTERNAL_CONTAINER_NUM = #PalletId
or PARENT = #PalletId
and INTERNAL_SHIPMENT_LINE_NUM is not null
select #PO = dbo.fn_BHS_AllPOPLTS_CLOSED(#PalletId, #Internal_Shipment_Num, #Internal_Shipment_Line_Num)
print #PO
if #PO is not null
Begin
select #PalletId 'INTERNAL_CONTAINER_NUM', '60' 'DOCUMENT_TYPE'
End
End
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [dbo].[fn_BHS_AllPOPLTS_CLOSED]
(#palletId numeric,
#Internal_Shipment_Num numeric,
#Internal_Shipment_Line_Num numeric)
RETURNS nvarchar(50)
AS
BEGIN
Declare #PO nvarchar(50)
Declare #OPENPO nvarchar(50)
Declare #IntShip as numeric
select #PO = isnull(sd.CUSTOMER_PO, 'FEIT')
from SHIPMENT_DETAIL sd
where sd.INTERNAL_SHIPMENT_LINE_NUM = #Internal_Shipment_Line_Num
and sd.internal_shipment_num = #Internal_Shipment_Num
select #OPENPO = isnull(sd.CUSTOMER_PO, '')
from shipping_container sc
join SHIPMENT_DETAIL sd on sd.INTERNAL_SHIPMENT_LINE_NUM = sc.INTERNAL_SHIPMENT_LINE_NUM
where sd.CUSTOMER_PO = #PO and sc.INTERNAL_SHIPMENT_NUM = #Internal_Shipment_Num
and sc.status < 600
if(isnull(#OPENPO, '') != '')
Begin
set #PO = null
End
return #PO
End
The stored procedure looks to have stored the cache from the previous edit I did although a known working version was restored.
Resolution for this was to run DBCC FREEPROCCACHE to clear the stored procedure cache and I was able to execute as expected.
Thanks Nemanja Perovic!
I have been wrecking my brain over this all weekend.
I have a SQL Server UDF to check whether a value passed as an argument will truncate.
CREATE FUNCTION willTruncate
(#fldValue nvarchar(200),
#fldSize smallint,
#fieldname nvarchar(40),
#tbl varchar(15))
RETURNS varchar(200)
AS
BEGIN
declare #Return varchar(200)
declare #fldLen smallint
SET #fldLen = len(#fldValue)
if #fldLen <= #fldSize
SET #Return = #fldValue
else
SET #Return = left(#fldValue,#fldSize-3) + '...'
RETURN #Return
END
The function works correctly to truncate the data the way I need it to when I use it in a SQL query like the sample below:
select top 10
'test' as db,
fname,
dbo.willTruncate(lname, 5, 'Last Name', 'People') as lastname,
dbo.willTruncate(address,40,'Address Line 1', 'People') as addressLine1
from
people
My problem is that I need to report back in some manner about the truncations so the end users can correct the record.
I have a table to populate and have tried to insert a record as part of the else clause, a separate function and a stored procedure. Each time I run the code it fails.
insert into tblTruncationErrors(trunkTable, trunkField, trunkValue)
values(#tbl, #fieldname, #fldValue)
or
CREATE FUNCTION dbo.logTruncation
(#fldValue nvarchar(200), #fieldname nvarchar(40), #tbl varchar(15))
RETURNS varchar(30)
AS
BEGIN
declare #Return int
insert into dbo.truncationErrors(trunkTable, trunkField, trunkValue)
Values (#tbl, #fieldname, #fldValue)
SET #Return = #fieldname
RETURN #Return
END
or
CREATE PROC sp_logTruncation
#tbl varchar(20),
#fldName varchar(30),
#fldValue varchar(200)
AS
SET NOCOUNT ON
insert into dbo.truncationErrors(trunkTable, trunkField, trunkValue)
Values (#tbl, #fldName, #fldValue)
SET NOCOUNT OFF
RETURN 1
GO
Running it as part of the original function code fails saying it needs to be done in a function or stored procedure. If I do it as a function it says I must use a stored procedure. Using it as a stored procedure results in a message saying it can only run from a function.
So, am I writing this wrong? Is there a way to do this. I am not insistent about it going into a table so a Excel or csv file output would be wonderful as well.
I want to run a stored procedure on each ID return by a SELECT query. Is there a simple way to do something like:
FOREACH (SELECT ID FROM myTABLE WHERE myName='bob') AS id
BEGIN
EXEC #return_value = [dbo].[spMYPROC]
#PARAM1 = id
#PARAM2 = 0
END
Since I just happened to answer a very similar question yesterday, I have this code handy. As others have stated, it may not be the best approach, but still it's nice to learn how to use a while loop anyway.
Assuming a table named "Customer"
declare #Id int
select #Id = MIN(Id)
from Customer c
while(select COUNT(1)
from Customer c
where c.Id >= #Id) > 0
begin
--run your sproc right here
select #Id = MIN(Id)
from Customer c
where c.Id > #Id
end
DECLARE #ID INT, #return_value INT
DECLARE c CURSOR FOR
SELECT
ID
FROM myTABLE
WHERE myName = 'bob'
OPEN c; FETCH NEXT FROM c INTO #ID
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC #return_value = [dbo].[spMYPROC]
#PARAM1 = #ID,
#PARAM2 = 0
FETCH NEXT FROM c INTO #ID
END
CLOSE c; DEALLOCATE c;
You have two option here
Option 1 Using Split Function
Pass a comma deliminated list of IDs and use a Split function Inside your Procedure to make split these values and do whatever you want to do with it.
To
Make it work you will need two thing
1) Create a Function which
accepts a Comma Deliminated string and split them.
2) Modify you
Store Procedure and add this function in there in a way that passed
parameter is passed to the function inside that store procedure and
that function split the values before passing it onto your store
Procedure .
Create this function 1st
Function Definition
CREATE FUNCTION [dbo].[FnSplit]
(
#List nvarchar(2000),
#SplitOn nvarchar(5)
)
RETURNS #RtnValue table (Id int identity(1,1), Value nvarchar(100))
AS
BEGIN
WHILE(Charindex(#SplitOn,#List)>0)
BEGIN
INSERT INTO #RtnValue (value)
SELECT VALUE = ltrim(rtrim(Substring(#List,1,Charindex(#SplitOn,#List)-1)))
SET #List = SUBSTRING(#List,Charindex(#SplitOn,#List)+len(#SplitOn),len(#List))
END
INSERT INTO #RtnValue (Value)
SELECT VALUE = ltrim(rtrim(#List))
RETURN
END
Modify you strored Procedure something like this
Stored Procedure
ALTER Procedure [dbo].[spMYPROC] (#Param1 VARCHAR(1000)= NULL)
AS
BEGIN
SELECT * FROM TableName
where ColumnNAME IN (SELECT Value FROM dbo.FnSplit(#Param1,','))
END
GO
Option 2 Table Type Parameter
Create a Table Type and alter your proc to accept a Table Type Parameter and do whatever you want to do with them values inside your proc.
TABLE TYPE
CREATE TYPE dbo.TYPENAME AS TABLE
(
Value int
)
GO
Stored Procedure to Accept That Type Param
ALTER PROCEDURE [dbo].[spMYPROC]
#TableParam TYPENAME READONLY
AS
BEGIN
SET NOCOUNT ON;
--Temp table to store passed Id values
declare #tmp_values table (value INT );
--Insert passed values to a table variable inside the proc
INSERT INTO #tmp_values (value)
SELECT Value FROM #TableParam
/* Do your stuff here whatever you want to do with Ids */
END
EXECUTE PROC
Declare a variable of that type and populate it with your values.
DECLARE #Table TYPENAME --<-- Variable of this TYPE
INSERT INTO #Table --<-- Populating the variable
SELECT ID FROM myTABLE WHERE myName='bob'
EXECUTE [dbo].[spMYPROC] #Table --<-- Stored Procedure Executed
I'm having a stored procedure which returns two result sets based on the success or failure.
SP success result set: name, id ,error,desc
SP failure result sret: error,desc
I'm using the following query to get the result of the stored procedure. It returns 0 for success and -1 for failure.
declare #ret int
DECLARE #tmp TABLE (
name char(70),
id int,
error char(2),
desc varchar(30)
)
insert into #tmp
EXEC #ret = sptest '100','King'
select #ret
select * from #tmp
If the SP is success the four field gets inserted into the temp table since the column matches.
But in case of failure the sp result set has only error and desc which does not matchs with no of columns in the temp table...
.I can't change the Sp, so I need to do some thing (not sure) in temp table to handle both failure and success.
You can't return 2 different recordsets and load the same temp table.
Neither can try and fill 2 different tables.
There are 2 options.
Modify your stored proc
All 4 columns are returned in all conditions
1st pair (name, ID) columns are NULL on error
2nd pair (error, desc) are NULL on success
If you are using SQL Server 2005 then use the TRY/CATCH to separate your success and fail code paths. The code below relies on using the new error handling to pass back the error result set via exception/RAISERROR.
Example:
CREATE PROC sptest
AS
DECLARE #errmsg varchar(2000)
BEGIN TRY
do stuff
SELECT col1, col2, col3, col4 FROM table etc
--do more stuff
END TRY
BEGIN CATCH
SELECT #errmsg = ERROR_MESSAGE()
RAISERROR ('Oops! %s', 16, 1, #errmsg)
END CATCH
GO
DECLARE #tmp TABLE ( name CHAR(70), id INT, error char(2), desc varchar(30)
BEGIN TRY
insert into #tmp
EXEC sptest '100','King'
select * from #tmp
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE()
END CATCH
My fault!!
Was too quick in the answer.
You need only to relv on the return value, so building up the logic against it is much better.
If you still want to use the temp table, then calling the sptest twice could be a way to deal with it (not optimal though), one time to get the return value and based on it then have 2 different temp tables you are filling up (one would be with the 4 fields, the other only with 2 fields).
declare #ret int
DECLARE #tmp TABLE (name CHAR(70), id INT, error char(2), desc varchar(30))
DECLARE #tmperror TABLE (error char(2), desc varchar(30))
EXEC #ret = sptest '100','King'
IF #ret != 0
BEGIN
INSERT INTO #tmperror
EXEC sptest '100','King';
SELECT * FROM #tmperror;
END
ELSE
BEGIN
INSERT INTO #tmp
EXEC sptest '100','King';
SELECT * FROM #tmp;
END
Keep in mind that this solution is not optimal.
Try modifying your table definition so that the first two columns are nullable:
DECLARE #tmp TABLE (
name char(70) null,
id int null,
error char(2),
desc varchar(30)
)
Hope this helps,
Bill
You cannot do this with just one call. You will have to call it once, either getting the return status and then branching depending on the status to the INSERT..EXEC command that will work for the number of columns that will be returned or Call it once, assuming success, with TRY..CATCH, and then in the Catch call it again assuming that it will fail (which is how it got to the CATCH).
Even better, would be to either re-write the stored procedure so that it returns a consistent column set or to write you own stored procedure, table-valued function or query, by extracting the code from this stored procedure and adapting it to your use. This is the proper answer in SQL.