为什么声明为 NVARCHAR(MAX) 的变量会丢弃字符串块?

Why is a Variable declared as NVARCHAR(MAX) dropping chunks of the string?

无论出于何种原因,查询都被构建为字符串并传递给另一个存储过程执行。

查询量很大。

超过一千行,我们 运行 遇到了一个需要我调试的问题。

查询被构建到声明的 NVARCHAR(MAX) 变量中,但是当我使用以下 -

打印它时发生了一些奇怪的事情
WHILE @Printed < @ToPrint BEGIN 
    PRINT(SUBSTRING(
        @sql, @Printed, 4000))
    SET @Printed = @Printed + 4000
    PRINT('Printed: ' + CONVERT(VARCHAR, @Printed))
END

在打印消息的某个地方,它只是...掉了一块,我不明白为什么。 NVARCHAR(MAX) 应该能够保持 War 和和平超过 100 次,而这个查询不是 War 和和平。

我知道 PRINT(...) 有一次只能打印 4000 个字符的限制(因此循环),但这并不能解释为什么 @sql 变量只是丢失一些地方。

如果有帮助,具体来说,在打印前 4,000 个字符后,块掉落的位置大约是 1,600 个字符。

为什么要这样做?我是否缺少在查询开始时设置系统变量(如 NOCOUNT 或 ARITHABORT?我什至不知道它们的作用,或者它们是否参与其中。


编辑:MCVE:Here. 要复制,复制粘贴到 Microsoft SQL Server Management Studio 并点击 'F5'。打印的消息将不包含完整的@sql。

这对我来说很好用:

DECLARE @sql nvarchar(max) = 
    REPLICATE(CONVERT(nvarchar(max), N'a'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'b'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'c'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'd'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'e'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'f'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'g'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'h'), 4000)
  + REPLICATE(CONVERT(nvarchar(max), N'i'), 4000);


PRINT LEN(@sql);  -- characters
PRINT DATALENGTH(@sql); -- bytes
PRINT '';

DECLARE @Printed int = 1, @ToPrint int = LEN(@sql);

WHILE @Printed < @ToPrint BEGIN 
    PRINT(SUBSTRING(
        @sql, @Printed, 4000))
    SET @Printed = @Printed + 4000
    PRINT('Printed: ' + CONVERT(varchar(11), @Printed)) -- *
END

* Always specify length.

输出为:

36000
72000

aaaaaaaaaa... 4000 As ...aaa
Printed: 4001
bbbbbbbbbb... 4000 Bs ...bbb
Printed: 8001
cccccccccc... 4000 Cs ...ccc
Printed: 12001
dddddddddd... 4000 Ds ...ddd
Printed: 16001
eeeeeeeeee... 4000 Es ...eee
Printed: 20001
ffffffffff... 4000 Cs ...fff
Printed: 24001
gggggggggg... 4000 As ...ggg
Printed: 28001
hhhhhhhhhh... 4000 Bs ...hhh
Printed: 32001
iiiiiiiiii... 4000 Cs ...iii
Printed: 36001

所以,我认为问题出在其他地方。无论如何,这是验证动态 SQL 内容的一种非常草率的方法。相反,我会这样做:

SELECT CONVERT(xml, @sql);

然后您可以单击输出单元格,它会在 XML 文本编辑器中打开以供查看(然后您可以将该输出复制并粘贴到查询 window 中,如果您需要 IntelliSense 或任何有机会执行,但你必须替换编码字符,如 &gt; --> >。我在这里讨论这种方法(和另一种方法):

如果您坚持以这种砌砖的方式进行操作,那么此时可能存在某种非打印字符或字符串终止字符。如果你说它大约是 5,600 个字符,那么你可以这样做:

DECLARE @i int = 5550, @c nchar(1);
WHILE @i <= 5650
BEGIN
  PRINT '';
  SET @c = SUBSTRING(@sql, @i, 1);
  PRINT '------   ' + RTRIM(@i) + '------:';
  PRINT 'Raw:     ' + @c;
  PRINT 'ASCII:   ' + ASCII(@c);
  PRINT 'UNICODE: ' + UNICODE(@c);
  SET @i += 1;
END

您应该能够向下扫描并匹配您在损坏的打印输出中看到的最后一个字符序列。然后寻找 Raw: 行为空且 ASCII: 行不是典型的任何内容(9101332).

但我不认为这是问题所在。我将回到之前的评论中,我建议字符串本身就是问题所在。在问题中,您提到了 @sql,但没有说明它是如何填充的。我敢打赌,您添加的某些字符串会被截断。需要注意的事项:

  • 中间 variables/parameters 声明为 varchar/nvarchar 但没有长度(sometimes leads to silent truncation at 1 character, and sometimes 30):

      DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.table ';
      DECLARE @where nvarchar = N'WHERE some condition...';
      SET @sql += @where;
      PRINT @sql;
    

    输出:

      SELECT * FROM dbo.table W
    
  • 中间 variables/parameters 声明为 varchar/nvarchar 但太短(这导致 silent 截断声明是):

      DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.table ';
      DECLARE @where nvarchar(10) = N'WHERE some condition...';
      SET @sql += @where;
      PRINT @sql;
    

    输出:

      SELECT * FROM dbo.table WHERE some
    
  • 显式 CONCATNULL,这导致 静默 丢弃任何 NULL 输入):

      DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.table ';
      DECLARE @where nvarchar(32);
      DECLARE @orderby nvarchar(32) = N' ORDER BY col1';
      SET @sql = CONCAT(@sql, @where, @orderby);
      PRINT @sql;
    

    输出:

      SELECT * FROM dbo.table  ORDER BY col1
    
  • 连接大于 4000 个字符的 Unicode 字符串文字时不使用 N 前缀 (example here):

      DECLARE @sql nvarchar(max) = '';
    
      SET @sql = @sql + '... literally 4001 characters ...';
    

    此处的输出(如示例所示)将被截断为 4,000 个字符。但是,如果您正确定义字符串,就不会发生这种情况:

      DECLARE @sql nvarchar(max) = N'';
    
      SET @sql = @sql + N'... literally 4001 characters ...';
    

在过于复杂的动态 SQL 生成中很难发现这些东西,因此简化并尝试任何您可以分而治之的方式来划分和征服最终字符串中的主要组件从来都不是一个坏主意。根据您尝试的重现,我几乎可以肯定地猜测这是“变量声明太短”的症状。最安全的方法是确保对动态 SQL 字符串的每个输入都应声明为 nvarchar(max);除了受元数据约束的实体名称外,没有真正充分的理由使用其他任何东西。