34

Given a stored procedure in SQL Server which has multiple select statements, is there a way to work with those results separately while calling the procedure?

For example:

alter procedure dbo.GetSomething
as
begin
    select * from dbo.Person;
    select * from dbo.Car;
end;

In .NET, if I call this proc, I can use a SqlDataReader to move between the two result sets, so I can easily retrieve all people and cars. In SQL however, when I execute the proc directly, I get both result sets.

If I call:

insert @myTempTable
    exec dbo.GetSomething;

Then it errors because the column definition doesn't match. If by some chance Person and Car have the same columns, it concatenates the two together, and @myTempTable gets all records from both tables, which obviously is no good either.

I can define new custom types representing the two result sets, and make those output parameters instead of having the multiple select statements, but I'm wondering if there's a better way - some way of pulling both results into temporary tables, or looping through the results, or something.

EDIT

Actually, after looking more closely, even output table parameters won't solve this - they're readonly, and that's still true in SQL 2012. (Connect ticket asking for this to be added)

Joe Enos
  • 39,478
  • 11
  • 80
  • 136
  • 2
    No. If you want to work with a single result set from one procedure, then you can only be returning one result set. – Kermit Nov 19 '13 at 21:56
  • 1
    Surprisingly, you *can* access multiple result sets from an ADO.NET app using `SqlDataReader` as you mentioned - but you **cannot** access multiple result sets from T-SQL ..... never made sense to me, but that's the way it is (at least for now) – marc_s Nov 19 '13 at 22:00
  • Say you have a column called 'type' on both Car and Person table, the new table can't have 2 columns named 'type'. It is possible that you explicitly list the column names on 1 of the selects, e.g. SELECT type AS carType from Car. – NoChance Nov 19 '13 at 22:23
  • @EmmadKareem It doesn't create one big result set with both sets of columns - rather if the two queries happen to have the same columns, you get a unioned result set when you select from the proc. Even that would only potentially be useful when I want the two queries to give identical types of results. The idea is these two queries give totally unrelated results, different number and types and names of columns. – Joe Enos Nov 19 '13 at 22:30
  • Thanks for clarification, I re-read your text, I was thinking of somersetting else. – NoChance Nov 19 '13 at 22:33
  • While it's tempting, stored procedures are really not meant for returning data. A table-valued function is generally better, though they have limitations. A workaround I use extensively is to create and populate temporary tables within the sproc, then the calling code can reads from these. –  Nov 20 '13 at 00:14
  • @JoeEnos, I have to ask: why are you doing this in the first place? Is there a specific reason that you don't have two separate proc calls, one for each particular query? Also, you might want to update the title of this question to specify "in T-SQL" so you don't get answers referring to .Net. – Solomon Rutzky Nov 20 '13 at 19:05
  • @srutzky I have a relatively large latency when connecting to SQL (the DB server is accessed over the internet), so I want to limit the number of DB calls. I already have a proc which returns multiple tables which I'm calling from my app, but sometimes I need to call a different proc which in turn calls the other and needs both result sets. Since it looks like there's no solution, I'll probably end up just shuffling things a little, so there's two individual procs with one result set each, a simple wrapper proc, and my complex proc which just calls the two individually as needed. – Joe Enos Nov 20 '13 at 19:11
  • @JoeEnos, having discrete procs for the result sets and a wrapper to combine them when needed sounds like a better setup regardless. However, I think I have a way to do this and am looking into it now. – Solomon Rutzky Nov 20 '13 at 19:21

10 Answers10

10
String myConnString  = "User ID="username";password="password";Initial Catalog=pubs;Data Source=Server";
SqlConnection myConnection = new SqlConnection(myConnString);
SqlCommand myCommand = new SqlCommand();
SqlDataReader myReader ;

myCommand.CommandType = CommandType.StoredProcedure;
myCommand.Connection = myConnection;
myCommand.CommandText = "MyProc";

try
{
    myConnection.Open();
    myReader = myCommand.ExecuteReader();

    while (myReader.Read())
    {
        //Write logic to process data for the first result.   
        }

    myReader.NextResult();
    while (myReader.Read())
    {
        //Write logic to process data for the second result.
    }
}
Romasz
  • 29,662
  • 13
  • 79
  • 154
Mahesh Gaikwad
  • 587
  • 1
  • 5
  • 11
6

It seems like there's no good simple way to do this, without a hack or a major paradigm shift. It looks like the best way is to just split out the original procs and end up with one more proc than before:

Old way:

create procedure dbo.GetSomething
as
begin
    select * from dbo.Person;
    select * from dbo.Car;
end;

New way:

create procedure dbo.GetPeople
as
begin
    select * from dbo.Person;
end;

create procedure dbo.GetCars
as
begin
    select * from dbo.Car;
end;

-- This gives the same result as before
create procedure dbo.GetSomething
as
begin
    exec dbo.GetPeople;
    exec dbo.GetCars;
end;

Then when I'm in a different proc and need both result sets, I'd just have to call them one at a time.

Joe Enos
  • 39,478
  • 11
  • 80
  • 136
5

In TSQL land, you're stuck.

Here is a trick (some may call semi-hacky) way that I used one time.

/*  START TSQL CODE */

/*  Stored Procedure Definition */

Use Northwind
GO


IF EXISTS 
    (
    SELECT * FROM INFORMATION_SCHEMA.ROUTINES
    WHERE ROUTINE_TYPE = N'PROCEDURE' and ROUTINE_SCHEMA = N'dbo' and ROUTINE_NAME = N'uspOrderDetailsByCustomerId'  
    )
BEGIN
    DROP PROCEDURE [dbo].[uspOrderDetailsByCustomerId]
END


GO

CREATE Procedure dbo.uspOrderDetailsByCustomerId
(
  @CustomerID nchar(5)
, @ResultSetIndicator smallint = 0
)
AS

BEGIN

    SET NOCOUNT ON



    /* ResultSet #1 */

    if (@ResultSetIndicator = 0 OR @ResultSetIndicator = 1)
    BEGIN 
        SELECT 
            c.CustomerID, c.CompanyName /*,c.ContactName,c.ContactTitle,c.[Address],c.City,c.Region,c.PostalCode,c.Country ,c.Phone,c.Fax */
        FROM 
            Customers c 
            JOIN Orders o ON c.CustomerID = o.CustomerID 
        WHERE 
            c.CustomerID = @CustomerID
    END


    /* */
    /* ResultSet #2 */ 

    if (@ResultSetIndicator = 0 OR @ResultSetIndicator = 2)
    BEGIN 

        SELECT o.OrderID,o.CustomerID /* ,o.EmployeeID,o.OrderDate,o.RequiredDate,o.ShippedDate,o.ShipVia ,o.Freight,o.ShipName,o.ShipAddress,o.OrderID,o.CustomerID,o.EmployeeID,o.OrderDate  */
        FROM 
            Orders o 
         WHERE 
            o.CustomerID = @CustomerID
        ORDER BY 
            o.CustomerID , o.OrderID 

    END


    /* */
    /* ResultSet #3 */

    if (@ResultSetIndicator = 0 OR @ResultSetIndicator = 3)
    BEGIN 
         SELECT od.OrderID,od.ProductID /* ,od.UnitPrice,od.Quantity,od.Discount  */
         FROM 
            [Order Details] od 
         WHERE 
            exists (select null from dbo.Orders  innerOrds where innerOrds.OrderID = od.OrderID and innerOrds.CustomerID = @CustomerID )
         ORDER BY 
            od.OrderID 

    END

    SET NOCOUNT OFF


END

GO 
/* Get everything */


exec dbo.uspOrderDetailsByCustomerId 'ALFKI'




    IF OBJECT_ID('tempdb..#TempCustomer') IS NOT NULL
    begin
            drop table #TempCustomer
    end


    CREATE TABLE #TempCustomer
    ( 
      [CustomerID] nchar(5)
    , [CompanyName] nvarchar(40)
    )

INSERT INTO #TempCustomer ( [CustomerID] , [CompanyName])
exec dbo.uspOrderDetailsByCustomerId 'ALFKI' , 1

Select * from #TempCustomer



    IF OBJECT_ID('tempdb..#TempOrders') IS NOT NULL
    begin
            drop table #TempOrders
    end


    CREATE TABLE #TempOrders
    ( 
        OrderID int
      , [CustomerID] nchar(5)

    )

INSERT INTO #TempOrders ( OrderID , [CustomerID] )
exec dbo.uspOrderDetailsByCustomerId 'ALFKI' , 2

Select * from #TempOrders






    IF OBJECT_ID('tempdb..#TempOrderDetails') IS NOT NULL
    begin
            drop table #TempOrderDetails
    end


    CREATE TABLE #TempOrderDetails
    ( 
        OrderID int
      , [ProductID] int

    )

INSERT INTO #TempOrderDetails ( OrderID , [ProductID] )
exec dbo.uspOrderDetailsByCustomerId 'ALFKI' , 3

Select * from #TempOrderDetails


    IF OBJECT_ID('tempdb..#TempOrderDetails') IS NOT NULL
    begin
            drop table #TempOrders
    end


    IF OBJECT_ID('tempdb..#TempOrders') IS NOT NULL
    begin
            drop table #TempOrders
    end



    IF OBJECT_ID('tempdb..#TempCustomer') IS NOT NULL
    begin
            drop table #TempCustomer
    end
granadaCoder
  • 26,328
  • 10
  • 113
  • 146
  • That's an interesting idea, thanks. A little bit hackish as you mention, but looks like it would work. – Joe Enos Nov 20 '13 at 20:40
  • I used it one time, on some code I inherited. I def do NOT recommend it as a best practice. But it works. – granadaCoder Nov 20 '13 at 20:48
  • Ah... I see what you did there. Will definitely help me out of a tight spot where I only need access to the one result set from a special case location. – Jaans Feb 05 '16 at 15:01
5

While this does not appear to be supported natively in T-SQL, if using a CLR Stored Procedure is an option for you, then you should be able to create a Stored Procedure in your preferred .Net language that uses the SqlDataReader.NextResult() method to advance to the desired result set and then send the SqlDataReader back via the SqlPipe.Send(SqlDataReader) method. You would just need to pass in the SQL to execute and the desired result set as parameters to this proc.

This would allow you to work with the proc as is, without modifying it to send back all or only one result set.

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • 2
    Thanks - not a huge fan of CLR procs just because they're a pain to work with, but that sounds like an interesting approach. – Joe Enos Nov 20 '13 at 20:43
2

You can put multiple result set in form of xml to a table

So that when you want to access all these result you parse those result set column to a tabular form

1

Der. Read the whole question before writing an answer! :-P

If you're trying to work with the results in TSQL land you're going to need to use some way to keep the results separate. Writing results to Temp tables is possibly your best bet since you won't need to depend on columns lining up (or not, as the case may be) and can deal with the data in a "natural" fashion for SQL Server. E.g.

create proc test_something
as begin
    select a, b into temp1 from table1
    select b, c into temp2 from table2
end
go

exec dbo.test_something()

select * from temp1
select * from temp2
Steve G
  • 993
  • 1
  • 7
  • 14
  • 4
    This will not work unless temp1 and temp2 are created outside of the proc, before the proc is called. Temp tables created within a sub-process are dropped when returning to the calling process. – Solomon Rutzky Nov 20 '13 at 19:11
  • This does work for me in SQL Server 2008. There's no need to create temp1 and temp2 outside of the proc since they're not actually temp tables, they're real tables. Of course, now you have to clean up after yourself... And, as always, YMMV. :) – Steve G Dec 04 '13 at 20:20
  • 1
    If the table is in fact not a real temp table you have the possibility of 2 or more users needing access to it at the same time.. if the parameters in are different.. the results at least on person gets will be wrong. – roblem Jan 26 '21 at 20:00
1

Would passing a parameter to the sp do the trick?

CREATE PROCEDURE  dostuff @parm1 int
AS

BEGIN
Declare @resultset Int
Set @resultset = @parm1

--0 = Select ranks
--1 = Select suits
--other - Select ALL

If @resultset = 0 
 SELECT [rank] FROM [ranks]
 Else If @resultset = 1
 SELECT [suit] FROM [suits]
 Else 
 SELECT * FROM [suits]
 cross join   [ranks] 
END
GO

 declare @mytemptbl table (rank text)
 insert @mytemptbl
  exec dostuff 0

 select * from @mytemptbl
Kalnode
  • 9,386
  • 3
  • 34
  • 62
level3looper
  • 1,015
  • 1
  • 7
  • 10
0

A usable approach is to invert the process and have the stored procedure accept names of temporary tables and use dynamic SQL to fill in the temporary tables with the desired results.

The caller has access to the data contained in the temporary tables when the SP returns.

-- local temporary tables avoid leaking to other sessions
create table #fast_cars (name varchar(max), top_speed float);
create table #awesome_people (name varchar(max), age int);

-- invoked SPs can access local temporary tables in scope
exec GetAmazingThings @name='%Wonder%'
    ,@cars='#fast_cars'
    ,@people='#awesome_people'

-- use 'returned' data
select name, top_speed from #fast_cars;
select name, age from #awesome_people;

Accepting the table names reduces “magic knowledge” of what tables are affected as the names are explicitly supplied. It also allows collecting the results of multiple calls while maintaining isolation, including during nesting.

The stored procedure might look a bit like..

create procedure GetAmazingThings
    @name varchar(100),
    -- output table names..
    ,@cars varchar(100)
    ,@people varchar(100)
as
    set @cars = quotename(@cars); -- bobby is not welcome
    declare @sql nvarchar(max);

    declare #_cars (name varchar(max), top_speed float);

    -- perform queries into local temp tables
    -- (this could also be done as the dynamic SQL to avoid a copy)
    insert into #_cars (name, top_speed)
    select Name, max(LapSpeed)
    from TonkaToys
    where Name like @name and VehicleType = 'car'
    group by Name;

    if patindex('[[]#%', @cars) > 0 -- given temp table
    begin
        -- copy result to supplied temp table
        set @sql = concat(N'
insert into ', @cars, ' (name, top_speed)
select name, top_speed
from #_cars
');
        exec sp_executesql @sql;
    end
    else
    begin
        -- just select result
        select name, top_speed from #cars
    end

    -- ditto for @people query/results
go

Notes:

  • There is at least one (and possibly more) data copies between temporary tables.
  • It is a bit cleaner in the SP if the dynamic SQL is isolated from the primary query. First query into a local temporary table and then copy that into the supplied temporary table using dynamic SQL.
  • If using global temporary tables, the SP can create the required table result-set. However, while handy, this can be problematic because the global temporary tables are shared between sessions.
  • The parameters can also be used to control what the SP “returns”, eg. skip the query, or select as result-set instead of writing to the temp table.
user2864740
  • 60,010
  • 15
  • 145
  • 220
-1

Create an SqlDataAdapter, set its SelectCommand to execute the SP "GetSomething", and then use the data adapter to fill a DataSet. The DataSet will contain as many DataTable's as you have "select" statements returning recordsets from the SP.

Here's what your code would look like:

System.Data.SqlClient.SqlDataAdapter da = new System.Data.SqlClient.SqlDataAdapter();
System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand();
cmd.Connection = myConnectionObject;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetSomething";
da.SelectCommand = cmd;

System.Data.DataSet ds = new DataSet();
da.Fill(ds);
// at this point, the (ds) object contains DataTables created from the recordsets returned by the SP
DataTable dt0 = ds.Tables[0];
DataTable dt1 = ds.Tables[1];

// note that dt0 corresponds to the FIRST recordset returned by the SP, etc.
-2

I know I am a little late to the party on this one but adding this only to help anyone else that comes across this thread and needs another option.

I recommend that you split the procedure calls up as indicated in the accepted answer by Joe Enos if you have that option, but if you do not then the information in this link may be an option for you.

https://khalidabuhakmeh.com/entity-framework-6-multiple-result-sets-with-stored-procedures

This is the approach I took for working with a procedure call that could not be split up that returned 6 different query results. The approach that I took was based largely on the information in that article and made this very easy and testable.

nbstrat
  • 108
  • 6
  • the goal seemed to be NOT to use .Net.. it was dealing with one stored proc calling another that returned multiple results sets and needed access to all of them without leaving the process boundarys of MSSql server. – roblem Jan 26 '21 at 20:02
  • @roblem - as I stated in my response, this was specifically for anyone else that comes across this thread and needed another option. I was in that exact position and offered this information ONLY as another alternative for anyone who could not do it directly at the database level. – nbstrat Jul 02 '21 at 11:51