so I have a sproc in a db.. lets call this db A. This db makes use of tables (t1, t2) in another db. Lets call this db B.
okay, so the way i call it right now is: A.dbo.My_Proc but i get another error:
Invalid object name 'dbo.t1'.
so how i tried supplying a parameter. In my Sproc i do, select * from #dbname.dbo.t1
however that results in an error. I can't put the sproc in db B.
While it is sufficient to hardcode it (if there is a way), db B changes every year, so it would be nice to "supply" a database.
I tried using use B; go but it gives me error saying can't have that in a sproc.
You could create a synonym:
EDIT: I see now that the synonym is needed for the table, not the proc. Got them switched. So you could create a synonym in database A for the table in database B:
USE A;
CREATE SYNONYM dbo.t1 FOR B.dbo.t1;
Then your procedure in A could simply say:
SELECT * FROM dbo.t1;
Without having to manually supply the database name at all, the query knows (based on the synonym) to get the data from the table in database B. When the database B changes to C, you can simply:
DROP SYNONYM dbo.t1;
CREATE SYNONYM dbo.t1 FOR C.dbo.t1;
If you used "real" database names in your narrative as opposed to arbitrary A/B names, it might lead to easier comprehension. Just a suggestion. :-)
/EDIT
The other option is to pass in the database name and construct via dynamic SQL. E.g. instead of select * from #dbname.dbo.t1 (which will never work), you could do:
DECLARE #sql NVARCHAR(MAX) = N'SELECT * FROM ' + QUOTENAME(#dbname) + '.dbo.t1;';
EXEC sp_executesql #sql;
But if this other database name really only changes once a year, I suggest that the synonym route is better overall.
When you execute exec someDB.dbo.SomeProc the execution context switches to someDB. So if the procedure issues a SELECT FROM dbo.t1 then dbo.t1 must be in someDB. IF you want the procedure to select from a 'supplied' database then the procedure must use dynamic-SQL:
create procedure someProc
#dbname sysname
as
begin
...
set #sql = N'SELECT ... FROM ' + quotename(#dbname) +N'.dbo.t1';
exec sp_executesql #sql;
...
end
Related
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.
I have a stored procedure that can get the number of records in a table, in which the #tableName is the parameter of the stored procedure. Let's call it FastCount:
SELECT OBJECT_NAME(object_id), SUM(row_count) AS rows
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID(#tableName)
AND index_id < 2
GROUP BY OBJECT_NAME(object_id);
Now, let's say I have 50 tables, like data_1950, data_1951, .....data_2000. I wrote a batch, query each table's records count, and put them into a temporary table. It works like a charm
CREATE TABLE #Temp
(
TableName varchar(30),
RecordsCount int
)
DECLARE #sql as varchar(max)
DECLARE #yearN as int = 1950
DECLARE #tbName as sysname
WHILE #yearN <= 2000
BEGIN
SET #tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,#yearN))
SET #sql = N'Exec [dbo].FastCount #tableName=' + #tbName
INSERT INTO #Temp
EXEC (#sql)
SET #yearN = #yearN + 1
END
SELECT * FROM #Temp
DROP TABLE #Temp
However, if I replace the dynamic SQL string part
SET #sql = N'Exec [dbo].FastCount #tableName=' + #tbName
INSERT INTO #Temp
EXEC (#sql)
with a straightforward call
INSERT INTO #Temp
EXEC [dbo].FastCount #tableName = #tbName
Then the whole batch just not work...
So I don't understand why... Should I always use dynamic SQL string and exec(#sql) when programmatically using the stored procedure. A big thanks for taking the time to look.
OK, here is what is happening in the two scenarios that you posed in your original question. (Yes, the reality is that there are probably better ways to achieve your end result, but let's look at the actual problem that you posed .... why is the behaviour of your INSERT / EXEC different, depending on how you made the call).
First, you have your variable declared, that will contain your table name:
DECLARE #tbName as sysname
Then you have your looping block that incrementally increases the year number, to generate the different table names. There's nothing inherently wrong with the looping block, so let's just look at an example using one of the table names, to see what's happening within the WHILE block. Take the first table name as the example, which would be [dbo].data_1950.
Your statement:
set #tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,#yearN))
ultimately takes the string "[dbo].data_1950" - which comes from concatenating '[dbo].data_' with the year number (in this case, 1950) converted to a string (varchar) - and passes it to the QUOTENAME() function. The QUOTENAME() function takes its input and a second parameter, which is the character that the input should be quoted with (if the 2nd parameter is not passed, then the default is []). Thus, if we then converted the #tbName variable to a string, it would appear like this:
[[dbo].data_1950]
Now we get to see the funky way that SQL deals with "sysname" data-types. (In fact, as you read further down, maybe the issue is not primarily tied to the "sysname" data-type, but anyhow, take away from this what you will). To be honest, "sysname" is, in itself, a little bit of a funky data-type anyway, which I tend to steer away from, unless absolutely necessary. But anyhow, on to the details of the issue that you were seeing.
Step 1 - I created a version of your stored proc, but I included a statement that would output the value of the #tableName parameter that was passed in. This gives us an opportunity to see what SQL is doing in the two different scenarios, and then explain why the results are different.
CREATE PROC [dbo].FastCount
(
#tableName varchar(100)
)
AS
BEGIN
PRINT #tableName;
SELECT OBJECT_NAME(object_id), SUM(row_count) AS rows
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID(#tableName)
AND index_id < 2
GROUP BY OBJECT_NAME(object_id);
END
Step 2 - our first scenario is executing the dynamic SQL.
set #tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,#yearN))
set #sql = N'Exec [dbo].FastCount #tableName=' + #tbName
Insert Into #Temp Exec(#sql)
Now, we know that the #tbName variable contains
[[dbo].data_1950]
and therefore we can then infer that the #sql variable contains
Exec [dbo].FastCount #tableName=[[dbo].data_1950]
so that is effectively the statement that is executed by the Exec(#sql) command.
When this runs, and we look at the output of the PRINT command, we see
[dbo].data_1950
and we see a result from our query (the table name and row count). This makes sense, of course, because our table name is "data_1950", and the schema of the table is "dbo", so the SELECT statement to get the row count is going to work as expected.
Step 3 - run the EXEC command directly, without the use of the #sql variable, ie.
Insert Into #Temp Exec [dbo].FastCount #tableName = #tbName
Now, when we look at the output of the PRINT command for this execution of the "FastCount" stored procedure, we see
[[dbo].data_1950]
Of course, this is now NOT going to produce the results that we expect, because we're telling SQL to find the row count for a table named "[dbo].data_1950" (in the absence of the specific schema, SQL will just assume the default schema. In this case, with a schema of [dbo], we'd be telling SQL to get the row count from a table named [dbo].[[dbo].data_1950] - which is clearly NOT the table name).
You should see the obvious difference - in one scenario, the parameter value that is passed into the stored is the "correct" reference to the table name, and in the other scenario it is not.
As a final step, let's look at how the "non-dynamic" SQL would be executed, to achieve the results that we need. In this instance, there's no need for the QUOTENAME() function:
set #tbName = N'[dbo].data_' + Convert(nvarchar,#yearN)
Insert Into #Temp Exec [dbo].FastCount #tableName = #tbName
When we run it in this way, we see the expected output ([dbo].data_1950) from the PRINT command, and we see the expected query results (containing the table name and row count).
Can I explain this behaviour, exactly? Errr, not necessarily ... maybe someone else will be able to explain specifically what is happening, and why. My only interpretation is that when the EXEC() statement is passed the dynamic sql (ie. #sql variable) it is first interpreting the entire string and stripping out identifiers (in the case, the surrounding [] ... on what basis is it making that decision, I don't know). As opposed to the non-dynamic execution, where the #tbName value ([[dbo].data_1950]) is just being passed straight in as the parameter, with no modification (and thus causing the unexpected end result that we saw).
Hopefully this information is useful to you (or, at least, to someone else in the future!).
In general you should avoid dynamic SQL, and you should avoid granting rights to execute dynamic SQL, unless absolutely necessary. This is for performance and security reasons.
The typical way to deal with this situation is to use a partitioned view:
CREATE VIEW DataView
AS
SELECT '1950' TableName, * FROM Data_1950
UNION ALL
SELECT '1951' TableName, * FROM Data_1951
UNION ALL
SELECT '1952' TableName, * FROM Data_1952
UNION ALL
SELECT '1953' TableName, * FROM Data_1953
UNION ALL
SELECT '1954' TableName, * FROM Data_1954
UNION ALL
SELECT '1955' TableName, * FROM Data_1955
(Keep adding select statements until you have covered all of your tables.)
Now to get your table counts all you need to do is execute this:
SELECT TableName, COUNT(*) RecordCount
FROM DataView
GROUP BY TableName
Isn't that much easier?
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 am working in a database where each user has a table in their schema called FindResults
eg: MyDatabase.User1.FindResults, MyDatabase.User2.FindResults, etc.
If I run a SELECT query on this table while logged in as one of the users it works just fine. However, I have a stored procedure (MyDatabase.dbo.ReadFindResults) that tries to run a SELECT query on this table, it fails because it tries to read MyDatabase.dbo.FindResults (does not exist). I have gotten around this by using dynamic SQL, but I was hoping there was a way to avoid this.
Is there a way to tell the stored procedure to use the current user's schema or perhaps something to change the scope to allow it to find the table I want?
EDIT: Here is the code for the stored procedure
-- Returns the IDs contained in the given find results set
CREATE PROCEDURE [dbo].[ReadFindResults]
#resultsid int -- The ID of the results set
AS
BEGIN
SELECT objectid FROM FindResults WHERE resultsid = #resultsid
END
GO
In your stored procedure you can say:
SELECT cols FROM MyDatabase..FindResults;
(Leaving out the schema name.)
However this seems very error-prone, and IMHO you should either have separate, schema-bound stored procedures as well, or a single table with a column to indicate whose row it is.
A hack could be to execute as the username:
DECLARE #sql NVARCHAR(255) = N'EXECUTE AS USER = N''' + SUSER_SNAME() + '''';
EXEC sp_executesql #sql;
SELECT whatever FROM Findresults;
But I haven't had the opportunity to test this (you're better equipped to test in your scenario anyway).
It still seems like a remarkably avoidable problem in the first place, but maybe that's just me.
You could use dynamic SQL like this:
DECLARE #sql NVARCHAR(1024) = "SELECT something FROM [userName].someTable"
DECLARE #userName NVARCHAR(255) = SUSER_SNAME()
SET #sql = REPLACE(#sql, "[userName]", #userName)
EXEC sp_executesql #sql
That too is a hakish way of doing that query. More reading about dynamic SQL here: http://www.sommarskog.se/dynamic_sql.html
Im at home right now and do not have access to a SQL Server. I will edit my post in the morning when Im at work if needed.
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.