在T-SQL中进行评估

9

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

eval(@dbname + 'MyTable')

目前我在创建一个字符串,然后使用exec()将该字符串作为查询运行。这很麻烦,我不想再创建一个字符串了。有没有一种方法可以评估变量或字符串,以便我可以执行类似以下的操作?

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

我希望它能够评估,以便最终呈现如下所示:
SELECT *
FROM myserver.mydatabase.dbo.MyTable

d03boy - 我删除了我的建议,因为我忘记了“Use”不能在存储过程中使用。我应该知道得更好,因为几个月前我就遇到过这个问题。如果我让你走上了错误的道路,很抱歉。 - Mark Brittingham
我不知道它不能使用,所以至少我学到了这么多 :) - Joe Phillips
d03boy - 我删除了我的第三个建议,即使用OPENROWSET,因为您无法在连接字符串或查询中使用变量。很遗憾。 - Hafthor
11个回答

16

9

没有更好的方法来解决这个问题。如果你接受这一点并看看其他东西,你会节省时间。

编辑:啊哈!关于OP的评论,“我们每个月都必须将数据加载到一个新的数据库中,否则它会变得太大。” 回想起来,这个问题有点微妙。

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

换句话说,这是DB管理员的问题,而不是DB使用者的问题。 如果你也是DB管理员,我建议你研究对这个表进行分区


阿门。听起来他的数据库架构是这里所有问题的根源。 - Paul Suart
为什么呢?我们每个月都必须将数据加载到一个新的数据库中,否则它会变得太大。 - Joe Phillips
这实际上不是我们的软件。我们只是在软件中每个月创建新的“项目”。我们可以对数据库进行一些工作,但由于我们对系统的了解不足,所以受到限制。 - Joe Phillips
此外,这涉及到许多项目,无论如何都将在不同的数据库中。因此,代码仍然必须是动态的。 - Joe Phillips
在这种情况下,我坚持我的第一个答案。 - harpo

6

尝试使用内置的sp_executesql函数。 你可以在存储过程中构建SQL字符串,然后调用该函数。

exec sp_executesql @SQLString.

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

EXEC sp_executesql @SQLString

这是他目前正在做的事情,但并不喜欢。 - Learning
1
那就是我想要避免的 :) - Joe Phillips

2

在SQL Server中,您无法指定动态表名。

有几个选项:

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

您已经说您不喜欢第一个选项,那么我们来试试第二个。

第一个选项是将混乱限制在一行内:

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 语句都将是相同的。 你可以随心所欲地重构它,但是这样做有许多缺点,包括在事务中创建了同义词,因此您不能同时运行两个查询(因为两个查询都会尝试创建临时表同义词)。根据锁定策略,一个查询将阻塞另一个查询。 同义词是永久性的,所以你需要在事务中进行操作。

1

有几个选项,但它们比你已经在做的方式更混乱。我建议你要么:
(1) 坚持当前的方法
(2) 既然你已经这样做了,就继续将 SQL 嵌入代码中。
(3) 要格外小心地验证输入,以避免 SQL 注入。

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


1

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

让我补充一些可能的细节:

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

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

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

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

    use database bar; exec sp_foo;

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

  1. 您可以将存储过程和数据库一起复制到新的数据库中。只要表名没有完全限定,您就可以在新数据库的表上操作。

    执行 sp_foo;

  2. 您可以在自己的数据库中安装单个规范化的存储过程副本,称之为 procs,并且表名没有完全限定,然后调用其完全限定名称:

    执行 procs.dbo.sp_foo;

  3. 您可以在每个单独的数据库中安装一个存根 sp_foo,该存根执行真实存储过程的完全限定名称,然后执行未限定的 sp_foo。存根将被调用,并且它将在 procs 中调用真实过程。(不幸的是,use database dbname 无法在存储过程内部执行。)

    --sp_foo 存根:
    create proc bar.dbo.sp_foo 
     @parm int
    as
    begin
      exec procs.dbo.sp_foo @parm;
    end
    go
无论如何,如果正在更改数据库,则应使用 WITH RECOMPILE 选项创建真实的存储过程,否则它将缓存错误表的执行计划。当然,存根不需要这个选项。

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列(或任何其他名称)和表的主键进行索引(我假设问题中的表模式相同?)。

显然,如果您的数据库列表经常更改,则这变得更加棘手 - 但是如果您必须创建这些数据库,那么同时维护此视图不应该产生太多开销!


这些数据库经常被添加,很难跟踪和维护一个合适的列表。 - Joe Phillips
然后编写一个数据库维护脚本,通过遍历服务器上的所有数据库并检查它们是否是适当的类型来创建视图(每晚执行?)。您还可以自动在选择中插入一个文本列,该列是数据库的名称(以便您可以传递它)。 - Stephen

1
你可以创建一个 SQL CLR 表值函数来访问表格。你必须将其绑定到模式,因为 TV-UDF 不支持动态模式。(我的示例包括 ID 和 Title 列 - 根据你的需求进行修改)
完成后,你应该能够执行以下查询:
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();
        }
    }
}

0
declare @sql varchar(256);
set @sql = 'select * into ##myGlobalTemporaryTable from '+@dbname
exec sp_executesql @sql

select * from ##myGlobalTemporaryTable

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


在我的情况下,这很可能会使用太多的资源。表格非常大。 - Joe Phillips

0

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

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

...

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接