如何在动态SQL语句中使用表变量?

77

在我的存储过程中,我在顶部声明了两个表变量。现在我正在尝试在动态SQL语句中使用该表变量,但在执行该存储过程时出现了错误。我正在使用Sql Server 2008。

这是我的查询的样子,

set @col_name =  'Assoc_Item_' 
              + Convert(nvarchar(2), @curr_row1);

set @sqlstat = 'update @RelPro set ' 
             + @col_name 
             + ' = (Select relsku From @TSku Where tid = ' 
             + Convert(nvarchar(2), @curr_row1) + ') Where RowID = ' 
             + Convert(nvarchar(2), @curr_row);

Exec(@sqlstat);

我遇到了以下错误,

必须声明表变量 "@RelPro"。 必须声明表变量 "@TSku"。

我尝试将表从动态查询的字符串块中移出,但没有成功。

8个回答

89
在SQL Server 2008+中,可以使用表值参数将表变量传递到动态SQL语句中,只要您不需要更新表本身中的值。因此,对于您发布的代码,您可以针对@TSku使用此方法,但无法使用@RelPro。
以下是示例语法。
CREATE TYPE MyTable AS TABLE 
( 
Foo int,
Bar int
);
GO


DECLARE @T AS MyTable;

INSERT INTO @T VALUES (1,2), (2,3)

SELECT *,
        sys.fn_PhysLocFormatter(%%physloc%%) AS [physloc]
FROM @T

EXEC sp_executesql
  N'SELECT *,
        sys.fn_PhysLocFormatter(%%physloc%%) AS [physloc]
    FROM @T',
  N'@T MyTable READONLY',
  @T=@T 

physloc列仅用于演示被子作用域引用的表变量确实是外部作用域中的同一变量,而不是副本。


我理解它通过引用传递表,从而减少了IO。不过,在嵌套存储过程调用或执行语句的情况下,我还没有验证过这一点。 - sheldonhull

78

您的EXEC在不同的上下文中执行,因此它不知道在原始上下文中声明的任何变量。您应该能够像下面的简单演示中所示一样使用临时表而不是表变量。

create table #t (id int)

declare @value nchar(1)
set @value = N'1'

declare @sql nvarchar(max)
set @sql = N'insert into #t (id) values (' + @value + N')'

exec (@sql)

select * from #t

drop table #t

1
也许是我搞错了,但是不同的上下文环境是否会导致临时表失效,除非使用全局临时表? - John Spiegel
3
不,内部作用域可以看到在父作用域中创建的#table。很容易测试。 - Aaron Bertrand
1
也许有一些关于SQL Server上下文的好链接?我真的不理解这个。如果动态SQL和表变量在同一批处理中 - 上下文在这里扮演什么角色? - FrenkyB

14

您不必使用动态SQL

update
    R
set
    Assoc_Item_1 = CASE WHEN @curr_row = 1 THEN foo.relsku ELSE Assoc_Item_1 END,
    Assoc_Item_2 = CASE WHEN @curr_row = 2 THEN foo.relsku ELSE Assoc_Item_2 END,
    Assoc_Item_3 = CASE WHEN @curr_row = 3 THEN foo.relsku ELSE Assoc_Item_3 END,
    Assoc_Item_4 = CASE WHEN @curr_row = 4 THEN foo.relsku ELSE Assoc_Item_4 END,
    Assoc_Item_5 = CASE WHEN @curr_row = 5 THEN foo.relsku ELSE Assoc_Item_5 END,
    ...
from
    (Select relsku From @TSku Where tid = @curr_row1) foo
    CROSS JOIN
    @RelPro R
Where
     R.RowID = @curr_row;

7

您不能这样做,因为表变量超出了范围。

您必须在动态SQL语句中声明表变量或创建临时表。

我建议您阅读这篇关于动态SQL的优秀文章。

http://www.sommarskog.se/dynamic_sql.html


2

嗯,我找到了解决方法并想与可能遇到同样问题的人分享。

让我从我一直面临的问题开始说起,

我一直在尝试执行一个动态SQL语句,该语句使用了我在存储过程顶部声明的两个临时表,但由于该动态SQL语句创建了一个新的作用域,所以我无法使用这些临时表。

解决方案:

我只需将它们更改为全局临时变量即可。

在下面找到我的存储过程。

CREATE PROCEDURE RAFCustom_Room_GetRelatedProducts
-- Add the parameters for the stored procedure here
@PRODUCT_SKU nvarchar(15) = Null

AS BEGIN -- 为了防止额外的结果集干扰SELECT语句,添加SET NOCOUNT ON。 SET NOCOUNT ON;

IF OBJECT_ID('tempdb..##RelPro', 'U') IS NOT NULL
BEGIN
    DROP TABLE ##RelPro
END

Create Table ##RelPro
(
    RowID int identity(1,1),
    ID int,
    Item_Name nvarchar(max),
    SKU nvarchar(max),
    Vendor nvarchar(max),
    Product_Img_180 nvarchar(max),
    rpGroup int,
    Assoc_Item_1 nvarchar(max),
    Assoc_Item_2 nvarchar(max),
    Assoc_Item_3 nvarchar(max),
    Assoc_Item_4 nvarchar(max),
    Assoc_Item_5 nvarchar(max),
    Assoc_Item_6 nvarchar(max),
    Assoc_Item_7 nvarchar(max),
    Assoc_Item_8 nvarchar(max),
    Assoc_Item_9 nvarchar(max),
    Assoc_Item_10 nvarchar(max)
);

Begin
    Insert ##RelPro(ID, Item_Name, SKU, Vendor, Product_Img_180, rpGroup)

    Select distinct zp.ProductID, zp.Name, zp.SKU,
        (Select m.Name From ZNodeManufacturer m(nolock) Where m.ManufacturerID = zp.ManufacturerID),
        'http://s0001.server.com/is/sw11/DG/' + 
        (Select m.Custom1 From ZNodeManufacturer m(nolock) Where m.ManufacturerID = zp.ManufacturerID) +
        '_' + zp.SKU + '_3?$SC_3243$', ep.RoomID
    From Product zp(nolock) Inner Join RF_ExtendedProduct ep(nolock) On ep.ProductID = zp.ProductID
    Where zp.ActiveInd = 1 And SUBSTRING(zp.SKU, 1, 2) <> 'GC' AND zp.Name <> 'PLATINUM' AND zp.SKU = (Case When @PRODUCT_SKU Is Not Null Then @PRODUCT_SKU Else zp.SKU End)
End

declare @curr_row int = 0,
        @tot_rows int= 0,
        @sku nvarchar(15) = null;

IF OBJECT_ID('tempdb..##TSku', 'U') IS NOT NULL
BEGIN
    DROP TABLE ##TSku
END
Create Table ##TSku (tid int identity(1,1), relsku nvarchar(15));

Select @curr_row = (Select MIN(RowId) From ##RelPro);
Select @tot_rows = (Select MAX(RowId) From ##RelPro);

while @curr_row <= @tot_rows
Begin
    select @sku = SKU from ##RelPro where RowID = @curr_row;

    truncate table ##TSku;

    Insert ##TSku(relsku)
    Select distinct top(10) tzp.SKU From Product tzp(nolock) INNER JOIN 
    [INTRANET].raf_FocusAssociatedItem assoc(nolock) ON assoc.associatedItemID = tzp.SKU
    Where (assoc.isActive=1) And (tzp.ActiveInd = 1) AND (assoc.productID = @sku)

    declare @curr_row1 int = (Select Min(tid) From ##TSku),
            @tot_rows1 int = (Select Max(tid) From ##TSku);

    If(@tot_rows1 <> 0)
    Begin
        While @curr_row1 <= @tot_rows1
        Begin
            declare @col_name nvarchar(15) = null,
                    @sqlstat nvarchar(500) = null;
            set @col_name =  'Assoc_Item_' + Convert(nvarchar(2), @curr_row1);
            set @sqlstat = 'update ##RelPro set ' + @col_name + ' = (Select relsku From ##TSku Where tid = ' + Convert(nvarchar(2), @curr_row1) + ') Where RowID = ' + Convert(nvarchar(2), @curr_row);
            Exec(@sqlstat);
            set @curr_row1 = @curr_row1 + 1;
        End
    End
    set @curr_row = @curr_row + 1;
End

Select * From ##RelPro;

结束 执行


5
我还没有测试你的代码,但是我认为你的临时表不一定需要是全局的。你可以使用一个非全局(本地?)临时表(只需使用单个 # 符号)。好处是它会在连接关闭后自动消失,并且仅在该连接内可见。也就是说,我相信如果使用全局临时表,如果有多个连接同时执行相同的过程,它们可能会由于共享同一个全局临时表而相互干扰。 - Dr. Wily's Apprentice
4
抱歉,你也有光标吗? - gbn
2
@Dr. Wily - 没错,如果全局临时表是在动态 SQL 内部创建的,为了防止在动态 SQL 批处理完成后它们会消失,就需要使用全局临时表。但如果临时表是在动态 SQL 批处理外部创建的话,那么本地临时表应该是可见的。 - Martin Smith
@Martin - 啊,我刚试了一下,看起来你是正确的;在动态语句内创建的临时表会消失。然而,在这种情况下似乎不是这种情况。这些临时表是由动态语句创建然后修改的。 - Dr. Wily's Apprentice

2
我认为这是不可能的(虽然请参考下面的更新);据我所知,表变量只存在于声明它的作用域内。但你可以使用临时表(使用create table语法,并在表名前加上#符号),它将可以访问创建它的作用域和动态语句的作用域。
更新:请参考Martin Smith的回答,了解如何使用表值参数将表变量传递给动态SQL语句。还请注意提到的限制:表值参数是只读的。

1

这里是使用动态T-SQL查询的示例,并提取结果,如果您有多列返回值(请注意动态表名):

DECLARE 
@strSQLMain nvarchar(1000),
@recAPD_number_key char(10),    
@Census_sub_code varchar(1),
@recAPD_field_name char(100),
@recAPD_table_name char(100),
@NUMBER_KEY varchar(10),

if object_id('[Permits].[dbo].[myTempAPD_Txt]') is not null 

    DROP TABLE [Permits].[dbo].[myTempAPD_Txt]

CREATE TABLE [Permits].[dbo].[myTempAPD_Txt]
(
    [MyCol1] char(10) NULL,
    [MyCol2] char(1) NULL,

)   
-- an example of what @strSQLMain is : @strSQLMain = SELECT @recAPD_number_key = [NUMBER_KEY], @Census_sub_code=TEXT_029 FROM APD_TXT0 WHERE Number_Key = '01-7212' 
SET @strSQLMain = ('INSERT INTO myTempAPD_Txt SELECT [NUMBER_KEY], '+ rtrim(@recAPD_field_name) +' FROM '+ rtrim(@recAPD_table_name) + ' WHERE Number_Key = '''+ rtrim(@Number_Key) +'''')      
EXEC (@strSQLMain)  
SELECT @recAPD_number_key = MyCol1, @Census_sub_code = MyCol2 from [Permits].[dbo].[myTempAPD_Txt]

DROP TABLE [Permits].[dbo].[myTempAPD_Txt]  

0

使用临时表可以解决问题,但是我在使用Exec时遇到了问题,所以我采用了以下使用sp_executesql的解决方案:

Create TABLE #tempJoin ( Old_ID int, New_ID int);

declare @table_name varchar(128);

declare @strSQL nvarchar(3072);

set @table_name = 'Object';

--build sql sting to execute
set @strSQL='INSERT INTO '+@table_name+' SELECT '+@columns+' FROM #tempJoin CJ
                        Inner Join '+@table_name+' sourceTbl On CJ.Old_ID = sourceTbl.Object_ID'

**exec sp_executesql @strSQL;**

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