web-dev-qa-db-ja.com

カーソルが原因でSSMSがクラッシュする

テーブルのグループからJSONテキストの1つのレコードを生成するカーソルがあります。カーソルが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

Windowsログから:

次の.NETランタイムエラーが最初に発生しました:

エラー7/20/2018 2:27:58 PM .NETランタイム1026なし

アプリケーション:Ssms.exeフレームワークバージョン:v4.0.30319説明:未処理の例外が原因でプロセスが終了しました。例外情報:System.Windows.Forms.TextBoxBase.CreateHandle()でSystem.Windows.Forms.Control.CreateHandle()でSystem.Windows.Forms.NativeWindow.CreateHandle(System.Windows.Forms.CreateParams)でSystem.ComponentModel.Win32Exception )System.Windows.Forms.Control.CreateControl(Boolean)at System.Windows.Forms.Control.CreateControl(Boolean)at System.Windows.Forms.Control.CreateControl()at System.Windows.Forms.Control.WmShowWindow( System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef)でSystem.Windows.Forms.Message ByRef)にSystem.Windows.Forms.ScrollableControl.WndProc(System.Windows.Forms.Message ByRef)でSystem.Windows.Forms.Control + ControlNativeWindowのSystem.Windows.Forms.UpDownBase.WndProc(System.Windows.Forms.Message ByRef)のSystem.Windows.Forms.ContainerControl.WndProc(System.Windows.Forms.Message ByRef) OnMessage(System.Windows.Forms.Message ByRef)at System.Windows.Forms.Control + ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef)Sy stem.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr、Int32、IntPtr、IntPtr)

2番目のアプリケーションエラー:

エラー7/20/2018 2:27:58 PMアプリケーションエラー1000(100)

障害が発生しているアプリケーション名:Ssms.exe、バージョン:2017.140.17277.0、タイムスタンプ:0x5b304116障害が発生しているモジュール名:KERNELBASE.dll、バージョン:10.0.14393.2189、タイムスタンプ:0x5abda7d6例外コード:0xe0434352障害オフセット:0x000daa12障害が発生しているプロセスID:0x3f6c障害が発生していますアプリケーションの開始時刻:0x01d4205466d2b650障害のあるアプリケーションパス:C:\ Program Files(x86)\ Microsoft SQL Server\140\Tools\Binn\ManagementStudio\Ssms.exe障害のあるモジュールパス:C:\ WINDOWS\System32\KERNELBASE.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
3
ivGeo
  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;
    
  2. 次に、CURSORを完全に取り除くことができます。これは、とにかく合計行数を取得するためにのみ使用されるため、WHILEループを単純なカウンターに変更するためです。

  3. 最後に、これら2つの部分をまとめると、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;
    

言われていることすべて:

  1. ループの反復ごとに@Records変数を再宣言しないでください。ループの前に一度宣言し、ループ内で毎回設定するだけです。
  2. @Body変数は何もしないので必要ありません。 CPUとRAM(および時間)を浪費する@Recordsの値に設定するだけです。
  3. ループの反復ごとにsp_OACreateを実行しますが、sp_OADestroy(またはそれが何であれ)を呼び出すことはありません。これにより、メモリ内に多くのオブジェクトが作成されることになると思います。また、mightは、Destroyの直前でもう一度sp_OAMethodを実行して、リクエストを閉じる必要があります。そうであるかどうかを確認する必要があります。孤立したネットワークソケットを開いたままにしたくない。
  4. 実際にこれが機能する可能性はありますが、OLE Automationストアドプロシージャ(つまりsp_OA*)を使用することはかなり危険です。SQLC2005のリリース以降、代わりにSQLCLRを使用することをお勧めします。 OLE Automation procsの代わりにSQLCLRを使用することには、次のような多くの利点があります。

    • NVARCHAR(MAX)およびVARCHAR(8000)でスタックする代わりに、NVARCHAR(4000)を使用できる。実際、XMLを送信することもできます。 OLEオートメーションプロシージャは、SQL Server 2000以降に追加されたデータ型を処理しません。
    • より良いセキュリティ
    • より良いメモリ処理
    • より安定した

    .NET HttpWebRequestクラスを使用できます。または、何もコーディングしたくない場合は、あらかじめ作成されたSQLCLR関数が SQL# (作成したもの)に存在します。関数はINET_GetWebPagesであり、さまざまなシナリオを処理します(つまり、カスタムヘッダーを渡したり、コンテンツをGETまたはPOSTとして送信したりできます)。ただし、明確にするために、INET_GetWebPagesはフルバージョンでのみ利用できます(つまり、無料バージョンでは利用できません)。

  5. それでも、SQL Server自体ではなくSSMSがクラッシュするのは奇妙に思えます。上記のコードをクリーンアップしても(SQLCLRの使用への切り替えを除く)すべてエラーが修正されない場合は、いつでもEXEC sp_OA*ステートメントをコメント化できます。すべてのEXEC sp_OA*ステートメントと、その後に続くIFステートメントをコメント化することから始めます。次に、プロシージャでエラーが発生しなくなった場合は、ステートメントのEXEC/IFの各ペアのコメントを1つずつ解除します(最初のセットを除く:sp_OACreateのコメントを解除すると、あなたが必要になります同じスコープ内のsp_OADestroyとペアにする!!)

これらすべての提案はさておき、クラッシュの原因となった実際の問題( chat で発見された)は、 SSMSに返される結果セットの完全な数。質問の最初のコードブロックには、クエリによって2775行が返されます。ストアドプロシージャのコンテキスト内では、結果セットはクライアントに返されません。すべての結果はVARCHAR(8000)変数に格納されます。しかし、テストでは、それは単なるSELECTです。各結果セットを個別のグリッドにダンプしない場合、SSMSはクラッシュしませんでした。

また、これを自分でコーディングする場合、SQL Server 2017(またはそれ以降)を使用している場合、コードをデプロイするときに「問題」が発生します。SQLServer 2016を使用している場合でも、アセンブリをEXTERNAL_ACCESSに設定する必要があります議会への署名とその他のいくつかの小さなステップが必要になります。 Visual Studioを使用して、または使用せずに、SQL Server 2017(以降)で機能する方法でこれを処理する方法については、こちらの投稿を参照してください。

SQLCLRとSQL Server 2017、パート2:「CLR厳密なセキュリティ」–ソリューション1 (セキュリティとアセンブリを処理する単一の自己完結型インストールスクリプトを目標としていますandは、外部依存関係がないため、バージョン管理や、開発、テスト、本番システム間での移行が容易になります)

5
Solomon Rutzky

SSMSの問題を回避するには、SET NOCOUNT ONを設定し、結果をクライアントに送信しないでください。

ServerXMLHTTP.Sendは、非推奨のsp_OAxxx COM相互運用ストアドプロシージャから呼び出される場合、8000文字の制限があるため、このジョブには適していません。

これは、SQL CLRストアドプロシージャ、またはPowerShell、Python、.NETなどの多くのクライアントプログラミング環境で簡単に実行できます。これらはすべてSQLエージェントジョブから呼び出すことができます。

また、作成しているCOMオブジェクトを適切に破棄していません。ループでsp_OACreateを呼び出しますが、sp_OADestroyは呼び出しません。

また、(おそらく2番目または3番目の手)私 15歳のUSENETの投稿 をコピーしたときに、sp_OAxxx COMからServerXmlHttpを使用する方法に関する行がありません。相互運用手順。

   exec @hr = sp_OAGetProperty @obj, 'status', @status OUT
   if @hr <0 begin  set @msg = 'sp_OAMethod read status failed' goto eh end

全体として、このプロセスを停止して別の言語で書くことをお勧めします。

FOR JSONクエリをHTTPエンドポイントに投稿して応答を取得する方法を始めるためのサンプルを以下に示します。

背景として、FOR JSONクエリは、JSONが行間で分割された1列の複数行の結果セットとしてクライアントにストリーミングされます。したがって、結果行を読み取って、コンテンツを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

これが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();
                    }
                }


            }
        }
    }
}

これは SQL Server Data Tools を使用して構築およびデプロイできます。これにより、SQL CLRを含むSQL Server開発のVisual Studioと完全に統合し、ソース管理のコーディング、デバッグ、展開、管理をカバーできます。

または、最低限、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

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