简体   繁体   中英

SQL Server: how to get a database name as a parameter in a stored procedure

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

EDIT

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.

Original Response

There are at least two ways to do this:

  1. 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;
  2. 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.

Read this article first.

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM