I have a stored procedure, usp_region and it has a select statement with 50 columns as the result set. This procedure is called by multiple other stored procedures in our application.
Most of the stored procedure pass a parameter to this procedure and display the result set that it returns. I have one stored procedure, usp_calculatedDisplay, that gets the columns from this stored procedure and inserts the values into a temp table and does some more calculations on the columns.
Here's a part of the code in usp_calculatedDisplay.
Begin Procedure
/* some sql statements */
Declare #tmptable
(
-- all the 50 columns that are returned from the usp_region procedure
)
Insert Into #tmptable
exec usp_region #regionId = #id
Select t.*, /* a few calculated columns here */
From #tmptable t
End of procedure
Every time I add a column to the usp_region procedure, I'll also have to make sure I have to add it to this procedure. Otherwise it breaks. It has become difficult to maintain it since it is highly possible for someone to miss adding a column to the usp_calculatedDisplay procedure when the column is added to the usp_region.
In order to overcome this problem, I decided to do this:
Select *
Into #tmptable
From OPENROWSET('SQLNCLI',
'Server=localhost;Trusted_Connection=yes;',
'EXEC [dbo].[usp_region]')
The problem is 'Ad Hoc Distributed Queries' component is turned off. So I can't use this approach to overcome this issue. I was wondering if there are any other ways of overcoming this problem. I would really appreciate any help. Thank you!
Every time I add a column to the usp_region procedure
SQL Server is a structured database and it does not meant to solve such cases that you need to change your structure every day.
If you add/remove columns so often then you probably did not choose the right type of database, and you better re-design your system.
It has become difficult to maintain it since it is highly possible for someone to miss adding a column to the usp_calculatedDisplay procedure when the column is added to the usp_region.
There are two simple solutions for this (1) using DDL Triggers - very bad idea but simple to implement and working. (2) Using my trick to select from stored procedure
Option 1: using DDL trigger
You can automate the entire procedure and ALTER the stored procedure usp_calculatedDisplay every time that the stored procedure usp_region is changed
https://learn.microsoft.com/en-us/sql/relational-databases/triggers/ddl-triggers
The basic approach is
CREATE OR ALTER TRIGGER NotGoodSolutionTrig ON DATABASE FOR ALTER_PROCEDURE AS BEGIN
DECLARE #var_xml XML = EVENTDATA();
IF(
#var_xml.value('(EVENT_INSTANCE/DatabaseName)[1]', 'sysname') = 'tempdb'
and
#var_xml.value('(EVENT_INSTANCE/SchemaName)[1]', 'sysname') = 'dbo'
and
#var_xml.value('(EVENT_INSTANCE/ObjectName)[1]', 'sysname') = 'usp_region'
)
BEGIN
-- Here you can parse the text of the stored procedure
-- and execute ALTER on the first SP
-- To make it simpler, you can design the procedure usp_region so the columns names will be in specific row or between to comment which will help us to find it
-- The code of the Stored Procedure which you need to parse is in the value of:
-- #var_xml.value('(EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)'))
-- For example we can print it
DECLARE #SP_Code NVARCHAR(MAX)
SET #SP_Code = CONVERT(NVARCHAR(MAX), #var_xml.value('(EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)'))
PRINT #SP_Code
-- In your case, you need to execute ALTER on the usp_calculatedDisplay procedure using the text from usp_region
END
END
Option 2: trick to select from stored procedure using sys.dm_exec_describe_first_result_set
This is simple and direct way to get what you need.
CREATE OR ALTER PROCEDURE usp_calculatedDisplay AS
-- Option: using simple table, so it will exists outsie the scope of the dynamic query
DROP TABLE IF EXISTS MyTable;
DECLARE #sqlCommand NVARCHAR(MAX)
select #sqlCommand = 'CREATE TABLE MyTable(' + STRING_AGG ([name] + ' ' + system_type_name, ',') + ');'
from sys.dm_exec_describe_first_result_set (N'EXEC usp_region', null,0)
PRINT #sqlCommand
EXECUTE sp_executesql #sqlCommand
INSERT MyTable EXECUTE usp_region;
SELECT * FROM MyTable;
GO
Note!!! Both solutions are not recommended in production. My advice is to avoid such needs by redesign your system. If you need to re-write 20 SP so do it and don't be lazy! Your goal should be what best for the database usage.
Related
We are writing a stored procedure responsible for getting a stored procedure name and returning a result containing the stored procedure columns and their data types.
However, we bumped into a problem executing a dynamic query to return the results of stored procedure, but we can't store it in a temp table!
You can see our query below:
DECLARE #ProcName VARCHAR(100)='spGetOraganizationsList',
#ParamName VARCHAR(100),#DataType VARCHAR(20),
#Query NVARCHAR(MAX)='EXEC '+'spGetOraganizationsList '
SELECT PARAMETER_NAME,DATA_TYPE
INTO #Tmp
FROM information_schema.PARAMETERS
WHERE SPECIFIC_NAME=#ProcName
DECLARE ParamCursor CURSOR
FOR SELECT * FROM #Tmp
OPEN ParamCursor
FETCH NEXT FROM ParamCursor
INTO #ParamName,#DataType
WHILE ##FETCH_STATUS = 0
BEGIN
SET #Query=#Query+#ParamName+'=Null,'
FETCH NEXT FROM ParamCursor INTO #ParamName,#DataType
END
CLOSE ParamCursor
DEALLOCATE ParamCursor
DROP TABLE #Tmp
EXEC sp_executesql #Query
The thing is I can't store the results of it in a temp table,
and OPENROWSET does not accept variables.
I think it comes from sql concept that it doesn't trust in result of stored procedures and because of that we cannot select on it or store it in a table by 'making in query table' method.
Unless you create a table and define it's columns and sql trust to you and you insert result of it into this table for example take below situation
Create table test (name varchar(10),family varchar(20))
Insert into test
Exec sp-testResult
Now if you define wrong column for your table you will receive query runtime error .actually sql doesn't predict result of sp and leaves it to you to define result of your stored procedure.
You can certainly INSERT the results of a stored procedure into a TEMP table:
CREATE PROCEDURE PurgeMe
AS
SELECT convert(int, 1) AS DaData
UNION
SELECT convert(int, 2)
GO
CREATE TABLE #Doodles (AnInteger int)
INSERT #Doodles EXECUTE PurgeMe
SELECT * FROM #Doodles
Questions arise about the SCOPE of TEMP tables, however. You might find that in your calling routine you will not be able to see a TEMP table created within your routine.
The solution to the SCOPE problem is to do the following:
Create a minimal TEMP table (say, with one column)
Use ALTER TABLE on the TEMP table within your routine to make its schema match
your needs (this can be tricky, but it can be done)
Put data into the TEMP table
return from your routine - the calling routine will now be able to access the temp
table
If this is of interest I can make a longer post with a stored procedure to do the above. It was written to facilitate dynamic SQL
Write select query as you want in the stored procedure. You will get the result without creating temp table.
Use global temp table and dynamic OPENROWSET
DROP TABLE ##Tmp;
GO
DECLARE #ProcName VARCHAR(100)='spGetOraganizationsList',
#ParamName VARCHAR(100), #DataType VARCHAR(20),
-- Mind to specify database and schema of the SP
#Query NVARCHAR(MAX)=' EXEC [mydb].[dbo].spGetOraganizationsList ';
SELECT PARAMETER_NAME,DATA_TYPE
INTO #Tmp
FROM information_schema.PARAMETERS
WHERE SPECIFIC_NAME=#ProcName;
-- Build SP exec
DECLARE ParamCursor CURSOR
FOR SELECT * FROM #Tmp
OPEN ParamCursor
FETCH NEXT FROM ParamCursor
INTO #ParamName,#DataType
WHILE ##FETCH_STATUS = 0
BEGIN
SET #Query=#Query+#ParamName+'=Null,'
FETCH NEXT FROM ParamCursor INTO #ParamName,#DataType
END
CLOSE ParamCursor
DEALLOCATE ParamCursor
SET #Query = left(#Query, len(#Query) - 1);
-- Build ad hoc distributed query which creates ##Tmp from SP exec.
SET #Query = 'SELECT * INTO ##Tmp FROM OPENROWSET(''SQLNCLI'', ''Server=localhost;Trusted_Connection=yes;'',''' + #Query + ''')';
EXEC (#Query);
-- Created by dynamic sql `##Tmp` is availabe in the current context.
SELECT *
FROM ##Tmp;
Don't forget to enable ad hoc distributed queries first.
sp_configure 'Show Advanced Options', 1
GO
RECONFIGURE
GO
sp_configure 'Ad Hoc Distributed Queries', 1
GO
RECONFIGURE
GO
EDIT
My answer solves only one problem, storing the result of a dynamic proc call in a temp table. And there are more problems.
First, #p=null just will not compile if the type of #p is user-defined table type. You need kind of declare #t myType;
exec mySp ... ,#p=#t ....
Next is the 'cannot retrieve matadata for sp because contain dynamic query' error you commented on. Looks like you need an application, SqlClr or standalone, which would be capable to read and parse Datasets returned by procs.
Finally, if an SP contains conditional sql which can return a result set of different schema depending on parameter values, the result of all those efforts is still questionable.
In C#, you can use an SqlDataReader or a DataTable to get the results from a stored procedure without knowing the schema beforehand. If you then want to write that data to a temporary table, I think you can do that from C# (though I've never tried to do it).
I have 2 stored procedures which returns same but unknown columns. I need to write a proc to combine results from both stored procedures. I tried OPENROWSET but problem is to provide the connection string in OPENROWSET function, even if I specify the connection string one time, it will be different for different environments and I think that will be the worst thing to change connection string each time I deploy the application in different environments or if the user is changed on server. Can someone help me to get this done in the best way.
I cannot write them as function since the procs are using temp tables.
Declare #connection nvarchar(200)
Declare #sql nvarchar(max)
Set #connection= 'Server=servername;initial
catalog=dbname;user=abc,password=xyz';
Set #sql='SELECT * INTO #temp1
FROM OPENROWSET(
''SQLNCLI'',
'''+ #connection + ''',
''EXEC sp_name '')'
Exec(#sql)
--- creating a temporary table
CREATE Table #Dynamic_Temp_Table (_field_only_for_create_ INT )
--- Addition of fields from the first recordset from the first procedure
DECLARE #SQL NVARCHAR(MAX)
SELECT #SQL=ISNULL(#SQL+',','ALTER TABLE #Dynamic_Temp_Table ADD ')+name+' '+system_type_name
FROM sys.dm_exec_describe_first_result_set('exec sp_proc_first', NULL, NULL)
order by column_ordinal
exec sp_executesql #SQL
--- Remove of the first unused column
ALTER TABLE #Dynamic_Temp_Table drop column _field_only_for_create_
--- Addition of the result from the first procedure
INSERT INTO #Dynamic_Temp_Table
exec sp_proc_first
--- Addition of the result from the second procedure
INSERT INTO #Dynamic_Temp_Table
exec sp_proc_second
--- result: exec sp_proc_first UNION ALL exec sp_proc_second
select * from #Dynamic_Temp_Table
--- result: exec sp_proc_first UNION exec sp_proc_second
select DISTINCT * from #Dynamic_Temp_Table
It is possible but not easy at all....
You can change your stored procedures to just create and populate a global temp tables (no select), then you can select both with an union.
To use OPENROWSET as you are doing it in your current approach you will need global temp table too, But as you mention that your stored procedures are using Temp Tables, OPENROWSET, OPENQUERY or sys.dm_exec_describe_first_result_set will not be able to determine the metadata to create the temp table.
Another option is to change your stored procedures to use variable tables instead of temp tables, then the metadata could be redetermined. #chrszcpl's answer https://stackoverflow.com/a/55632401/10932521 is a very good solution if you are able to do that.
If this is not possible (I assume that this isn't, otherwise the columns would't be unknown) because you are using dynamic sql in your procedures, or you simply can't touch those procedures for any reason, I think that the cheapest solution is create a third stored procedure which returns the dynamic columns definition that the other procedures will return..
I have written a stored procedure as follows (this is a simplified version - the SP does a lot of other things but these are the key parts):
CREATE PROCEDURE [dbo].[_uspCustomSP]
AS
BEGIN
CREATE TABLE #custno(custno int)
INSERT INTO #custno
EXEC usp_GetCustomerNo
DECLARE #custnumber nvarchar(5)
SET #custnumber = (SELECT custno FROM #custno)
DROP TABLE #custno-- drop table so fresh each time
END
This SP works as I want it to. However, I want to be able to refer to the value of #custnumber in a different stored procedure. Is there any way of persisting the value of #custnumber but without rerunning usp_GetCustomerNo (as every time it is run, the value of #custnumber changes - I want to be able to use the exact number as stored in the variable.)
EDIT: I've had a really helpful response below suggesting I include an output parameters. I have thought about this but I'm not sure how to refer to this output elsewhere (in a different SP) without re-running the entire SP at the same time.
Apologies if I've not included enough information.
Many thanks,
Helen
You can have the stored procedure return the value:
CREATE PROCEDURE [dbo].[_uspCustomSP] (
#custnumber nvarchar(5) OUTPUT
) AS
BEGIN
CREATE TABLE #custno(custno int) ;
INSERT INTO #custno (custno)
EXEC usp_GetCustomerNo;
SELECT #custnumber = custno FROM #custno ;
DROP TABLE #custno-- drop table so fresh each time
END;
Having said that, I have some comments on the stored procedure:
There is no need to drop the temporary table. I prefer table variables, because it is obvious they go out of scope.
I think it is dangerous to return a single value in a table. Why not use a scalar function or OUTPUT parameter for usp_GetCustomerNo?
You should get in the habit of putting semicolons at the end of statements and always using a column list with INSERT.
You would call the stored procedure as:
declare #custnumber nvarchar(5);
exec sp_executesql _uspCustomSP,
N'#custnumber nvarchar(5) output',
#custnumber=#custnumber output;
I have a stored procedure, let's call it stored procedure 'B'. Stored procedure 'B' calls stored procedure 'A' which returns a resultset that needs to be inserted into a temp table within stored procedure 'B', in order to do further mutations. Because of nested inserts, I have used OPENROWSET (and tried OPENQUERY too).
Now, it seems to work great! However, next to returning a resultset, stored procedure 'A' also does INSERTS in a table. The weird thing is, when stored procedure 'A' is executed from within stored procedure 'B', stored procedure 'A' only returns the resultset, and does NO insert at all. It just seems to skip the entire INSERT INTO statement. I have tried putting dummy SELECT 'test' breakpoints before and after the INSERT, and they are executed fine! How is this possible?
This query looks like this (I changed data and columns up a bit):
DECLARE #SQL NVARCHAR(MAX)
SET #SQL = 'INSERT INTO #Temp (1,2,3)
SELECT * FROM OPENROWSET (
''SQLOLEDB'',
''Server=(local);TRUSTED_CONNECTION=yes;'',
''SET FMTONLY OFF EXECUTE StoredProcedureA
#Parameter1 = '''''+#InputValue1+'''''
,#Parameter_2 = '''''+#InputValue2+'''''
''
)'
EXEC(#SQL)
No errors are returned. The resultset (SELECT statement from procedure A) is correctly loaded into #Temp within procedure B. But the INSERT that is done within procedure A is not executed.
Does openquery/openrowset not allow INSERTS and only execute SELECT outputs? I thought, maybe its a security/rights issue? Is there any other way to workaround this issue?
Thanks in advance guys!
It is because you are using a temporary table denoted by #.
The scope of this table is ends when your nested stored procedure ends and the temporary table is dropped.
So the insert happens, the table just doesn't exist anymore.
If you create the table before starting your nested procedure you can solve this problem. You can just drop the table in you Procedure B if you really want it gone.
I have a SQL Server that houses Several Databases. I have a Main Database that holds several tables with entities and ID numbers. Then, each one of those entities has a correlating database (not a table, but database) with all of its information. For example, if the an entity in the MAIN database has an ID number of 1, there would be an SubDatabase1 Database on the same SQL Server.
Essentially, what I am trying to do is create a stored procedure in the MAIN Database, that collects data from the SUB Database, but the SUB database I collect from should be determined based on the ID number passed to the Proc.
I know this is totally incorrect, but I am wondering if someone can shine some light on this for me.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE GetInstallationCount
-- Add the parameters for the stored procedure here
#installId int=0
AS
BEGIN
SET NOCOUNT ON;
//Trying to make the DatabaseName dynamic here!!
select count(*) from dbo.Installation#installId.Names
END
GO
Thanks - J
Read up on how to create dynamic SQL, particularly sp_executesql. This should get you started:
DECLARE #theSql varchar(1000)
DECLARE #installId int
SET #installId = 1
SET #theSql = 'SELECT COUNT(*) FROM dbo.Installation' + CAST(#installId as nvarchar) + '.Names'
EXEC (#theSql)
You have to use dynamic SQL to do that. Table names and database names cannot be resolved at runtime in any other way.
Here is a good introduction to this technique by Scott Mitchell.
As often, the answer to such a question is dynamic SQL:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE GetInstallationCount
-- Add the parameters for the stored procedure here
#installId int=0
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql nvarchar(MAX)
SET #sql = 'select count(*) from dbo.Installation' + Cast(#installId as nvarchar) + '.Names'
EXECUTE dbo.sp_executesql #sql
END
GO
Definately could be done by building up the select string dynamically and executing but it would be nasty.
You could get very flashy and try create synonyms of the fly, use them in the queries and then drop them but I'm not sure it would be worth it.
Use synonyms. For example this sets synonym dbo.MySpecialTable to point to table dbo.SomeTable in database DB_3.
IF object_id(N'SN', N'dbo.MySpecialTable') IS NOT NULL
DROP SYNONYM dbo.MySpecialTable
CREATE SYNONYM dbo.MySpecialTable FOR [DB_3].[dbo].[SomeTable]
With this in place, write all your queries to use synonyms instead of real table names. Synonyms have DB scope, so manage "target switching" at one place, maybe in a stored procedure.