`sp_executesql` 真的接受 `nvarchar(max)` 参数吗?

Posted

技术标签:

【中文标题】`sp_executesql` 真的接受 `nvarchar(max)` 参数吗?【英文标题】:Does `sp_executesql` really accepts the `nvarchar(max)` argument? 【发布时间】:2016-09-29 13:16:41 【问题描述】:

总结:EXEC sp_executesql @code 对于@code 中超过 4000 的内容会失败,但 @code 不会被截断为 4000 个 unicode 字符。

我在 SQL Server 2014 Developer Edition 上观察到问题。

更多细节:我的 SQL 安装脚本动态定义了一些代码,因为它应该修改代码以反映环境(仅一次,在安装期间)。让以下@datasource 变量捕获特定环境的结果:

DECLARE @datasource nvarchar(100) = N'testdb.dbo.source_table'

@code变量被声明为nvarchar(max)类型,REPLACE函数用于根据需要修改字符串(即用@datasource内容替换占位符)——见sn-p 下面。

在 Management Studio 中使用@code 执行sp_executesql 时,会显示以下错误:

消息 156,级别 15,状态 1,过程 my_sp,第 86 行 关键字“AS”附近的语法不正确。 消息 102,级别 15,状态 1,过程 my_sp,第 88 行 'WHERE' 附近的语法不正确。

下面的 sn-p 是上述方式失败的代码的精确副本(待复制)。功能可能并不重要——可能只是代码的长度。 @code 内容显然被sp_executesql 截断;但是,它不应该是(见下文):

-- ... repeated from above
DECLARE @datasource nvarchar(100) = N'testdb.dbo.source_table'

DECLARE @code nvarchar(MAX) = REPLACE(N'
-- Comment comment comment comment comment comment comment comment comment.
-- Comment comment comment comment comment comment comment comment comment.
-- Comment comment comment comment comment comment comment comment comment.
CREATE PROCEDURE dbo.my_sp
AS
BEGIN
    SET NOCOUNT ON
    DECLARE @result int = -555   -- Comment comment comment comment comment.

    -- Comment comment comment comment comment comment comment comment comment.
    -- Comment comment comment comment comment comment comment comment comment.
    DECLARE @info_table TABLE (
        action nvarchar(10),    -- Comment comment comment comment comment
        firmaID int,            -- Comment comment comment comment comment
        kod numeric(8, 0),      -- Comment comment comment comment comment
        oz1 nvarchar(40),       -- Comment comment comment comment comment
        oz2 nvarchar(40),       -- Comment comment comment comment comment
        oz3 nvarchar(40),
        oz4 nvarchar(40)
    )

-- Comment comment comment comment comment comment comment comment comment.
    BEGIN TRANSACTION tran_firmy
    BEGIN TRY
        MERGE dbo.firmy AS target
        USING (SELECT kod, ico, dic, nazev,
               oz1, oz2, oz3, oz4,
               jeaktivni,
               ulice, mesto, psc
               FROM @datasource) AS source
        ON target.kod = source.kod
        WHEN MATCHED AND (COALESCE(target.ico, '''') != COALESCE(source.ico, '''')
                          OR COALESCE(target.dic, '''') != COALESCE(source.dic, '''')
                          OR COALESCE(target.nazev, '''') != COALESCE(source.nazev, '''')
                          OR COALESCE(target.nepouzivat_oz1, '''') != COALESCE(source.oz1, '''')
                          OR COALESCE(target.nepouzivat_oz2, '''') != COALESCE(source.oz2, '''')
                          OR COALESCE(target.nepouzivat_oz3, '''') != COALESCE(source.oz3, '''')
                          OR COALESCE(target.nepouzivat_oz4, '''') != COALESCE(source.oz4, '''')
                          OR COALESCE(target.jeaktivni, 0) != COALESCE(source.jeaktivni, 0)
                          OR COALESCE(target.ulice, '''') != COALESCE(source.ulice, '''')
                          OR COALESCE(target.mesto, '''') != COALESCE(source.mesto, '''')
                          OR COALESCE(target.psc, '''') != COALESCE(source.psc, '''')
                          ) THEN
            UPDATE
            SET target.ico = source.ico,
                target.dic = source.dic,
                target.nazev = source.nazev,
                target.nepouzivat_oz1 = source.oz1,
                target.nepouzivat_oz2 = source.oz2,
                target.nepouzivat_oz3 = source.oz3,
                target.nepouzivat_oz4 = source.oz4,
                target.jeaktivni = source.jeaktivni,
                target.ulice = source.ulice,
                target.mesto = source.mesto,
                target.psc = source.psc,
                target.changed = GETDATE(),
                target.changedby = ''dialog''
        WHEN NOT MATCHED THEN
            INSERT (kod, ico, dic, nazev,
                    nepouzivat_oz1, nepouzivat_oz2, nepouzivat_oz3, nepouzivat_oz4,
                    jeaktivni,
                    ulice, mesto, psc,
                    created, createdby)
            VALUES (source.kod, source.ico, source.dic, source.nazev,
                    source.oz1, source.oz2, source.oz3, source.oz4,
                    source.jeaktivni,
                    source.ulice, source.mesto, source.psc,
                    GETDATE(), ''dialog'')
        OUTPUT
            $action AS action,  -- INSERT or UPDATE
            inserted.ID AS firmaID,
            inserted.kod AS kod,
            inserted.nepouzivat_oz1 AS oz1,
            inserted.nepouzivat_oz2 AS oz2,
            inserted.nepouzivat_oz3 AS oz3,
            inserted.nepouzivat_oz4 AS oz4
        INTO @info_table;

        -- Comment comment comment comment comment comment comment comment comment.
        -- Comment comment comment comment comment comment comment comment comment.
        SET @result = @@ROWCOUNT

        -- Comment comment comment comment comment comment comment comment comment.
        -- Comment comment comment comment comment comment comment comment comment.
        DELETE FROM obchodni_zastupci AS ozt
        WHERE ozt.kod IN (
            SELECT kod FROM @info_table AS it WHER it.action = ''UPDATE''
            )

        -- Comment comment comment comment comment comment comment comment comment.
        -- Comment comment comment comment comment comment comment comment comment.
        UPDATE dodaci_adresy
            SET custID = f.ID
        FROM firmy AS f,  dodaci_adresy AS da
        WHERE da.custID IS NULL AND f.kod = da.kod_firmy

        COMMIT TRANSACTION tran_firmy
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION tran_firmy
        SET @result = -1  -- Comment comment comment comment comment comment comment comment comment.

    END CATCH
    RETURN @result          -- Comment comment comment comment comment comment comment comment comment.
END', N'@datasource', N'testdb.dbo.source_table')


-- The following prints only show that the full-length string is there
PRINT SUBSTRING(@code, 0, 4000)
PRINT '-----------------------------------------------------------'
PRINT SUBSTRING(@code, 4000, 10000)


EXEC sp_executesql @code

-- The following command also does not work (uncomment it).
-- EXEC(@code)

-- Even splitting to two variables and passing the concatenation 
-- does not work. 
-- DECLARE @code1 nvarchar(MAX) = SUBSTRING(@code, 0, 4000)
-- DECLARE @code2 nvarchar(MAX) = SUBSTRING(@code, 4000, 10000)
-- EXEC(@code1 + @code2)

注意两个PRINT 命令。第一个打印前 4000 个字符,第二个打印其余字符。它在行的中间被剪切,但它仅用于表明@code 确实包含完整的字符串。

sp_executesql (Transact-SQL) 的文档说:

[@stmt=] 声明

[...] 字符串的大小仅受可用数据库服务器的限制 记忆。在 64 位服务器上,字符串的大小限制为 2 GB, nvarchar(max) 的最大大小。

我在其他地方找到了使用EXEC(@code) 的提示,它没有sp_executesql 的限制。但是,它与文档的上述引用部分相矛盾。而且EXEC(@code)也不起作用。

当替换后的相同内容复制/粘贴到SQL控制台时,它可以工作(即创建过程)。

如何破案?

【问题讨论】:

您是在寻求解决方案来解决问题,还是在询问(如您的标题中所暗示的)sp_ExecuteSQL 的内部工作原理以及它是否真正使用nvarchar (max)?您的问题似乎是问后者,但您的结束问题 How to solve the case? 暗示前者。你问的是哪个? 首先,我需要找到任何解决方案。其次,我需要澄清。医生说它应该可以工作,但它不起作用。我已经更新了 sn-p 的结尾——替代解决方案也不起作用。请尝试复制/粘贴查看。 【参考方案1】:

sp_executesql 接受NVARCHAR(MAX)。问题是查询模板中的以下语句有错误:

    DELETE FROM obchodni_zastupci AS ozt
    WHERE ozt.kod IN (
        SELECT kod FROM @info_table AS it WHER it.action = ''UPDATE''
        )

应该是:如下:

    DELETE FROM obchodni_zastupci
    WHERE obchodni_zastupci.kod IN (
        SELECT kod FROM @info_table AS it WHERE it.action = ''UPDATE''
        )

完整的查询应该如下所示:

DECLARE @datasource nvarchar(100) = N'testdb.dbo.source_table'
DECLARE @template NVARCHAR(MAX) = N'
-- Comment comment comment comment comment comment comment comment comment.
-- Comment comment comment comment comment comment comment comment comment.
-- Comment comment comment comment comment comment comment comment comment.
CREATE PROCEDURE dbo.my_sp
AS
BEGIN
    SET NOCOUNT ON
    DECLARE @result int = -555   -- Comment comment comment comment comment.

    -- Comment comment comment comment comment comment comment comment comment.
    -- Comment comment comment comment comment comment comment comment comment.
    DECLARE @info_table TABLE (
        action nvarchar(10),    -- Comment comment comment comment comment
        firmaID int,            -- Comment comment comment comment comment
        kod numeric(8, 0),      -- Comment comment comment comment comment
        oz1 nvarchar(40),       -- Comment comment comment comment comment
        oz2 nvarchar(40),       -- Comment comment comment comment comment
        oz3 nvarchar(40),
        oz4 nvarchar(40)
    )

-- Comment comment comment comment comment comment comment comment comment.
    BEGIN TRANSACTION tran_firmy
    BEGIN TRY
        MERGE dbo.firmy AS target
        USING (SELECT kod, ico, dic, nazev,
               oz1, oz2, oz3, oz4,
               jeaktivni,
               ulice, mesto, psc
               FROM @datasource) AS source
        ON target.kod = source.kod
        WHEN MATCHED AND (COALESCE(target.ico, '''') != COALESCE(source.ico, '''')
                          OR COALESCE(target.dic, '''') != COALESCE(source.dic, '''')
                          OR COALESCE(target.nazev, '''') != COALESCE(source.nazev, '''')
                          OR COALESCE(target.nepouzivat_oz1, '''') != COALESCE(source.oz1, '''')
                          OR COALESCE(target.nepouzivat_oz2, '''') != COALESCE(source.oz2, '''')
                          OR COALESCE(target.nepouzivat_oz3, '''') != COALESCE(source.oz3, '''')
                          OR COALESCE(target.nepouzivat_oz4, '''') != COALESCE(source.oz4, '''')
                          OR COALESCE(target.jeaktivni, 0) != COALESCE(source.jeaktivni, 0)
                          OR COALESCE(target.ulice, '''') != COALESCE(source.ulice, '''')
                          OR COALESCE(target.mesto, '''') != COALESCE(source.mesto, '''')
                          OR COALESCE(target.psc, '''') != COALESCE(source.psc, '''')
                          ) THEN
            UPDATE
            SET target.ico = source.ico,
                target.dic = source.dic,
                target.nazev = source.nazev,
                target.nepouzivat_oz1 = source.oz1,
                target.nepouzivat_oz2 = source.oz2,
                target.nepouzivat_oz3 = source.oz3,
                target.nepouzivat_oz4 = source.oz4,
                target.jeaktivni = source.jeaktivni,
                target.ulice = source.ulice,
                target.mesto = source.mesto,
                target.psc = source.psc,
                target.changed = GETDATE(),
                target.changedby = ''dialog''
        WHEN NOT MATCHED THEN
            INSERT (kod, ico, dic, nazev,
                    nepouzivat_oz1, nepouzivat_oz2, nepouzivat_oz3, nepouzivat_oz4,
                    jeaktivni,
                    ulice, mesto, psc,
                    created, createdby)
            VALUES (source.kod, source.ico, source.dic, source.nazev,
                    source.oz1, source.oz2, source.oz3, source.oz4,
                    source.jeaktivni,
                    source.ulice, source.mesto, source.psc,
                    GETDATE(), ''dialog'')
        OUTPUT
            $action AS action,  -- INSERT or UPDATE
            inserted.ID AS firmaID,
            inserted.kod AS kod,
            inserted.nepouzivat_oz1 AS oz1,
            inserted.nepouzivat_oz2 AS oz2,
            inserted.nepouzivat_oz3 AS oz3,
            inserted.nepouzivat_oz4 AS oz4
        INTO @info_table;

        -- Comment comment comment comment comment comment comment comment comment.
        -- Comment comment comment comment comment comment comment comment comment.
        SET @result = @@ROWCOUNT

        -- Comment comment comment comment comment comment comment comment comment.
        -- Comment comment comment comment comment comment comment comment comment.
        DELETE FROM obchodni_zastupci
        WHERE obchodni_zastupci.kod IN (
            SELECT kod FROM @info_table AS it WHERE it.action = ''UPDATE''
            )

        -- Comment comment comment comment comment comment comment comment comment.
        -- Comment comment comment comment comment comment comment comment comment.
        UPDATE dodaci_adresy
            SET custID = f.ID
        FROM firmy AS f,  dodaci_adresy AS da
        WHERE da.custID IS NULL AND f.kod = da.kod_firmy

        COMMIT TRANSACTION tran_firmy
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION tran_firmy
        SET @result = -1  -- Comment comment comment comment comment comment comment comment comment.

    END CATCH
    RETURN @result          -- Comment comment comment comment comment comment comment comment comment.
END'


DECLARE @code nvarchar(MAX) = REPLACE(@template, N'@datasource', N'testdb.dbo.source_table');

exec (@code);

【讨论】:

@pepr 在修复成功运行的语法问题后,我认为这个问题与字符串的长度没有任何关系。但是,我不禁觉得这是对动态 sql 的一种不好使用,因为它很难维护。如果您在 Visual Studio 中有一个数据库项目,或者我猜没有它,您可以查看使用 sqlcmd 来设置变量 @EdmondQuinton:你是对的。我明天去看看。 你是对的。这是我这边的双重错误。最初,我确实使用了nvarchar(4000),这导致了非常相似的错误。然后我添加了有问题的部分。然后我将变量类型改为nvarchar(MAX),出现了类似的错误——这让我想到nvarchar(MAX)不起作用。非常感谢你的一双眼睛;)【参考方案2】:

您的查询看起来超过了 nvarchar 4000 的最大限制,在这种情况下,您必须将动态查询分成两部分

Declare @QueryA NVARCHAR(MAX),@QueryB NVARCHAR(MAX)

SET @QueryA='SELECT * FROM'
SET @QueryB=' Employee'

EXEC (@QueryA+@QueryB)

注意:如果还是同样的错误,请尝试拆分更多部分

【讨论】:

我认为你错过了问题的重点,但老实说很难说...整个帖子似乎在问为什么会发生这种情况 它真的使用nvarchar (max)吗?,但最后一句话要求解决方法......这回答了解决方法,但我认为这不是OP的预期问题。在他们澄清之前很难说清楚。 谢谢,桑迪普。 (不,我没有投反对票 :) 我已将最后一条注释掉的行添加到 sn-p。请尝试复制/粘贴 sn-p 并取消注释最后几行以尝试。它也不起作用。 Sandip -- 您应该在此处添加的是,“是的,它接受 NVARCHAR(MAX),但如果它超过 4000 个字符,如果没有解决方法,它将无法正常工作” 我不赞成阅读我的回答以了解原因。我已经用 100,000 个 NVARCHAR 字符测试了 EXEC 和 sp_executesql,从 2008 年开始没有问题 +【参考方案3】:

我不知道为什么会出错:

Msg 156, Level 15, State 1, Procedure my_sp, Line 86
Incorrect syntax near the keyword 'AS'.
Msg 102, Level 15, State 1, Procedure my_sp, Line 88
Incorrect syntax near 'WHERE'.

被解释为您的字符串长度可能太长。这显然是一个语法错误。正如 Edmon 指出的那样,你有 2 个错误。

无论如何,我发布此答案是为了消除另一个答案正在创造的神话,以及您在问题中的建议,即长度是一个问题,因为您的陈述超过了4,000 个字符。这是一个脚本,用于创建100,000 字符长度NVARCHAR SQL 语句并以EXEC (@SQL)sp_executeSQL 执行它。在 SQL 2008 SP4-OD 10.0.6547.0 (x64) 上执行都没有问题,在 2014 SP2 上也没有问题。

因此,自 2008 年版以来,至少会看起来没有问题,不需要任何解决方法

DECLARE @CharacterLength INT = 100000
DECLARE @SQL NVARCHAR(MAX) = 'SELECT ' + CHAR(39)

DECLARE @i INT = 1

WHILE (LEN(@SQL) <= @CharacterLength - 2)
BEGIN

    SET @SQL = @SQL + 'A'

END

SET @SQL = @SQL + CHAR(39)

PRINT 'Total Length: ' + CAST(LEN(@SQL) AS VARCHAR(100))

EXECUTE sp_executesql @sql

PRINT 'No Problem with sp_executesql'

BEGIN TRY
    PRINT 'Total Length: ' + CAST(LEN(@SQL) AS VARCHAR(100))
    EXEC (@SQL)
    PRINT 'No Problem with EXEC (@SQL)'
END TRY
BEGIN CATCH
    PRINT 'Yep never got here because there was no problem with over this character limit'
END CATCH

【讨论】:

【参考方案4】:

同样的情况困扰了我一段时间。对我来说,解决方案不是声明一个以上的 NVARCHAR(MAX) 变量。

在构建动态 SQL 时,您可能会将 NVARCHAR(MAX) 用于组合到传递给 sp_executesql 的最终 SQL 查询变量中的子字符串。

NVARCHAR(MAX) 的 MAX 内存分配为 2GB。您的服务器可能正在划分声明的完整 2GB PER NVARCHAR(MAX)。例如,如果您声明了三个 NVARCHAR(MAX) 变量,则您的服务器可能会分配 6GB 来执行您的脚本。这可能足以使您的 RAM 过载,具体取决于运行时执行的其他操作。

如果您知道子字符串都将少于 8,000 个字符,请使用 VARCHAR(8000) 而不是 NVARCHAR(MAX) 作为子字符串。只需将 NVARCHAR(MAX) 用于传递给 sp_executesql 的最终字符串变量(所有子字符串变量都组合在一起)。

这就是为我解决这个问题的方法。

【讨论】:

不,服务器不会为每个最大变量分配 2GB 的 RAM。无论您的问题是什么,您都清楚地误解了正在发生的事情

以上是关于`sp_executesql` 真的接受 `nvarchar(max)` 参数吗?的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server-聚焦sp_executesql执行动态SQL查询性能真的比exec好?

使用 sp_executesql 的非最佳执行计划

当使用 sp_executesql 作为过滤器时,保护 t-sql 动态代码的最佳方法是啥

javascript 用vue进行nva-test

SQLServer : EXEC和sp_executesql的区别

sp_executesql 使用