光标导致 SSMS 崩溃

ivG*_*Geo 3 sql-server ssms cursors json

我有一个游标,它从一组表中生成一条 JSON 文本记录。光标一直在使 SSMS 崩溃。该脚本运行了一段时间,然后 SSMS 失败。下面是我编写的导致崩溃的代码。

DECLARE @ROW_ID int  -- Here we create a variable that will contain the ID of each row.

DECLARE JSON_CURSOR CURSOR   -- Here we prepare the cursor and give the select statement to iterate through
FOR

        SELECT  -- Our select statement (here you can do whatever work you wish)
            ROW_NUMBER() OVER (ORDER BY NAME_2-1,NAME_2-2,FIELD_1-1,FIELD_1-2) AS ROWID
        FROM
            (
            SELECT 
                FIELD_1-1
                ,FIELD_1-2
                ,NAME_1-1
                ,NAME_1-2
            FROM 
                (
                SELECT FIELD_1-1,FIELD_1-2,NAME,VALUE
                FROM TABLE_1
                WHERE NAME IN ('NAME_1-1','NAME_1-2')
                ) AS SRC
            PIVOT
                (
                MAX(VALUE_1) FOR NAME IN ([FIELD_1-1],[FIELD_1-2])
                ) AS PVT
            ) AS T0
        LEFT JOIN TABLE_2 AS [P] ON T0.NAME_1-2=P.NAME_2-2;

OPEN JSON_CURSOR -- This charges the results to memory

FETCH NEXT FROM JSON_CURSOR INTO @ROW_ID -- We fetch the first result

WHILE @@FETCH_STATUS = 0 --If the fetch went well then we go for it
BEGIN

    SELECT * FROM
        (
        SELECT  -- Our select statement (here you can do whatever work you wish)
            FIELD_2-1
            ,FIELD_2-2
            ,FIELD_1-1
            ,FIELD_1-2
            ,T0.NAME_1-1
            ,ROW_NUMBER() OVER (ORDER BY FIELD_2-1,FIELD_2-2,FIELD_1-1,FIELD_1-2) AS ROWID
        FROM
            (
            SELECT 
                FIELD_1-1
                ,FIELD_1-2
                ,NAME_1-1
                ,NAME_1-2
            FROM 
                (
                SELECT FIELD_1-1,FIELD_1-2,NAME,VALUE
                FROM TABLE_1
                WHERE NAME IN ('NAME_1-1','NAME_1-2')
                ) AS SRC
            PIVOT
                (
                MAX(VALUE_1) FOR NAME IN ([FIELD_1-1],[FIELD_1-2])
                ) AS PVT
            ) AS T0
        LEFT JOIN TABLE_2 AS [P] ON T0.NAME_1-2=P.NAME_2-2
        ) AS T1
    WHERE ROWID = @ROW_ID  -- In regards to our latest fetched ID
    order by (FIELD_2-1,FIELD_2-2,FIELD_1-1,FIELD_1-2)
    FOR JSON PATH, ROOT('FIELD_2-1');

FETCH NEXT FROM JSON_CURSOR INTO @ROW_ID -- Once the work is done we fetch the next result

END
-- We arrive here when @@FETCH_STATUS shows there are no more results to treat
CLOSE JSON_CURSOR  
DEALLOCATE JSON_CURSOR -- CLOSE and DEALLOCATE remove the data from memory and clean up the process
Run Code Online (Sandbox Code Playgroud)

从 Windows 日志:

首先发生以下 .NET 运行时错误:

错误 7/20/2018 2:27:58 PM .NET 运行时 1026 无

应用程序:Ssms.exe 框架版本:v4.0.30319 描述:由于未处理的异常,进程被终止。异常信息:System.Windows.Forms.NativeWindow.CreateHandle(System.Windows.Forms.CreateParams) 处 System.Windows.Forms.Control.CreateHandle() 处 System.Windows.Forms.TextBoxBase.CreateHandle() 处的 System.ComponentModel.Win32Exception ) 在 System.Windows.Forms.Control.CreateControl(Boolean) 在 System.Windows.Forms.Control.CreateControl(Boolean) 在 System.Windows.Forms.Control.CreateControl() 在 System.Windows.Forms.Control.WmShowWindow( System.Windows.Forms.Message ByRef) at System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.ScrollableControl.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.ContainerControl。

第二个应用程序错误:

错误 7/20/2018 下午 2:27:58 应用程序错误 1000 (100)

错误的应用程序名称:Ssms.exe,版本:2017.140.17277.0,时间戳:0x5b304116 错误的模块名称:KERNELBASE.dll,版本:10.0.14393.2189,时间戳:0x5abda7d6 异常代码:430x0x0620 错​​误代码:0x5b304120应用程序启动时间:0x01d4205466d2b650 错误应用程序路径:C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\Ssms.exe 错误模块路径:C:\WINDOWS\System32\KERSELBASE.dll 报告 ID: 03b3b0c6-0839-4562-a71f-f5b4fc0a3029 故障包全名: 故障包相关应用程序 ID:

此脚本的目的是以 JSON 格式输出单个条目,该条目将发布到云中的数据服务。同样在 SSMS 中,我将结果发送到网格。

这是我正在创建的存储过程的完整查询。

    /****** Object:  StoredProcedure [dbo].[sp_acQ-Zerion_POST_HTTP]    Script Date: 6/15/2018 10:48:28 AM ******/
    SET ANSI_NULLS ON
    GO

    SET QUOTED_IDENTIFIER ON
    GO


    /* FILL IN WITH DB */
    ALTER PROCEDURE [dbo].[sp_acQ-Zerion_POST_HTTP] --@ID varchar(50) 
    AS

    /* define variables */
    Declare @hr int;
    Declare @Object as Int;
    Declare @ResponseText as Varchar(8000);
    Declare @src varchar(255), @desc varchar(255),@status int,@msg varchar(255);

    -------------------------------------------------------------------------------------
    /* Cursor Pt 1 */
    -------------------------------------------------------------------------------------

     DECLARE @ROW_ID int  -- Here we create a variable that will contain the ID of each row.

        DECLARE JSON_CURSOR CURSOR   -- Here we prepare the cursor and give the select statement to iterate through
        FOR

                SELECT  -- Our select statement (here you can do whatever work you wish)
                    ROW_NUMBER() OVER (ORDER BY NAME_2-1,NAME_2-2,FIELD_1-1,FIELD_1-2) AS ROWID
                FROM
                    (
                    SELECT 
                        FIELD_1-1
                        ,FIELD_1-2
                        ,NAME_1-1
                        ,NAME_1-2
                    FROM 
                        (
                        SELECT FIELD_1-1,FIELD_1-2,NAME,VALUE
                        FROM TABLE_1
                        WHERE NAME IN ('NAME_1-1','NAME_1-2')
                        ) AS SRC
                    PIVOT
                        (
                        MAX(VALUE_1) FOR NAME IN ([FIELD_1-1],[FIELD_1-2])
                        ) AS PVT
                    ) AS T0
                LEFT JOIN TABLE_2 AS [P] ON T0.NAME_1-2=P.NAME_2-2
                WHERE NAME_1-1 IN ('True','False');          

        OPEN JSON_CURSOR -- This charges the results to memory

        FETCH NEXT FROM JSON_CURSOR INTO @ROW_ID -- We fetch the first result

        WHILE @@FETCH_STATUS = 0 --If the fetch went well then we go for it
        BEGIN
    -------------------------------------------------------------------------------------
    -------------------------------------------------------------------------------------

    Declare @Records as Varchar(8000)=
        (
    -------------------------------------------------------------------------------------
    /* Cursor Pt 2 */
    -------------------------------------------------------------------------------------

            SELECT * FROM
                (
                SELECT  -- Our select statement (here you can do whatever work you wish)
                    FIELD_2-1
                    ,FIELD_2-2
                    ,FIELD_1-1
                    ,FIELD_1-2
                    ,T0.NAME_1-1
                    ,ROW_NUMBER() OVER (ORDER BY FIELD_2-1,FIELD_2-2,FIELD_1-1,FIELD_1-2) AS ROWID
                FROM
                    (
                    SELECT 
                        FIELD_1-1
                        ,FIELD_1-2
                        ,NAME_1-1
                        ,NAME_1-2
                    FROM 
                        (
                        SELECT FIELD_1-1,FIELD_1-2,NAME,VALUE
                        FROM TABLE_1
                        WHERE NAME IN ('NAME_1-1','NAME_1-2')
                        ) AS SRC
                    PIVOT
                        (
                        MAX(VALUE_1) FOR NAME IN ([FIELD_1-1],[FIELD_1-2])
                        ) AS PVT
                    ) AS T0
                LEFT JOIN TABLE_2 AS [P] ON T0.NAME_1-2=P.NAME_2-2
                WHERE NAME_1-1 IN ('True','False')
                ) AS T1
            WHERE ROWID = @ROW_ID  -- In regards to our latest fetched ID
            order by (FIELD_2-1,FIELD_2-2,FIELD_1-1,FIELD_1-2)
            FOR JSON PATH, ROOT('FIELD_2-1');

    -------------------------------------------------------------------------------------
    -------------------------------------------------------------------------------------
        )

    /* wrap records in JSON object */
    Declare @Body as varchar(8000) = @Records

    /* create XMLHTTP object and send object via HTTP POST */
    Exec @hr=sp_OACreate 'MSXML2.ServerXMLHTTP', @Object OUT;
    if @hr <> 0 begin Raiserror('sp_OACreate MSXML2.ServerXMLHttp.3.0 failed', 16,1) return end


    Exec @hr = sp_OAMethod @Object, 'open', NULL, 'post','https://dataflownode.zerionsoftware.com/domain/solutions/services/webhooks/4b9b0f4b8a4b4387ec1642fdaabec7b400d5c938-7be9d5a63b5cba8ab72cd3410429e2635f68a687', 'false'
    if @hr <>0 begin set @msg = 'sp_OAMethod Open failed' goto eh end

    Exec @hr = sp_OAMethod @Object, 'setRequestHeader', null, 'Content-Type', 'application/json'
    if @hr <>0 begin set @msg = 'sp_OAMethod setRequestHeader failed' goto eh end

    Exec @hr = sp_OAMethod @Object, 'send', null, @Body
    if @hr <>0 begin set @msg = 'sp_OAMethod Send failed' goto eh end

    if @status <> 200 begin set @msg = 'sp_OAMethod http status ' + str(@status) goto eh end

    Exec @hr = sp_OAMethod @Object, 'responseText', @ResponseText OUT--PUT
    Select @ResponseText

    -------------------------------------------------------------------------------------
    /* Cursor Pt 3 */
    -------------------------------------------------------------------------------------

    FETCH NEXT FROM JSON_CURSOR INTO @ROW_ID -- Once the work is done we fetch the next result

    END
    -- We arrive here when @@FETCH_STATUS shows there are no more results to treat
    CLOSE JSON_CURSOR  
    DEALLOCATE JSON_CURSOR -- CLOSE and DEALLOCATE remove the data from memory and clean up the process

    -------------------------------------------------------------------------------------
    -------------------------------------------------------------------------------------

    --if @hr <>0 begin set @msg = 'sp_OAMethod read response failed' goto
    IF @hr <> 0  
    BEGIN  
       EXEC sp_OAGetErrorInfo @object  
        RETURN  
    goto
    eh end

    /* clean-up after data is sent */
    Exec @hr=sp_OADestroy @Object
    return
    eh:
    Raiserror(@msg, 16, 1)
    return

    IF @hr <> 0  
    BEGIN  
       EXEC sp_OAGetErrorInfo @object, @src OUT, @desc OUT   
       raiserror('Error Creating COM Component 0x%x, %s, %s',16,1, @hr, @src, @desc)  
        RETURN  



    END;  
    GO
Run Code Online (Sandbox Code Playgroud)

Sol*_*zky 5

  1. 首先,看起来你在这里做了大量的额外工作。游标中的查询是WHILE循环中查询的子集,数据在任何时间点都不会发生变化。所以它只是一遍又一遍地执行相同的查询,只是针对不同的行。将初始查询的结果存储到本地临时表中,然后将其用于 CURSOR 和 WHILE 循环查询应该效率更高:

    CREATE TABLE #Data
    (
      [RowID] INT NOT NULL IDENTITY(1, 1),
      [FIELD_2-1] {data_type},
      [FIELD_2-2] {data_type},
      [FIELD_1-1] {data_type},
      [FIELD_1-2] {data_type},
      [T0.NAME_1-1] {data_type}
    );
    
    DECLARE @TotalRows INT;
    
    INSERT INTO #Data ([FIELD_2-1], [FIELD_2-2], [FIELD_1-1], [FIELD_1-2],
                       [T0.NAME_1-1])
      SELECT [FIELD_2-1], [FIELD_2-2], [FIELD_1-1], [FIELD_1-2], [T0.NAME_1-1]
      FROM (
            SELECT  -- Our select statement (here you can do whatever work you wish)
                FIELD_2-1
                ,FIELD_2-2
                ,FIELD_1-1
                ,FIELD_1-2
                ,T0.NAME_1-1
            FROM
                (
                SELECT 
                    FIELD_1-1
                    ,FIELD_1-2
                    ,NAME_1-1
                    ,NAME_1-2
                FROM 
                    (
                    SELECT FIELD_1-1,FIELD_1-2,NAME,VALUE
                    FROM TABLE_1
                    WHERE NAME IN ('NAME_1-1','NAME_1-2')
                    ) AS SRC
                PIVOT
                    (
                    MAX(VALUE_1) FOR NAME IN ([FIELD_1-1],[FIELD_1-2])
                    ) AS PVT
                ) AS T0
            LEFT JOIN TABLE_2 AS [P] ON T0.NAME_1-2=P.NAME_2-2
            ) AS T1
        ORDER BY [FIELD_2-1], [FIELD_2-2], [FIELD_1-1], [FIELD_1-2];
    
    SET @TotalRows = @@ROWCOUNT;
    
    Run Code Online (Sandbox Code Playgroud)
  2. 然后,您可以CURSOR完全摆脱 ,因为它无论如何只用于获取总行数,并将WHILE循环更改为简单计数器。

  3. 最后,将这两部分放在一起,WHILE循环变为:

    DECLARE @Index INT = 1,
            @Records VARCHAR(8000);
    
    WHILE (@Index <= @TotalRows)
    BEGIN
    
      SET @Records = (
    
      SELECT [FIELD_2-1], [FIELD_2-2], [FIELD_1-1], [FIELD_1-2], [T0.NAME_1-1], [RowID]
      FROM   #Data
      WHERE  [RowID] = @Index
      FOR JSON PATH, ROOT('FIELD_2-1')
                     );
    
      SET @Index += 1;
    END;
    
    Run Code Online (Sandbox Code Playgroud)

综上所述:

  1. 您可能不应该@Records在循环的每次迭代中重新声明变量。在循环之前声明一次,并且每次在循环内都设置它。
  2. 您不需要该@Body变量,因为它不执行任何操作。您只需将其设置为 的值@Records,这会浪费 CPU 和 RAM(以及时间)。
  3. 每个循环的每次迭代,您执行sp_OACreate,但您从不调用sp_OADestroy(或任何调用)。我猜这最终会在内存中创建许多对象。您可能还需要再执行sp_OAMethod一次,就在Destroy关闭请求之前。您需要四处检查以查看是否是这种情况。您不想让任何孤立的网络套接字保持打开状态。
  4. 虽然您实际上可能会使其工作,但使用 OLE 自动化存储过程(即sp_OA*)是相当危险的。自 SQL Server 2005 发布以来,它们已被弃用,转而使用 SQLCLR。使用 SQLCLR 代替 OLE 自动化过程有很多优点,包括:

    • 能够使用NVARCHAR(MAX)而不是被VARCHAR(8000)和卡住NVARCHAR(4000)。事实上,你甚至可以发送XML. OLE 自动化过程不处理任何在 SQL Server 2000 之后添加的数据类型。
    • 更好的安全性
    • 更好的内存处理
    • 更稳定

    您可以使用 .NETHttpWebRequest类。或者,如果您不想编写任何代码,SQL#(我创建的)中存在一个预先完成的 SQLCLR 函数。该函数是INET_GetWebPages,它处理各种各样的场景(即,您可以传入自定义标头、将内容发送为GETorPOST等)。但是,需要明确的是,INET_GetWebPages仅在完整版中可用(即不在免费版中)

  5. 尽管如此,崩溃的是 SSMS 而不是 SQL Server 本身,这似乎很奇怪。如果按照上面的建议清理代码(除了切换到使用 SQLCLR 之外的所有代码)都不能修复错误,您可以随时注释掉这些EXEC sp_OA*语句。首先注释掉所有EXEC sp_OA*语句以及IF每个语句之后的语句。然后,如果这使 proc 不再出错,则开始一个一个地取消注释每个EXEC/IF对语句(除了第一组:如果取消注释sp_OACreate,则必须将其与sp_OADestroy 同一范围内的an 配对!)

抛开所有这些建议不谈,导致崩溃的实际问题(正如我们在聊天中发现的那样)最终是返回到 SSMS 的结果集数量太多。在问题的第一个代码块中,查询返回了 2775 行。在存储过程的上下文中,没有结果集返回给客户端;所有结果都存储在一个VARCHAR(8000)变量中。但在测试中,它只是一个直线SELECT。当不将每个结果集转储到单独的网格时,SSMS 不会崩溃。

此外,如果您自己去这个代码,你会如果使用SQL Server 2017年(或更新版本)遇到“问题”部署代码时,即使使用SQL Server 2016年,您还需要设置大会EXTERNAL_ACCESS哪将需要签署大会和其他一些小步骤。请在此处查看我的帖子,了解有关如何使用或不使用 Visual Studio 以及以适用于 SQL Server 2017(或更新版本)的方式处理此问题的说明:

SQLCLR 与 SQL Server 2017,第 2 部分:“CLR 严格安全性”——解决方案 1(目标是处理安全性和程序集的单一、自包含安装脚本,并且没有任何外部依赖项,因此很容易开发、测试和生产系统之间的版本和/或转移)


Dav*_*oft 5

要解决 SSMS 问题,请设置 NOCOUNT ON 并且不要将结果发送给客户端。

ServerXMLHTTP.Send,当从不推荐使用的 sp_OAxxx COM 互操作存储过程调用时,有 8000 个字符的限制,因此它不是这项工作的好选择。

在 SQL CLR 存储过程或许多客户端编程环境(如 PowerShell、Python、.NET 等)中执行此操作是微不足道的。所有这些都可以从 SQL 代理作业调用。

此外,您没有正确销毁您正在创建的 COM 对象。您在循环中调用 sp_OACreate,但不调用 sp_OADestroy。

此外,当您复制(可能是二手或三手)我15 年前关于如何使用 sp_OAxxx COM 互操作程序中的 ServerXmlHttp 的USENET 帖子时,您还缺少一行。

   exec @hr = sp_OAGetProperty @obj, 'status', @status OUT
   if @hr <0 begin  set @msg = 'sp_OAMethod read status failed' goto eh end
Run Code Online (Sandbox Code Playgroud)

总而言之,我建议您停止并用不同的语言编写此过程。

这是一个示例,可让您开始了解如何将 FOR JSON 查询发布到 HTTP 端点并获取响应。

作为背景,FOR JSON查询作为一列、多行的结果集流式传输到客户端,其中 JSON 跨行。因此,您可以通读结果行并将内容发布到 HTTP 端点。无论文档有多大,都不会在 SQL 中进行缓冲。

你这样称呼

declare @rc int  = 0
declare @body nvarchar(max) 

exec postjson 'select * from sys.objects for json path', 'http://localhost:51801/api/values',  @rc out, @body out

select @rc, @body
Run Code Online (Sandbox Code Playgroud)

这是 C# 源代码

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;
using System.Net;
using System.IO;

public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void PostJSON(string sqlForJSONQuery, string targetURI, out int responseCode, out string responseBody)
    {
        using (var con = new SqlConnection("Context Connection=true"))
        {
            con.Open();
            var cmd = con.CreateCommand();
            cmd.CommandText = sqlForJSONQuery;

            using (var rdr = cmd.ExecuteReader())
            {
                var req = WebRequest.CreateHttp(targetURI);
                req.Method = "Post";
                req.ContentType = "application/json";
                using (var rs = req.GetRequestStream())
                {

                    while (rdr.Read())
                    {
                        var val = rdr.GetString(0);
                        //SqlContext.Pipe.Send(val);
                        var buf = Encoding.UTF8.GetBytes(val);
                        rs.Write(buf, 0, buf.Length);
                    }
                }
                HttpWebResponse resp;
                try
                {
                    resp = (HttpWebResponse)req.GetResponse();
                }
                catch (WebException ex)
                {
                    resp = (HttpWebResponse)ex.Response;

                }

                responseCode = (int)resp.StatusCode;

                using (var respStream = resp.GetResponseStream())
                {
                    using (var sr = new StreamReader(respStream, Encoding.UTF8))
                    {
                        responseBody = sr.ReadToEnd();
                    }
                }


            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以使用SQL Server Data Tools构建和部署它。这为您的 SQL Server 开发提供了完整的 Visual Studio 集成,包括 SQL CLR,涵盖编码、调试、部署和管理源代码控制。

或者至少,这里是如何只用 SQL Server 上的命令行来做到这一点。

在您的 SQL Server 上创建一个名为 的目录c:\PostJSON,并在其中PostJSON.cs使用上述源代码创建。然后在该文件夹中打开命令提示符并运行:

PS C:\PostJSON> C:\windows\Microsoft.NET\Framework64\v4.0.30319\csc /out:PostJson.dll /target:library PostJSON.cs
Run Code Online (Sandbox Code Playgroud)

要将PostJSON.cs文件编译为PostJSON.dll.

然后从您的数据库运行此脚本以安装和测试存储过程:

drop procedure if exists postjson
if exists (select * from sys.assemblies where name = 'PostJSON')
 drop assembly PostJSON

exec sp_configure 'show advanced options', 1
reconfigure
exec sp_configure 'clr enabled', 1;
reconfigure

go
DECLARE @asmBin varbinary(max) = (
        SELECT BulkColumn 
        FROM OPENROWSET (BULK 'c:\PostJSON\PostJson.dll', SINGLE_BLOB) a
        );

DECLARE @hash varbinary(64);
SELECT @hash = HASHBYTES('SHA2_512', @asmBin);

declare @description nvarchar(4000) = N'PostJSON';

if not exists (select * from sys.trusted_assemblies where hash = @hash)
begin
  EXEC sys.sp_add_trusted_assembly @hash = @hash,
                                   @description = @description;
end

CREATE ASSEMBLY [PostJSON]
    AUTHORIZATION [dbo]
    FROM @asmBin
    WITH PERMISSION_SET = EXTERNAL_ACCESS;  

exec('
CREATE PROCEDURE PostJson  @sqlForJSONQuery nvarchar(max), 
                           @targetURI nvarchar(max), 
                           @responseCode int out, 
                           @responseBody nvarchar(max) out 
AS EXTERNAL NAME PostJSON.StoredProcedures.PostJSON
')


go
--test

declare @rc int
declare @body nvarchar(max)

exec postjson 'select ''Hello world'' msg for json path', 'http:\\bing.com', @rc out, @body out

select @rc, @body
Run Code Online (Sandbox Code Playgroud)