动态 SQL 而不必在 SQL 中使用完全限定的 table 名称(Openrowset?)
Dynamic SQL without having to use fully qualified table names in SQL (Openrowset?)
我有大量预先存在的 sql select 语句。
从 [Server_A] 上的存储过程中,我想在多个不同的 SQL 服务器和数据库上执行这些语句中的每一个(列表存储在本地 table 在 [Server_A] 上,return 结果在 [Server_A] 上 table。
但是,我不想在 sql 语句中使用完全限定的 table 名称。我想执行 "select * from users",而不是 "select * from ServerName.DatabaseName.SchemaName.Users"
我已经使用 Openrowset 进行了调查,但我无法找到任何可以将服务器名称和数据库名称指定为连接属性而不是物理指定的示例嵌入在实际的 SQL 语句中。
Openrowset 有这个能力吗?是否有替代方法(从存储过程中,而不是诉诸 Powershell 或其他一些非常不同的方法?)
必然table"Why do I want to do this?"
- 可以的(在连接中指定服务器和数据库
属性,然后在所有数据库中使用完全通用的 sql)
几乎所有访问 SQL 服务器的其他语言。
- 将我所有现有的复合体 SQL 更改为完全合格的是
巨大的 PITA(此外,你根本不应该这样做)
如果你想在一个实例中的每个数据库上执行一个 sql 语句,你可以使用(不受支持,非官方,但广泛使用)exec sp_MSforeachdb
像这样:
EXEC sp_Msforeachdb 'use [?]; select * from users'
这相当于通过
遍历每个数据库
use db...
go
select * from users
这是一个有趣的问题,因为我在谷歌上搜索了很多很多小时,发现有几个人试图做与问题中所问的完全相同的事情。
最常见的回复:
- 你为什么要这样做?
- 你不能那样做,你必须完全限定你的对象名称
幸运的是,我偶然发现了答案,而且非常简单。我认为部分问题在于,不同的提供程序和连接字符串有太多的变体,并且有太多可能出错的地方,而当一个人出错时,错误消息通常不是很有启发性。
无论如何,这是您的操作方式:
如果您使用的是静态 SQL:
select * from OPENROWSET('SQLNCLI','Server=ServerName[\InstanceName];Database=AdventureWorks2012;Trusted_Connection=yes','select top 10 * from HumanResources.Department')
如果您使用动态 SQL - 因为 OPENROWSET 不接受变量作为参数,您可以使用这样的方法(只是作为一个人为的例子):
declare @sql nvarchar(4000) = N'select * from OPENROWSET(''SQLNCLI'',''Server=Server=ServerName[\InstanceName];Database=AdventureWorks2012;Trusted_Connection=yes'',''@zzz'')'
set @sql = replace(@sql,'@zzz','select top 10 * from HumanResources.Department')
EXEC sp_executesql @sql
值得注意:如果您认为将此语法包装在一个漂亮的 Table 接受@ServerName、@DatabaseName、@SQL 的有价值函数中会很好——您不能,因为 TVF结果集列必须在编译时确定。
相关阅读:
结论:
OPENROWSET 是您至少可以 100% 避免对象名称的某些完全限定的唯一方法;即使使用 EXEC AT,您仍然必须在对象前加上数据库名称。
额外提示:普遍的意见似乎是不应使用 OPENROWSET "because it is a security risk"(没有关于风险的任何详细信息)。我的理解是,仅当您使用 SQL 服务器身份验证时才会存在风险,此处有更多详细信息:
https://technet.microsoft.com/en-us/library/ms187873%28v=sql.90%29.aspx?f=255&MSPPError=-2147217396
当连接到另一个数据源时,SQL 服务器为 Windows 已验证的登录适当地模拟登录;但是,SQL 服务器无法模拟 SQL 服务器验证登录。因此,对于 SQL 服务器验证登录,SQL 服务器可以通过使用 Windows 帐户的安全上下文访问另一个数据源,例如文件、非关系数据源(如 Active Directory) SQL 服务器服务是 运行。这样做可能会使此类登录访问他们没有权限的另一个数据源,但 SQL 服务器服务所在的帐户 运行 确实有权限。当您使用 SQL 服务器身份验证登录时,应考虑这种可能性。
这可以通过 SQLCLR 轻松完成。如果结果集是动态的,那么它需要是存储过程而不是 TVF。
假设您正在执行存储过程,您只需:
- 传入
@ServerName, @DatabaseName, @SQL
- 创建一个
SqlConnection
,连接字符串为:String.Concat("Server=", ServerName.Value, "; Database=", DatabaseName.Value, "; Trusted_Connection=yes; Enlist=false;")
或使用 ConnectionStringBuilder
- 为
SqlConnection
创建一个 SqlCommand
并使用 SQL.Value
。
- 通过
SqlContext.WindowsIdentity.Impersonate();
启用模拟
_Connection.Open();
- 撤消模拟 -- 只需要建立连接
_Reader = Command.ExecuteReader();
SqlContext.Pipe.Send(_Reader);
- 在
finally
子句中处理 Reader、命令、连接和 ImpersonationContext
与启用 Ad Hoc 分布式查询访问相比,此方法的安全问题较少,因为它更加隔离和可控。它还不允许 SQL 服务器登录获得提升的权限,因为当代码执行 Impersonate()
方法时 SQL 服务器登录将出错。
此外,这种方法允许返回多个结果集,这是 OPENROWSET 不允许的:
Although the query might return multiple result sets, OPENROWSET returns only the first one.
更新
根据对此答案的评论修改伪代码:
- 传入
@QueryID
- 创建
SqlConnection
(_MetaDataConnection),连接字符串为:Context Connection = true;
- 查询 _MetaDataConnection 以通过
SqlDataReader
基于 QueryID.Value
获取 ServerName
、DatabaseName
和 Query
- 使用以下连接字符串创建另一个
SqlConnection
(_QueryConnection):String.Concat("Server=", _Reader["ServerName"].Value, "; Database=", _Reader["DatabaseName"].Value, "; Trusted_Connection=yes; Enlist=false;")
或使用 ConnectionStringBuilder
- 使用
_Reader["SQL"].Value
. 为 _QueryConnection 创建 SqlCommand
(_QueryCommand)
- 使用_MetaDataConnection,根据
QueryID.Value
查询获取参数名称和值
- 循环
SqlDataReader
创建 SqlParameter
s 并添加到 _QueryCommand
_MetaDataConnection.Close();
- 通过
SqlContext.WindowsIdentity.Impersonate();
启用模拟
_QueryConnection.Open();
- 撤消模拟 -- 只需要建立连接
_Reader = _QueryCommand.ExecuteReader();
SqlContext.Pipe.Send(_Reader);
- 处理
finally
子句中的 Readers、命令、连接和 ImpersonationContext
我有大量预先存在的 sql select 语句。
从 [Server_A] 上的存储过程中,我想在多个不同的 SQL 服务器和数据库上执行这些语句中的每一个(列表存储在本地 table 在 [Server_A] 上,return 结果在 [Server_A] 上 table。
但是,我不想在 sql 语句中使用完全限定的 table 名称。我想执行 "select * from users",而不是 "select * from ServerName.DatabaseName.SchemaName.Users"
我已经使用 Openrowset 进行了调查,但我无法找到任何可以将服务器名称和数据库名称指定为连接属性而不是物理指定的示例嵌入在实际的 SQL 语句中。
Openrowset 有这个能力吗?是否有替代方法(从存储过程中,而不是诉诸 Powershell 或其他一些非常不同的方法?)
必然table"Why do I want to do this?"
- 可以的(在连接中指定服务器和数据库 属性,然后在所有数据库中使用完全通用的 sql) 几乎所有访问 SQL 服务器的其他语言。
- 将我所有现有的复合体 SQL 更改为完全合格的是 巨大的 PITA(此外,你根本不应该这样做)
如果你想在一个实例中的每个数据库上执行一个 sql 语句,你可以使用(不受支持,非官方,但广泛使用)exec sp_MSforeachdb
像这样:
EXEC sp_Msforeachdb 'use [?]; select * from users'
这相当于通过
遍历每个数据库use db...
go
select * from users
这是一个有趣的问题,因为我在谷歌上搜索了很多很多小时,发现有几个人试图做与问题中所问的完全相同的事情。
最常见的回复:
- 你为什么要这样做?
- 你不能那样做,你必须完全限定你的对象名称
幸运的是,我偶然发现了答案,而且非常简单。我认为部分问题在于,不同的提供程序和连接字符串有太多的变体,并且有太多可能出错的地方,而当一个人出错时,错误消息通常不是很有启发性。
无论如何,这是您的操作方式:
如果您使用的是静态 SQL:
select * from OPENROWSET('SQLNCLI','Server=ServerName[\InstanceName];Database=AdventureWorks2012;Trusted_Connection=yes','select top 10 * from HumanResources.Department')
如果您使用动态 SQL - 因为 OPENROWSET 不接受变量作为参数,您可以使用这样的方法(只是作为一个人为的例子):
declare @sql nvarchar(4000) = N'select * from OPENROWSET(''SQLNCLI'',''Server=Server=ServerName[\InstanceName];Database=AdventureWorks2012;Trusted_Connection=yes'',''@zzz'')'
set @sql = replace(@sql,'@zzz','select top 10 * from HumanResources.Department')
EXEC sp_executesql @sql
值得注意:如果您认为将此语法包装在一个漂亮的 Table 接受@ServerName、@DatabaseName、@SQL 的有价值函数中会很好——您不能,因为 TVF结果集列必须在编译时确定。
相关阅读:
结论:
OPENROWSET 是您至少可以 100% 避免对象名称的某些完全限定的唯一方法;即使使用 EXEC AT,您仍然必须在对象前加上数据库名称。
额外提示:普遍的意见似乎是不应使用 OPENROWSET "because it is a security risk"(没有关于风险的任何详细信息)。我的理解是,仅当您使用 SQL 服务器身份验证时才会存在风险,此处有更多详细信息:
https://technet.microsoft.com/en-us/library/ms187873%28v=sql.90%29.aspx?f=255&MSPPError=-2147217396
当连接到另一个数据源时,SQL 服务器为 Windows 已验证的登录适当地模拟登录;但是,SQL 服务器无法模拟 SQL 服务器验证登录。因此,对于 SQL 服务器验证登录,SQL 服务器可以通过使用 Windows 帐户的安全上下文访问另一个数据源,例如文件、非关系数据源(如 Active Directory) SQL 服务器服务是 运行。这样做可能会使此类登录访问他们没有权限的另一个数据源,但 SQL 服务器服务所在的帐户 运行 确实有权限。当您使用 SQL 服务器身份验证登录时,应考虑这种可能性。
这可以通过 SQLCLR 轻松完成。如果结果集是动态的,那么它需要是存储过程而不是 TVF。
假设您正在执行存储过程,您只需:
- 传入
@ServerName, @DatabaseName, @SQL
- 创建一个
SqlConnection
,连接字符串为:String.Concat("Server=", ServerName.Value, "; Database=", DatabaseName.Value, "; Trusted_Connection=yes; Enlist=false;")
或使用ConnectionStringBuilder
- 为
SqlConnection
创建一个SqlCommand
并使用SQL.Value
。 - 通过
SqlContext.WindowsIdentity.Impersonate();
启用模拟
_Connection.Open();
- 撤消模拟 -- 只需要建立连接
_Reader = Command.ExecuteReader();
SqlContext.Pipe.Send(_Reader);
- 在
finally
子句中处理 Reader、命令、连接和 ImpersonationContext
与启用 Ad Hoc 分布式查询访问相比,此方法的安全问题较少,因为它更加隔离和可控。它还不允许 SQL 服务器登录获得提升的权限,因为当代码执行 Impersonate()
方法时 SQL 服务器登录将出错。
此外,这种方法允许返回多个结果集,这是 OPENROWSET 不允许的:
Although the query might return multiple result sets, OPENROWSET returns only the first one.
更新
根据对此答案的评论修改伪代码:
- 传入
@QueryID
- 创建
SqlConnection
(_MetaDataConnection),连接字符串为:Context Connection = true;
- 查询 _MetaDataConnection 以通过
SqlDataReader
基于 - 使用以下连接字符串创建另一个
SqlConnection
(_QueryConnection):String.Concat("Server=", _Reader["ServerName"].Value, "; Database=", _Reader["DatabaseName"].Value, "; Trusted_Connection=yes; Enlist=false;")
或使用ConnectionStringBuilder
- 使用
_Reader["SQL"].Value
. 为 _QueryConnection 创建 - 使用_MetaDataConnection,根据
QueryID.Value
查询获取参数名称和值
- 循环
SqlDataReader
创建SqlParameter
s 并添加到 _QueryCommand _MetaDataConnection.Close();
- 通过
SqlContext.WindowsIdentity.Impersonate();
启用模拟
_QueryConnection.Open();
- 撤消模拟 -- 只需要建立连接
_Reader = _QueryCommand.ExecuteReader();
SqlContext.Pipe.Send(_Reader);
- 处理
finally
子句中的 Readers、命令、连接和 ImpersonationContext
QueryID.Value
获取 ServerName
、DatabaseName
和 Query
SqlCommand
(_QueryCommand)