web-dev-qa-db-ja.com

SQL CLRスカラー関数を使用してHASHBYTESをシミュレートするスケーラブルな方法は何ですか?

ETLプロセスの一部として、ステージングからの行をレポートデータベースと比較して、データが最後にロードされてから実際に変更されている列があるかどうかを調べます。

比較は、テーブルの一意のキーと他のすべての列のある種のハッシュに基づいています。現在 HASHBYTESSHA2_256アルゴリズムとともに使用しており、多数の同時ワーカースレッドがすべてHASHBYTESを呼び出している場合、大規模サーバーではスケーリングしないことがわかりました。

1秒あたりのハッシュで測定されたスループットは、96コアサーバーでテストした場合、16を超える同時スレッドを増加させません。同時MAXDOP 8クエリの数を1〜12に変更してテストします。MAXDOP 1を使用したテストでは、同じスケーラビリティのボトルネックが示されました。

回避策として、SQL CLRソリューションを試してみたいと思います。要件を述べる私の試みはここにあります:

  • 関数は並列クエリに参加できる必要があります
  • 関数は確定的でなければなりません
  • 関数はNVARCHARまたはVARBINARY文字列の入力を受け取る必要があります(関連するすべての列が連結されます)
  • 文字列の一般的な入力サイズは、100〜20000文字です。 20000は最大ではありません
  • ハッシュの衝突の可能性は、MD5アルゴリズムとほぼ同じかそれ以上である必要があります。 CHECKSUMは衝突が多すぎるため機能しません。
  • 関数は大規模サーバーで適切にスケーリングする必要があります(スレッド数が増加しても、スレッドあたりのスループットは大幅に低下しないはずです)

Application Reasons™の場合、レポートテーブルのハッシュの値を節約できないと仮定します。これは、トリガーまたは計算列をサポートしないCCIです(他の問題もあり、私が知りたくない)。

SQL CLR関数を使用してHASHBYTESをシミュレートするスケーラブルな方法は何ですか?私の目標は、大規模サーバーで可能な限り1秒あたりのハッシュを取得することで表現できるため、パフォーマンスも重要です。私はCLRにひどいので、これを達成する方法がわかりません。だれでも答える動機がある場合は、できるだけ早くこの質問に賞金を追加する予定です。以下は、ユースケースを大まかに示すクエリの例です。

DROP TABLE IF EXISTS #CHANGED_IDS;

SELECT stg.ID INTO #CHANGED_IDS
FROM (
    SELECT ID,
    CAST( HASHBYTES ('SHA2_256', 
        CAST(FK1 AS NVARCHAR(19)) + 
        CAST(FK2 AS NVARCHAR(19)) + 
        CAST(FK3 AS NVARCHAR(19)) + 
        CAST(FK4 AS NVARCHAR(19)) + 
        CAST(FK5 AS NVARCHAR(19)) + 
        CAST(FK6 AS NVARCHAR(19)) + 
        CAST(FK7 AS NVARCHAR(19)) + 
        CAST(FK8 AS NVARCHAR(19)) + 
        CAST(FK9 AS NVARCHAR(19)) + 
        CAST(FK10 AS NVARCHAR(19)) + 
        CAST(FK11 AS NVARCHAR(19)) + 
        CAST(FK12 AS NVARCHAR(19)) + 
        CAST(FK13 AS NVARCHAR(19)) + 
        CAST(FK14 AS NVARCHAR(19)) + 
        CAST(FK15 AS NVARCHAR(19)) + 
        CAST(STR1 AS NVARCHAR(500)) +
        CAST(STR2 AS NVARCHAR(500)) +
        CAST(STR3 AS NVARCHAR(500)) +
        CAST(STR4 AS NVARCHAR(500)) +
        CAST(STR5 AS NVARCHAR(500)) +
        CAST(COMP1 AS NVARCHAR(1)) + 
        CAST(COMP2 AS NVARCHAR(1)) + 
        CAST(COMP3 AS NVARCHAR(1)) + 
        CAST(COMP4 AS NVARCHAR(1)) + 
        CAST(COMP5 AS NVARCHAR(1)))
     AS BINARY(32)) HASH1
    FROM HB_TBL WITH (TABLOCK)
) stg
INNER JOIN (
    SELECT ID,
    CAST(HASHBYTES ('SHA2_256', 
        CAST(FK1 AS NVARCHAR(19)) + 
        CAST(FK2 AS NVARCHAR(19)) + 
        CAST(FK3 AS NVARCHAR(19)) + 
        CAST(FK4 AS NVARCHAR(19)) + 
        CAST(FK5 AS NVARCHAR(19)) + 
        CAST(FK6 AS NVARCHAR(19)) + 
        CAST(FK7 AS NVARCHAR(19)) + 
        CAST(FK8 AS NVARCHAR(19)) + 
        CAST(FK9 AS NVARCHAR(19)) + 
        CAST(FK10 AS NVARCHAR(19)) + 
        CAST(FK11 AS NVARCHAR(19)) + 
        CAST(FK12 AS NVARCHAR(19)) + 
        CAST(FK13 AS NVARCHAR(19)) + 
        CAST(FK14 AS NVARCHAR(19)) + 
        CAST(FK15 AS NVARCHAR(19)) + 
        CAST(STR1 AS NVARCHAR(500)) +
        CAST(STR2 AS NVARCHAR(500)) +
        CAST(STR3 AS NVARCHAR(500)) +
        CAST(STR4 AS NVARCHAR(500)) +
        CAST(STR5 AS NVARCHAR(500)) +
        CAST(COMP1 AS NVARCHAR(1)) + 
        CAST(COMP2 AS NVARCHAR(1)) + 
        CAST(COMP3 AS NVARCHAR(1)) + 
        CAST(COMP4 AS NVARCHAR(1)) + 
        CAST(COMP5 AS NVARCHAR(1)) )
 AS BINARY(32)) HASH1
    FROM HB_TBL_2 WITH (TABLOCK)
) rpt ON rpt.ID = stg.ID
WHERE rpt.HASH1 <> stg.HASH1
OPTION (MAXDOP 8);

少し単純化するために、おそらく次のようなものをベンチマークに使用します。月曜日にHASHBYTESで結果を投稿します。

CREATE TABLE dbo.HASH_ME (
    ID BIGINT NOT NULL,
    FK1 BIGINT NOT NULL,
    FK2 BIGINT NOT NULL,
    FK3 BIGINT NOT NULL,
    FK4 BIGINT NOT NULL,
    FK5 BIGINT NOT NULL,
    FK6 BIGINT NOT NULL,
    FK7 BIGINT NOT NULL,
    FK8 BIGINT NOT NULL,
    FK9 BIGINT NOT NULL,
    FK10 BIGINT NOT NULL,
    FK11 BIGINT NOT NULL,
    FK12 BIGINT NOT NULL,
    FK13 BIGINT NOT NULL,
    FK14 BIGINT NOT NULL,
    FK15 BIGINT NOT NULL,
    STR1 NVARCHAR(500) NOT NULL,
    STR2 NVARCHAR(500) NOT NULL,
    STR3 NVARCHAR(500) NOT NULL,
    STR4 NVARCHAR(500) NOT NULL,
    STR5 NVARCHAR(2000) NOT NULL,
    COMP1 TINYINT NOT NULL,
    COMP2 TINYINT NOT NULL,
    COMP3 TINYINT NOT NULL,
    COMP4 TINYINT NOT NULL,
    COMP5 TINYINT NOT NULL
);

INSERT INTO dbo.HASH_ME WITH (TABLOCK)
SELECT RN,
RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000,
RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000,
RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000, RN % 1000000,
REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 30)
,REPLICATE(CHAR(65 + RN % 10 ), 1000),
0,1,0,1,0
FROM (
    SELECT TOP (100000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
OPTION (MAXDOP 1);

SELECT MAX(HASHBYTES('SHA2_256',
CAST(N'' AS NVARCHAR(MAX)) + N'|' +
CAST(FK1 AS NVARCHAR(19)) + N'|' +
CAST(FK2 AS NVARCHAR(19)) + N'|' +
CAST(FK3 AS NVARCHAR(19)) + N'|' +
CAST(FK4 AS NVARCHAR(19)) + N'|' +
CAST(FK5 AS NVARCHAR(19)) + N'|' +
CAST(FK6 AS NVARCHAR(19)) + N'|' +
CAST(FK7 AS NVARCHAR(19)) + N'|' +
CAST(FK8 AS NVARCHAR(19)) + N'|' +
CAST(FK9 AS NVARCHAR(19)) + N'|' +
CAST(FK10 AS NVARCHAR(19)) + N'|' +
CAST(FK11 AS NVARCHAR(19)) + N'|' +
CAST(FK12 AS NVARCHAR(19)) + N'|' +
CAST(FK13 AS NVARCHAR(19)) + N'|' +
CAST(FK14 AS NVARCHAR(19)) + N'|' +
CAST(FK15 AS NVARCHAR(19)) + N'|' +
CAST(STR1 AS NVARCHAR(500)) + N'|' +
CAST(STR2 AS NVARCHAR(500)) + N'|' +
CAST(STR3 AS NVARCHAR(500)) + N'|' +
CAST(STR4 AS NVARCHAR(500)) + N'|' +
CAST(STR5 AS NVARCHAR(2000)) + N'|' +
CAST(COMP1 AS NVARCHAR(1)) + N'|' +
CAST(COMP2 AS NVARCHAR(1)) + N'|' +
CAST(COMP3 AS NVARCHAR(1)) + N'|' +
CAST(COMP4 AS NVARCHAR(1)) + N'|' +
CAST(COMP5 AS NVARCHAR(1)) )
)
FROM dbo.HASH_ME
OPTION (MAXDOP 1);
31
Joe Obbish

変更を探すだけなので、暗号化ハッシュ関数は必要ありません。

オープンソースのより高速な非暗号化ハッシュの1つから選択できます Data.HashFunctionライブラリ Brandon Dahlerによるもので、許可およびOSI承認の下でライセンスされています MITライセンスSpookyHashが一般的な選択肢です。

実装例

ソースコード

_using Microsoft.SqlServer.Server;
using System.Data.HashFunction.SpookyHash;
using System.Data.SqlTypes;

public partial class UserDefinedFunctions
{
    [SqlFunction
        (
            DataAccess = DataAccessKind.None,
            SystemDataAccess = SystemDataAccessKind.None,
            IsDeterministic = true,
            IsPrecise = true
        )
    ]
    public static byte[] SpookyHash
        (
            [SqlFacet (MaxSize = 8000)]
            SqlBinary Input
        )
    {
        ISpookyHashV2 sh = SpookyHashV2Factory.Instance.Create();
        return sh.ComputeHash(Input.Value).Hash;
    }

    [SqlFunction
        (
            DataAccess = DataAccessKind.None,
            IsDeterministic = true,
            IsPrecise = true,
            SystemDataAccess = SystemDataAccessKind.None
        )
    ]
    public static byte[] SpookyHashLOB
        (
            [SqlFacet (MaxSize = -1)]
            SqlBinary Input
        )
    {
        ISpookyHashV2 sh = SpookyHashV2Factory.Instance.Create();
        return sh.ComputeHash(Input.Value).Hash;
    }
}
_

ソースは2つの関数を提供します。1つは8000バイト以下の入力用で、1つはLOBバージョンです。非LOBバージョンは大幅に高速化されます。

LOBバイナリを COMPRESS でラップして、8000バイトの制限内に収めることができる場合があります(パフォーマンスにとって価値がある場合)。または、LOBをサブ8000バイトのセグメントに分割するか、LOBの場合のためにHASHBYTESの使用を予約することもできます(長い入力はより適切にスケーリングされるため)。

ビルド済みコード

自分でパッケージを取得してすべてをコンパイルできることは明らかですが、簡単なテストを簡単にするために、以下のアセンブリを作成しました。

https://Gist.github.com/SQLKiwi/365b265b476bf86754457fc9514b23

T-SQL関数

_CREATE FUNCTION dbo.SpookyHash
(
    @Input varbinary(8000)
)
RETURNS binary(16)
WITH 
    RETURNS NULL ON NULL INPUT, 
    EXECUTE AS OWNER
AS EXTERNAL NAME Spooky.UserDefinedFunctions.SpookyHash;
GO
CREATE FUNCTION dbo.SpookyHashLOB
(
    @Input varbinary(max)
)
RETURNS binary(16)
WITH 
    RETURNS NULL ON NULL INPUT, 
    EXECUTE AS OWNER
AS EXTERNAL NAME Spooky.UserDefinedFunctions.SpookyHashLOB;
GO
_

使用法

質問のサンプルデータが与えられた使用例:

_SELECT
    HT1.ID
FROM dbo.HB_TBL AS HT1
JOIN dbo.HB_TBL_2 AS HT2
    ON HT2.ID = HT1.ID
    AND dbo.SpookyHash
    (
        CONVERT(binary(8), HT2.FK1) + 0x7C +
        CONVERT(binary(8), HT2.FK2) + 0x7C +
        CONVERT(binary(8), HT2.FK3) + 0x7C +
        CONVERT(binary(8), HT2.FK4) + 0x7C +
        CONVERT(binary(8), HT2.FK5) + 0x7C +
        CONVERT(binary(8), HT2.FK6) + 0x7C +
        CONVERT(binary(8), HT2.FK7) + 0x7C +
        CONVERT(binary(8), HT2.FK8) + 0x7C +
        CONVERT(binary(8), HT2.FK9) + 0x7C +
        CONVERT(binary(8), HT2.FK10) + 0x7C +
        CONVERT(binary(8), HT2.FK11) + 0x7C +
        CONVERT(binary(8), HT2.FK12) + 0x7C +
        CONVERT(binary(8), HT2.FK13) + 0x7C +
        CONVERT(binary(8), HT2.FK14) + 0x7C +
        CONVERT(binary(8), HT2.FK15) + 0x7C +
        CONVERT(varbinary(1000), HT2.STR1) + 0x7C +
        CONVERT(varbinary(1000), HT2.STR2) + 0x7C +
        CONVERT(varbinary(1000), HT2.STR3) + 0x7C +
        CONVERT(varbinary(1000), HT2.STR4) + 0x7C +
        CONVERT(varbinary(1000), HT2.STR5) + 0x7C +
        CONVERT(binary(1), HT2.COMP1) + 0x7C +
        CONVERT(binary(1), HT2.COMP2) + 0x7C +
        CONVERT(binary(1), HT2.COMP3) + 0x7C +
        CONVERT(binary(1), HT2.COMP4) + 0x7C +
        CONVERT(binary(1), HT2.COMP5)
    )
    <> dbo.SpookyHash
    (
        CONVERT(binary(8), HT1.FK1) + 0x7C +
        CONVERT(binary(8), HT1.FK2) + 0x7C +
        CONVERT(binary(8), HT1.FK3) + 0x7C +
        CONVERT(binary(8), HT1.FK4) + 0x7C +
        CONVERT(binary(8), HT1.FK5) + 0x7C +
        CONVERT(binary(8), HT1.FK6) + 0x7C +
        CONVERT(binary(8), HT1.FK7) + 0x7C +
        CONVERT(binary(8), HT1.FK8) + 0x7C +
        CONVERT(binary(8), HT1.FK9) + 0x7C +
        CONVERT(binary(8), HT1.FK10) + 0x7C +
        CONVERT(binary(8), HT1.FK11) + 0x7C +
        CONVERT(binary(8), HT1.FK12) + 0x7C +
        CONVERT(binary(8), HT1.FK13) + 0x7C +
        CONVERT(binary(8), HT1.FK14) + 0x7C +
        CONVERT(binary(8), HT1.FK15) + 0x7C +
        CONVERT(varbinary(1000), HT1.STR1) + 0x7C +
        CONVERT(varbinary(1000), HT1.STR2) + 0x7C +
        CONVERT(varbinary(1000), HT1.STR3) + 0x7C +
        CONVERT(varbinary(1000), HT1.STR4) + 0x7C +
        CONVERT(varbinary(1000), HT1.STR5) + 0x7C +
        CONVERT(binary(1), HT1.COMP1) + 0x7C +
        CONVERT(binary(1), HT1.COMP2) + 0x7C +
        CONVERT(binary(1), HT1.COMP3) + 0x7C +
        CONVERT(binary(1), HT1.COMP4) + 0x7C +
        CONVERT(binary(1), HT1.COMP5)
    );
_

LOBバージョンを使用する場合、最初のパラメーターをキャストまたはvarbinary(max)に変換する必要があります。

実行計画

plan


安全な不気味な

Data.HashFunctionライブラリは、SQL ServerによってUNSAFEと見なされる多数のCLR言語機能を使用します。 SAFEステータスと互換性のある基本的なSpooky Hashを書くことは可能です。 Jon HannaのSpookilySharp に基づいて私が書いた例は以下のとおりです。

https://Gist.github.com/SQLKiwi/7a5bb26b0bee56f6d28a1d26669ce8f2

21
Paul White 9

SQLCLRを使用すると、並列処理が向上するかどうかがわかりません。ただし、無料バージョンの SQL# SQLCLRライブラリ(私が書いた)til_HashBinaryと呼ばれるハッシュ関数があるため、テストは本当に簡単です。サポートされるアルゴリズムは、MD5、SHA1、SHA256、SHA384、およびSHA512です。

これはVARBINARY(MAX)値を入力として受け取るため、各フィールドの文字列バージョンを(現在行っているように)連結してからVARBINARY(MAX)に変換するか、または直接各列のVARBINARYと変換された値を連結します(文字列や文字列からVARBINARYへの追加の変換を処理していないため、これはより高速になる可能性があります)。以下は、これらのオプションの両方を示す例です。また、HASHBYTES関数も表示されるため、値がSQL#.Util_HashBinaryと同じであることがわかります。

VARBINARY値を連結したときのハッシュ結果は、NVARCHAR値を連結したときのハッシュ結果と一致しないことに注意してください。これは、INT値「1」のバイナリ形式が0x00000001であるのに対し、UTF-16LE(つまりNVARCHAR)形式の「1」のINT値(ハッシュ形式が機能するのはバイナリ形式なので)は0x3100です。

_SELECT so.[object_id],
       SQL#.Util_HashBinary(N'SHA256',
                            CONVERT(VARBINARY(MAX),
                                    CONCAT(so.[name], so.[schema_id], so.[create_date])
                                   )
                           ) AS [SQLCLR-ConcatStrings],
       HASHBYTES(N'SHA2_256',
                 CONVERT(VARBINARY(MAX),
                         CONCAT(so.[name], so.[schema_id], so.[create_date])
                        )
                ) AS [BuiltIn-ConcatStrings]
FROM sys.objects so;


SELECT so.[object_id],
       SQL#.Util_HashBinary(N'SHA256',
                            CONVERT(VARBINARY(500), so.[name]) + 
                            CONVERT(VARBINARY(500), so.[schema_id]) +
                            CONVERT(VARBINARY(500), so.[create_date])
                           ) AS [SQLCLR-ConcatVarBinaries],
       HASHBYTES(N'SHA2_256',
                 CONVERT(VARBINARY(500), so.[name]) + 
                 CONVERT(VARBINARY(500), so.[schema_id]) +
                 CONVERT(VARBINARY(500), so.[create_date])
                ) AS [BuiltIn-ConcatVarBinaries]
FROM sys.objects so;
_

以下を使用して、非LOBスプーキーにさらに匹敵するものをテストできます。

_CREATE FUNCTION [SQL#].[Util_HashBinary8k]
(@Algorithm [nvarchar](50), @BaseData [varbinary](8000))
RETURNS [varbinary](8000) 
WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT
AS EXTERNAL NAME [SQL#].[UTILITY].[HashBinary];
_

注:til_HashBinaryは、.NETに組み込まれているマネージドSHA256アルゴリズムを使用するため、「bcrypt」ライブラリを使用しないでください。

質問のその側面以外に、このプロセスに役立つ可能性があるいくつかの追加の考えがあります。

追加の考え1(ハッシュを事前に計算し、少なくとも一部)

あなたはいくつかのことを述べました:

  1. ステージングの行をレポートデータベースと比較して、データが最後に読み込まれてから実際に変更されている列があるかどうかを調べます。

    そして:

  2. レポートテーブルのハッシュの値を保存できません。トリガーや計算列をサポートしないCCIです。

    そして:

  3. テーブルはETLプロセスの外部で更新できます

このレポートの表のデータは一定期間安定しており、このETLプロセスによってのみ変更されるようです。

他に何もこのテーブルを変更しない場合、結局のところ、トリガーやインデックス付きビューは本当に必要ありません(私は最初はそうするかもしれないと思っていました)。

レポートテーブルのスキーマを変更できないため、少なくとも事前計算されたハッシュ(および計算されたときのUTC時間)を含む関連テーブルを作成することは可能ですか?これにより、事前に計算された値を次回と比較して、ハッシュの計算が必要な入力値のみを残すことができます。これにより、HASHBYTESまたは_SQL#.Util_HashBinary_の呼び出し回数が半分になります。インポートプロセス中に、このハッシュのテーブルに単純に結合します。

また、このテーブルのハッシュを単に更新する別のストアドプロシージャを作成します。現在に変更された関連行のハッシュを更新し、変更された行のタイムスタンプを更新するだけです。このプロシージャは、このテーブルを更新する他のプロセスの最後に実行できます。また、このETLが開始する30〜60分前に実行するようにスケジュールすることもできます(実行にかかる時間、およびこれらの他のプロセスのいずれが実行されるかによって異なります)。同期されていない行があると思われる場合は、手動で実行することもできます。

その後、次のことが指摘されました。

500以上のテーブルがあります

テーブルの数が多いと、現在のハッシュを含めるためにテーブルごとに追加のテーブルを作成することが難しくなりますが、標準のスキーマであるため、スクリプト化できるため、これは不可能ではありません。スクリプトは、ソーステーブルの名前とソーステーブルのPK列の検出を考慮するだけで済みます。

それでも、最終的に最もスケーラブルであることが判明したハッシュアルゴリズムに関係なく、私は非常に少なくともいくつかのテーブルを見つけることをお勧めします(おそらく他のテーブルよりもはるかに大きいテーブルがあるかもしれません) 500テーブル)、および現在のハッシュを取得するように関連テーブルを設定して、ETLプロセスの前に「現在の」値を把握できるようにします。最速の関数でさえ、そもそもそれを呼び出す必要がないため、パフォーマンスを上げることはできません;-)。

追加の考え2(VARBINARYの代わりにNVARCHAR

SQLCLRと組み込みのHASHBYTESに関係なく、直接VARBINARYに変換することをお勧めしますの方が速いので。文字列を連結することは、非常に効率的ではありません。 そして、それはそもそも非文字列値を文字列に変換することに加えて、追加の作業が必要です(作業量は基本タイプに基づいて異なると思います:DATETIMEBIGINTよりも多く必要ですが、VARBINARYに変換すると、基本的な値が得られます(ほとんどの場合)。

そして実際、他のテストと同じデータセットをテストし、HASHBYTES(N'SHA2_256',...)を使用すると、1分間で計算された合計ハッシュが23.415%増加しました。そして、その増加は、VARBINARYの代わりにNVARCHARを使用する以外に何もしなかったためです! ????(詳細は コミュニティwikiの回答 を参照してください)

追加の考え3(入力パラメーターに注意してください)

さらなるテストにより、(この実行量で)パフォーマンスに影響を与える1つの領域が入力パラメーターであることがわかりました。

現在SQL#ライブラリにあるtil_HashBinary SQLCLR関数には2つの入力パラメーターがあります。1つはVARBINARY(ハッシュする値)、もう1つはNVARCHAR(アルゴリズムを使用する)。これは、HASHBYTES関数の署名をミラーリングしたためです。ただし、NVARCHARパラメーターを削除し、SHA256のみを実行する関数を作成すると、パフォーマンスが非常に向上することがわかりました。 NVARCHARパラメータをINTに切り替えることも役立つと思いますが、余分なINTパラメータを使用しなくても少なくとも少し速く。

また、_SqlBytes.Value_は_SqlBinary.Value_よりもパフォーマンスが向上する可能性があります。

このテスト用に、2つの新しい関数til_HashSHA256Binaryおよびtil_HashSHA256Binary8kを作成しました。これらは、SQL#の次のリリースに含まれる予定です(日付はまだ設定されていません)。

また、テスト方法が少し改善される可能性があることもわかったので、以下のコミュニティWiki回答のテストハーネスを更新して、以下を含めました。

  1. sQLCLRアセンブリの事前読み込みにより、読み込み時間のオーバーヘッドが結果をゆがめないようにします。
  2. 衝突をチェックするための検証手順。見つかった場合は、一意/個別の行の数と行の総数が表示されます。これにより、衝突の数(存在する場合)が特定のユースケースの制限を超えているかどうかを判断できます。いくつかの使用例は少数の衝突を許容するかもしれませんし、他は何も必要としないかもしれません。超高速機能は、必要なレベルの精度への変更を検出できない場合は役に立ちません。たとえば、O.P。から提供されたテストハーネスを使用して、行数を10万行に増やし(元は10kでした)、CHECKSUMが9kを超える衝突を登録しました。これは9%(yikes)です。

追加の考え#4(HASHBYTES + SQLCLRを一緒に?)

ボトルネックの場所によっては、組み込みHASHBYTESとSQLCLR UDFを組み合わせて同じハッシュを実行すると役立つ場合もあります。組み込み関数がSQLCLR操作とは異なる方法で個別に制約されている場合、このアプローチは、HASHBYTESまたはSQLCLRを個別に実行するよりも同時に実行できる可能性があります。間違いなくテストする価値があります。

追加の考え5(ハッシュオブジェクトキャッシュ?)

David Browneの回答 で提案されているハッシュアルゴリズムオブジェクトのキャッシュは確かに興味深いと思われるので、試してみたところ、次の2つの重要な点が見つかりました。

  1. 何らかの理由で、パフォーマンスの改善はあったとしてもあまり提供されていないようです。私は何か間違ったことをしたかもしれませんが、これが私が試したものです:

    _static readonly ConcurrentDictionary<int, SHA256Managed> hashers =
        new ConcurrentDictionary<int, SHA256Managed>();
    
    [return: SqlFacet(MaxSize = 100)]
    [SqlFunction(IsDeterministic = true)]
    public static SqlBinary FastHash([SqlFacet(MaxSize = 1000)] SqlBytes Input)
    {
        SHA256Managed sh = hashers.GetOrAdd(Thread.CurrentThread.ManagedThreadId,
                                            i => new SHA256Managed());
    
        return sh.ComputeHash(Input.Value);
    }
    _
  2. ManagedThreadId値は、特定のクエリのすべてのSQLCLR参照で同じであるように見えます。同じ関数への複数の参照と、異なる関数への参照をテストしました。3つすべてに異なる入力値が与えられ、異なる(ただし期待される)戻り値が返されました。両方のテスト関数の出力は、ManagedThreadIdを含む文字列と、ハッシュ結果の文字列表現でした。 ManagedThreadId値は、クエリ内のすべてのUDF参照、およびすべての行で同じでした。しかし、ハッシュ結果は同じ入力文字列では同じであり、異なる入力文字列では異なっていました。

    テストで誤った結果は見られませんでしたが、これにより競合状態の可能性が高まるのではないですか?ディクショナリのキーが特定のクエリで呼び出されるすべてのSQLCLRオブジェクトで同じである場合、それらはそのキーに格納されている同じ値またはオブジェクトを共有しているでしょう。ここで機能しているように見えても(ある程度、パフォーマンスはそれほど向上していないようですが、機能的には何も壊れていません)、このアプローチが他のシナリオで機能するという確信はありません。

17
Solomon Rutzky

これは伝統的な答えではありませんが、これまでに述べたいくつかの手法のベンチマークを投稿すると役立つと思いました。 SQL Server 2017 CU9を搭載した96コアサーバーでテストしています。

スケーラビリティの問題の多くは、いくつかのグローバルな状態をめぐって競合する並行スレッドが原因です。たとえば、従来のPFSページの競合を考えてみます。これは、メモリ内の同じページを変更する必要のあるワーカースレッドが多すぎる場合に発生する可能性があります。コードがより効率的になると、より速くラッチを要求する可能性があります。それは競合を増やします。簡単に言えば、グローバルな状態がより厳しく争われているため、効率的なコードはスケーラビリティの問題につながる可能性が高くなります。グローバル状態に頻繁にアクセスしないため、遅いコードはスケーラビリティの問題を引き起こす可能性が低くなります。

HASHBYTESスケーラビリティは、部分的には入力文字列の長さに基づいています。私の理論では、これが発生する理由は、HASHBYTES関数が呼び出されたときにグローバル状態へのアクセスが必要になるというものでした。観察しやすいグローバルな状態は、SQL Serverの一部のバージョンでは、呼び出しごとにメモリページを割り当てる必要があることです。観察するのが難しいのは、なんらかのOSの競合があることです。その結果、コードがHASHBYTESを呼び出す頻度が低くなると、競合が減少します。 HASHBYTES呼び出しの割合を減らす1つの方法は、呼び出しごとに必要なハッシュ処理の量を増やすことです。ハッシュ処理は、部分的には入力文字列の長さに基づいています。アプリケーションで見たスケーラビリティの問題を再現するには、デモデータを変更する必要がありました。合理的な最悪のシナリオは、21のBIGINT列を持つテーブルです。テーブルの定義は下部のコードに含まれています。 Local Factors™を削減するために、比較的小さなテーブルで動作する_MAXDOP 1_クエリを同時に使用しています。私の簡単なベンチマークコードは一番下にあります。

関数は異なるハッシュ長を返すことに注意してください。 _MD5_とSpookyHashはどちらも128ビットのハッシュであり、_SHA256_は256ビットのハッシュです。

結果(NVARCHARVARBINARYの変換および連結)

VARBINARYは、NVARCHARに変換して連結するかどうかを確認するために、NVARCHARよりも効率的でパフォーマンスが高いため、_RUN_HASHBYTES_SHA2_256_ストアドプロシージャのバージョンが作成されました。同じテンプレートから(下記のBENCHMARKING CODEセクションの「ステップ5」を参照)。唯一の違いは次のとおりです。

  1. ストアドプロシージャ名は__NVC_で終わります
  2. CAST関数のBINARY(8)NVARCHAR(15)に変更されました
  3. _0x7C_は_N'|'_に変更されました

その結果:

_CAST(FK1 AS NVARCHAR(15)) + N'|' +
_

の代わりに:

_CAST(FK1 AS BINARY(8)) + 0x7C +
_

次の表は、1分間に実行されるハッシュの数を示しています。テストは、以下に示す他のテストで使用されたサーバーとは異なるサーバーで実行されました。

_╔════════════════╦══════════╦══════════════╗
║    Datatype    ║  Test #  ║ Total Hashes ║
╠════════════════╬══════════╬══════════════╣
║ NVARCHAR       ║        1 ║     10200000 ║
║ NVARCHAR       ║        2 ║     10300000 ║
║ NVARCHAR       ║  AVERAGE ║ * 10250000 * ║
║ -------------- ║ -------- ║ ------------ ║
║ VARBINARY      ║        1 ║     12500000 ║
║ VARBINARY      ║        2 ║     12800000 ║
║ VARBINARY      ║  AVERAGE ║ * 12650000 * ║
╚════════════════╩══════════╩══════════════╝
_

平均だけを見て、VARBINARYに切り替えるメリットを計算できます。

_SELECT (12650000 - 10250000) AS [IncreaseAmount],
       ROUND(((126500000 - 10250000) / 10250000) * 100.0, 3) AS [IncreasePercentage]
_

それは返します:

_IncreaseAmount:    2400000.0
IncreasePercentage:   23.415
_

結果(ハッシュアルゴリズムと実装)

次の表は、1分間に実行されるハッシュの数を示しています。たとえば、84個の同時クエリでCHECKSUMを使用すると、タイムアウトになる前に20億を超えるハッシュが実行されました。

_╔════════════════════╦════════════╦════════════╦════════════╗
║      Function      ║ 12 threads ║ 48 threads ║ 84 threads ║
╠════════════════════╬════════════╬════════════╬════════════╣
║ CHECKSUM           ║  281250000 ║ 1122440000 ║ 2040100000 ║
║ HASHBYTES MD5      ║   75940000 ║  106190000 ║  112750000 ║
║ HASHBYTES SHA2_256 ║   80210000 ║  117080000 ║  124790000 ║
║ CLR Spooky         ║  131250000 ║  505700000 ║  786150000 ║
║ CLR SpookyLOB      ║   17420000 ║   27160000 ║   31380000 ║
║ SQL# MD5           ║   17080000 ║   26450000 ║   29080000 ║
║ SQL# SHA2_256      ║   18370000 ║   28860000 ║   32590000 ║
║ SQL# MD5 8k        ║   24440000 ║   30560000 ║   32550000 ║
║ SQL# SHA2_256 8k   ║   87240000 ║  159310000 ║  155760000 ║
╚════════════════════╩════════════╩════════════╩════════════╝
_

スレッド秒あたりの作業に関して測定された同じ数値を表示したい場合:

_╔════════════════════╦════════════════════════════╦════════════════════════════╦════════════════════════════╗
║      Function      ║ 12 threads per core-second ║ 48 threads per core-second ║ 84 threads per core-second ║
╠════════════════════╬════════════════════════════╬════════════════════════════╬════════════════════════════╣
║ CHECKSUM           ║                     390625 ║                     389736 ║                     404782 ║
║ HASHBYTES MD5      ║                     105472 ║                      36872 ║                      22371 ║
║ HASHBYTES SHA2_256 ║                     111403 ║                      40653 ║                      24760 ║
║ CLR Spooky         ║                     182292 ║                     175590 ║                     155982 ║
║ CLR SpookyLOB      ║                      24194 ║                       9431 ║                       6226 ║
║ SQL# MD5           ║                      23722 ║                       9184 ║                       5770 ║
║ SQL# SHA2_256      ║                      25514 ║                      10021 ║                       6466 ║
║ SQL# MD5 8k        ║                      33944 ║                      10611 ║                       6458 ║
║ SQL# SHA2_256 8k   ║                     121167 ║                      55316 ║                      30905 ║
╚════════════════════╩════════════════════════════╩════════════════════════════╩════════════════════════════╝
_

すべての方法に関する簡単な考察:

  • CHECKSUM:予想通り非常に優れたスケーラビリティ
  • HASHBYTES:スケーラビリティの問題には、呼び出しごとに1つのメモリ割り当てとOSで消費される大量のCPUが含まれます
  • Spooky:驚くほど優れたスケーラビリティ
  • _Spooky LOB_:スピンロック_SOS_SELIST_SIZED_SLOCK_が制御不能にスピンします。これはCLR関数を介したLOBの受け渡しに関する一般的な問題だと思いますが、よくわかりません
  • _Util_HashBinary_:同じスピンロックによってヒットされるように見えます。これについては、私ができることはおそらく多くないので、これまでは調べていません。

spin your lock

  • _Util_HashBinary 8k_:非常に驚くべき結果、ここで何が起こっているのかわからない

より小さいサーバーでテストされた最終結果:

_╔═════════════════════════╦════════════════════════╦════════════════════════╗
║     Hash Algorithm      ║ Hashes over 11 threads ║ Hashes over 44 threads ║
╠═════════════════════════╬════════════════════════╬════════════════════════╣
║ HASHBYTES SHA2_256      ║               85220000 ║              167050000 ║
║ SpookyHash              ║              101200000 ║              239530000 ║
║ Util_HashSHA256Binary8k ║               90590000 ║              217170000 ║
║ SpookyHashLOB           ║               23490000 ║               38370000 ║
║ Util_HashSHA256Binary   ║               23430000 ║               36590000 ║
╚═════════════════════════╩════════════════════════╩════════════════════════╝
_

ベンチマークコード

設定1:テーブルとデータ

_DROP TABLE IF EXISTS dbo.HASH_SMALL;

CREATE TABLE dbo.HASH_SMALL (
    ID BIGINT NOT NULL,
    FK1 BIGINT NOT NULL,
    FK2 BIGINT NOT NULL,
    FK3 BIGINT NOT NULL,
    FK4 BIGINT NOT NULL,
    FK5 BIGINT NOT NULL,
    FK6 BIGINT NOT NULL,
    FK7 BIGINT NOT NULL,
    FK8 BIGINT NOT NULL,
    FK9 BIGINT NOT NULL,
    FK10 BIGINT NOT NULL,
    FK11 BIGINT NOT NULL,
    FK12 BIGINT NOT NULL,
    FK13 BIGINT NOT NULL,
    FK14 BIGINT NOT NULL,
    FK15 BIGINT NOT NULL,
    FK16 BIGINT NOT NULL,
    FK17 BIGINT NOT NULL,
    FK18 BIGINT NOT NULL,
    FK19 BIGINT NOT NULL,
    FK20 BIGINT NOT NULL
);

INSERT INTO dbo.HASH_SMALL WITH (TABLOCK)
SELECT RN,
4000000 - RN, 4000000 - RN
,200000000 - RN, 200000000 - RN
, RN % 500000 , RN % 500000 , RN % 500000
, RN % 500000 , RN % 500000 , RN % 500000 
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
, 100000 - RN % 100000, RN % 100000
FROM (
    SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.LOG_HASHES;
CREATE TABLE dbo.LOG_HASHES (
LOG_TIME DATETIME,
HASH_ALGORITHM INT,
SESSION_ID INT,
NUM_HASHES BIGINT
);
_

設定2:マスター実行プロセス

_GO
CREATE OR ALTER PROCEDURE dbo.RUN_HASHES_FOR_ONE_MINUTE (@HashAlgorithm INT)
AS
BEGIN
DECLARE @target_end_time DATETIME = DATEADD(MINUTE, 1, GETDATE()),
        @query_execution_count INT = 0;

SET NOCOUNT ON;

DECLARE @ProcName NVARCHAR(261); -- schema_name + proc_name + '[].[]'

DECLARE @RowCount INT;
SELECT @RowCount = SUM(prtn.[row_count])
FROM   sys.dm_db_partition_stats prtn
WHERE  prtn.[object_id] = OBJECT_ID(N'dbo.HASH_SMALL')
AND    prtn.[index_id] < 2;


-- Load Assembly if not loaded to prevent load time from skewing results
DECLARE @OptionalInitSQL NVARCHAR(MAX);
SET @OptionalInitSQL = CASE @HashAlgorithm
       WHEN 1 THEN N'SELECT @Dummy = dbo.SpookyHash(0x1234);'
       WHEN 2 THEN N'' -- HASHBYTES
       WHEN 3 THEN N'' -- HASHBYTES
       WHEN 4 THEN N'' -- CHECKSUM
       WHEN 5 THEN N'SELECT @Dummy = dbo.SpookyHashLOB(0x1234);'
       WHEN 6 THEN N'SELECT @Dummy = SQL#.Util_HashBinary(N''MD5'', 0x1234);'
       WHEN 7 THEN N'SELECT @Dummy = SQL#.Util_HashBinary(N''SHA256'', 0x1234);'
       WHEN 8 THEN N'SELECT @Dummy = SQL#.Util_HashBinary8k(N''MD5'', 0x1234);'
       WHEN 9 THEN N'SELECT @Dummy = SQL#.Util_HashBinary8k(N''SHA256'', 0x1234);'
/* -- BETA / non-public code
       WHEN 10 THEN N'SELECT @Dummy = SQL#.Util_HashSHA256Binary8k(0x1234);'
       WHEN 11 THEN N'SELECT @Dummy = SQL#.Util_HashSHA256Binary(0x1234);'
*/
   END;


IF (RTRIM(@OptionalInitSQL) <> N'')
BEGIN
    SET @OptionalInitSQL = N'
SET NOCOUNT ON;
DECLARE @Dummy VARBINARY(100);
' + @OptionalInitSQL;

    RAISERROR(N'** Executing optional initialization code:', 10, 1) WITH NOWAIT;
    RAISERROR(@OptionalInitSQL, 10, 1) WITH NOWAIT;
    EXEC (@OptionalInitSQL);
    RAISERROR(N'-------------------------------------------', 10, 1) WITH NOWAIT;
END;


SET @ProcName = CASE @HashAlgorithm
                    WHEN 1 THEN N'dbo.RUN_SpookyHash'
                    WHEN 2 THEN N'dbo.RUN_HASHBYTES_MD5'
                    WHEN 3 THEN N'dbo.RUN_HASHBYTES_SHA2_256'
                    WHEN 4 THEN N'dbo.RUN_CHECKSUM'
                    WHEN 5 THEN N'dbo.RUN_SpookyHashLOB'
                    WHEN 6 THEN N'dbo.RUN_SR_MD5'
                    WHEN 7 THEN N'dbo.RUN_SR_SHA256'
                    WHEN 8 THEN N'dbo.RUN_SR_MD5_8k'
                    WHEN 9 THEN N'dbo.RUN_SR_SHA256_8k'
/* -- BETA / non-public code
                    WHEN 10 THEN N'dbo.RUN_SR_SHA256_new'
                    WHEN 11 THEN N'dbo.RUN_SR_SHA256LOB_new'
*/
                    WHEN 13 THEN N'dbo.RUN_HASHBYTES_SHA2_256_NVC'
                END;

RAISERROR(N'** Executing proc: %s', 10, 1, @ProcName) WITH NOWAIT;

WHILE GETDATE() < @target_end_time
BEGIN
    EXEC @ProcName;

    SET @query_execution_count = @query_execution_count + 1;
END;

INSERT INTO dbo.LOG_HASHES
VALUES (GETDATE(), @HashAlgorithm, @@SPID, @RowCount * @query_execution_count);

END;
GO
_

設定3:衝突検出プロセス

_GO
CREATE OR ALTER PROCEDURE dbo.VERIFY_NO_COLLISIONS (@HashAlgorithm INT)
AS
SET NOCOUNT ON;

DECLARE @RowCount INT;
SELECT @RowCount = SUM(prtn.[row_count])
FROM   sys.dm_db_partition_stats prtn
WHERE  prtn.[object_id] = OBJECT_ID(N'dbo.HASH_SMALL')
AND    prtn.[index_id] < 2;


DECLARE @CollisionTestRows INT;
DECLARE @CollisionTestSQL NVARCHAR(MAX);
SET @CollisionTestSQL = N'
SELECT @RowsOut = COUNT(DISTINCT '
+ CASE @HashAlgorithm
       WHEN 1 THEN N'dbo.SpookyHash('
       WHEN 2 THEN N'HASHBYTES(''MD5'','
       WHEN 3 THEN N'HASHBYTES(''SHA2_256'','
       WHEN 4 THEN N'CHECKSUM('
       WHEN 5 THEN N'dbo.SpookyHashLOB('
       WHEN 6 THEN N'SQL#.Util_HashBinary(N''MD5'','
       WHEN 7 THEN N'SQL#.Util_HashBinary(N''SHA256'','
       WHEN 8 THEN N'SQL#.[Util_HashBinary8k](N''MD5'','
       WHEN 9 THEN N'SQL#.[Util_HashBinary8k](N''SHA256'','
--/* -- BETA / non-public code
       WHEN 10 THEN N'SQL#.[Util_HashSHA256Binary8k]('
       WHEN 11 THEN N'SQL#.[Util_HashSHA256Binary]('
--*/
   END
+ N'
    CAST(FK1 AS BINARY(8)) + 0x7C +
    CAST(FK2 AS BINARY(8)) + 0x7C +
    CAST(FK3 AS BINARY(8)) + 0x7C +
    CAST(FK4 AS BINARY(8)) + 0x7C +
    CAST(FK5 AS BINARY(8)) + 0x7C +
    CAST(FK6 AS BINARY(8)) + 0x7C +
    CAST(FK7 AS BINARY(8)) + 0x7C +
    CAST(FK8 AS BINARY(8)) + 0x7C +
    CAST(FK9 AS BINARY(8)) + 0x7C +
    CAST(FK10 AS BINARY(8)) + 0x7C +
    CAST(FK11 AS BINARY(8)) + 0x7C +
    CAST(FK12 AS BINARY(8)) + 0x7C +
    CAST(FK13 AS BINARY(8)) + 0x7C +
    CAST(FK14 AS BINARY(8)) + 0x7C +
    CAST(FK15 AS BINARY(8)) + 0x7C +
    CAST(FK16 AS BINARY(8)) + 0x7C +
    CAST(FK17 AS BINARY(8)) + 0x7C +
    CAST(FK18 AS BINARY(8)) + 0x7C +
    CAST(FK19 AS BINARY(8)) + 0x7C +
    CAST(FK20 AS BINARY(8))  ))
FROM dbo.HASH_SMALL;';

PRINT @CollisionTestSQL;

EXEC sp_executesql
  @CollisionTestSQL,
  N'@RowsOut INT OUTPUT',
  @RowsOut = @CollisionTestRows OUTPUT;


IF (@CollisionTestRows <> @RowCount)
BEGIN
    RAISERROR('Collisions for algorithm: %d!!!  %d unique rows out of %d.',
    16, 1, @HashAlgorithm, @CollisionTestRows, @RowCount);
END;
GO
_

設定4:クリーンアップ(DROP All Test Procs)

_DECLARE @SQL NVARCHAR(MAX) = N'';
SELECT @SQL += N'DROP PROCEDURE [dbo].' + QUOTENAME(sp.[name])
            + N';' + NCHAR(13) + NCHAR(10)
FROM  sys.objects sp
WHERE sp.[name] LIKE N'RUN[_]%'
AND   sp.[type_desc] = N'SQL_STORED_PROCEDURE'
AND   sp.[name] <> N'RUN_HASHES_FOR_ONE_MINUTE'

PRINT @SQL;

EXEC (@SQL);
_

設定5:テストプロシージャの生成

_SET NOCOUNT ON;

DECLARE @TestProcsToCreate TABLE
(
  ProcName sysname NOT NULL,
  CodeToExec NVARCHAR(261) NOT NULL
);
DECLARE @ProcName sysname,
        @CodeToExec NVARCHAR(261);

INSERT INTO @TestProcsToCreate VALUES
  (N'SpookyHash', N'dbo.SpookyHash('),
  (N'HASHBYTES_MD5', N'HASHBYTES(''MD5'','),
  (N'HASHBYTES_SHA2_256', N'HASHBYTES(''SHA2_256'','),
  (N'CHECKSUM', N'CHECKSUM('),
  (N'SpookyHashLOB', N'dbo.SpookyHashLOB('),
  (N'SR_MD5', N'SQL#.Util_HashBinary(N''MD5'','),
  (N'SR_SHA256', N'SQL#.Util_HashBinary(N''SHA256'','),
  (N'SR_MD5_8k', N'SQL#.[Util_HashBinary8k](N''MD5'','),
  (N'SR_SHA256_8k', N'SQL#.[Util_HashBinary8k](N''SHA256'',')
--/* -- BETA / non-public code
  , (N'SR_SHA256_new', N'SQL#.[Util_HashSHA256Binary8k]('),
  (N'SR_SHA256LOB_new', N'SQL#.[Util_HashSHA256Binary](');
--*/
DECLARE @ProcTemplate NVARCHAR(MAX),
        @ProcToCreate NVARCHAR(MAX);

SET @ProcTemplate = N'
CREATE OR ALTER PROCEDURE dbo.RUN_{{ProcName}}
AS
BEGIN
DECLARE @dummy INT;
SET NOCOUNT ON;

SELECT @dummy = COUNT({{CodeToExec}}
    CAST(FK1 AS BINARY(8)) + 0x7C +
    CAST(FK2 AS BINARY(8)) + 0x7C +
    CAST(FK3 AS BINARY(8)) + 0x7C +
    CAST(FK4 AS BINARY(8)) + 0x7C +
    CAST(FK5 AS BINARY(8)) + 0x7C +
    CAST(FK6 AS BINARY(8)) + 0x7C +
    CAST(FK7 AS BINARY(8)) + 0x7C +
    CAST(FK8 AS BINARY(8)) + 0x7C +
    CAST(FK9 AS BINARY(8)) + 0x7C +
    CAST(FK10 AS BINARY(8)) + 0x7C +
    CAST(FK11 AS BINARY(8)) + 0x7C +
    CAST(FK12 AS BINARY(8)) + 0x7C +
    CAST(FK13 AS BINARY(8)) + 0x7C +
    CAST(FK14 AS BINARY(8)) + 0x7C +
    CAST(FK15 AS BINARY(8)) + 0x7C +
    CAST(FK16 AS BINARY(8)) + 0x7C +
    CAST(FK17 AS BINARY(8)) + 0x7C +
    CAST(FK18 AS BINARY(8)) + 0x7C +
    CAST(FK19 AS BINARY(8)) + 0x7C +
    CAST(FK20 AS BINARY(8)) 
    )
    )
    FROM dbo.HASH_SMALL
    OPTION (MAXDOP 1);

END;
';

DECLARE CreateProcsCurs CURSOR READ_ONLY FORWARD_ONLY LOCAL FAST_FORWARD
FOR SELECT [ProcName], [CodeToExec]
    FROM @TestProcsToCreate;

OPEN [CreateProcsCurs];

FETCH NEXT
FROM  [CreateProcsCurs]
INTO  @ProcName, @CodeToExec;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    -- First: create VARBINARY version
    SET @ProcToCreate = REPLACE(REPLACE(@ProcTemplate,
                                        N'{{ProcName}}',
                                        @ProcName),
                                N'{{CodeToExec}}',
                                @CodeToExec);

    EXEC (@ProcToCreate);

    -- Second: create NVARCHAR version (optional: built-ins only)
    IF (CHARINDEX(N'.', @CodeToExec) = 0)
    BEGIN
        SET @ProcToCreate = REPLACE(REPLACE(REPLACE(@ProcToCreate,
                                                    N'dbo.RUN_' + @ProcName,
                                                    N'dbo.RUN_' + @ProcName + N'_NVC'),
                                            N'BINARY(8)',
                                            N'NVARCHAR(15)'),
                                    N'0x7C',
                                    N'N''|''');

        EXEC (@ProcToCreate);
    END;

    FETCH NEXT
    FROM  [CreateProcsCurs]
    INTO  @ProcName, @CodeToExec;
END;

CLOSE [CreateProcsCurs];
DEALLOCATE [CreateProcsCurs];
_

テスト1:衝突のチェック

_EXEC dbo.VERIFY_NO_COLLISIONS 1;
EXEC dbo.VERIFY_NO_COLLISIONS 2;
EXEC dbo.VERIFY_NO_COLLISIONS 3;
EXEC dbo.VERIFY_NO_COLLISIONS 4;
EXEC dbo.VERIFY_NO_COLLISIONS 5;
EXEC dbo.VERIFY_NO_COLLISIONS 6;
EXEC dbo.VERIFY_NO_COLLISIONS 7;
EXEC dbo.VERIFY_NO_COLLISIONS 8;
EXEC dbo.VERIFY_NO_COLLISIONS 9;
EXEC dbo.VERIFY_NO_COLLISIONS 10;
EXEC dbo.VERIFY_NO_COLLISIONS 11;
_

TEST 2:パフォーマンステストの実行

_EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 1;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 2;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 3; -- HASHBYTES('SHA2_256'
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 4;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 5;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 6;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 7;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 8;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 9;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 10;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 11;
EXEC dbo.RUN_HASHES_FOR_ONE_MINUTE 13; -- NVC version of #3


SELECT *
FROM   dbo.LOG_HASHES
ORDER BY [LOG_TIME] DESC;
_

解決する検証の問題

単一のSQLCLR UDFのパフォーマンステストに焦点を当てている間、早い段階で議論された2つの問題はテストに組み込まれていませんが、理想的にはどのアプローチがを満たすかを判断するために調査する必要があります(= /// =)の要件。

  1. 関数はクエリごとに2回実行されます(インポート行に対して1回、現在の行に対して1回)。これまでのテストでは、テストクエリで一度しかUDFを参照していませんでした。この要素はオプションのランキングを変更しないかもしれませんが、念のために無視してはいけません。
  2. その後削除されたコメントで、Paul Whiteは次のように述べています。

    HASHBYTESをCLRスカラー関数に置き換えることの1つの欠点-CLR関数はバッチモードを使用できないが、HASHBYTESは使用できるようです。それはパフォーマンス面で重要かもしれません。

    これは考慮すべきことであり、明らかにテストが必要です。 SQLCLRオプションが組み込みのHASHBYTESよりもメリットがない場合は、関連するテーブルに既存のハッシュ(少なくとも最大のテーブル)をキャプチャする ソロモンの提案 に重みを追加します。 。

12
Joe Obbish

関数呼び出しで作成されたオブジェクトをプールしてキャッシュすることにより、おそらくパフォーマンスを向上させ、すべての.NETアプローチのスケーラビリティを向上させることができます。上記のPaul WhiteのコードのEG:

static readonly ConcurrentDictionary<int,ISpookyHashV2> hashers = new ConcurrentDictonary<ISpookyHashV2>()
public static byte[] SpookyHash([SqlFacet (MaxSize = 8000)] SqlBinary Input)
{
    ISpookyHashV2 sh = hashers.GetOrAdd(Thread.CurrentThread.ManagedThreadId, i => SpookyHashV2Factory.Instance.Create());

    return sh.ComputeHash(Input.Value).Hash;
}

SQL CLRは、静的/共有変数の使用を推奨しませんが、共有変数を読み取り専用としてマークすると、共有変数を使用できるようになります。もちろん、これはConcurrentDictionaryのようないくつかの変更可能な型の単一のインスタンスを割り当てることができるため、意味がありません。