可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I've got a stored procedure that allows an IN parameter specify what database to use. I then use a pre-decided table in that database for a query. The problem I'm having is concatenating the table name to that database name within my queries. If T-SQL had an evaluate function I could do something like
eval(@dbname + 'MyTable')
Currently I'm stuck creating a string and then using exec()
to run that string as a query. This is messy and I would rather not have to create a string. Is there a way I can evaluate a variable or string so I can do something like the following?
SELECT *
FROM eval(@dbname + 'MyTable')
I would like it to evaluate so it ends up appearing like this:
SELECT *
FROM myserver.mydatabase.dbo.MyTable
回答1:
Read this... The Curse and Blessings of Dynamic SQL, help me a lot understanding how to solve this type of problems.
回答2:
There's no "neater" way to do this. You'll save time if you accept it and look at something else.
EDIT: Aha! Regarding the OP's comment that "We have to load data into a new database each month or else it gets too large.". Surprising in retrospect that no one remarked on the faint smell of this problem.
SQL Server offers native mechanisms for dealing with tables that get "too large" (in particular, partitioning), which will allow you to address the table as a single entity, while dividing the table into separate files in the background, thus eliminating your current problem altogether.
To put it another way, this is a problem for your DB administrator, not the DB consumer. If that happens to be you as well, I suggest you look into partitioning this table.
回答3:
try the sp_executesql built in function.
You can basically build up your SQL string in your proc, then call
exec sp_executesql @SQLString.
DECLARE @SQLString nvarchar(max)
SELECT @SQLString = '
SELECT *
FROM ' + @TableName
EXEC sp_executesql @SQLString
回答4:
You can't specify a dynamic table name in SQL Server.
There are a few options:
- Use dynamic SQL
- Play around with synonyms (which means less dynamic SQL, but still some)
You've said you don't like 1, so lets go for 2.
First option is to restrict the messyness to one line:
begin transaction t1;
declare @statement nvarchar(100);
set @statement = 'create synonym temptablesyn for db1.dbo.test;'
exec sp_executesql @statement
select * from db_syn
drop synonym db_syn;
rollback transaction t1;
I'm not sure I like this, but it may be your best option. This way all of the SELECTs will be the same.
You can refactor this to your hearts content, but there are a number of disadvantages to this, including the synonym is created in a transaction, so you can't have two
of the queries running at the same
time (because both will be trying to
create temptablesyn). Depending
upon the locking strategy, one will
block the other.
Synonyms are permanent, so this is why you need to do this in a transaction.
回答5:
There are a few options, but they are messier than the way you are already doing. I suggest you either:
(1) Stick with the current approach
(2) Go ahead and embed the SQL in the code since you are doing it anyway.
(3) Be extra careful to validate your input to avoid SQL Injection.
Also, messiness isn't the only problem with dynamic SQL. Remember the following:
(1) Dynamic SQL thwarts the server's ability to create a reusable execution plan.
(2) The ExecuteSQL command breaks the ownership chain. That means the code will run in the context of the user who calls the stored procedure NOT the owner of the procedure. This might force you to open security on whatever table the statement is running against and create other security issues.
回答6:
Just a thought, but if you had a pre-defined list of these databases, then you could create a single view in the database that you connect to to join them - something like:
CREATE VIEW dbo.all_tables
AS
SELECT your_columns,
'db_name1' AS database_name
FROM db_name1.dbo.your_table
UNION ALL
SELECT your_columns,
'db_name2'
FROM db_name2.dbo.your_table
etc...
Then, you could pass your database name in to your stored procedure and simply use it as a paramater in a WHERE clause. If the tables are large, you might consider using an indexed view, indexed on the new database_name column (or whatever you call it) and the tables' primary key (I'm assuming from the question that the tables' schemas are the same?).
Obviously if your list of database changes frequently, then this becomes more problematic - but if you're having to create these databases anyway, then maintaining this view at the same time shouldn't be too much of an overhead!
回答7:
I think Mark Brittingham has the right idea (here:
http://stackoverflow.com/questions/688425/evaluate-in-t-sql/718223#718223), which is to issue a use database
command and write the sp to NOT fully qualify the table name. As he notes, this will act on tables in the login's current database.
Let me add a few possible elaborations:
From a comment by the OP, I gather the database is changed once a month, when it gets "too large". ("We have to load data into a new database each month or else it gets too large. – d03boy")
User logins have a default database, set with sp_defaultdb (deprecated) or ALTER LOGIN. If each month you move on to the new database, and don't need to run the sp on the older copies, just change the login's default db monthly, and again, don't fully qualify the table name.
The database to use can be set in the client login:
sqlcmd -U login_id -P password -d db_name
, then exec the sp from there.
You can establish a connection to the database using the client of your choice (command line, ODBC, JDBC), then issue a use database
command, the exec the sp.
use database bar;
exec sp_foo;
Once the database has been set using one of the above, you have three choices for executing the stored procedure:
You could just copy the sp along with the database, in to the new database. As long as the table name is NOT fully qualified, you'll operate on the new database's table.
exec sp_foo;
You could install the single canonical copy of the sp in its own database, call it procs
, with the tablename not fully qualified, and then call its fuly qualified name:
exec procs.dbo.sp_foo;
You could, in each individual database, install a stub sp_foo
that execs the fully qualified name of the real sp, and then exec sp_foo
without qualifying it. The stub will be called, and it will call the real procedure in procs
. (Unfortunately, use database dbname
cannot be executed from within an sp.)
--sp_foo stub:
create proc bar.dbo.sp_foo
@parm int
as
begin
exec procs.dbo.sp_foo @parm;
end
go
However this is done, if the database is being changed, the real sp should be created with the WITH RECOMPILE
option, otherwise it'll cache an execution plan for the wrong table. The stub of course doesn't need this.
回答8:
You could create a SQL CLR Table-Valued UDF to access the tables. You have to tie it to the schema because TV-UDFs don't support dynamic schema. (My sample includes an ID and a Title column - modify for your needs)
Once you've done this, you should be able to do the follow query:
SELECT * FROM dbo.FromMyTable('table1')
You can include a multipart name in that string too.
SELECT * FROM dbo.FromMyTable('otherdb..table1')
to return the ID,Title columns from that table.
You will likely need to enable SQL CLR and turn on the TRUSTWORTHY option:
sp_configure 'clr enabled',1
go
reconfigure
go
alter database mydatabase set trustworthy on
Create a C# SQL Project, add a new UDF file, paste this in there. Set Project Property, Database, Permission Level to external. Build, deploy. Can be done without VisualStudio. Let me know if you need that.
using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections;
using System.Data.SqlClient;
[assembly: CLSCompliant(true)]
namespace FromMyTable
{
public static partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read, IsDeterministic = true, SystemDataAccess = SystemDataAccessKind.Read, IsPrecise = true, FillRowMethodName = "FillRow",
TableDefinition = "id int, title nvarchar(1024)")]
public static IEnumerable FromMyTable(SqlString tableName)
{
return new FromMyTable(tableName.Value);
}
public static void FillRow(object row, out SqlInt32 id, out SqlString title)
{
MyTableSchema v = (MyTableSchema)row;
id = new SqlInt32(v.id);
title = new SqlString(v.title);
}
}
public class MyTableSchema
{
public int id;
public string title;
public MyTableSchema(int id, string title) { this.id = id; this.title = title; }
}
internal class FromMyTable : IEnumerable
{
string tableName;
public FromMyTable(string tableName)
{
this.tableName = tableName;
}
public IEnumerator GetEnumerator()
{
return new FromMyTableEnum(tableName);
}
}
internal class FromMyTableEnum : IEnumerator
{
SqlConnection cn;
SqlCommand cmd;
SqlDataReader rdr;
string tableName;
public FromMyTableEnum(string tableName)
{
this.tableName = tableName;
Reset();
}
public MyTableSchema Current
{
get { return new MyTableSchema((int)rdr["id"], (string)rdr["title"]); }
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
bool b = rdr.Read();
if (!b) { rdr.Dispose(); cmd.Dispose(); cn.Dispose(); rdr = null; cmd = null; cn = null; }
return b;
}
public void Reset()
{
// note: cannot use a context connection here because it will be closed
// in between calls to the enumerator.
if (cn == null) { cn = new SqlConnection("server=localhost;database=mydatabase;Integrated Security=true;"); cn.Open(); }
if (cmd == null) cmd = new SqlCommand("select id, title FROM " + tableName, cn);
if (rdr != null) rdr.Dispose();
rdr = cmd.ExecuteReader();
}
}
}
回答9:
declare @sql varchar(256);
set @sql = 'select * into ##myGlobalTemporaryTable from '+@dbname
exec sp_executesql @sql
select * from ##myGlobalTemporaryTable
copies into a global temporary table which you can then use like a regular table
回答10:
If you have a reasonably manageable number of databases, it may be best to use a pre-defined conditional statement like:
if (@dbname = 'db1')
select * from db1..MyTable
if (@dbname = 'db2')
select * from db2..MyTable
if (@dbname = 'db3')
select * from db3..MyTable
...
you can generate this proc as part of your database creation scripts if you are changing the list of databases available to query.
This avoids security concerns with dynamic sql. You can also improve performance by replacing the 'select' statements with stored procedures targeting each database (1 cached execution plan per query).
回答11:
if exists (select * from master..sysservers where srvname = 'fromdb')
exec sp_dropserver 'fromdb'
go
declare @mydb nvarchar(99);
set @mydb='mydatabase'; -- variable to select database
exec sp_addlinkedserver @server = N'fromdb',
@srvproduct = N'',
@provider = N'SQLOLEDB',
@datasrc = @@servername,
@catalog = @mydb
go
select * from OPENQUERY(fromdb, 'select * from table1')
go