以编程方式使用某些存储过程时,我应该始终使用动态 sql 吗?

Should I always use dynamic sql when programatically use some stored procedure?

我有一个存储过程,可以获取table中的记录数,其中@tableName是存储过程的参数。我们称它为 FastCount:

SELECT OBJECT_NAME(object_id), SUM(row_count) AS rows
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID(@tableName)
  AND index_id < 2
GROUP BY OBJECT_NAME(object_id);

现在,假设我有 50 个 table,例如 data_1950、data_1951、.....data_2000。我写了一个batch,查询每个table的记录数,放到一个临时的table中。它就像一个魅力

CREATE TABLE #Temp
(
    TableName varchar(30), 
    RecordsCount int
)

DECLARE @sql as varchar(max)
DECLARE @yearN as int = 1950
DECLARE @tbName as sysname

WHILE @yearN <= 2000
BEGIN
    SET @tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,@yearN))
    SET @sql = N'Exec [dbo].FastCount @tableName=' + @tbName

    INSERT INTO #Temp 
        EXEC (@sql)
    SET @yearN = @yearN + 1
END

SELECT * FROM #Temp

DROP TABLE #Temp

但是,如果我替换动态 SQL 字符串部分

SET @sql = N'Exec [dbo].FastCount @tableName=' + @tbName

INSERT INTO #Temp 
    EXEC (@sql)

直接调用

INSERT INTO #Temp 
    EXEC [dbo].FastCount @tableName = @tbName

然后整个批次就不行了...

所以我不明白为什么...在以编程方式使用存储过程时,我是否应该始终使用动态 SQL 字符串和 exec(@sql)。非常感谢您抽出时间来看。

一般情况下,您应该避免使用动态 SQL,并且您应该避免授予执行动态 SQL 的权限,除非绝对必要。这是出于性能和安全原因。

处理这种情况的典型方法是使用 partitioned view:

CREATE VIEW DataView
AS
    SELECT '1950' TableName, * FROM Data_1950
    UNION ALL
    SELECT '1951' TableName, * FROM Data_1951
    UNION ALL
    SELECT '1952' TableName, * FROM Data_1952
    UNION ALL
    SELECT '1953' TableName, * FROM Data_1953
    UNION ALL
    SELECT '1954' TableName, * FROM Data_1954
    UNION ALL
    SELECT '1955' TableName, * FROM Data_1955

(继续添加 select 语句,直到涵盖所有 table 语句。)

现在要获得 table 计数,您需要做的就是执行此操作:

SELECT TableName, COUNT(*) RecordCount
FROM DataView
GROUP BY TableName

是不是简单多了?

好的,这是您在原始问题中提出的两个场景中发生的情况。 (是的,现实是可能有更好的方法来实现你的最终结果,但让我们看看你提出的实际问题......为什么你的 INSERT / EXEC 的行为不同,这取决于你如何打电话).

首先,您声明了变量,它将包含您的 table 姓名:

DECLARE @tbName as sysname

然后你的循环块会逐渐增加年份数,以生成不同的 table 名称。循环块本身并没有什么错误,所以让我们看一个使用 table 名称之一的示例,看看 WHILE 块中发生了什么。以第一个table名字为例,就是[dbo].data_1950.

您的声明:

set @tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,@yearN))

最终采用字符串“[dbo].data_1950” - 它来自将“[dbo].data_”与年份数字(在本例中为 1950)连接转换为字符串(varchar) - 并将其传递给 QUOTENAME() 函数。 QUOTENAME() 函数接受它的输入和第二个参数,这是输入应该用引号引起来的字符(如果没有传递第二个参数,则默认为 [])。因此,如果我们随后将 @tbName 变量转换为字符串,它将显示为:

[[dbo].data_1950]

现在我们可以看到 SQL 处理“sysname”数据类型的时髦方式。 (事实上​​,当您进一步阅读时,也许问题主要与“sysname”数据类型无关,但无论如何,请从中删除您想要的内容)。老实说,“sysname”本身是一种有点古怪的数据类型,除非绝对必要,否则我倾向于避开它。但无论如何,关于您所看到的问题的详细信息。

第 1 步 - 我创建了您的存储过程的一个版本,但我包含了一个语句,该语句将输出传入的 @tableName 参数的值。这让我们有机会看到什么SQL是在两种不同的场景下做的,然后解释为什么结果不同。

CREATE PROC [dbo].FastCount
(
    @tableName varchar(100)
)
AS
BEGIN
    PRINT @tableName;

    SELECT OBJECT_NAME(object_id), SUM(row_count) AS rows
    FROM sys.dm_db_partition_stats
    WHERE object_id = OBJECT_ID(@tableName)
    AND index_id < 2
    GROUP BY OBJECT_NAME(object_id);
END

第 2 步 - 我们的第一个场景是执行动态 SQL.

set @tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,@yearN))
set @sql = N'Exec [dbo].FastCount @tableName=' + @tbName
Insert Into #Temp Exec(@sql)

现在,我们知道@tbName 变量包含

[[dbo].data_1950]

因此我们可以推断出@sql变量包含

Exec [dbo].FastCount @tableName=[[dbo].data_1950]

所以这实际上是由 Exec(@sql) 命令执行的语句。

当这 运行s 时,我们查看 PRINT 命令的输出,我们看到

[dbo].data_1950

并且我们看到了查询的结果(table 名称和行数)。这当然是有道理的,因为我们的table名称是“data_1950”,而table的模式是“dbo”,所以SELECT语句得到行数将按预期工作。

第 3 步 - 运行 直接执行 EXEC 命令,不使用 @sql 变量,即

Insert Into #Temp Exec [dbo].FastCount @tableName = @tbName

现在,当我们查看执行“FastCount”存储过程的 PRINT 命令的输出时,我们看到

[[dbo].data_1950]

当然,现在这不会产生我们预期的结果,因为我们告诉 SQL 查找名为“[dbo].table 的行数。data_1950"(在没有特定模式的情况下,SQL 将只采用默认模式。在这种情况下,使用 [dbo] 模式,我们会告诉 SQL 获取名为 [dbo].[[dbo].data_1950] 的 table 中的行计数 - 这显然不是 table 名称)。

您应该看到明显的区别 - 在一种情况下,传递到存储中的参数值是对 table 名称的“正确”引用,而在另一种情况下则不是。

作为最后一步,让我们看看如何执行“非动态”SQL,以获得我们需要的结果。在这种情况下,不需要 QUOTENAME() 函数:

set @tbName = N'[dbo].data_' + Convert(nvarchar,@yearN)
Insert Into #Temp Exec [dbo].FastCount @tableName = @tbName

当我们以这种方式 运行 时,我们从 PRINT 命令中看到了预期的输出 ([dbo].data_1950),并且我们看到了预期的查询结果(包含 table 名称和行数)。

我能准确解释一下这种行为吗?呃,不一定......也许其他人能够具体解释发生了什么,以及为什么。我唯一的解释是,当 EXEC() 语句传递动态 sql (即 @sql 变量)时,它首先解释整个字符串并剥离标识符(在这种情况下,周围的 [ ] ... 它是根据什么做出那个决定的,我不知道)。与非动态执行相反,@tbName 值 ([[dbo].data_1950]) 只是作为参数直接传递,没有修改(因此导致我们意想不到的最终结果锯)。

希望这些信息对您有用(或者至少对将来的其他人有用!)。