【问题标题】:Dynamic SQL without having to use fully qualified table names in SQL (Openrowset?)动态 SQL,无需在 SQL 中使用完全限定的表名(Openrowset?)
【发布时间】:2015-05-03 11:46:34
【问题描述】:

我有大量预先存在的 sql 选择语句。

从 [Server_A] 上的存储过程,我想在多个不同的 SQL Server 和数据库上执行这些语句中的每一个(列表存储在 [Server_A] 上的本地表中,并将结果返回到上的表中[服务器_A]。

但是,我不想在我的 sql 语句中使用完全限定的表名。我想执行“select * from users”,而不是“select * from ServerName.DatabaseName.SchemaName.Users”

我使用 Openrowset 进行了调查,但我找不到任何可以将服务器名称和数据库名称都指定为作为连接的属性的示例,而不是物理嵌入到实际 SQL 中声明。

Openrowset 能做到这一点吗?是否有其他方法可以做到这一点(从存储过程中,而不是诉诸 Powershell 或其他一些非常不同的方法?)

不可避免的“我为什么要这样做?”

  • 你可以做到(在连接中指定服务器和数据库 属性,然后在所有数据库中使用完全通用的 sql) 几乎所有其他访问 SQL Server 的语言。
  • 将我所有预先存在的复杂 SQL 更改为完全限定是一个 巨大的 PITA(此外,您根本不应该这样做)

【问题讨论】:

  • 为什么不使用完全限定名?求朋友。
  • ddddd 抱歉不知道如何删除评论
  • 为什么不使用完全限定?嗯,因为我正在对多个数据库执行大型预先存在的非完全限定 SQL?

标签: sql-server tsql


【解决方案1】:

这可以通过 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、Command、Connection 和 ImpersonationContext

与启用 Ad Hoc 分布式查询访问相比,这种方法的安全问题更少,因为它更加隔离和可控。它也不允许 SQL Server 登录获得提升的权限,因为当代码执行 Impersonate() 方法时,SQL Server 登录会出错。

此外,这种方法允许返回多个结果集,这是OPENROWSET 不允许的:

虽然查询可能返回多个结果集,但 OPENROWSET 只返回第一个。


更新

在这个答案上基于 cmets 修改伪代码:

  • 传入@QueryID
  • 使用以下连接字符串创建SqlConnection (_MetaDataConnection):Context Connection = true;
  • 根据QueryID.Value 通过SqlDataReader 查询_MetaDataConnection 以获取ServerNameDatabaseNameQuery
  • 使用以下连接字符串创建另一个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 以创建SqlParameters 并添加到_QueryCommand
  • _MetaDataConnection.Close();
  • 通过SqlContext.WindowsIdentity.Impersonate();启用模拟
  • _QueryConnection.Open();
  • 撤消模拟 -- 只需要建立连接
  • _Reader = _QueryCommand.ExecuteReader();
  • SqlContext.Pipe.Send(_Reader);
  • finally 子句中处理 Readers、Commands、Connections 和 ImpersonationContext

【讨论】:

  • 哇,这就是我希望得到的答案!问题:通过 SQLCLR,我是否可以执行动态 SQL,包括一个参数集合(从元数据构建),其成员将在运行时通过解析分隔的 Parameter:Value 字符串来填充,就像在 C# 中一样,与您在设计时必须知道的方式不同使用 SqlServer(使用 openrowset 时除外)?
  • @tbone 嗯,是的,但是你低估了纯 T-SQL 可以做的事情。 1) 您可以通过传入 XML、分隔列表或 TVP 对 proc 执行“动态”参数。 2) 您可以通过将对sp_executesql 的调用视为动态SQL 的第二级来向sp_executesql 提供动态参数列表。这意味着您还可以通过SET @SQL = 'EXEC [' + @DatabaseName + '].sys.sp_executesql @Query, @ParamList, ' + @ParamVals; 动态注入数据库名称,而不需要完全限定的对象;-)。但这只是仅供参考,因为这不会让您获得远程服务器部分。
  • @tbone 另外,如果参数存储在数据库中,那么为什么要查询它们,将它们连接成一个分隔的 K/V 对字符串,传递它们,只是为了将它们解析成什么数量到 2 列表?只需传入 QueryID 或用于获取这些值的任何内容,然后在 .NET 代码中传递 SELECT ParamName, ParamValue FROM ParamTable WHERE QueryID = @QueryID;,然后只需循环通过 SqlDataReader 获取离散字段值。它的工作量更少,更不容易出错:-)。您甚至可以在 SQLCLR proc 中获取 ServerName 和 DatabaseName,因此只需将 @QueryID 传递给 proc。
  • 我已经在做 K/V 部分(一般用于设置参数值)。正如您所注意到的,上述答案中最重要的缺失部分是远程服务器部分。目标是完全删除 @SQL 中服务器和数据库的任何完全限定或令牌替换。
  • 您对 SQLCLR 了解得够多,知道是否可以完全从 SQL 中抽象出服务器和数据库规范,就像您在 C#、ADO 等中所做的那样?
【解决方案2】:

如果你想在一个实例中的每个数据库上执行一条 sql 语句,你可以使用(不受支持的、非官方的、但被广泛使用的)exec sp_MSforeachdb 像这样:

EXEC sp_Msforeachdb 'use [?];  select * from users'

这相当于通过一个

遍历每个数据库
use db...
go
select * from users

【讨论】:

    【解决方案3】:

    这是一个有趣的问题,因为我用谷歌搜索了很多很多小时,发现有几个人试图做与问题中提出的完全相同的事情。

    最常见的回答:

    • 为什么要这样做?
    • 不能这样做,您必须完全限定您的对象名称

    幸运的是,我偶然发现了答案,而且非常简单。我认为问题的一部分在于,不同的提供者和连接字符串有很多变体,而且有很多事情可能出错,而当出现问题时,错误消息通常不是很有启发性。

    不管怎样,这就是你的做法:

    如果您使用的是静态 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
    

    值得注意的是:如果您认为将这种语法封装在一个接受@ServerName、@DatabaseName、@SQL 的漂亮表值函数中会很好 - 您不能,因为 TVF 的结果集列必须在编译时确定。

    相关阅读:

    http://blogs.technet.com/b/wardpond/archive/2005/08/01/the-openrowset-trick-accessing-stored-procedure-output-in-a-select-statement.aspx

    http://blogs.technet.com/b/wardpond/archive/2009/03/20/database-programming-the-openrowset-trick-revisited.aspx

    结论:
    OPENROWSET 是您可以 100% 避免至少某些完全限定的对象名称的唯一方法;即使使用 EXEC AT,您仍然需要在对象前面加上数据库名称。

    额外提示:普遍的观点似乎是不应使用 OPENROWSET,“因为它存在安全风险”(没有任何关于风险的详细信息)。我的理解是,只有在您使用 SQL Server 身份验证时才有风险,这里有更多详细信息:

    https://technet.microsoft.com/en-us/library/ms187873%28v=sql.90%29.aspx?f=255&MSPPError=-2147217396

    当连接到另一个数据源时,SQL Server 会为 Windows 身份验证登录适当地模拟登录;但是,SQL Server 无法模拟经过 SQL Server 身份验证的登录。因此,对于经过 SQL Server 身份验证的登录,SQL Server 可以使用运行 SQL Server 服务的 Windows 帐户的安全上下文来访问另一个数据源,例如文件、Active Directory 等非关系数据源。这样做可能会使此类登录名访问他们没有权限的另一个数据源,但运行 SQL Server 服务的帐户确实具有权限。当您使用 SQL Server 身份验证登录时,应考虑这种可能性。

    【讨论】:

    • 关于“如果您认为将这种语法包装在一个不错的表值函数中会很好......”:虽然结果集确实不能改变,但您也不能做 Dynamic T-SQL 函数中的 SQL ;-)。而且,OPENROWSET 并不是解决此问题的唯一方法。我在回答中详细介绍了另一种方法:)。
    • 这是一个有趣的问题@srutzky - 使用 OpenRowset,我可以有一个不确定(列)结果集(至少我认为我可以,也许我需要仔细检查?),但没有其他方法可以做到这。我完全可以理解为什么不鼓励它,但是应该在技术上禁止它吗? (假设我没有完全误解现实。)
    • 关于“没有其他方法可以做到这一点”:不一定是真的。我建议的想法也允许动态结果集。不同之处在于OPENROWSET 是一个函数,它返回一个可以轻松交互的表,而我建议一个不能加入的 SQLCLR 存储过程(除非你想变得狡猾并使用 OPENQUERY / OPENROWSET 来调用本地服务器 - -loopback——执行 SQLCLR proc,但不确定它们是否允许调用 SQLCLR)。尽管如此,OPENROWSET / OPENQUERY 对它们执行的 SQL 可以执行的操作有限制,而 SQLCLR proc 可以执行任何操作。
    • 但是从 SQL Server 的角度来看,CLR SP 就像其他的一样,不是吗,所以你不能加入它?如果它是 TVF,那么即使在 CLR 中,您仍然受限于一组静态返回的列,不是吗?我从未尝试将 SP 包装在 CTE 中,但我怀疑这是否可行。
    • 是的,SQLCLR 存储过程不能用于 SELECT / JOIN / 等。另外更正了 TVF 需要静态结果集。 OPENROWSET 的好处是它是一个函数,因此可以更轻松地与之交互。但是在这里使用存储过程应该不是问题,因为它可以在INSERT INTO TableName EXEC StoredProcName 中使用。如果查询对不同的表返回不同的结果集,只要所有的字段都返回并且顺序相同,那么你只需要表名来构建'INSERT INTO '+@Table+' EXEC Proc '+CONVERT(VARCHAR(10),@QueryID)的动态查询,对吧?
    猜你喜欢
    • 2018-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-04
    • 2016-01-06
    • 1970-01-01
    • 1970-01-01
    • 2021-05-09
    相关资源
    最近更新 更多