How to detect interface break between stored procedure - sql-server

I am working on a large project with a lot of stored procedures. I came into the following situation where a developer modified the arguments of a stored procedure which was called by another stored procedure.
Unfortunately, nothing prevents the ALTER PROC to complete.
Is there a way to perform those checks afterwards ?
What would be the guidelines to avoid getting into that kind of problems ?
Here is a sample code to reproduce this behavior :
CREATE PROC Test1 #arg1 int
AS
BEGIN
PRINT CONVERT(varchar(32), #arg1)
END
GO
CREATE PROC Test2 #arg1 int
AS
BEGIN
DECLARE #arg int;
SET #arg = #arg1+1;
EXEC Test1 #arg;
END
GO
EXEC Test2 1;
GO
ALTER PROC Test1 #arg1 int, #arg2 int AS
BEGIN
PRINT CONVERT(varchar(32), #arg1)
PRINT CONVERT(varchar(32), #arg2)
END
GO
EXEC Test2 1;
GO
DROP PROC Test2
DROP PROC Test1
GO

Sql server 2005 has a system view sys.sql_dependencies that tracks dependencies. Unfortunately, it's not all that reliable (For more info, see this answer). Oracle, however, is much better in that regard. So you could switch. There's also a 3rd party vendor, Redgate, who has Sql Dependency Tracker. Never tested it myself but there is a trial version available.
I have the same problem so I implemented my poor man's solution by creating a stored procedure that can search for strings in all the stored procedures and views in the current database. By searching on the name of the changed stored procedure I can (hopefully) find EXEC calls.
I used this on sql server 2000 and 2008 so it probably also works on 2005. (Note : #word1, #word2, etc must all be present but that can easily be changed in the last SELECT if you have different needs.)
CREATE PROCEDURE [dbo].[findWordsInStoredProceduresViews]
#word1 nvarchar(4000) = null,
#word2 nvarchar(4000) = null,
#word3 nvarchar(4000) = null,
#word4 nvarchar(4000) = null,
#word5 nvarchar(4000) = null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- create temp table
create table #temp
(
id int identity(1,1),
Proc_id INT,
Proc_Name SYSNAME,
Definition NTEXT
)
-- get the names of the procedures that meet our criteria
INSERT #temp(Proc_id, Proc_Name)
SELECT id, OBJECT_NAME(id)
FROM syscomments
WHERE OBJECTPROPERTY(id, 'IsProcedure') = 1 or
OBJECTPROPERTY(id, 'IsView') = 1
GROUP BY id, OBJECT_NAME(id)
-- initialize the NTEXT column so there is a pointer
UPDATE #temp SET Definition = ''
-- declare local variables
DECLARE
#txtPval binary(16),
#txtPidx INT,
#curText NVARCHAR(4000),
#counterId int,
#maxCounterId int,
#counterIdInner int,
#maxCounterIdInner int
-- set up a double while loop to get the data from syscomments
select #maxCounterId = max(id)
from #temp t
create table #tempInner
(
id int identity(1,1),
curName SYSNAME,
curtext ntext
)
set #counterId = 0
WHILE (#counterId < #maxCounterId)
BEGIN
set #counterId = #counterId + 1
insert into #tempInner(curName, curtext)
SELECT OBJECT_NAME(s.id), text
FROM syscomments s
INNER JOIN #temp t
ON s.id = t.Proc_id
WHERE t.id = #counterid
ORDER BY s.id, colid
select #maxCounterIdInner = max(id)
from #tempInner t
set #counterIdInner = 0
while (#counterIdInner < #maxCounterIdInner)
begin
set #counterIdInner = #counterIdInner + 1
-- get the pointer for the current procedure name / colid
SELECT #txtPval = TEXTPTR(Definition)
FROM #temp
WHERE id = #counterId
-- find out where to append the #temp table's value
SELECT #txtPidx = DATALENGTH(Definition)/2
FROM #temp
WHERE id = #counterId
select #curText = curtext
from #tempInner
where id = #counterIdInner
-- apply the append of the current 8KB chunk
UPDATETEXT #temp.definition #txtPval #txtPidx 0 #curtext
end
truncate table #tempInner
END
-- check our filter
SELECT Proc_Name, Definition
FROM #temp t
WHERE (#word1 is null or definition LIKE '%' + #word1 + '%') AND
(#word2 is null or definition LIKE '%' + #word2 + '%') AND
(#word3 is null or definition LIKE '%' + #word3 + '%') AND
(#word4 is null or definition LIKE '%' + #word4 + '%') AND
(#word5 is null or definition LIKE '%' + #word5 + '%')
ORDER BY Proc_Name
-- clean up
DROP TABLE #temp
DROP TABLE #tempInner
END

You can use sp_refreshsqlmodule to attempt to re-validate SPs (this also updates dependencies), but it won't validate this particular scenario with parameters at the caller level (it will validate things like invalid columns in tables and views).
http://www.mssqltips.com/tip.asp?tip=1294 has a number of techniques, including sp_depends
Dependency information is stored in the SQL Server metadata, including parameter columns/types for each SP and function, but it isn't obvious how to validate all the calls, but it is possible to locate them and inspect them.

Related

combine #sql query with temp table in SQL [duplicate]

In my stored procedure I declared two table variables on top of my procedure. Now I am trying to use that table variable within a dynamic sql statement but I get this error at the time of execution of that procedure. I am using Sql Server 2008.
This is how my query looks like,
set #col_name = 'Assoc_Item_'
+ Convert(nvarchar(2), #curr_row1);
set #sqlstat = 'update #RelPro set '
+ #col_name
+ ' = (Select relsku From #TSku Where tid = '
+ Convert(nvarchar(2), #curr_row1) + ') Where RowID = '
+ Convert(nvarchar(2), #curr_row);
Exec(#sqlstat);
And I get the following errors,
Must declare the table variable "#RelPro".
Must declare the table variable "#TSku".
I have tried to take the table outside of the string block of dynamic query but to no avail.
On SQL Server 2008+ it is possible to use Table Valued Parameters to pass in a table variable to a dynamic SQL statement as long as you don't need to update the values in the table itself.
So from the code you posted you could use this approach for #TSku but not for #RelPro
Example syntax below.
CREATE TYPE MyTable AS TABLE
(
Foo int,
Bar int
);
GO
DECLARE #T AS MyTable;
INSERT INTO #T VALUES (1,2), (2,3)
SELECT *,
sys.fn_PhysLocFormatter(%%physloc%%) AS [physloc]
FROM #T
EXEC sp_executesql
N'SELECT *,
sys.fn_PhysLocFormatter(%%physloc%%) AS [physloc]
FROM #T',
N'#T MyTable READONLY',
#T=#T
The physloc column is included just to demonstrate that the table variable referenced in the child scope is definitely the same one as the outer scope rather than a copy.
Your EXEC executes in a different context, therefore it is not aware of any variables that have been declared in your original context. You should be able to use a temp table instead of a table variable as shown in the simple demo below.
create table #t (id int)
declare #value nchar(1)
set #value = N'1'
declare #sql nvarchar(max)
set #sql = N'insert into #t (id) values (' + #value + N')'
exec (#sql)
select * from #t
drop table #t
You don't have to use dynamic SQL
update
R
set
Assoc_Item_1 = CASE WHEN #curr_row = 1 THEN foo.relsku ELSE Assoc_Item_1 END,
Assoc_Item_2 = CASE WHEN #curr_row = 2 THEN foo.relsku ELSE Assoc_Item_2 END,
Assoc_Item_3 = CASE WHEN #curr_row = 3 THEN foo.relsku ELSE Assoc_Item_3 END,
Assoc_Item_4 = CASE WHEN #curr_row = 4 THEN foo.relsku ELSE Assoc_Item_4 END,
Assoc_Item_5 = CASE WHEN #curr_row = 5 THEN foo.relsku ELSE Assoc_Item_5 END,
...
from
(Select relsku From #TSku Where tid = #curr_row1) foo
CROSS JOIN
#RelPro R
Where
R.RowID = #curr_row;
You can't do this because the table variables are out of scope.
You would have to declare the table variable inside the dynamic SQL statement or create temporary tables.
I would suggest you read this excellent article on dynamic SQL.
http://www.sommarskog.se/dynamic_sql.html
Well, I figured out the way and thought to share with the people out there who might run into the same problem.
Let me start with the problem I had been facing,
I had been trying to execute a Dynamic Sql Statement that used two temporary tables I declared at the top of my stored procedure, but because that dynamic sql statment created a new scope, I couldn't use the temporary tables.
Solution:
I simply changed them to Global Temporary Variables and they worked.
Find my stored procedure underneath.
CREATE PROCEDURE RAFCustom_Room_GetRelatedProducts
-- Add the parameters for the stored procedure here
#PRODUCT_SKU nvarchar(15) = Null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF OBJECT_ID('tempdb..##RelPro', 'U') IS NOT NULL
BEGIN
DROP TABLE ##RelPro
END
Create Table ##RelPro
(
RowID int identity(1,1),
ID int,
Item_Name nvarchar(max),
SKU nvarchar(max),
Vendor nvarchar(max),
Product_Img_180 nvarchar(max),
rpGroup int,
Assoc_Item_1 nvarchar(max),
Assoc_Item_2 nvarchar(max),
Assoc_Item_3 nvarchar(max),
Assoc_Item_4 nvarchar(max),
Assoc_Item_5 nvarchar(max),
Assoc_Item_6 nvarchar(max),
Assoc_Item_7 nvarchar(max),
Assoc_Item_8 nvarchar(max),
Assoc_Item_9 nvarchar(max),
Assoc_Item_10 nvarchar(max)
);
Begin
Insert ##RelPro(ID, Item_Name, SKU, Vendor, Product_Img_180, rpGroup)
Select distinct zp.ProductID, zp.Name, zp.SKU,
(Select m.Name From ZNodeManufacturer m(nolock) Where m.ManufacturerID = zp.ManufacturerID),
'http://s0001.server.com/is/sw11/DG/' +
(Select m.Custom1 From ZNodeManufacturer m(nolock) Where m.ManufacturerID = zp.ManufacturerID) +
'_' + zp.SKU + '_3?$SC_3243$', ep.RoomID
From Product zp(nolock) Inner Join RF_ExtendedProduct ep(nolock) On ep.ProductID = zp.ProductID
Where zp.ActiveInd = 1 And SUBSTRING(zp.SKU, 1, 2) <> 'GC' AND zp.Name <> 'PLATINUM' AND zp.SKU = (Case When #PRODUCT_SKU Is Not Null Then #PRODUCT_SKU Else zp.SKU End)
End
declare #curr_row int = 0,
#tot_rows int= 0,
#sku nvarchar(15) = null;
IF OBJECT_ID('tempdb..##TSku', 'U') IS NOT NULL
BEGIN
DROP TABLE ##TSku
END
Create Table ##TSku (tid int identity(1,1), relsku nvarchar(15));
Select #curr_row = (Select MIN(RowId) From ##RelPro);
Select #tot_rows = (Select MAX(RowId) From ##RelPro);
while #curr_row <= #tot_rows
Begin
select #sku = SKU from ##RelPro where RowID = #curr_row;
truncate table ##TSku;
Insert ##TSku(relsku)
Select distinct top(10) tzp.SKU From Product tzp(nolock) INNER JOIN
[INTRANET].raf_FocusAssociatedItem assoc(nolock) ON assoc.associatedItemID = tzp.SKU
Where (assoc.isActive=1) And (tzp.ActiveInd = 1) AND (assoc.productID = #sku)
declare #curr_row1 int = (Select Min(tid) From ##TSku),
#tot_rows1 int = (Select Max(tid) From ##TSku);
If(#tot_rows1 <> 0)
Begin
While #curr_row1 <= #tot_rows1
Begin
declare #col_name nvarchar(15) = null,
#sqlstat nvarchar(500) = null;
set #col_name = 'Assoc_Item_' + Convert(nvarchar(2), #curr_row1);
set #sqlstat = 'update ##RelPro set ' + #col_name + ' = (Select relsku From ##TSku Where tid = ' + Convert(nvarchar(2), #curr_row1) + ') Where RowID = ' + Convert(nvarchar(2), #curr_row);
Exec(#sqlstat);
set #curr_row1 = #curr_row1 + 1;
End
End
set #curr_row = #curr_row + 1;
End
Select * From ##RelPro;
END
GO
I don't think that is possible (though refer to the update below); as far as I know a table variable only exists within the scope that declared it. You can, however, use a temp table (use the create table syntax and prefix your table name with the # symbol), and that will be accessible within both the scope that creates it and the scope of your dynamic statement.
UPDATE: Refer to Martin Smith's answer for how to use a table-valued parameter to pass a table variable in to a dynamic SQL statement. Also note the limitation mentioned: table-valued parameters are read-only.
Here is an example of using a dynamic T-SQL query and then extracting the results should you have more than one column of returned values (notice the dynamic table name):
DECLARE
#strSQLMain nvarchar(1000),
#recAPD_number_key char(10),
#Census_sub_code varchar(1),
#recAPD_field_name char(100),
#recAPD_table_name char(100),
#NUMBER_KEY varchar(10),
if object_id('[Permits].[dbo].[myTempAPD_Txt]') is not null
DROP TABLE [Permits].[dbo].[myTempAPD_Txt]
CREATE TABLE [Permits].[dbo].[myTempAPD_Txt]
(
[MyCol1] char(10) NULL,
[MyCol2] char(1) NULL,
)
-- an example of what #strSQLMain is : #strSQLMain = SELECT #recAPD_number_key = [NUMBER_KEY], #Census_sub_code=TEXT_029 FROM APD_TXT0 WHERE Number_Key = '01-7212'
SET #strSQLMain = ('INSERT INTO myTempAPD_Txt SELECT [NUMBER_KEY], '+ rtrim(#recAPD_field_name) +' FROM '+ rtrim(#recAPD_table_name) + ' WHERE Number_Key = '''+ rtrim(#Number_Key) +'''')
EXEC (#strSQLMain)
SELECT #recAPD_number_key = MyCol1, #Census_sub_code = MyCol2 from [Permits].[dbo].[myTempAPD_Txt]
DROP TABLE [Permits].[dbo].[myTempAPD_Txt]
Using Temp table solves the problem but I ran into issues using Exec so I went with the following solution of using sp_executesql:
Create TABLE #tempJoin ( Old_ID int, New_ID int);
declare #table_name varchar(128);
declare #strSQL nvarchar(3072);
set #table_name = 'Object';
--build sql sting to execute
set #strSQL='INSERT INTO '+#table_name+' SELECT '+#columns+' FROM #tempJoin CJ
Inner Join '+#table_name+' sourceTbl On CJ.Old_ID = sourceTbl.Object_ID'
**exec sp_executesql #strSQL;**

SQL: How to insert the result of a stored procedure to a temp table [duplicate]

How do I do a SELECT * INTO [temp table] FROM [stored procedure]? Not FROM [Table] and without defining [temp table]?
Select all data from BusinessLine into tmpBusLine works fine.
select *
into tmpBusLine
from BusinessLine
I am trying the same, but using a stored procedure that returns data, is not quite the same.
select *
into tmpBusLine
from
exec getBusinessLineHistory '16 Mar 2009'
Output message:
Msg 156, Level 15, State 1, Line 2
Incorrect syntax near the keyword
'exec'.
I have read several examples of creating a temporary table with the same structure as the output stored procedure, which works fine, but it would be nice to not supply any columns.
You can use OPENROWSET for this. Have a look. I've also included the sp_configure code to enable Ad Hoc Distributed Queries, in case it isn't already enabled.
CREATE PROC getBusinessLineHistory
AS
BEGIN
SELECT * FROM sys.databases
END
GO
sp_configure 'Show Advanced Options', 1
GO
RECONFIGURE
GO
sp_configure 'Ad Hoc Distributed Queries', 1
GO
RECONFIGURE
GO
SELECT * INTO #MyTempTable FROM OPENROWSET('SQLNCLI', 'Server=(local)\SQL2008;Trusted_Connection=yes;',
'EXEC getBusinessLineHistory')
SELECT * FROM #MyTempTable
If you want to do it without first declaring the temporary table, you could try creating a user-defined function rather than a stored procedure and make that user-defined function return a table. Alternatively, if you want to use the stored procedure, try something like this:
CREATE TABLE #tmpBus
(
COL1 INT,
COL2 INT
)
INSERT INTO #tmpBus
Exec SpGetRecords 'Params'
In SQL Server 2005 you can use INSERT INTO ... EXEC to insert the result of a stored procedure into a table. From MSDN's INSERT documentation (for SQL Server 2000, in fact):
--INSERT...EXECUTE procedure example
INSERT author_sales EXECUTE get_author_sales
This is an answer to a slightly modified version of your question. If you can abandon the use of a stored procedure for a user-defined function, you can use an inline table-valued user-defined function. This is essentially a stored procedure (will take parameters) that returns a table as a result set; and therefore will place nicely with an INTO statement.
Here's a good quick article on it and other user-defined functions. If you still have a driving need for a stored procedure, you can wrap the inline table-valued user-defined function with a stored procedure. The stored procedure just passes parameters when it calls select * from the inline table-valued user-defined function.
So for instance, you'd have an inline table-valued user-defined function to get a list of customers for a particular region:
CREATE FUNCTION CustomersByRegion
(
#RegionID int
)
RETURNS TABLE
AS
RETURN
SELECT *
FROM customers
WHERE RegionID = #RegionID
GO
You can then call this function to get what your results a such:
SELECT * FROM CustomersbyRegion(1)
Or to do a SELECT INTO:
SELECT * INTO CustList FROM CustomersbyRegion(1)
If you still need a stored procedure, then wrap the function as such:
CREATE PROCEDURE uspCustomersByRegion
(
#regionID int
)
AS
BEGIN
SELECT * FROM CustomersbyRegion(#regionID);
END
GO
I think this is the most 'hack-less' method to obtain the desired results. It uses the existing features as they were intended to be used without additional complications. By nesting the inline table-valued user-defined function in the stored procedure, you have access to the functionality in two ways. Plus! You have only one point of maintenance for the actual SQL code.
The use of OPENROWSET has been suggested, but this is not what the OPENROWSET function was intended to be used for (From Books Online):
Includes all connection information
that is required to access remote data
from an OLE DB data source. This
method is an alternative to accessing
tables in a linked server and is a
one-time, ad hoc method of connecting
and accessing remote data by using OLE
DB. For more frequent references to
OLE DB data sources, use linked
servers instead.
Using OPENROWSET will get the job done, but it will incur some additional overhead for opening up local connections and marshalling data. It also may not be an option in all cases since it requires an ad hoc query permission which poses a security risk and therefore may not be desired. Also, the OPENROWSET approach will preclude the use of stored procedures returning more than one result set. Wrapping multiple inline table-value user-defined functions in a single stored procedure can achieve this.
Select ##ServerName
EXEC sp_serveroption ##ServerName, 'DATA ACCESS', TRUE
SELECT *
INTO #tmpTable
FROM OPENQUERY(YOURSERVERNAME, 'EXEC db.schema.sproc 1')
Easiest Solution:
CREATE TABLE #temp (...);
INSERT INTO #temp
EXEC [sproc];
If you don't know the schema then you can do the following. Please
note that there are severe security risks in this method.
SELECT *
INTO #temp
FROM OPENROWSET('SQLNCLI',
'Server=localhost;Trusted_Connection=yes;',
'EXEC [db].[schema].[sproc]')
When the stored procedure returns a lot of columns and you do not want to manually "create" a temporary table to hold the result, I've found the easiest way is to go into the stored procedure and add an "into" clause on the last select statement and add 1=0 to the where clause.
Run the stored procedure once and go back and remove the SQL code you just added. Now, you'll have an empty table matching the stored procedure's result. You could either "script table as create" for a temporary table or simply insert directly into that table.
declare #temp table
(
name varchar(255),
field varchar(255),
filename varchar(255),
filegroup varchar(255),
size varchar(255),
maxsize varchar(255),
growth varchar(255),
usage varchar(255)
);
INSERT #temp Exec sp_helpfile;
select * from #temp;
If the results table of your stored proc is too complicated to type out the "create table" statement by hand, and you can't use OPENQUERY OR OPENROWSET, you can use sp_help to generate the list of columns and data types for you. Once you have the list of columns, it's just a matter of formatting it to suit your needs.
Step 1: Add "into #temp" to the output query (e.g. "select [...] into #temp from [...]").
The easiest way is to edit the output query in the proc directly. if you can't change the stored proc, you can copy the contents into a new query window and modify the query there.
Step 2: Run sp_help on the temp table. (e.g. "exec tempdb..sp_help #temp")
After creating the temp table, run sp_help on the temp table to get a list of the columns and data types including the size of varchar fields.
Step 3: Copy the data columns & types into a create table statement
I have an Excel sheet that I use to format the output of sp_help into a "create table" statement. You don't need anything that fancy, just copy and paste into your SQL editor. Use the column names, sizes, and types to construct a "Create table #x [...]" or "declare #x table [...]" statement which you can use to INSERT the results of the stored procedure.
Step 4: Insert into the newly created table
Now you'll have a query that's like the other solutions described in this thread.
DECLARE #t TABLE
(
--these columns were copied from sp_help
COL1 INT,
COL2 INT
)
INSERT INTO #t
Exec spMyProc
This technique can also be used to convert a temp table (#temp) to a table variable (#temp). While this may be more steps than just writing the create table statement yourself, it prevents manual error such as typos and data type mismatches in large processes. Debugging a typo can take more time than writing the query in the first place.
Does your stored procedure only retrieve the data or modify it too? If it's used only for retrieving, you can convert the stored procedure into a function and use the Common Table Expressions (CTEs) without having to declare it, as follows:
with temp as (
select * from dbo.fnFunctionName(10, 20)
)
select col1, col2 from temp
However, whatever needs to be retrieved from the CTE should be used in one statement only. You cannot do a with temp as ... and try to use it after a couple of lines of SQL. You can have multiple CTEs in one statement for more complex queries.
For example,
with temp1020 as (
select id from dbo.fnFunctionName(10, 20)
),
temp2030 as (
select id from dbo.fnFunctionName(20, 30)
)
select * from temp1020
where id not in (select id from temp2030)
If the OPENROWSET is causing you issues, there is another way from 2012 onwards; make use of sys.dm_exec_describe_first_result_set_for_object, as mentioned here: Retrieve column names and types of a stored procedure?
First, create this stored procedure to generate the SQL for the temporary table:
CREATE PROCEDURE dbo.usp_GetStoredProcTableDefinition(
#ProcedureName nvarchar(128),
#TableName nvarchar(128),
#SQL nvarchar(max) OUTPUT
)
AS
SET #SQL = 'CREATE TABLE ' + #tableName + ' ('
SELECT #SQL = #SQL + '['+name +'] '+ system_type_name +'' + ','
FROM sys.dm_exec_describe_first_result_set_for_object
(
OBJECT_ID(#ProcedureName),
NULL
);
--Remove trailing comma
SET #SQL = SUBSTRING(#SQL,0,LEN(#SQL))
SET #SQL = #SQL +')'
To use the procedure, call it in the following way:
DECLARE #SQL NVARCHAR(MAX)
exec dbo.usp_GetStoredProcTableDefinition
#ProcedureName='dbo.usp_YourProcedure',
#TableName='##YourGlobalTempTable',#SQL = #SQL OUTPUT
INSERT INTO ##YourGlobalTempTable
EXEC [dbo].usp_YourProcedure
select * from ##YourGlobalTempTable
Note that I'm using a global temporary table. That's because using EXEC to run the dynamic SQL creates its own session, so an ordinary temporary table would be out of scope to any subsequent code. If a global temporary table is a problem, you can use an ordinary temporary table, but any subsequent SQL would need to be dynamic, that is, also executed by the EXEC statement.
Quassnoi put me most of the way there, but one thing was missing:
****I needed to use parameters in the stored procedure.****
And OPENQUERY does not allow for this to happen:
So I found a way to work the system and also not have to make the table definition so rigid, and redefine it inside another stored procedure (and of course take the chance it may break)!
Yes, you can dynamically create the table definition returned from the stored procedure by
using the OPENQUERY statement with bogus varaiables (as long the NO RESULT SET returns the
same number of fields and in the same position as a dataset with good data).
Once the table is created, you can use exec stored procedure into the temporary table all day long.
And to note (as indicated above) you must enable data access,
EXEC sp_serveroption 'MYSERVERNAME', 'DATA ACCESS', TRUE
Code:
declare #locCompanyId varchar(8)
declare #locDateOne datetime
declare #locDateTwo datetime
set #locDateOne = '2/11/2010'
set #locDateTwo = getdate()
--Build temporary table (based on bogus variable values)
--because we just want the table definition and
--since openquery does not allow variable definitions...
--I am going to use bogus variables to get the table defintion.
select * into #tempCoAttendanceRpt20100211
FROM OPENQUERY(DBASESERVER,
'EXEC DATABASE.dbo.Proc_MyStoredProc 1,"2/1/2010","2/15/2010 3:00 pm"')
set #locCompanyId = '7753231'
insert into #tempCoAttendanceRpt20100211
EXEC DATABASE.dbo.Proc_MyStoredProc #locCompanyId,#locDateOne,#locDateTwo
set #locCompanyId = '9872231'
insert into #tempCoAttendanceRpt20100211
EXEC DATABASE.dbo.Proc_MyStoredProc #locCompanyId,#locDateOne,#locDateTwo
select * from #tempCoAttendanceRpt20100211
drop table #tempCoAttendanceRpt20100211
Thanks for the information which was provided originally...
Yes, finally I do not have to create all these bogus (strict) table defintions when using data from
another stored procedure or database, and yes you can use parameters too.
Search reference tags:
SQL 2005 stored procedure into temp table
openquery with stored procedure and variables 2005
openquery with variables
execute stored procedure into temp table
Update: this will not work with temporary tables so I had to resort to manually creating the temporary table.
Bummer notice: this will not work with temporary tables, http://www.sommarskog.se/share_data.html#OPENQUERY
Reference: The next thing is to define LOCALSERVER. It may look like a keyword in the example, but it is in fact only a name. This is how you do it:
sp_addlinkedserver #server = 'LOCALSERVER', #srvproduct = '',
#provider = 'SQLOLEDB', #datasrc = ##servername
To create a linked server, you must have the permission ALTER ANY SERVER, or be a member of any of the fixed server roles sysadmin or setupadmin.
OPENQUERY opens a new connection to SQL Server. This has some implications:
The procedure that you call with OPENQUERY cannot refer temporary tables created in the current connection.
The new connection has its own default database (defined with sp_addlinkedserver, default is master), so all object specification must include a database name.
If you have an open transaction and are holding locks when you call OPENQUERY, the called procedure can not access what you lock. That is, if you are not careful you will block yourself.
Connecting is not for free, so there is a performance penalty.
If you're lucky enough to have SQL 2012 or higher, you can use dm_exec_describe_first_result_set_for_object
I have just edited the sql provided by gotqn. Thanks gotqn.
This creates a global temp table with name same as procedure name. The temp table can later be used as required. Just don't forget to drop it before re-executing.
declare #procname nvarchar(255) = 'myProcedure',
#sql nvarchar(max)
set #sql = 'create table ##' + #procname + ' ('
begin
select #sql = #sql + '[' + r.name + '] ' + r.system_type_name + ','
from sys.procedures AS p
cross apply sys.dm_exec_describe_first_result_set_for_object(p.object_id, 0) AS r
where p.name = #procname
set #sql = substring(#sql,1,len(#sql)-1) + ')'
execute (#sql)
execute('insert ##' + #procname + ' exec ' + #procname)
end
This stored proc does the job:
CREATE PROCEDURE [dbo].[ExecIntoTable]
(
#tableName NVARCHAR(256),
#storedProcWithParameters NVARCHAR(MAX)
)
AS
BEGIN
DECLARE #driver VARCHAR(10)
DECLARE #connectionString NVARCHAR(600)
DECLARE #sql NVARCHAR(MAX)
DECLARE #rowsetSql NVARCHAR(MAX)
SET #driver = '''SQLNCLI'''
SET #connectionString =
'''server=' +
CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(256)) +
COALESCE('\' + CAST(SERVERPROPERTY('InstanceName') AS NVARCHAR(256)), '') +
';trusted_connection=yes'''
SET #rowsetSql = '''EXEC ' + REPLACE(#storedProcWithParameters, '''', '''''') + ''''
SET #sql = '
SELECT
*
INTO
' + #tableName + '
FROM
OPENROWSET(' + #driver + ',' + #connectionString + ',' + #rowsetSql + ')'
EXEC (#sql)
END
GO
It's a slight rework of this: Insert stored procedure results into table so that it actually works.
If you want it to work with a temporary table then you will need to use a ##GLOBAL table and drop it afterwards.
In order to insert the first record set of a stored procedure into a temporary table you need to know the following:
only the first row set of the stored procedure can be inserted into a temporary table
the stored procedure must not execute dynamic T-SQL statement (sp_executesql)
you need to define the structure of the temporary table first
The above may look as limitation, but IMHO it perfectly makes sense - if you are using sp_executesql you can once return two columns and once ten, and if you have multiple result sets, you cannot insert them into several tables as well - you can insert maximum in two table in one T-SQL statement (using OUTPUT clause and no triggers).
So, the issue is mainly how to define the temporary table structure before performing the EXEC ... INTO ... statement.
sys.dm_exec_describe_first_result_set_for_object
sys.dm_exec_describe_first_result_set
sp_describe_first_result_set
The first works with OBJECT_ID while the second and the third works with Ad-hoc queries as well. I prefer to use the DMV instead of the sp as you can use CROSS APPLY and build the temporary table definitions for multiple procedures at the same time.
SELECT p.name, r.*
FROM sys.procedures AS p
CROSS APPLY sys.dm_exec_describe_first_result_set_for_object(p.object_id, 0) AS r;
Also, pay attention to the system_type_name field as it can be very useful. It stores the column complete definition. For, example:
smalldatetime
nvarchar(max)
uniqueidentifier
nvarchar(1000)
real
smalldatetime
decimal(18,2)
and you can use it directly in most of the cases to create the table definition.
So, I think in most of the cases (if the stored procedure match certain criteria) you can easily build dynamic statements for solving such issues (create the temporary table, insert the stored procedure result in it, do what you need with the data).
Note, that the objects above fail to define the first result set data in some cases like when dynamic T-SQL statements are executed or temporary tables are used in the stored procedure.
I'm creating a table with the following schema and data.
Create a stored procedure.
Now I know what the result of my procedure is, so I am performing the following query.
CREATE TABLE [dbo].[tblTestingTree](
[Id] [int] IDENTITY(1,1) NOT NULL,
[ParentId] [int] NULL,
[IsLeft] [bit] NULL,
[IsRight] [bit] NULL,
CONSTRAINT [PK_tblTestingTree] PRIMARY KEY CLUSTERED
(
[Id] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[tblTestingTree] ON
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (1, NULL, NULL, NULL)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (2, 1, 1, NULL)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (3, 1, NULL, 1)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (4, 2, 1, NULL)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (5, 2, NULL, 1)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (6, 3, 1, NULL)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (7, 3, NULL, 1)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (8, 4, 1, NULL)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (9, 4, NULL, 1)
INSERT [dbo].[tblTestingTree] ([Id], [ParentId], [IsLeft], [IsRight]) VALUES (10, 5, 1, NULL)
SET IDENTITY_INSERT [dbo].[tblTestingTree] OFF
VALUES (10, 5, 1, NULL)
SET IDENTITY_INSERT [dbo].[tblTestingTree] On
create procedure GetDate
as
begin
select Id,ParentId from tblTestingTree
end
create table tbltemp
(
id int,
ParentId int
)
insert into tbltemp
exec GetDate
select * from tbltemp;
If the query doesn't contain parameter, use OpenQuery else use OpenRowset.
Basic thing would be to create schema as per stored procedure and insert into that table. e.g.:
DECLARE #abc TABLE(
RequisitionTypeSourceTypeID INT
, RequisitionTypeID INT
, RequisitionSourcingTypeID INT
, AutoDistOverride INT
, AllowManagerToWithdrawDistributedReq INT
, ResumeRequired INT
, WarnSupplierOnDNRReqSubmission INT
, MSPApprovalReqd INT
, EnableMSPSupplierCounterOffer INT
, RequireVendorToAcceptOffer INT
, UseCertification INT
, UseCompetency INT
, RequireRequisitionTemplate INT
, CreatedByID INT
, CreatedDate DATE
, ModifiedByID INT
, ModifiedDate DATE
, UseCandidateScheduledHours INT
, WeekEndingDayOfWeekID INT
, AllowAutoEnroll INT
)
INSERT INTO #abc
EXEC [dbo].[usp_MySp] 726,3
SELECT * FROM #abc
Code
CREATE TABLE #T1
(
col1 INT NOT NULL,
col2 NCHAR(50) NOT NULL,
col3 TEXT NOT NULL,
col4 DATETIME NULL,
col5 NCHAR(50) NULL,
col6 CHAR(2) NULL,
col6 NCHAR(100) NULL,
col7 INT NULL,
col8 NCHAR(50) NULL,
col9 DATETIME NULL,
col10 DATETIME NULL
)
DECLARE #Para1 int
DECLARE #Para2 varchar(32)
DECLARE #Para3 varchar(100)
DECLARE #Para4 varchar(15)
DECLARE #Para5 varchar (12)
DECLARE #Para6 varchar(1)
DECLARE #Para7 varchar(1)
SET #Para1 = 1025
SET #Para2 = N'6as54fsd56f46sd4f65sd'
SET #Para3 = N'XXXX\UserName'
SET #Para4 = N'127.0.0.1'
SET #Para5 = N'XXXXXXX'
SET #Para6 = N'X'
SET #Para7 = N'X'
INSERT INTO #T1
(
col1,
col2,
col3,
col4,
col5,
col6,
col6,
col7,
col8,
col9,
col10,
)
EXEC [dbo].[usp_ProcedureName] #Para1, #Para2, #Para3, #Para4, #Para5, #Para6, #Para6
I hope this helps. Please qualify as appropriate.
I found Passing Arrays/DataTables into Stored Procedures which might give you another idea on how you might go solving your problem.
The link suggests to use an Image type parameter to pass into the stored procedure. Then in the stored procedure, the image is transformed into a table variable containing the original data.
Maybe there is a way this can be used with a temporary table.
I met the same problem and here is what I did for this from Paul's suggestion. The main part is here is to use NEWID() to avoid multiple users run the store procedures/scripts at the same time, the pain for global temporary table.
DECLARE #sql varchar(max) = '',
#tmp_global_table varchar(255) = '##global_tmp_' + CONVERT(varchar(36), NEWID())
SET #sql = #sql + 'select * into [' + #tmp_global_table + '] from YOURTABLE'
EXEC(#sql)
EXEC('SELECT * FROM [' + #tmp_global_table + ']')
Another method is to create a type and use PIPELINED to then pass back your object. This is limited to knowing the columns however. But it has the advantage of being able to do:
SELECT *
FROM TABLE(CAST(f$my_functions('8028767') AS my_tab_type))
This can be done in SQL Server 2014+ provided the stored procedure only returns one table. If anyone finds a way of doing this for multiple tables I'd love to know about it.
DECLARE #storedProcname NVARCHAR(MAX) = ''
SET #storedProcname = 'myStoredProc'
DECLARE #strSQL AS VARCHAR(MAX) = 'CREATE TABLE myTableName '
SELECT #strSQL = #strSQL+STUFF((
SELECT ',' +name+' ' + system_type_name
FROM sys.dm_exec_describe_first_result_set_for_object (OBJECT_ID(#storedProcname),0)
FOR XML PATH('')
),1,1,'(') + ')'
EXEC (#strSQL)
INSERT INTO myTableName
EXEC ('myStoredProc #param1=1, #param2=2')
SELECT * FROM myTableName
DROP TABLE myTableName
This pulls the definition of the returned table from system tables, and uses that to build the temp table for you. You can then populate it from the stored procedure as stated before.
There are also variants of this that work with Dynamic SQL too.
After searching around I found a way to create a temp table dynamically for any stored procedure without using OPENROWSET or OPENQUERY using a generic schema of Stored Procedure's result definition especially when you are not database Administrator.
Sql server has a buit-in proc sp_describe_first_result_set that can provide you with schema of any procedures resultset. I created a schema table from results of this procedure and manually set all the field to NULLABLE.
declare #procname varchar(100) = 'PROCEDURENAME' -- your procedure name
declare #param varchar(max) = '''2019-06-06''' -- your parameters
declare #execstr nvarchar(max) = N'exec ' + #procname
declare #qry nvarchar(max)
-- Schema table to store the result from sp_describe_first_result_set.
create table #d
(is_hidden bit NULL, column_ordinal int NULL, name sysname NULL, is_nullable bit NULL, system_type_id int NULL, system_type_name nvarchar(256) NULL,
max_length smallint NULL, precision tinyint NULL, scale tinyint NULL, collation_name sysname NULL, user_type_id int NULL, user_type_database sysname NULL,
user_type_schema sysname NULL,user_type_name sysname NULL,assembly_qualified_type_name nvarchar(4000),xml_collection_id int NULL,xml_collection_database sysname NULL,
xml_collection_schema sysname NULL,xml_collection_name sysname NULL,is_xml_document bit NULL,is_case_sensitive bit NULL,is_fixed_length_clr_type bit NULL,
source_server sysname NULL,source_database sysname NULL,source_schema sysname NULL,source_table sysname NULL,source_column sysname NULL,is_identity_column bit NULL,
is_part_of_unique_key bit NULL,is_updateable bit NULL,is_computed_column bit NULL,is_sparse_column_set bit NULL,ordinal_in_order_by_list smallint NULL,
order_by_list_length smallint NULL,order_by_is_descending smallint NULL,tds_type_id int NULL,tds_length int NULL,tds_collation_id int NULL,
tds_collation_sort_id tinyint NULL)
-- Get result set definition of your procedure
insert into #d
EXEC sp_describe_first_result_set #exestr, NULL, 0
-- Create a query to generate and populate a global temp table from above results
select
#qry = 'Create table ##t(' +
stuff(
(select ',' + name + ' '+ system_type_name + ' NULL'
from #d d For XML Path, TYPE)
.value(N'.[1]', N'nvarchar(max)')
, 1,1,'')
+ ')
insert into ##t
Exec '+#procname+' ' + #param
Exec sp_executesql #qry
-- Use below global temp table to query the data as you may
select * from ##t
-- **WARNING** Don't forget to drop the global temp table ##t.
--drop table ##t
drop table #d
Developed and tested on Sql Server version - Microsoft SQL Server 2016 (RTM) - 13.0.1601.5(Build 17134:)
You can tweak the schema for your SQL server version that you are using (if needed).
It's a simple 2 step process:
- create a temporary table
- Insert into the temporary table.
Code to perform the same:
CREATE TABLE #tempTable (Column1 int, Column2 varchar(max));
INSERT INTO #tempTable
EXEC [app].[Sproc_name]
#param1 = 1,
#param2 =2;
If you know the parameters that are being passed and if you don't have access to make sp_configure, then edit the stored procedure with these parameters and the same can be stored in a ##global table.
A few years late to the question, but I needed something like this for some quick and dirty code generation. I believe as others have stated it is just easier to define the temp table up front, but this method should work for simple stored procedure queries or sql statments.
This will be a little convoluted, but it borrows from the contributors here as well as Paul White's solution from DBA Stack Exchange Get stored procedure result column-types. Again, to reiterate this approach & example is not designed for processes in a multi user environment. In this case the table definition is being set for a short time in a global temp table for reference by a code generation template process.
I haven't fully tested this so there may be caveats so you may want to go to the MSDN link in Paul White's answer. This applies to SQL 2012 and higher.
First use the stored procedure sp_describe_first_result_set which resembles Oracle's describe.
This will evaluate the first row of the first result set so if your stored procedure or statement returns multiple queries it will only describe the first result.
I created a stored proc to break down the tasks that returns a single field to select from to create the temp table definition.
CREATE OR ALTER PROCEDURE [dbo].[sp_GetTableDefinitionFromSqlBatch_DescribeFirstResultSet]
(
#sql NVARCHAR(4000)
,#table_name VARCHAR(100)
,#TableDefinition NVARCHAR(MAX) OUTPUT
)
AS
BEGIN
SET NOCOUNT ON
DECLARE #TempTableDefinition NVARCHAR(MAX)
DECLARE #NewLine NVARCHAR(4) = CHAR(13)+CHAR(10)
DECLARE #ResultDefinition TABLE ( --The View Definition per MSDN
is_hidden bit NOT NULL
, column_ordinal int NOT NULL
, [name] sysname NULL
, is_nullable bit NOT NULL
, system_type_id int NOT NULL
, system_type_name nvarchar(256) NULL
, max_length smallint NOT NULL
, [precision] tinyint NOT NULL
, scale tinyint NOT NULL
, collation_name sysname NULL
, user_type_id int NULL
, user_type_database sysname NULL
, user_type_schema sysname NULL
, user_type_name sysname NULL
, assembly_qualified_type_name nvarchar(4000)
, xml_collection_id int NULL
, xml_collection_database sysname NULL
, xml_collection_schema sysname NULL
, xml_collection_name sysname NULL
, is_xml_document bit NOT NULL
, is_case_sensitive bit NOT NULL
, is_fixed_length_clr_type bit NOT NULL
, source_server sysname NULL
, source_database sysname NULL
, source_schema sysname NULL
, source_table sysname NULL
, source_column sysname NULL
, is_identity_column bit NULL
, is_part_of_unique_key bit NULL
, is_updateable bit NULL
, is_computed_column bit NULL
, is_sparse_column_set bit NULL
, ordinal_in_order_by_list smallint NULL
, order_by_is_descending smallint NULL
, order_by_list_length smallint NULL
, tds_type_id int NOT NULL
, tds_length int NOT NULL
, tds_collation_id int NULL
, tds_collation_sort_id tinyint NULL
)
--Insert the description into table variable
INSERT #ResultDefinition
EXEC sp_describe_first_result_set #sql
--Now Build the string to create the table via union select statement
;WITH STMT AS (
SELECT N'CREATE TABLE ' + #table_name + N' (' AS TextVal
UNION ALL
SELECT
CONCAT(
CASE column_ordinal
WHEN 1 THEN ' ' ELSE ' , ' END --Determines if comma should precede
, QUOTENAME([name]) , ' ', system_type_name -- Column Name and SQL TYPE
,CASE is_nullable
WHEN 0 THEN ' NOT NULL' ELSE ' NULL' END --NULLABLE CONSTRAINT
) AS TextVal
FROM #ResultDefinition WHERE is_hidden = 0 -- May not be needed
UNION ALL
SELECT N');' + #NewLine
)
--Now Combine the rows to a single String
SELECT #TempTableDefinition = COALESCE (#TempTableDefinition + #NewLine + TextVal, TextVal) FROM STMT
SELECT #TableDefinition = #TempTableDefinition
END
The conundrum is that you need to use a global table, but you need to make it unique enough
so you can drop and create from it frequently without worrying about a collision.
In the example I used a Guid (FE264BF5_9C32_438F_8462_8A5DC8DEE49E) for the global variable replacing the hyphens with underscore
DECLARE #sql NVARCHAR(4000) = N'SELECT ##SERVERNAME as ServerName, GETDATE() AS Today;'
DECLARE #GlobalTempTable VARCHAR(100) = N'##FE264BF5_9C32_438F_8462_8A5DC8DEE49E_MyTempTable'
--#sql can be a stored procedure name like dbo.foo without parameters
DECLARE #TableDef NVARCHAR(MAX)
DROP TABLE IF EXISTS #MyTempTable
DROP TABLE IF EXISTS ##FE264BF5_9C32_438F_8462_8A5DC8DEE49E_MyTempTable
EXEC [dbo].[sp_GetTableDefinitionFromSqlBatch_DescribeFirstResultSet]
#sql, #GlobalTempTable, #TableDef OUTPUT
--Creates the global table ##FE264BF5_9C32_438F_8462_8A5DC8DEE49E_MyTempTable
EXEC sp_executesql #TableDef
--Now Call the stored procedure, SQL Statement with Params etc.
INSERT ##FE264BF5_9C32_438F_8462_8A5DC8DEE49E_MyTempTable
EXEC sp_executesql #sql
--Select the results into your undefined Temp Table from the Global Table
SELECT *
INTO #MyTempTable
FROM ##FE264BF5_9C32_438F_8462_8A5DC8DEE49E_MyTempTable
SELECT * FROM #MyTempTable
DROP TABLE IF EXISTS #MyTempTable
DROP TABLE IF EXISTS ##FE264BF5_9C32_438F_8462_8A5DC8DEE49E_MyTempTable
Again, I have only tested it with simple stored procedure queries and simple queries so your mileage may vary. Hope this helps someone.
Here is my T-SQL with parameters
--require one time execution if not configured before
sp_configure 'Show Advanced Options', 1
GO
RECONFIGURE
GO
--require one time execution if not configured before
sp_configure 'Ad Hoc Distributed Queries', 1
GO
RECONFIGURE
GO
--the query
DECLARE #param1 int = 1, #param2 int = 2
DECLARE #SQLStr varchar(max) = 'SELECT * INTO #MyTempTable
FROM OPENROWSET(''SQLNCLI'',
''Server=ServerName;Database=DbName;Trusted_Connection=yes'',
''exec StoredProcedureName '+ CAST(#param1 AS varchar(15)) +','+ CAST(#param2 AS varchar(15)) +''') AS a ;
select * from #MyTempTable;
drop table #MyTempTable
';
EXECUTE(#SQLStr);
Well, you do have to create a temp table, but it doesn't have to have the right schema....I've created a stored procedure that modifies an existing temp table so that it has the required columns with the right data type and order (dropping all existing columns, adding new columns):
GO
create procedure #TempTableForSP(#tableId int, #procedureId int)
as
begin
declare #tableName varchar(max) = (select name
from tempdb.sys.tables
where object_id = #tableId
);
declare #tsql nvarchar(max);
declare #tempId nvarchar(max) = newid();
set #tsql = '
declare #drop nvarchar(max) = (select ''alter table tempdb.dbo.' + #tableName
+ ' drop column '' + quotename(c.name) + '';''+ char(10)
from tempdb.sys.columns c
where c.object_id = ' +
cast(#tableId as varchar(max)) + '
for xml path('''')
)
alter table tempdb.dbo.' + #tableName + ' add ' + QUOTENAME(#tempId) + ' int;
exec sp_executeSQL #drop;
declare #add nvarchar(max) = (
select ''alter table ' + #tableName
+ ' add '' + name
+ '' '' + system_type_name
+ case when d.is_nullable=1 then '' null '' else '''' end
+ char(10)
from sys.dm_exec_describe_first_result_set_for_object('
+ cast(#procedureId as varchar(max)) + ', 0) d
order by column_ordinal
for xml path(''''))
execute sp_executeSQL #add;
alter table ' + #tableName + ' drop column ' + quotename(#tempId) + ' ';
execute sp_executeSQL #tsql;
end
GO
create table #exampleTable (pk int);
declare #tableId int = object_Id('tempdb..#exampleTable')
declare #procedureId int = object_id('examplestoredProcedure')
exec #TempTableForSP #tableId, #procedureId;
insert into #exampleTable
exec examplestoredProcedure
Note this won't work if sys.dm_exec_describe_first_result_set_for_object can't determine the results of the stored procedure (for instance if it uses a temp table).
If you let dynamic SQL create a temp table, this table is owned by the Dynamic SQL connection, as opposed to the connection your stored procedure is called from.
DECLARE #COMMA_SEPARATED_KEYS varchar(MAX);
DROP TABLE IF EXISTS KV;
CREATE TABLE KV (id_person int, mykey varchar(30), myvalue int);
INSERT INTO KV VALUES
(1, 'age', 16),
(1, 'weight', 63),
(1, 'height', 175),
(2, 'age', 26),
(2, 'weight', 83),
(2, 'height', 185);
WITH cte(mykey) AS (
SELECT DISTINCT mykey FROM KV
)
SELECT #COMMA_SEPARATED_KEYS=STRING_AGG(mykey,',') FROM cte;
SELECT #COMMA_SEPARATED_KEYS AS keys;
DECLARE #ExecuteExpression varchar(MAX);
DROP TABLE IF EXISTS #Pivoted;
SET #ExecuteExpression = N'
SELECT *
INTO #Pivoted
FROM
(
SELECT
mykey,
myvalue,
id_person
FROM KV
) AS t
PIVOT(
MAX(t.myvalue)
FOR mykey IN (COMMA_SEPARATED_KEYS)
) AS pivot_table;
';
SET #ExecuteExpression = REPLACE(#ExecuteExpression, 'COMMA_SEPARATED_KEYS', #COMMA_SEPARATED_KEYS);
EXEC(#ExecuteExpression);
SELECT * FROM #Pivoted;
Msg 208, Level 16, State 0
Invalid object name '#Pivoted'.
This is because #Pivoted is owned by the Dynamic SQL connection. So the last instruction
SELECT * FROM #Pivoted
fails.
One way to not face this issue is to make sure all references to #Pivoted are made from inside the dynamic query itself:
DECLARE #COMMA_SEPARATED_KEYS varchar(MAX);
DROP TABLE IF EXISTS KV;
CREATE TABLE KV (id_person int, mykey varchar(30), myvalue int);
INSERT INTO KV VALUES
(1, 'age', 16),
(1, 'weight', 63),
(1, 'height', 175),
(2, 'age', 26),
(2, 'weight', 83),
(2, 'height', 185);
WITH cte(mykey) AS (
SELECT DISTINCT mykey FROM KV
)
SELECT #COMMA_SEPARATED_KEYS=STRING_AGG(mykey,',') FROM cte;
SELECT #COMMA_SEPARATED_KEYS AS keys;
DECLARE #ExecuteExpression varchar(MAX);
DROP TABLE IF EXISTS #Pivoted;
SET #ExecuteExpression = N'
SELECT *
INTO #Pivoted
FROM
(
SELECT
mykey,
myvalue,
id_person
FROM KV
) AS t
PIVOT(
MAX(t.myvalue)
FOR mykey IN (COMMA_SEPARATED_KEYS)
) AS pivot_table;
SELECT * FROM #Pivoted;
';
SET #ExecuteExpression = REPLACE(#ExecuteExpression, 'COMMA_SEPARATED_KEYS', #COMMA_SEPARATED_KEYS);
EXEC(#ExecuteExpression);
First, modify your stored procedure to save the end results in to a temp table. By doing this we are creating a table matching with the SP output fields. And then have a select statement to save that temp table to a any table name. Then execute the SP as explained in step 2
Step 1: modify your stored procedure to save the end results in to a temp table
[your stored procedure]
into #table_temp //this will insert the data to a temp table
from #table_temp
select * into SP_Output_Table_1 from #table_temp //this will save data to a actual table
Step 2: Execute the SP as below that will insert records to your table
Insert SP_Output_Table_1
EXE You_SP_Nane #Parameter1 = 52, #parameter2 =1

INSERT INTO with exec with multiple result sets

SQL Server allows me to insert the returned result set of a stored procedure as:
DECLARE #T TABLE (
ID int,
Name varchar(255),
Amount money)
INSERT INTO #T
exec dbo.pVendorBalance
This works as long as the stored procedure only returns 1 result set.
Is there a way to make this work if the stored procedure returns several result sets?
E.g.
DECLARE #T1 (...)
DECLARE #T2 (...)
INSERT INTO #T1 THEN INTO #T2
exec dbo.pVendorBalance
One workaround to this problem is using OUTPUT parameters (JSON/XML) instead of resultsets.
CREATE TABLE tab1(ID INT, Name NVARCHAR(10), Amount MONEY);
INSERT INTO tab1(ID, Name, Amount)
VALUES (1, 'Alexander', 10),(2, 'Jimmy', 100), (6, 'Billy', 20);
CREATE PROCEDURE dbo.pVendorBalance
AS
BEGIN
-- first resultset
SELECT * FROM tab1 WHERE ID <=2;
-- second resultset
SELECT * FROM tab1 WHERE ID > 5;
END;
Version with OUT params:
CREATE PROCEDURE dbo.pVendorBalance2
#resultSet1 NVARCHAR(MAX) OUT,
#resultSet2 NVARCHAR(MAX) OUT
AS
BEGIN
SELECT #resultSet1 = (SELECT * FROM tab1 WHERE ID <=2 FOR JSON AUTO),
#resultSet2 = (SELECT * FROM tab1 WHERE ID > 5 FOR JSON AUTO);
END;
And final call:
DECLARE #r1 NVARCHAR(MAX), #r2 NVARCHAR(MAX);
EXEC dbo.pVendorBalance2 #r1 OUT, #r2 OUT;
-- first resultset as table
SELECT *
INTO #t1
FROM OpenJson(#r1)
WITH (ID int '$.ID', [Name] NVARCHAR(50) '$.Name',Amount money '$.Amount');
-- second resultset as table
SELECT *
INTO #t2
FROM OpenJson(#r2)
WITH (ID int '$.ID', [Name] NVARCHAR(50) '$.Name',Amount money '$.Amount');
SELECT * FROM #t1;
SELECT * FROM #t2;
DBFiddle Demo
EDIT:
Second approach is to use tSQLt.ResultSetFilter CLR function (part of tSQLt testing framework):
The ResultSetFilter procedure provides the ability to retrieve a single result set from a statement which produces multiple result sets.
CREATE TABLE #DatabaseSize (
database_name nvarchar(128),
database_size varchar(18),
unallocated_space varchar(18)
);
CREATE TABLE #ReservedSpaceUsed (
reserved VARCHAR(18),
data VARCHAR(18),
index_size VARCHAR(18),
unused VARCHAR(18)
);
INSERT INTO #DatabaseSize
EXEC tSQLt.ResultSetFilter 1, 'EXEC sp_spaceused';
INSERT INTO #ReservedSpaceUsed
EXEC tSQLt.ResultSetFilter 2, 'EXEC sp_spaceused';
SELECT * FROM #DatabaseSize;
SELECT * FROM #ReservedSpaceUsed;
No. But there is more of a work around since you cannot do an insert into with a procedure that returns multiple results with a different number of columns.
If you are allowed to modify the stored procedure, then you can declare temp tables outside of the procedure and populate them within the stored procedure. Then you can do whatever you need with them outside of the stored procedure.
CREATE TABLE #result1(Each column followed by data type of first result.);
----Example: CREATE TABLE #result1(Column1 int, Column2 varchar(10))
CREATE TABLE #result2(Each column followed by data type of second result.);
EXEC pVendorBalance;
SELECT * FROM #result1;
SELECT * FROM #result2;
I had a similar requirement, and ended up using the a CLR function which you can read about here (it's the answer with the InsertResultSetsToTables method, by user Dan Guzman):
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/da5328a7-5dab-44b3-b2b1-4a8d6d7798b2/insert-into-table-one-or-multiple-result-sets-from-stored-procedure?forum=transactsql
You need to create a SQL Server CLR project in Visual Studio to get going. I had a project already written by a co-worker that I could just expand, but if you're starting from scratch, try reading this guide:
http://www.emoreau.com/Entries/Articles/2015/04/SQL-CLR-Integration-in-2015-year-not-product-version.aspx
If you've succeeded in writing and publishing the CLR project to the database, here is an example of using it I wrote:
-- declare a string with the SQL you want to execute (typically an SP call that returns multiple result sets)
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'exec usp_SomeProcedure #variable1 = ' + #variable1 + '...' -- piece together a long SQL string from various parameters
-- create temp tables (one per result set) to hold the output; could also be actual tables (non-temp) if you want
CREATE TABLE #results_1(
[CustomerId] INT, [Name] varchar(500), [Address] varchar(500)
);
CREATE TABLE #results_2(
[SomeId] UNIQUEIDENTIFIER, [SomeData] INT, [SomethingElse] DateTime
);
-- on the exemplary 'CustomerDatabase' database, there is an SP (created automatically by the SQL CLR project deployment process in Visual Studio) which performs the actual call to the .NET assembly, and executes the .NET code
-- the CLR stored procedure CLR_InsertResultSetsToTables executes the SQL defined in the parameter #sourceQuery, and outputs multiple result sets into the specified list of tables (#targetTableList)
EXEC CustomerDatabase.dbo.CLR_InsertResultSetsToTables #sourceQuery = #sql, #targetTableList = N'#results_1,#results_2';
-- The output of the SP called in #sql is now dumped in the two temp tables and can be used for whatever in regular SQL
SELECT * FROM #results_1;
SELECT * FROM #results_2;
We can do it in the following way
Consider the input SP (which returns 2 tables as output) as usp_SourceData
Alter the usp_SourceData to accept a parameter as 1 and 2
Adjust the SP in a way that when
usp_SourceData '1' is executed it will return first table
and when
usp_SourceData '2' is executed it will return second table.
Actually stored procedures can return multiple result sets, or no result sets, it's pretty arbitrary. Because of this, I don't know of any way to navigate those results from other SQL code calling a stored procedure.
However, you CAN use the returned result set from a table-valued user defined function. It's just like a regular UDF, but instead of returning a scalar value you return a query result. Then you can use that UDF like any other table.
INSERT INTO #T SELECT * FROM dbp.pVendorBalanceUDF()
http://technet.microsoft.com/en-us/library/ms191165(v=sql.105).aspx
DROP TABLE ##Temp
DECLARE #dtmFrom VARCHAR(60) = '2020-12-01 00:00:00', #dtmTo VARCHAR(60) = '2020-12-02 23:59:59.997',#numAdmDscTransID VARCHAR(60) =247054
declare #procname nvarchar(255) = 'spGetCashUnpaidBills',
#procWithParam nvarchar(255) = '[dbo].[spGetCashUnpaidBills] #dtmFromDate= ''' +#dtmFrom+ ''' ,#dtmToDate= ''' +#dtmTo+''',#numCompanyID=1,#numAdmDscTransID='+ #numAdmDscTransID +',#tnyShowIPCashSchemeBills=1',
#sql nvarchar(max),
#tableName Varchar(60) = 'Temp'
set #sql = 'create table ##' + #tableName + ' ('
begin
select #sql = #sql + '[' + r.name + '] ' + r.system_type_name + ','
from sys.procedures AS p
cross apply sys.dm_exec_describe_first_result_set_for_object(p.object_id, 0) AS r
where p.name = #procname
set #sql = substring(#sql,1,len(#sql)-1) + ')'
execute (#sql)
execute('insert ##' + #tableName + ' exec ' + #procWithParam)
end
SELECT *FROM ##Temp
If the both result sets have same number of columns then
insert into #T1 exec dbo.pVendorBalance
will insert the union of both data set into #T1.
If not
Then edit dbo.pVendorBalance and insert results into temporary tables and in outer stored proc, select from those temporary tables.
Another way(If you need it), you can try
SELECT * into #temp
from OPENROWSET('SQLNCLI', 'Server=(local)\\(instance);Trusted_Connection=yes;',
'EXEC dbo.pVendorBalance')
it will take first dataset.

SQL variable to hold list of integers

I'm trying to debug someone else's SQL reports and have placed the underlying reports query into a query windows of SQL 2012.
One of the parameters the report asks for is a list of integers. This is achieved on the report through a multi-select drop down box. The report's underlying query uses this integer list in the where clause e.g.
select *
from TabA
where TabA.ID in (#listOfIDs)
I don't want to modify the query I'm debugging but I can't figure out how to create a variable on the SQL Server that can hold this type of data to test it.
e.g.
declare #listOfIDs int
set listOfIDs = 1,2,3,4
There is no datatype that can hold a list of integers, so how can I run the report query on my SQL Server with the same values as the report?
Table variable
declare #listOfIDs table (id int);
insert #listOfIDs(id) values(1),(2),(3);
select *
from TabA
where TabA.ID in (select id from #listOfIDs)
or
declare #listOfIDs varchar(1000);
SET #listOfIDs = ',1,2,3,'; --in this solution need put coma on begin and end
select *
from TabA
where charindex(',' + CAST(TabA.ID as nvarchar(20)) + ',', #listOfIDs) > 0
Assuming the variable is something akin to:
CREATE TYPE [dbo].[IntList] AS TABLE(
[Value] [int] NOT NULL
)
And the Stored Procedure is using it in this form:
ALTER Procedure [dbo].[GetFooByIds]
#Ids [IntList] ReadOnly
As
You can create the IntList and call the procedure like so:
Declare #IDs IntList;
Insert Into #IDs Select Id From dbo.{TableThatHasIds}
Where Id In (111, 222, 333, 444)
Exec [dbo].[GetFooByIds] #IDs
Or if you are providing the IntList yourself
DECLARE #listOfIDs dbo.IntList
INSERT INTO #listofIDs VALUES (1),(35),(118);
You are right, there is no datatype in SQL-Server which can hold a list of integers. But what you can do is store a list of integers as a string.
DECLARE #listOfIDs varchar(8000);
SET #listOfIDs = '1,2,3,4';
You can then split the string into separate integer values and put them into a table. Your procedure might already do this.
You can also use a dynamic query to achieve the same outcome:
DECLARE #SQL nvarchar(8000);
SET #SQL = 'SELECT * FROM TabA WHERE TabA.ID IN (' + #listOfIDs + ')';
EXECUTE (#SQL);
Note: I haven't done any sanitation on this query, please be aware that it's vulnerable to SQL injection. Clean as required.
For SQL Server 2016+ and Azure SQL Database, the STRING_SPLIT function was added that would be a perfect solution for this problem. Here is the documentation:
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql
Here is an example:
/*List of ids in a comma delimited string
Note: the ') WAITFOR DELAY ''00:00:02''' is a way to verify that your script
doesn't allow for SQL injection*/
DECLARE #listOfIds VARCHAR(MAX) = '1,3,a,10.1,) WAITFOR DELAY ''00:00:02''';
--Make sure the temp table was dropped before trying to create it
IF OBJECT_ID('tempdb..#MyTable') IS NOT NULL DROP TABLE #MyTable;
--Create example reference table
CREATE TABLE #MyTable
([Id] INT NOT NULL);
--Populate the reference table
DECLARE #i INT = 1;
WHILE(#i <= 10)
BEGIN
INSERT INTO #MyTable
SELECT #i;
SET #i = #i + 1;
END
/*Find all the values
Note: I silently ignore the values that are not integers*/
SELECT t.[Id]
FROM #MyTable as t
INNER JOIN
(SELECT value as [Id]
FROM STRING_SPLIT(#listOfIds, ',')
WHERE ISNUMERIC(value) = 1 /*Make sure it is numeric*/
AND ROUND(value,0) = value /*Make sure it is an integer*/) as ids
ON t.[Id] = ids.[Id];
--Clean-up
DROP TABLE #MyTable;
The result of the query is 1,3
In the end i came to the conclusion that without modifying how the query works i could not store the values in variables. I used SQL profiler to catch the values and then hard coded them into the query to see how it worked. There were 18 of these integer arrays and some had over 30 elements in them.
I think that there is a need for MS/SQL to introduce some aditional datatypes into the language. Arrays are quite common and i don't see why you couldn't use them in a stored proc.
There is a new function in SQL called string_split if you are using list of string.
Ref Link STRING_SPLIT (Transact-SQL)
DECLARE #tags NVARCHAR(400) = 'clothing,road,,touring,bike'
SELECT value
FROM STRING_SPLIT(#tags, ',')
WHERE RTRIM(value) <> '';
you can pass this query with in as follows:
SELECT *
FROM [dbo].[yourTable]
WHERE (strval IN (SELECT value FROM STRING_SPLIT(#tags, ',') WHERE RTRIM(value) <> ''))
I use this :
1-Declare a temp table variable in the script your building:
DECLARE #ShiftPeriodList TABLE(id INT NOT NULL);
2-Allocate to temp table:
IF (SOME CONDITION)
BEGIN
INSERT INTO #ShiftPeriodList SELECT ShiftId FROM [hr].[tbl_WorkShift]
END
IF (SOME CONDITION2)
BEGIN
INSERT INTO #ShiftPeriodList
SELECT ws.ShiftId
FROM [hr].[tbl_WorkShift] ws
WHERE ws.WorkShift = 'Weekend(VSD)' OR ws.WorkShift = 'Weekend(SDL)'
END
3-Reference the table when you need it in a WHERE statement :
INSERT INTO SomeTable WHERE ShiftPeriod IN (SELECT * FROM #ShiftPeriodList)
You can't do it like this, but you can execute the entire query storing it in a variable.
For example:
DECLARE #listOfIDs NVARCHAR(MAX) =
'1,2,3'
DECLARE #query NVARCHAR(MAX) =
'Select *
From TabA
Where TabA.ID in (' + #listOfIDs + ')'
Exec (#query)

Insert/Update/Delete with function in SQL Server

Can we perform Insert/Update/Delete statement with SQL Server Functions. I have tried with but SQL Server error is occured.
Error:
Invalid use of side-effecting or time-dependent operator in 'DELETE' within a function.
AnyBody have any Idea why we can not use Insert/Update/Delete statements with SQL Server functions.
Waiting for your good idea's
No, you cannot.
From SQL Server Books Online:
User-defined functions cannot be used
to perform actions that modify the
database state.
Ref.
Yes, you can!))
Disclaimer: This is not a solution, it is more of a hack to test out something. User-defined functions cannot be used to perform actions that modify the database state.
I found one way to make INSERT, UPDATE or DELETE in function using xp_cmdshell.
So you need just to replace the code inside #sql variable.
CREATE FUNCTION [dbo].[_tmp_func](#orderID NVARCHAR(50))
RETURNS INT
AS
BEGIN
DECLARE #sql varchar(4000), #cmd varchar(4000)
SELECT #sql = 'INSERT INTO _ord (ord_Code) VALUES (''' + #orderID + ''') '
SELECT #cmd = 'sqlcmd -S ' + ##servername +
' -d ' + db_name() + ' -Q "' + #sql + '"'
EXEC master..xp_cmdshell #cmd, 'no_output'
RETURN 1
END
Functions in SQL Server, as in mathematics, can not be used to modify the database. They are intended to be read only and can help developer to implement command-query separation. In other words, asking a question should not change the answer. When your program needs to modify the database use a stored procedure instead.
You can't update tables from a function like you would a stored procedure, but you CAN update table variables.
So for example, you can't do this in your function:
create table MyTable
(
ID int,
column1 varchar(100)
)
update [MyTable]
set column1='My value'
but you can do:
declare #myTable table
(
ID int,
column1 varchar(100)
)
Update #myTable
set column1='My value'
Yes, you can.
However, it requires SQL CLR with EXTERNAL_ACCESS or UNSAFE permission and specifying a connection string. This is obviously not recommended.
For example, using Eval SQL.NET (a SQL CLR which allow to add C# syntax in SQL)
CREATE FUNCTION [dbo].[fn_modify_table_state]
(
#conn VARCHAR(8000) ,
#sql VARCHAR(8000)
)
RETURNS INT
AS
BEGIN
RETURN SQLNET::New('
using(var connection = new SqlConnection(conn))
{
connection.Open();
using(var command = new SqlCommand(sql, connection))
{
return command.ExecuteNonQuery();
}
}
').ValueString('conn', #conn).ValueString('sql', #sql).EvalReadAccessInt()
END
GO
DECLARE #conn VARCHAR(8000) = 'Data Source=XPS8700;Initial Catalog=SqlServerEval_Debug;Integrated Security=True'
DECLARE #sql VARCHAR(8000) = 'UPDATE [Table_1] SET Value = -1 WHERE Name = ''zzz'''
DECLARE #rowAffecteds INT = dbo.fn_modify_table_state(#conn, #sql)
Documentation: Modify table state within a SQL Function
Disclaimer: I'm the owner of the project Eval SQL.NET
You can have a table variable as a return type and then update or insert on a table based on that output.
In other words, you can set the variable output as the original table, make the modifications and then do an insert to the original table from function output.
It is a little hack but if you insert the #output_table from the original table and then say for example:
Insert into my_table
select * from my_function
then you can achieve the result.
We can't say that it is possible of not their is some other way exist to perform update operation in user-defined Function. Directly DML is not possible in UDF it is for sure.
Below Query is working perfectly:
create table testTbl
(
id int identity(1,1) Not null,
name nvarchar(100)
)
GO
insert into testTbl values('ajay'),('amit'),('akhil')
Go
create function tblValued()
returns Table
as
return (select * from testTbl where id = 1)
Go
update tblValued() set name ='ajay sharma' where id = 1
Go
select * from testTbl
Go
"Functions have only READ-ONLY Database Access"
If DML operations would be allowed in functions then function would be prety similar to stored Procedure.
No, you can not do Insert/Update/Delete.
Functions only work with select statements. And it has only READ-ONLY Database Access.
In addition:
Functions compile every time.
Functions must return a value or result.
Functions only work with input parameters.
Try and catch statements are not used in functions.
CREATE FUNCTION dbo.UdfGetProductsScrapStatus
(
#ScrapComLevel INT
)
RETURNS #ResultTable TABLE
(
ProductName VARCHAR(50), ScrapQty FLOAT, ScrapReasonDef VARCHAR(100), ScrapStatus VARCHAR(50)
) AS BEGIN
INSERT INTO #ResultTable
SELECT PR.Name, SUM([ScrappedQty]), SC.Name, NULL
FROM [Production].[WorkOrder] AS WO
INNER JOIN
Production.Product AS PR
ON Pr.ProductID = WO.ProductID
INNER JOIN Production.ScrapReason AS SC
ON SC.ScrapReasonID = WO.ScrapReasonID
WHERE WO.ScrapReasonID IS NOT NULL
GROUP BY PR.Name, SC.Name
UPDATE #ResultTable
SET ScrapStatus =
CASE WHEN ScrapQty > #ScrapComLevel THEN 'Critical'
ELSE 'Normal'
END
RETURN
END
Functions are not meant to be used that way, if you wish to perform data change you can just create a Stored Proc for that.
if you need to run the delete/insert/update you could also run dynamic statements. i.e.:
declare
#v_dynDelete NVARCHAR(500);
SET #v_dynDelete = 'DELETE some_table;';
EXEC #v_dynDelete
Just another alternative using sp_executesql (tested only in SQL 2016).
As previous posts noticed, atomicity must be handled elsewhere.
CREATE FUNCTION [dbo].[fn_get_service_version_checksum2]
(
#ServiceId INT
)
RETURNS INT
AS
BEGIN
DECLARE #Checksum INT;
SELECT #Checksum = dbo.fn_get_service_version(#ServiceId);
DECLARE #LatestVersion INT = (SELECT MAX(ServiceVersion) FROM [ServiceVersion] WHERE ServiceId = #ServiceId);
-- Check whether the current version already exists and that it's the latest version.
IF EXISTS(SELECT TOP 1 1 FROM [ServiceVersion] WHERE ServiceId = #ServiceId AND [Checksum] = #Checksum AND ServiceVersion = #LatestVersion)
RETURN #LatestVersion;
-- Insert the new version to the table.
EXEC sp_executesql N'
INSERT INTO [ServiceVersion] (ServiceId, ServiceVersion, [Checksum], [Timestamp])
VALUES (#ServiceId, #LatestVersion + 1, #Checksum, GETUTCDATE());',
N'#ServiceId INT = NULL, #LatestVersion INT = NULL, #Checksum INT = NULL',
#ServiceId = #ServiceId,
#LatestVersion = #LatestVersion,
#Checksum = #Checksum
;
RETURN #LatestVersion + 1;
END;

Resources