T-SQL 动态 SQL 和临时表

Posted

技术标签:

【中文标题】T-SQL 动态 SQL 和临时表【英文标题】:T-SQL Dynamic SQL and Temp Tables 【发布时间】:2011-02-24 10:53:41 【问题描述】:

看起来使用动态 SQL 通过 EXECUTE 字符串方法创建的 #temptables 具有不同的范围,并且不能被同一存储过程中的“固定”SQL 引用。 但是,我可以在子序列动态 SQL 中引用由动态 SQL 语句创建的临时表,但似乎存储过程不会将查询结果返回给调用客户端,除非 SQL 已修复。

一个简单的2表场景: 我有 2 张桌子。我们称它们为订单和项目。 Order 的主键是 OrderId,Items 的主键是 ItemId。 Items.OrderId 是标识父订单的外键。一个订单可以有 1 到 n 个商品。

我希望能够为用户提供一个非常灵活的“查询构建器”类型的界面,以允许用户选择他想查看的项目。筛选条件可以基于 Items 表和/或父 Order 表中的字段。如果 Item 满足过滤条件,包括父 Order 的条件(如果存在),则应在查询中返回该 Item 以及父 Order。

通常,我想大多数人会在 Item 表和父 Order 表之间构建一个连接。我想改为执行 2 个单独的查询。一个返回所有符合条件的项目,另一个返回所有不同的父订单。原因有两个,你可能同意也可能不同意。

第一个原因是我需要查询父 Order 表中的所有列,如果我执行一个查询以将 Orders 表连接到 Items 表,我将多次重复 Order 信息。由于每个订单通常有大量商品,我想避免这种情况,因为这会导致更多数据传输到胖客户端。相反,如前所述,我想在数据集中单独返回两个表,并使用其中的两个表来填充自定义订单和子项目客户端对象。 (我对 LINQ 或实体框架还不够了解。我手动构建我的对象)。我想返回两个表而不是一个表的第二个原因是因为我已经有另一个过程返回给定 OrderId 的所有项目以及父 Order 并且我想使用相同的 2 表方法以便我可以重用客户端代码从返回的 2 个数据表中填充我的自定义 Order 和 Client 对象。

我希望做的是:

在客户端上构造一个动态 SQL 字符串,将订单表连接到 Items 表,并按照在 Winform 胖客户端应用程序上创建的自定义过滤器指定的过滤器对每个表进行适当的过滤。客户端上的 SQL 构建看起来像这样:

TempSQL = "

    INSERT INTO #ItemsToQuery
       OrderId, ItemsId
    FROM
       Orders, Items 
    WHERE
       Orders.OrderID = Items.OrderId AND
       /* Some unpredictable Order filters go here */
      AND
       /* Some unpredictable Items filters go here */
    "

然后,我会调用一个存储过程,

CREATE PROCEDURE GetItemsAndOrders(@tempSql as text)
   Execute (@tempSQL) --to create the #ItemsToQuery table

SELECT * FROM Items WHERE Items.ItemId IN (SELECT ItemId FROM #ItemsToQuery)

SELECT * FROM Orders WHERE Orders.OrderId IN (SELECT DISTINCT OrderId FROM #ItemsToQuery)

这种方法的问题在于,#ItemsToQuery 表是由动态 SQL 创建的,因此无法从以下 2 个静态 SQL 访问,如果我将静态 SQL 更改为动态,则不会将任何结果传递回胖客户端。

我想到了 3 个,但我正在寻找一个更好的:

1) 第一个 SQL 可以通过从客户端执行动态构造的 SQL 来执行。然后可以将结果作为表传递给上述存储过程的修改版本。我熟悉将表数据作为 XML 传递。如果我这样做了,那么存储过程可以使用静态 SQL 将数据插入到临时表中,因为它是由动态 SQL 创建的,因此可以毫无问题地查询。 (我还可以研究传递新的 Table 类型参数而不是 XML。)但是,我想避免将可能很大的列表传递给存储过程。

2) 我可以执行来自客户端的所有查询。

第一个是这样的:

SELECT Items.* FROM Orders, Items WHERE Order.OrderId = Items.OrderId AND (dynamic filter)
SELECT Orders.* FROM Orders, Items WHERE Order.OrderId = Items.OrderId AND (dynamic filter)

这仍然使我能够重用我的客户端对象填充代码,因为订单和项目继续在两个不同的表中返回。

我有一种感觉,在我的存储过程中我可能有一些使用表数据类型的选项,但这对我来说也是新的,我希望能稍微用勺子喂它。

如果您对我所写的内容进行了如此深入的了解,我会感到惊讶,但如果是这样,我将不胜感激您对如何最好地完成这一点的任何想法。

【问题讨论】:

您绝对可以将动态 sql 的结果返回给调用应用程序。这里的大多数答案都解决了您提出的其他观点。 【参考方案1】:

我遇到了@Muflix 提到的同样问题。当您不知道要返回的列或动态生成的列时,我所做的是创建一个具有唯一 ID 的全局表,然后在我完成后将其删除,这看起来像所示下面:

DECLARE @DynamicSQL NVARCHAR(MAX)
DECLARE @DynamicTable VARCHAR(255) = 'DynamicTempTable_' + CONVERT(VARCHAR(36), NEWID())
DECLARE @DynamicColumns NVARCHAR(MAX)

--Get "@DynamicColumns", example: SET @DynamicColumns = '[Column1], [Column2]'

SET @DynamicSQL = 'SELECT ' + @DynamicColumns + ' INTO [##' + @DynamicTable + ']' + 
     ' FROM [dbo].[TableXYZ]'

EXEC sp_executesql @DynamicSQL

SET @DynamicSQL = 'IF OBJECT_ID(''tempdb..##' + @DynamicTable + ''' , ''U'') IS NOT NULL ' + 
    ' BEGIN DROP TABLE [##' + @DynamicTable + '] END'

EXEC sp_executesql @DynamicSQL

当然不是最好的解决方案,但这似乎对我有用。

【讨论】:

【参考方案2】:

第一种方法 - 在同一个动态 SQL 调用中包含多个语句:

DECLARE @DynamicQuery NVARCHAR(MAX)

SET @DynamicQuery = 'Select * into #temp from (select * from tablename) alias 
select * from #temp
drop table #temp'

EXEC sp_executesql @DynamicQuery

第二种方法 - 使用全局临时表:(小心,您需要特别注意全局变量。)

IF OBJECT_ID('tempdb..##temp2') IS NULL
BEGIN
    EXEC (
            'create table ##temp2 (id int)
             insert ##temp2 values(1)'
            )

    SELECT *
    FROM ##temp2
END

完成后不要忘记手动删除##temp2 对象:

IF (OBJECT_ID('tempdb..##temp2') IS NOT NULL)
BEGIN
     DROP Table ##temp2
END

注意:如果您不了解数据库的完整结构,请勿使用此方法 2。

【讨论】:

这行不通。在执行动态查询之前,您不能对表的内容进行操作。【参考方案3】:

您首先需要先创建您的表,然后它将在动态 SQL 中可用。

这行得通:

CREATE TABLE #temp3 (id INT)
EXEC ('insert #temp3 values(1)')

SELECT *
FROM #temp3

这不起作用:

EXEC (
        'create table #temp2 (id int)
         insert #temp2 values(1)'
        )

SELECT *
FROM #temp2

换句话说:

    创建临时表 执行过程 从临时表中选择

这里是完整的例子:

CREATE PROC prTest2 @var VARCHAR(100)
AS
EXEC (@var)
GO

CREATE TABLE #temp (id INT)

EXEC prTest2 'insert #temp values(1)'

SELECT *
FROM #temp

【讨论】:

我认为这也有效:插入#temptable exec ('select ?? from ??'); 问题是当我们不知道列的定义时,那又如何呢? 假设您知道该表存在,将目标表中的前 1 个放入#someTable。然后截断#someTable。这又快又脏。免责声明:例如,varchars 的列大小可能不代表整个集合中某处的最大大小。如果您担心从 sys.tables 和 sys.columns 构建临时表。你应该只需要这样做一次。如果您对动态 SQL 有好处,也可以动态完成【参考方案4】:

来自动态 SQL 的结果集返回给客户端。我已经做了很多。

关于通过临时表和变量共享数据以及在 SQL 和它生成的动态 SQL 之间共享数据的问题,您是对的。

我认为在尝试使您的临时表正常工作时,您可能会有些困惑,因为您绝对可以从执行动态 SQL 的 SP 获取数据:

USE SandBox
GO

CREATE PROCEDURE usp_DynTest(@table_type AS VARCHAR(255))
AS 
BEGIN
    DECLARE @sql AS VARCHAR(MAX) = 'SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ''' + @table_type + ''''
    EXEC (@sql)
END
GO

EXEC usp_DynTest 'BASE TABLE'
GO

EXEC usp_DynTest 'VIEW'
GO

DROP PROCEDURE usp_DynTest
GO

还有:

USE SandBox
GO

CREATE PROCEDURE usp_DynTest(@table_type AS VARCHAR(255))
AS 
BEGIN
    DECLARE @sql AS VARCHAR(MAX) = 'SELECT * INTO #temp FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ''' + @table_type + '''; SELECT * FROM #temp;'
    EXEC (@sql)
END
GO

EXEC usp_DynTest 'BASE TABLE'
GO

EXEC usp_DynTest 'VIEW'
GO

DROP PROCEDURE usp_DynTest
GO

【讨论】:

如果您在 proc 中创建临时表,它将无法正常工作,您需要先创建临时表,然后您可以在 proc 中填充它..另见我的示例 @SQLMenace - 我明白你在说什么。我的观点是,您可以从动态 SQL 返回集合(它们可以使用自己的临时表并从中返回)。我将添加第二个示例。【参考方案5】:

我强烈建议您阅读http://www.sommarskog.se/arrays-in-sql-2005.html

我个人喜欢传递逗号分隔的文本列表,然后将其与文本解析到表格函数并加入它的方法。如果您首先在连接中创建临时表方法,则它可以工作。但是感觉有点乱。

【讨论】:

我宁愿传递 XML 而不是传递 CSV 。虽然更冗长,但它允许灵活地修改和传递额外的列。而且 SQL 已经知道如何解析 XML。但是我看到了将客户端数据集传递到服务器端表变量的示例。很干净。尽管如此,它不如临时表恕我直言,这是一种不太可能扩展的方法。 SQL Server 2008+(或 2005?)支持一旦定义就可以通过客户端提供的表值(例如,ADO.NET 数据集可以很好地工作)。 SQL Server 2016+ 支持 JSON。

以上是关于T-SQL 动态 SQL 和临时表的主要内容,如果未能解决你的问题,请参考以下文章

T-SQL: where xxx IN 临时表

T-SQL 公用表表达式(CTE)

带有随机NEWID列T-SQL的临时表

T-SQL 之 公用表表达式(CTE)

sql server中的临时表表变量和公用表表达式

SQL Server 中的临时表使用