Dynamic Datasource in SQL Server Stored Procudure - sql

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.

Related

Select results from stored procedure into a table

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.

How to union records of two stored procedures without knowing columns

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..

Select from table with current user's schema

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.

Creating a User Defined function in Stored Procedure in SQL 2005

I have a stored procedure in which i want to create a user defined function - Split (splits a string separated with delimiters and returns the strings in a table), make use of the function and finally drop the function.
My question is that whether i can create a user defined function inside a stored procedure and drop it finally?
Thank you.
Regards
NLV
Technically...yes you could but that does not mean you should. You would have to be careful about avoiding GO statements (just use Exec for each batch) but you could do something like:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE dbo.Test
AS
Declare #Sql nvarchar(max)
Set #Sql = 'CREATE FUNCTION dbo.Foo
(
)
RETURNS TABLE
AS
RETURN
(
SELECT 0 As Bar
)'
Exec(#Sql)
Select *
From dbo.Foo()
Set #Sql = 'Drop Function dbo.Foo'
Exec(#Sql)
Return
GO
Exec dbo.Test
That said, I would strongly recommend against this sort of solution, especially if the function you want is something that would be useful like a Split function. I would recommend just creating the UDF and using it and leaving it until you might use it again.
CTEs would be good here too, depending on what your UDF is trying to do.

How can I get dbX.dbo.procA to run in the context of dbY?

I want to separate a specific set of stored procedures into a seperate database … sort of like a library. However, when I run these procs from another database, they appear to call objects from within the library database.
Here is some source code which demonstrates the problem
use mylibdb
go
create proc gettablecount
as
begin
declare #cnt int;
select #cnt=count(*)
from sysobjects
where xtype='U';
print 'Table count : ' + cast( #cnt as nvarchar);
end
go
use adventureworks
go
exec mylibdb.dbo.gettablecount;
print '';
select count(*) as [table count]
from sysobjects
where xtype='U';
Running that code will print out
Table count : 0
table count
71
(1 row(s) affected)
Notice the proc queries the mylibdb.dbo.sysobjects table, not adventureworks.dbo.sysobjects.
Does anybody know how I can do this without dynamic sql?
You could treat the library as code instead of database objects and create the procedures in the database schemas that use the "library". I would do this by storing your procedures and other shared db objects as CREATE scripts that you can modify and version.
dynamic sql is your safest bet, your other option is to create the proc in the model database and it will be available in every database you create after that (but not in the current ones so you will have to add the proc to those)
I've determined this cannot be done, so I saved the scripts as script files, rather than in procs.
So, my proc would be reduced to this anonymous block:
begin
declare #cnt int;
select #cnt=count(*)
from sysobjects
where xtype='U';
print 'Table count : ' + cast( #cnt as nvarchar);
end
PS-This is just a 'For The Record' type answer.
An alternative is to create sp_ procedures in the master database:
http://www.mssqltips.com/tip.asp?tip=1612