【问题标题】:SQL Server stored procedure with while condition containing table variable带有包含表变量的 while 条件的 SQL Server 存储过程
【发布时间】:2019-11-25 22:04:23
【问题描述】:

我有一张表,其中国家名称定期更改,例如 my_table_US_NA、my_table_CAN_NA、my_table_MEX_NA 等:

create table my_table_US_NA(id int)
insert into my_table_US_NA(id) values (1)
insert into my_table_US_NA(id) values (2)
insert into my_table_US_NA(id) values (3)
insert into my_table_US_NA(id) values (4)

select * from my_table_US_NA

id
----
 1
 2
 3
 4

我有一个这样的存储过程:

create procedure my_looping_procedure (@Country varchar(10))
as
begin
    declare @MyTable varchar(50), @COUNTER int

    set @COUNTER = 1
    set @MyTable = concat('my_table_', @Country, '_NA')

    while (@COUNTER <= (select max(id) from @MyTable))
    begin
        set @COUNTER = @COUNTER + 1
        print @COUNTER
    end
end

当我尝试编译该过程时,我得到了这个错误:

消息 1087,级别 16,状态 1,过程 my_looping_procedure,第 15 行 [批处理开始行 0]
必须声明表变量“@MyTable”

我尝试将 while 循环移动到它自己的小变量中:

create procedure my_looping_procedure (@Country varchar(10))
as
begin
    declare @MyTable varchar(50),
            @sql_loop varchar(max),
            @COUNTER int

    set @COUNTER = 1
    set @MyTable = concat('my_table_', @Country, '_NA')

    -- inner variable here
    select @sql_loop = '
while (' + @COUNTER + '<= (select max(id) from ' + @MyTable + '))
    begin
        set ' + @COUNTER + ' = ' + @COUNTER + ' + 1
        print ' + @COUNTER + '
    end'

    exec(@sql_loop)
end

编译但当我尝试执行它时返回错误exec my_looping_procedure:

消息 245,级别 16,状态 1,过程 my_looping_procedure,第 16 行 [批处理开始第 26 行]
将 varchar 值 'WHILE (' 转换为数据类型 int 时转换失败。

我尝试在@sql_loop 中声明和设置所有变量:

alter procedure my_looping_procedure (@Country varchar(10))
as
begin
    declare @sql_loop varchar(max)

    select @sql_loop = '
declare 
@MyTable varchar(50),
@COUNTER INT
SET @COUNTER = 1
set @MyTable = concat(''my_table_'', ' + @Country + ', ''_NA'')

WHILE (@COUNTER <= (select max(id) from ' + @MyTable + '))
    BEGIN   
        SET @COUNTER = @COUNTER + 1
        print @COUNTER
    end'
exec(@sql_loop)
end

这会编译,但执行时仍然出错:

消息 1087,第 16 层,状态 1,第 38 行
必须声明表变量“@MyTable”。

然后我再次在开头声明了@MyTable 变量:

alter procedure my_looping_procedure (@Country varchar(10))
as
begin
    declare 
            @MyTable varchar(50),
            @sql_loop varchar(max)

    set @MyTable = concat('my_table_', @Country, '_NA')

    select @sql_loop = '
declare 
@MyTable varchar(50),
@COUNTER INT,
@Country varchar(10),
SET @COUNTER = 1
set @MyTable = concat(''my_table_'', ' + @Country + ', ''_NA'')

WHILE (@COUNTER <= (select max(id) from ' + @MyTable + ' ))
    BEGIN   
        SET @COUNTER = @COUNTER + 1
        print @COUNTER
    end'

    exec(@sql_loop)
end

这实际上编译但抱怨国家:

消息 207,第 16 层,状态 1,第 37 行
列名“US”无效。

最后,我把最初的表集语句注释掉了:

alter procedure my_looping_procedure (@Country varchar(10))
as
begin
    declare
            @MyTable varchar(50),
            @sql_loop varchar(max)
    -- set @MyTable = concat('my_table_', @Country, '_NA')

    select @sql_loop = '
declare 
@MyTable varchar(50),
@COUNTER INT,
@Country varchar(10),
@MaxCount int
SET @COUNTER = 1
set @MyTable = concat(''my_table_'', ' + @Country + ', ''_NA'')

WHILE (@COUNTER <= (select max(id) from ' + @MyTable + ' ))
    BEGIN   
        SET @COUNTER = @COUNTER + 1
        print @COUNTER
    end'

    exec(@sql_loop)
end

这会编译并运行,但什么也不做。

谁能弄清楚我做错了什么?

一些背景: 这是参数和 while 循环问题的示例,而不是实际代码。至于为什么这样做,最初的设计只是针对一个硬编码的国家。当添加更多国家/地区时,脚本会被复制并使用硬编码的新国家/地区。 最初的设计师不再在公司工作。我目前的任务只是制作一段通用的代码,无论我们添加多少个国家都可以使用。有数百个这样的脚本,项目上的时间和资源很少。

我非常感谢使用临时表的建议,但这些表用于其他进程。在我们解决流程的潜在问题之前,我们一直坚持这种设计。

【问题讨论】:

  • 我看到一堆代码,但毫无疑问。你想做什么?
  • 这似乎是设计问题,而不是(非常注入开放代码)。每个柜台不应该有一个表格,应该有一个表格,有一个国家列。另外,LOOP 的目的是在这里吗?你所做的就是PRINT每个ID;你真的想要做什么不需要循环。
  • 事实上,你为什么要声明像@Country 这样的变量。我(愚蠢地)假设它们是参数,但事实并非如此。
  • 这是参数和 while 循环问题的示例,而不是实际代码,因为代码处理的是我雇主的财务信息。至于为什么会这样,我从一个已经不在公司的人那里继承了设计。

标签: sql sql-server tsql stored-procedures


【解决方案1】:

毫无疑问,您为什么要这样做(但是这些 cmets 非常有用,应该仔细考虑)。这是您的工作代码:

create table #my_table_US_NA(id int);

insert into #my_table_US_NA(id) values (1),(2),(3),(4);

declare @MyTable varchar(50), @Country varchar(10);
set @Country = 'US';
set @MyTable = quotename(concat('#my_table_', @Country, '_NA'));

declare @Sql nvarchar(max) = 'declare @COUNTER INT = 1; WHILE (@COUNTER <= (select max(id) from [' + @MyTable + ']))
    BEGIN   
        SET @COUNTER = @COUNTER + 1
        print @COUNTER
    end';

exec(@Sql);

drop table #my_table_US_NA;

注意 1:我已根据 Larnu 的建议添加了quotename,以避免注射的可能性。

注意 2:您的表设计与关系数据库的预期使用方式不一致。您通常不会为每个国家/地区提供单独的表格,您通常会有一个国家/地区列,允许您按国家/地区对表格进行分段。没有好的设计最终应该依赖于动态 SQL,当然您可能在某些边缘情况下需要它,但不是您的主要业务流程。

【讨论】:

  • 我同意。当您需要使用动态 SQL 来处理应该是一个简单的查询时,您就知道自己的设计(非常)有缺陷。
  • 使用不同的表来存储不同位置的相同数据是自找麻烦。我无法想象它在系统的其他方面引入的复杂性。
  • 我完全同意所有关于糟糕设计的 cmets。运气好的话,项目将持续足够长的时间来修复代码。
【解决方案2】:

我认为您想从特定国家/地区的表中收集数据,然后循环访问特定于国家/地区的数据。我会采用使用“临时”表的方法,以便您可以从动态 SQL 语句中插入数据。这就是我的意思:

create procedure my_looping_procedure as
begin
create table #MyTable (id int)
declare @COUNTER int, @Country varchar(3), @MyTable varchar(50), @sql varchar(100)

SET @COUNTER = 1
set @Country = 'US'
set @MyTable = concat('my_table_', @Country, '_NA')
set @sql = 'insert #MyTable (id) select * from ' + @MyTable
exec(@sql)

WHILE (@COUNTER<= (select max(id) from #MyTable))
    BEGIN   
        SET @COUNTER = @COUNTER + 1
        print @COUNTER
    end
end
go
exec my_looping_procedure

【讨论】:

  • 这与我在戴尔的回答中提到的问题相同。这将开放注入如果这成为参数化,
【解决方案3】:

我最终通过将 while 循环内的表达式声明为文本字符串来解决该问题,如下所示:set @WhileExpr = concat('@COUNTER &lt;= (select max(id) from ', @MyTable) 然后在 while 括号内使用它 WHILE (' + @WhileExpr + '))

很抱歉浪费了您的时间。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-09-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多