9

我有一个存储过程,它允许 IN 参数指定要使用的数据库。然后,我在该数据库中使用预先确定的表进行查询。我遇到的问题是在我的查询中将表名连接到该数据库名。如果 T-SQL 有一个评估函数,我可以做类似的事情

eval(@dbname + 'MyTable')

目前我一直在创建一个字符串,然后使用exec()该字符串作为查询运行。这很混乱,我宁愿不必创建一个字符串。有没有办法可以评估变量或字符串,以便我可以执行以下操作?

SELECT *
FROM eval(@dbname + 'MyTable')

我希望它进行评估,所以它最终看起来像这样:

SELECT *
FROM myserver.mydatabase.dbo.MyTable
4

11 回答 11

16

阅读这篇...动态 SQL 的诅咒和祝福,帮助我理解如何解决这类问题。

于 2009-04-01T20:59:38.480 回答
9

没有“更整洁”的方法可以做到这一点。如果你接受它并看看别的东西,你会节省时间。

编辑: 啊哈!关于 OP 的评论“我们必须每个月将数据加载到一个新的数据库中,否则它会变得太大。”。令人惊讶的是,没有人评论这个问题的微弱气味。

SQL Server 提供了处理“太大”的表(特别是分区)的本机机制,这将允许您将表作为单个实体来处理,同时在后台将表划分为单独的文件,从而消除您当前的完全有问题。

换句话说,这是您的数据库管理员的问题,而不是数据库使用者的问题。如果您也遇到这种情况,我建议您考虑对该表进行分区。

于 2009-03-27T03:40:26.507 回答
6

尝试 sp_executesql 内置函数。你基本上可以在你的 proc 中建立你的 SQL 字符串,然后调用

exec sp_executesql @SQLString.

DECLARE @SQLString nvarchar(max)
SELECT @SQLString = '
SELECT *
FROM  ' +  @TableName 

EXEC sp_executesql @SQLString
于 2009-03-27T03:29:59.683 回答
2

您不能在 SQL Server 中指定动态表名。

有几个选项:

  1. 使用动态 SQL
  2. 玩弄同义词(这意味着更少的动态 SQL,但仍然有一些)

你说你不喜欢 1,所以让我们去 2。

第一种选择是将混乱限制在一行:

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;

我不确定我是否喜欢这个,但这可能是你最好的选择。这样,所有的 SELECT 都将是相同的。

您可以根据自己的喜好对其进行重构,但这有很多缺点,包括同义词是在事务中创建的,因此您不能同时运行两个查询(因为两者都将尝试创建 temptablesyn)。根据锁定策略,一个会阻止另一个。

同义词是永久的,所以这就是您需要在事务中执行此操作的原因。

于 2009-04-01T11:16:40.393 回答
1

有一些选择,但它们比您已经在做的方式更混乱。我建议您:
(1)坚持当前的方法
(2)继续将SQL嵌入代码中,因为无论如何您都在这样做。
(3) 要格外小心地验证您的输入以避免 SQL 注入。

此外,混乱并不是动态 SQL 的唯一问题。请记住以下几点:
(1) 动态 SQL 阻碍了服务器创建可重用执行计划的能力。
(2) ExecuteSQL 命令打破了所有权链。这意味着代码将在调用存储过程的用户而不是过程的所有者的上下文中运行。这可能会迫使您在运行该语句的任何表上打开安全性并产生其他安全问题。

于 2009-04-01T21:17:07.580 回答
1

只是一个想法,但是如果您有这些数据库的预定义列表,那么您可以在连接到它们的数据库中创建一个视图 - 例如:

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

然后,您可以将数据库名称传递给您的存储过程,并简单地将其用作 WHERE 子句中的参数。如果表很大,您可能会考虑使用索引视图,在新的 database_name 列(或您所称的任何名称)和表的主键(我从问题中假设表的架构相同? )。

显然,如果您的数据库列表经常更改,那么这将变得更加成问题 - 但如果您无论如何都必须创建这些数据库,那么同时维护此视图不应该是太大的开销!

于 2009-04-04T07:20:18.667 回答
1

我认为 Mark Brittingham 的想法是正确的(这里:h ttp://stackoverflow.com/questions/688425/evaluate-in-t-sql/718223#718223),即发出use database命令并将 sp 写入不完全限定表名。正如他所指出的,这将作用于登录当前数据库中的表。

让我添加一些可能的详细说明:

从 OP 的评论中,我收集到数据库每月更改一次,当它变得“太大”时。(“我们必须每个月将数据加载到一个新数据库中,否则它会变得太大。- d03boy”)

  1. 用户登录有一个默认数据库,使用 sp_defaultdb(不推荐)或 ALTER LOGIN 设置。如果每个月您都转移到新数据库,并且不需要在旧副本上运行 sp,只需每月更改登录的默认数据库,同样,不要完全限定表名。

  2. 要使用的数据库可以在客户端 login: 中设置 sqlcmd -U login_id -P password -d db_name,然后从那里执行 sp。

  3. 您可以使用您选择的客户端(命令行、ODBC、JDBC)建立与数据库的连接,然后发出use database命令,exec sp。

    使用数据库栏;执行 sp_foo;

使用上述方法之一设置数据库后,您有三种选择来执行存储过程:

  1. 您可以将 sp 与数据库一起复制到新数据库中。只要表名不是完全限定的,您就会对新数据库的表进行操作。

    执行 sp_foo;

  2. 您可以在其自己的数据库中安装 sp 的单个规范副本,调用它procs,表名不是完全限定的,然后调用它的完全限定名:

    执行 procs.dbo.sp_foo;

  3. 您可以在每个单独的数据库中安装一个sp_foo执行实际 sp 的完全限定名称的存根,然后sp_foo在不限定它的情况下执行它。将调用存根,它会调用procs. (不幸的是,use database dbname不能从 sp 中执行。)

    --sp_foo 存根:
    创建过程 bar.dbo.sp_foo
     @parm 整数
    作为
    开始
      执行 procs.dbo.sp_foo @parm;
    结尾
    去

但是这样做了,如果数据库正在被更改,则应该使用该WITH RECOMPILE选项创建真正的 sp,否则它将为错误的表缓存执行计划。存根当然不需要这个。

于 2009-04-05T13:26:52.850 回答
1

您可以创建一个 SQL CLR 表值 UDF 来访问这些表。您必须将其绑定到模式,因为 TV-UDF 不支持动态模式。(我的示例包括一个 ID 和一个标题列 - 根据您的需要进行修改)

完成此操作后,您应该能够执行以下查询:

SELECT * FROM dbo.FromMyTable('table1')

您也可以在该字符串中包含一个多部分名称。

SELECT * FROM dbo.FromMyTable('otherdb..table1')

从该表中返回 ID、Title 列。

您可能需要启用 SQL CLR 并打开 TRUSTWORTHY 选项:

sp_configure 'clr enabled',1
go
reconfigure
go
alter database mydatabase set trustworthy on

创建一个 C# SQL 项目,添加一个新的 UDF 文件,将其粘贴到那里。将项目属性、数据库、权限级别设置为外部。构建、部署。可以在没有 VisualStudio 的情况下完成。如果您需要,请告诉我。

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();
        }
    }
}
于 2009-04-06T18:48:07.920 回答
0
declare @sql varchar(256);
set @sql = 'select * into ##myGlobalTemporaryTable from '+@dbname
exec sp_executesql @sql

select * from ##myGlobalTemporaryTable

复制到全局临时表中,然后您可以像常规表一样使用它

于 2009-04-05T02:12:30.757 回答
0

如果您有合理可管理的数据库数量,最好使用预定义的条件语句,例如:

if (@dbname = 'db1')
  select * from db1..MyTable
if (@dbname = 'db2')
  select * from db2..MyTable
if (@dbname = 'db3')
  select * from db3..MyTable

...

如果您要更改可查询的数据库列表,您可以生成此过程作为数据库创建脚本的一部分。

这避免了动态 sql 的安全问题。您还可以通过将“select”语句替换为针对每个数据库的存储过程来提高性能(每个查询一个缓存的执行计划)。

于 2009-04-05T05:30:06.117 回答
0
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 
于 2009-04-08T21:42:41.230 回答