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

Wil*_*ill 1 sql-server nvarchar max

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

查询量很大。

超过一千行,我们遇到了一个问题,需要我对其进行调试。

查询被构建到声明的NVARCHAR(MAX)变量中,但是当我使用以下命令打印它时,发生了奇怪的事情 -

WHILE @Printed < @ToPrint BEGIN 
    PRINT(SUBSTRING(
        @sql, @Printed, 4000))
    SET @Printed = @Printed + 4000
    PRINT('Printed: ' + CONVERT(VARCHAR, @Printed))
END
Run Code Online (Sandbox Code Playgroud)

在打印消息的某个地方,它只是......丢失了一大块,我不明白为什么。NVARCHAR(MAX)应该能够举行战争与和平超过 100 次,并且这个查询不是战争与和平。

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

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

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


编辑:MCVE:这里。要重现,请复制粘贴到 Microsoft SQL Server Management Studio 中并按“F5”。打印的消息将不包含完整的@sql。

Aar*_*and 6

这对我来说效果很好:

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
Run Code Online (Sandbox Code Playgroud)

*始终指定长度

输出是:

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
Run Code Online (Sandbox Code Playgroud)

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

SELECT CONVERT(xml, @sql);
Run Code Online (Sandbox Code Playgroud)

然后,您可以单击输出单元格,它将在 XML 文本编辑器中打开以供审阅(如果您想要 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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 中间变量/参数声明为varchar/nvarchar但没有长度(有时会导致在 1 个字符处无提示截断,有时会导致 30 个字符):

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

    输出:

    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
    
    Run Code Online (Sandbox Code Playgroud)
  • 中间变量/参数声明为varchar/nvarchar但太短(无论声明是什么都会导致静默截断):

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

    输出:

    SELECT CONVERT(xml, @sql);
    
    Run Code Online (Sandbox Code Playgroud)
  • 显式CONCAT使用NULL,这会导致默默地删除任何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;
    
    Run Code Online (Sandbox Code Playgroud)

    输出:

    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
    
    Run Code Online (Sandbox Code Playgroud)
  • 连接超过 4000 个字符的 Unicode 字符串文字时不使用 N 前缀(示例见此处):

      DECLARE @sql nvarchar(max) = '';
    
      SET @sql = @sql + '... literally 4001 characters ...';
    
    Run Code Online (Sandbox Code Playgroud)

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

      DECLARE @sql nvarchar(max) = N'';
    
      SET @sql = @sql + N'... literally 4001 characters ...';
    
    Run Code Online (Sandbox Code Playgroud)

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