I'm trying to create a simple stored procedure which queries a sys.tables table.
CREATE PROCEDURE dbo.test
@dbname NVARCHAR(255),
@col NVARCHAR(255)
AS
SET NOCOUNT ON
SET XACT_ABORT ON
USE @dbname
SELECT TOP 100 *
FROM sys.tables
WHERE name = @col
GO
This does not seem to work cause I should put GO after USE @dbname but this terminates the creation of this procedure? How can I put this database selction into this procedure so that a user can give a database name as a parameter for this proc?
If you use EXEC @Var
(without brackets - ie not EXEC (@Var)
) SQL Server looks for a stored procedure matching the name passed in @Var
. You can use three part naming for this.
If sys.sp_executesql
is called with a three part name the context is set to the database in which it is called.
So you can do this with zero SQL injection risk as below.
CREATE PROCEDURE dbo.test @dbname SYSNAME,
@col SYSNAME
AS
SET NOCOUNT, XACT_ABORT ON;
DECLARE @db_sp_executesql NVARCHAR(300) = QUOTENAME(@dbname) + '.sys.sp_executesql'
EXEC @db_sp_executesql N'
SELECT TOP 100 *
FROM sys.columns
WHERE name = @col',
N'@col sysname',
@col = @col
Even if the above wasn't possible I'd still argue that it is perfectly possible to use dynamic SQL for this in a safe manner as here.
CREATE PROCEDURE dbo.test
@dbname SYSNAME, /*Use Correct Datatypes for identifiers*/
@col SYSNAME
AS
SET NOCOUNT ON
SET XACT_ABORT ON
IF DB_ID(@dbname) IS NULL /*Validate the database name exists*/
BEGIN
RAISERROR('Invalid Database Name passed',16,1)
RETURN
END
DECLARE @dynsql nvarchar(max)
/*Use QUOTENAME to correctly escape any special characters*/
SET @dynsql = N'USE '+ QUOTENAME(@dbname) + N'
SELECT TOP 100 *
FROM sys.tables
WHERE name = @col'
/*Use sp_executesql to leave the WHERE clause parameterised*/
EXEC sp_executesql @dynsql, N'@col sysname', @col = @col
My answer assumes some things which make this approach effectively useless. Unfortunately, SO will not let me delete the answer. I recommend @MartinSmith's answer (below in this thread). I think there's still some useful information here, BUT it doesn't actually solve the original problem. Godspeed.
There are at least two ways to do this:
Use a case/switch statement (or ,in my example, a naive if..else
block) to compare the parameter against a list of databases, and execute a using statement based on that. This has the advantage of limiting the databases that the proc can access to a known set, rather than allowing access anything and everything that the user account has rights to.
declare @dbname nvarchar(255); set @dbname = 'db1'; if @dbname = 'db1' use db1; else if @dbname = 'db2' use db2;
Dynamic SQL. I HATE dynamic SQL. It's a huge security hole and almost never necessary. (to put this in perspective: In 17 years of professional development, I have never had to deploy a production system which used dynamic SQL). If you decide to go this route, limit the code that is dynamically called/created to a using statement, and a call to another stored proc do do the actual work. You can't just dynamically execute the using
statement by itself due to scope rules.
declare @sql nvarchar(255); set @sql = 'using '+@dbname+'; exec mydatabase..do_work_proc;';
of course, in your example, you could just do
set @sql='select * from '+@dbname+'.sys.tables';
the .<schema_name>.
resolution operator allows you to query objects in a different database without using a use
statement.
There are some very, very rare circumstances in which it may be desirable to allow a sproc to use an arbitrary database. In my opinion, the only acceptable use is a code generator, or some sort of database analysis tool which cannot know the required information ahead of time.
Update Turns out you can't use
in a stored procedure, leaving dynamic SQL as the only obvious method. Still, I'd consider using
select top 100 * from db_name.dbo.table_name
rather than a use
.
The only way to do this is to use Dynamic SQL , which is powerful but dangerous.
A different way to the same end is to use a system stored procedure.
See SQL Stored Procedure(s) - Execution From Multiple Databases .
If the procedure name starts with "sp_", is in the master db and marked with sys.sp_MS_MarkSystemObject, then it can be invoked like this:
Exec somedb.dbo.Test;
Exec anotherdb.dbo.Test;
Or like this:
Declare @Proc_Name sysname;
Set @Proc_Name = 'somedb.dbo.Test';
Exec @Proc_Name;
Parameters can be used too.
Using this technique requires using the 'sp_' prefix and putting code into a system database. It is your choice if that offsets not using dynamic SQL.
or use Powershell to run a script and passs the dbname
# Script #1 - clear the existing CLR functions, sps, and assemblies
$scriptFile = "Script_Drop_Functions_and_Other_Programmability_Objects.sql";
# Standard PARAMETERS to the PS cmdlet Invoke-Sqlcmd - these are splatted here to standardize. This PARAMETERS includes the standard flags e.g. ServerInstance as well as the variables for each script
$sqlcmdParameters = @{
ServerInstance = $serverName #cmd flag
Database = $dbName #cmd flag
InputFile = $scriptFile #cmd flag, changes for each invocation
Variable = $sqlCmdVariables #cmd flag, changes for each invocation
Verbose = $true #cmd flag, set to true by default for additional output detail
};
<# PowerShell numbers the types of 'stream' messages according to their type from 1 to 6 - Use these numbers in order to control the returned output:
(1) success message, which is the output of the success command execution. This corresponds to SQL Server query results
(2) Error messages.
(3) Warning messages.
(4) Verbose messages. This corresponds to SQL Server PRINT statements
(5) Debug massages.
(6) Information massages.
Type 1 will be put in file $logFileResults
Type 4 will be put in file $logFileOutput
#>
(Invoke-Sqlcmd @sqlcmdParameters >> $logFileOutput) 4>> $logFileResults
The following is a clever HACK that does exactly what the OP is asking. We use something like this in-house that allows QA testers to pull data from any number of a list of client databases into a mock testing database where the stored procedure actually resides.
To demonstrate this example I executed the following on several databases accessible from my server just to create a table to test against
select * into MyTestTable from sys.all_columns
I then created the following stored procedure on my target database. I commented it I think well enough for someone to pretty much get what's going on. I won't argue the merits and/or dangers of this approach. It works. It doesn't require dumping all of your code into dynamic SQL strings, etc.
create procedure AnyDbStoredProcedure
@dbname varchar(200),
@count int
as
-- You can add additional parameters as needed above.
-- Modify the call to sp_executesql at the bottom of the
-- stored procedure as needed to include or exclude parameters.
set nocount on
-- MARK THE START OF THE STORED PROCEDURE CODE YOU
-- ACTUALLY WANT TO EXECUTE WITH A COMMENTED GUID.
-- (SEE BELOW)
declare @guid varchar(38) = '-- {5E105697-D144-4073-A1A6-330A264159DF}'
-- You can call your stored procedure what you like,
-- but its name must be copied to the parameter here.
declare @sp_name nvarchar(200) = 'AnyDbStoredProcedure'
-- EVERY DATABASE OBJECT YOUR SCRIPT REFERENCES MUST
-- INCLUDE THE FULLY QUALIFIED DATABASE NAME.
-- WHATEVER DATABASE YOU USED, PUT ITS NAME HERE:
declare @dbname_to_replace varchar(20) = '[FloridaDev]'
declare @spbody table (
line_num int not null identity(1,1),
[Text] varchar(max))
declare @sql nvarchar(max) = '', @params_sql nvarchar(max) = ''
declare @text varchar(max), @sp_body_started int = 0, @in_params bit = 0
insert @spbody ([Text])
exec sp_helptext @sp_name
declare csr cursor fast_forward for
select [Text] from @spbody
order by line_num
open csr
fetch next from csr into @text
while @@FETCH_STATUS = 0
begin
if LEFT(@text, 16) = 'create procedure'
begin
set @in_params = 1
end
else if @text = 'as' + char(13) + char(10) and @in_params = 1
begin
set @in_params = 0
end
else if @in_params = 1
begin
set @params_sql = @params_sql + @text
end
else if CHARINDEX(@guid, @text) > 0
begin
-- Ignoring the first instance found which is in
-- the variable declaration above.
set @sp_body_started = @sp_body_started + 1
end
else if @sp_body_started = 2
begin
set @text = REPLACE(@text, @dbname_to_replace, @dbname)
set @sql = @sql + @text
end
fetch next from csr into @text
end
close csr
deallocate csr
set nocount off
print '== PARAMS =='
print @params_sql
print '============'
print @sql
-- BE SURE TO PASS YOUR ADDITIONAL PARAMETERS IF YOU HAVE ANY.
exec sp_executesql @sql, @params_sql, 'dummy_for_@dbname', @count
return
-- AS MENTIONED AT THE TOP, MARK THE START OF THE STORED PROCEDURE
-- CODE YOU ACTUALLY WANT TO EXECUTE WITH A COMMENTED GUID.
-- {5E105697-D144-4073-A1A6-330A264159DF}
select object_name(object_id), name
from (
select *, row = row_number() over (order by object_id, column_id)
from [FloridaDev].dbo.MyTestTable
) as tmp
where tmp.row <= @count -- EXAMPLE USES EXTRA PARAMETER
go
exec AnyDbStoredProcedure 'Florida', 20
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.