Was ist eine skalierbare Methode zur Simulation von HASHBYTES mithilfe einer SQL CLR-Skalarfunktion?

29

Im Rahmen unseres ETL-Prozesses vergleichen wir Zeilen aus dem Staging mit der Berichtsdatenbank, um festzustellen, ob sich eine der Spalten seit dem letzten Laden der Daten tatsächlich geändert hat.

Der Vergleich basiert auf dem eindeutigen Schlüssel der Tabelle und einer Art Hash aller anderen Spalten. Wir verwenden derzeit HASHBYTESden SHA2_256Algorithmus und haben festgestellt, dass er auf großen Servern nicht skaliert, wenn viele gleichzeitige Worker-Threads alle aufrufen HASHBYTES.

Der in Hashes pro Sekunde gemessene Durchsatz steigt nach 16 gleichzeitigen Threads beim Testen auf einem 96-Core-Server nicht an. Ich teste, indem ich die Anzahl der gleichzeitigen MAXDOP 8Abfragen von 1 bis 12 ändere. Tests mit MAXDOP 1zeigten den gleichen Skalierbarkeitsengpass.

Als Workaround möchte ich eine SQL CLR-Lösung ausprobieren. Hier ist mein Versuch, die Anforderungen anzugeben:

  • Die Funktion muss an parallelen Abfragen teilnehmen können
  • Die Funktion muss deterministisch sein
  • Die Funktion muss die Eingabe eines NVARCHARoder eines VARBINARYStrings annehmen (alle relevanten Spalten sind miteinander verkettet)
  • Die typische Eingabegröße der Zeichenfolge beträgt 100 - 20000 Zeichen. 20000 ist keine max
  • Die Wahrscheinlichkeit einer Hash-Kollision sollte ungefähr gleich oder besser sein als der MD5-Algorithmus. CHECKSUMfunktioniert bei uns nicht, weil es zu viele kollisionen gibt.
  • Die Funktion muss auf großen Servern gut skalierbar sein (der Durchsatz pro Thread sollte mit zunehmender Anzahl von Threads nicht wesentlich abnehmen)

Nehmen Sie für Application Reasons ™ an, dass ich den Wert des Hash für die Berichtstabelle nicht speichern kann. Es ist eine CCI, die keine Trigger oder berechneten Spalten unterstützt (es gibt auch andere Probleme, auf die ich nicht eingehen möchte).

Was ist eine skalierbare Methode zum Simulieren HASHBYTESmit einer SQL CLR-Funktion? Mein Ziel kann darin bestehen, so viele Hashes pro Sekunde wie möglich auf einem großen Server zu erhalten. Daher ist auch die Leistung von Bedeutung. Ich bin schrecklich mit CLR, deshalb weiß ich nicht, wie ich das erreichen soll. Wenn es jemanden zum Antworten motiviert, plane ich, dieser Frage ein Kopfgeld hinzuzufügen, sobald ich dazu in der Lage bin. Nachfolgend finden Sie eine Beispielabfrage, die den Anwendungsfall sehr grob darstellt:

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);

Um die Sache ein wenig zu vereinfachen, werde ich für das Benchmarking wahrscheinlich Folgendes verwenden. Ich werde die Ergebnisse mit HASHBYTESam Montag veröffentlichen:

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);
Joe Obbish
quelle

Antworten:

18

Da Sie nur nach Änderungen suchen, benötigen Sie keine kryptografische Hash-Funktion.

Sie können aus einem der schnelleren nicht-kryptografischen Hashes in der Open-Source- Data.HashFunction-Bibliothek von Brandon Dahler wählen , die unter der zulässigen und von OSI genehmigten MIT-Lizenz lizenziert ist .SpookyHashist eine beliebte Wahl.

Beispielimplementierung

Quellcode

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;
    }
}

Die Quelle bietet zwei Funktionen, eine für Eingaben von 8000 Byte oder weniger, und eine LOB-Version. Die Nicht-LOB-Version sollte wesentlich schneller sein.

Möglicherweise können Sie eine LOB-Binärdatei einbinden COMPRESS, um die 8000-Byte-Grenze zu unterschreiten, wenn sich dies für die Leistung als lohnend herausstellt. Alternativ können Sie das LOB in Sub-8000-Byte-Segmente aufteilen oder einfach die Verwendung HASHBYTESfür den LOB-Fall reservieren (da längere Eingaben besser skalieren).

Vorgefertigter Code

Sie können sich natürlich das Paket selbst schnappen und alles kompilieren, aber ich habe die folgenden Assemblys erstellt, um das schnelle Testen zu vereinfachen:

https://gist.github.com/SQLKiwi/365b265b476bf86754457fc9514b2300

T-SQL-Funktionen

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

Verwendung

Ein Anwendungsbeispiel anhand der Beispieldaten in der Frage:

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)
    );

Bei Verwendung der LOB-Version sollte der erste Parameter umgewandelt oder konvertiert werden varbinary(max).

Ausführungsplan

planen


Sicher gruselig

Die Data.HashFunction- Bibliothek verwendet eine Reihe von CLR- Sprachfunktionen , die UNSAFEvon SQL Server berücksichtigt werden. Es ist möglich, einen grundlegenden Spooky Hash zu schreiben, der mit dem SAFEStatus kompatibel ist . Ein Beispiel, das ich basierend auf Jon Hannas SpookilySharp geschrieben habe, ist unten:

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

Paul White sagt GoFundMonica
quelle
16

Ich bin mir nicht sicher, ob die Parallelität mit SQLCLR deutlich besser wird. Es ist jedoch sehr einfach zu testen, da es eine Hash-Funktion in der freien Version der SQL # SQLCLR-Bibliothek (die ich geschrieben habe) namens Util_HashBinary gibt . Unterstützte Algorithmen sind: MD5, SHA1, SHA256, SHA384 und SHA512.

Es wird ein VARBINARY(MAX)Wert als Eingabe verwendet, sodass Sie entweder die Zeichenfolgenversion jedes Felds verketten (wie Sie es gerade tun) und dann in konvertieren können VARBINARY(MAX), oder Sie können direkt zu VARBINARYjeder Spalte gehen und die konvertierten Werte verketten (dies ist möglicherweise schneller als zuvor) Sie haben es nicht mit Strings oder der zusätzlichen Konvertierung von String nach VARBINARY) zu tun . Im Folgenden finden Sie ein Beispiel, das beide Optionen zeigt. Außerdem wird die HASHBYTESFunktion angezeigt, sodass Sie sehen können, dass die Werte zwischen ihr und SQL # .Util_HashBinary identisch sind .

Bitte beachten Sie, dass die Hash-Ergebnisse beim Verketten der VARBINARYWerte nicht mit den Hash-Ergebnissen beim Verketten der NVARCHARWerte übereinstimmen. Dies liegt daran, dass die Binärform des INTWerts "1" 0x00000001 ist, während die UTF-16LE (dh NVARCHAR) -Form des INTWerts "1" (in Binärform, da auf diese Weise eine Hashing-Funktion ausgeführt wird) 0x3100 ist.

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;

Sie können etwas Vergleichbareres mit dem Nicht-LOB-Spooky testen, indem Sie Folgendes verwenden:

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];

Hinweis: Util_HashBinary verwendet den in .NET integrierten verwalteten SHA256-Algorithmus und sollte nicht die Bibliothek "bcrypt" verwenden.

Über diesen Aspekt der Frage hinaus gibt es einige zusätzliche Gedanken, die diesen Prozess unterstützen könnten:

Zusätzlicher Gedanke # 1 (Hashes vorberechnen, zumindest einige)

Sie haben ein paar Dinge erwähnt:

  1. Wir vergleichen Zeilen aus dem Staging mit der Berichtsdatenbank, um festzustellen, ob sich eine der Spalten seit dem letzten Laden der Daten tatsächlich geändert hat.

    und:

  2. Ich kann den Wert des Hash für die Berichtstabelle nicht speichern. Es ist eine CCI, die keine Trigger oder berechneten Spalten unterstützt

    und:

  3. Die Tabellen können außerhalb des ETL-Prozesses aktualisiert werden

Es hört sich so an, als ob die Daten in dieser Berichtstabelle für einen bestimmten Zeitraum stabil sind und nur durch diesen ETL-Prozess geändert werden.

Wenn diese Tabelle durch nichts anderes geändert wird, brauchen wir doch keinen Trigger oder indizierten View (ich dachte ursprünglich, dass Sie das könnten).

Da Sie das Schema der Berichtstabelle nicht ändern können, ist es zumindest möglich, eine zugehörige Tabelle zu erstellen, die den vorberechneten Hash (und die UTC-Zeit, zu der er berechnet wurde) enthält. Dies würde es Ihnen ermöglichen, einen vorberechneten Wert für den Vergleich mit dem nächsten Mal zu haben, wobei nur der eingehende Wert übrig bleibt, für den der Hash berechnet werden muss. Dies würde die Anzahl der Anrufe entweder auf die Hälfte HASHBYTESoder SQL#.Util_HashBinaryauf die Hälfte reduzieren . Sie würden sich während des Importvorgangs einfach dieser Hashtabelle anschließen.

Sie würden auch eine separate gespeicherte Prozedur erstellen, die einfach die Hashes dieser Tabelle aktualisiert. Es werden nur die Hashes aller verwandten Zeilen aktualisiert, die aktuell geändert wurden, und der Zeitstempel für diese geänderten Zeilen wird aktualisiert. Dieser Prozess kann / sollte am Ende eines anderen Prozesses ausgeführt werden, der diese Tabelle aktualisiert. Es kann auch geplant werden, 30 bis 60 Minuten vor dem Start dieser ETL ausgeführt zu werden (abhängig davon, wie lange die Ausführung dauert und wann einer dieser anderen Prozesse möglicherweise ausgeführt wird). Es kann sogar manuell ausgeführt werden, wenn Sie den Verdacht haben, dass Zeilen nicht synchron sind.

Es wurde darauf hingewiesen, dass:

Es gibt über 500 Tische

Aufgrund der vielen Tabellen ist es schwieriger, für jede Tabelle eine zusätzliche Tabelle mit den aktuellen Hashes zu erstellen. Dies ist jedoch nicht unmöglich, da Skripte erstellt werden könnten, da es sich um ein Standardschema handelt. Das Scripting muss nur den Namen der Quelltabelle und die Entdeckung der PK-Spalte (n) der Quelltabelle berücksichtigen.

Unabhängig davon, welcher Hash-Algorithmus sich letztendlich als am besten skalierbar erweist, empfehle ich dennoch dringend , mindestens ein paar Tabellen zu finden (möglicherweise gibt es einige, die VIEL größer sind als die restlichen 500 Tabellen) und eine zugehörige Tabelle für die Erfassung einzurichten Aktuelle Hashes, damit die "aktuellen" Werte vor dem ETL-Prozess bekannt sind. Sogar die schnellste Funktion kann es nicht übertreffen, sie überhaupt erst aufrufen zu müssen ;-).

Zusätzlicher Gedanke # 2 ( VARBINARYanstelle von NVARCHAR)

Unabhängig von SQLCLR vs built-in HASHBYTES, würde ich noch empfehlen , direkt zu konvertieren VARBINARYwie sollte schneller sein. Das Verketten von Zeichenfolgen ist einfach nicht besonders effizient. Und das ist in erster Linie zusätzlich zur Konvertierung von Nicht-String-Werten in Strings, was zusätzlichen Aufwand erfordert (ich gehe davon aus, dass der Aufwand je nach Basistyp unterschiedlich ist: DATETIMEErfordert mehr als BIGINT), während die Konvertierung in VARBINARYeinfach den zugrunde liegenden Wert ergibt (in den meisten Fällen).

Tatsächlich HASHBYTES(N'SHA2_256',...)ergab das Testen desselben Datensatzes, den die anderen verwendeten und verwendeten Tests verwendeten, einen Anstieg der Gesamtzahl der in einer Minute berechneten Hashes um 23,415%. Und diese Erhöhung war nichts anderes, als zu verwenden, VARBINARYanstatt NVARCHAR! 😸 ( Details siehe Community-Wiki-Antwort )

Zusätzlicher Gedanke # 3 (Eingabeparameter beachten)

Weitere Tests ergaben, dass ein Bereich, der sich auf die Leistung auswirkt (über dieses Volumen von Ausführungen), Eingabeparameter sind: wie viele und welche Art (en).

Die Util_HashBinary SQLCLR-Funktion, die sich derzeit in meiner SQL # -Bibliothek befindet, verfügt über zwei Eingabeparameter: einen VARBINARY(zu hashender Wert) und einen NVARCHAR(zu verwendender Algorithmus). Dies liegt an meiner Spiegelung der Signatur der HASHBYTESFunktion. Ich stellte jedoch fest, dass sich die NVARCHARLeistung recht gut verbesserte , wenn ich den Parameter entfernte und eine Funktion erstellte, die nur SHA256 ausführte. Ich gehe davon aus, dass auch das Umschalten des NVARCHARParameters auf INTgeholfen hätte, aber ich gehe auch davon aus, dass das Fehlen des zusätzlichen INTParameters zumindest geringfügig schneller ist.

Auch SqlBytes.Valuekönnte eine bessere Leistung als SqlBinary.Value.

Für diesen Test habe ich zwei neue Funktionen erstellt: Util_HashSHA256Binary und Util_HashSHA256Binary8k . Diese werden in der nächsten Version von SQL # enthalten sein (noch kein Datum dafür festgelegt).

Ich habe auch festgestellt, dass die Testmethode leicht verbessert werden kann. Daher habe ich das Testgeschirr in der folgenden Wiki-Antwort aktualisiert und Folgendes hinzugefügt:

  1. Vorabladen der SQLCLR-Assemblys, um sicherzustellen, dass der Zeitaufwand für das Laden die Ergebnisse nicht verzerrt.
  2. ein Überprüfungsverfahren zum Überprüfen auf Kollisionen. Wenn welche gefunden werden, werden die Anzahl der eindeutigen / unterschiedlichen Zeilen und die Gesamtanzahl der Zeilen angezeigt. Auf diese Weise kann festgestellt werden, ob die Anzahl der Kollisionen (sofern vorhanden) den Grenzwert für den angegebenen Anwendungsfall überschreitet. Einige Anwendungsfälle können möglicherweise eine geringe Anzahl von Kollisionen zulassen, andere erfordern möglicherweise keine. Eine superschnelle Funktion ist nutzlos, wenn sie Änderungen der gewünschten Genauigkeit nicht erkennen kann. Mit dem vom OP bereitgestellten CHECKSUMTestkabel habe ich beispielsweise die Zeilenzahl auf 100.000 Zeilen erhöht (ursprünglich waren es 10.000) und festgestellt, dass über 9.000 Kollisionen registriert wurden, was 9% (Huch) entspricht.

Zusätzlicher Gedanke # 4 ( HASHBYTES+ SQLCLR zusammen?)

Je nachdem, wo sich der Engpass befindet, kann es sogar hilfreich sein, eine Kombination aus integrierter HASHBYTESund einer SQLCLR-UDF zu verwenden, um denselben Hash auszuführen. Wenn integrierte Funktionen anders oder getrennt von SQLCLR-Operationen eingeschränkt werden, kann dieser Ansatz möglicherweise mehr gleichzeitig als entweder HASHBYTESoder SQLCLR einzeln ausführen. Es lohnt sich auf jeden Fall zu testen.

Zusätzlicher Gedanke # 5 (Zwischenspeichern von Hashing-Objekten?)

Das Zwischenspeichern des Hashing-Algorithmus-Objekts, wie es in David Brownes Antwort vorgeschlagen wurde, scheint sicherlich interessant zu sein, also habe ich es versucht und die folgenden zwei Punkte von Interesse gefunden:

  1. Aus welchem ​​Grund auch immer, es scheint nicht viel, wenn überhaupt, Leistungsverbesserung zu bringen. Ich hätte etwas falsch machen können, aber hier ist, was ich versucht habe:

    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. Der ManagedThreadIdWert scheint für alle SQLCLR-Referenzen in einer bestimmten Abfrage gleich zu sein. Ich habe mehrere Verweise auf dieselbe Funktion sowie einen Verweis auf eine andere Funktion getestet, wobei alle drei unterschiedliche Eingabewerte erhielten und unterschiedliche (aber erwartete) Rückgabewerte zurückgaben. Für beide Testfunktionen war die Ausgabe eine Zeichenfolge, die sowohl die ManagedThreadIdZeichenfolgendarstellung des Hash-Ergebnisses enthielt . Der ManagedThreadIdWert war für alle UDF-Referenzen in der Abfrage und für alle Zeilen gleich. Das Hash-Ergebnis war jedoch für dieselbe Eingabezeichenfolge identisch und für unterschiedliche Eingabezeichenfolgen unterschiedlich.

    Obwohl ich bei meinen Tests keine fehlerhaften Ergebnisse gesehen habe, würde dies nicht die Chancen auf eine Rennsituation erhöhen? Wenn der Schlüssel des Wörterbuchs für alle in einer bestimmten Abfrage aufgerufenen SQLCLR-Objekte gleich ist, teilen sie sich denselben Wert oder dasselbe Objekt, das für diesen Schlüssel gespeichert ist, oder? Der Punkt, der zu sein schien, obwohl es hier zu funktionieren schien (bis zu einem gewissen Grad schien es nicht viel Leistungsgewinn zu geben, aber funktionell war nichts kaputt), gibt mir kein Vertrauen, dass dieser Ansatz in anderen Szenarien funktionieren wird.

Solomon Rutzky
quelle
11

Dies ist keine traditionelle Antwort, aber ich dachte, es wäre hilfreich, Benchmarks für einige der bisher erwähnten Techniken zu veröffentlichen. Ich teste auf einem 96 Core Server mit SQL Server 2017 CU9.

Viele Skalierbarkeitsprobleme werden durch gleichzeitige Threads verursacht, die um einen globalen Status konkurrieren. Betrachten Sie beispielsweise klassische PFS-Seitenkonflikte. Dies kann passieren, wenn zu viele Worker-Threads dieselbe Seite im Speicher ändern müssen. Wenn der Code effizienter wird, wird der Latch möglicherweise schneller angefordert. Das erhöht die Konkurrenz. Einfach ausgedrückt führt effizienter Code mit größerer Wahrscheinlichkeit zu Problemen bei der Skalierbarkeit, da der globale Status strenger umstritten ist. Langsamer Code verursacht mit geringerer Wahrscheinlichkeit Skalierbarkeitsprobleme, da nicht so häufig auf den globalen Status zugegriffen wird.

HASHBYTESDie Skalierbarkeit basiert teilweise auf der Länge der Eingabezeichenfolge. Meine Theorie war, warum dies auftritt, ist, dass der Zugriff auf einen globalen Status erforderlich ist, wenn die HASHBYTESFunktion aufgerufen wird. Der einfach zu beobachtende globale Status ist, dass bei einigen Versionen von SQL Server eine Speicherseite pro Aufruf zugewiesen werden muss. Das Schwierigste ist, zu beobachten, dass es irgendeine Art von Betriebssystemkonflikt gibt. Als Ergebnis, wenn Spalten. Die Definition der Tabelle ist im Code unten enthalten. Um Local Factors ™ zu reduzieren, verwende ich gleichzeitige Abfragen, die auf relativ kleinen Tabellen ausgeführt werden. Mein kurzer Benchmark-Code befindet sich unten.HASHBYTES es zu Konflikten der Code weniger häufig aufruft. Eine Möglichkeit, die Anrufrate zu senken, HASHBYTESbesteht darin, die Anzahl der pro Anruf erforderlichen Hashing-Vorgänge zu erhöhen. Die Hashing-Arbeit basiert teilweise auf der Länge der Eingabezeichenfolge. Um das Skalierbarkeitsproblem zu reproduzieren, das ich in der Anwendung sah, musste ich die Demo-Daten ändern. Ein vernünftiges Worst-Case-Szenario ist eine Tabelle mit 21BIGINTMAXDOP 1

Beachten Sie, dass die Funktionen unterschiedliche Hash-Längen zurückgeben. MD5und SpookyHashsind beide 128-Bit-Hashes, SHA256ist ein 256-Bit-Hash.

ERGEBNISSE ( NVARCHARvs. VARBINARYKonvertierung und Verkettung)

Um festzustellen, ob die Konvertierung in und Verkettung VARBINARYwirklich effizienter / performanter ist als NVARCHAR, wurde eine NVARCHARVersion der RUN_HASHBYTES_SHA2_256gespeicherten Prozedur aus derselben Vorlage erstellt (siehe "Schritt 5" im Abschnitt BENCHMARKING CODE weiter unten). Die einzigen Unterschiede sind:

  1. Der Name der gespeicherten Prozedur endet mit _NVC
  2. BINARY(8)für die CASTfunktion wurde geändertNVARCHAR(15)
  3. 0x7C wurde geändert, um zu sein N'|'

Ergebend:

CAST(FK1 AS NVARCHAR(15)) + N'|' +

anstatt:

CAST(FK1 AS BINARY(8)) + 0x7C +

Die nachstehende Tabelle enthält die Anzahl der in einer Minute durchgeführten Hashes. Die Tests wurden auf einem anderen Server durchgeführt als für die anderen unten angegebenen Tests.

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

Wenn wir uns nur die Durchschnittswerte ansehen, können wir den Nutzen eines Wechsels zu Folgendem berechnen VARBINARY:

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

Das gibt zurück:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

ERGEBNISSE (Hash-Algorithmen und Implementierungen)

Die nachstehende Tabelle enthält die Anzahl der in einer Minute durchgeführten Hashes. Zum Beispiel führte die Verwendung CHECKSUMvon 84 gleichzeitigen Abfragen dazu, dass über 2 Milliarden Hashes ausgeführt wurden, bevor die Zeit abgelaufen war.

╔════════════════════╦════════════╦════════════╦════════════╗
      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 
╚════════════════════╩════════════╩════════════╩════════════╝

Wenn Sie es vorziehen, die gleichen Zahlen in Bezug auf die Arbeit pro Thread-Sekunde zu sehen:

╔════════════════════╦════════════════════════════╦════════════════════════════╦════════════════════════════╗
      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 
╚════════════════════╩════════════════════════════╩════════════════════════════╩════════════════════════════╝

Einige kurze Gedanken zu allen Methoden:

  • CHECKSUM: erwartungsgemäß sehr gute Skalierbarkeit
  • HASHBYTES: Skalierbarkeitsprobleme umfassen eine Speicherzuweisung pro Aufruf und einen hohen CPU-Aufwand im Betriebssystem
  • Spooky: überraschend gute Skalierbarkeit
  • Spooky LOB: Der Spinlock SOS_SELIST_SIZED_SLOCKdreht sich außer Kontrolle. Ich vermute, dass dies ein allgemeines Problem bei der Weitergabe von LOBs über CLR-Funktionen ist, bin mir aber nicht sicher
  • Util_HashBinary: Sieht so aus, als würde es vom selben Spinlock getroffen. Ich habe mir das bisher noch nicht angesehen, weil ich wahrscheinlich nicht viel dagegen tun kann:

Dreh dein Schloss

  • Util_HashBinary 8k: sehr überraschende Ergebnisse, nicht sicher, was hier los ist

Auf einem kleineren Server getestete Endergebnisse:

╔═════════════════════════╦════════════════════════╦════════════════════════╗
     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 
╚═════════════════════════╩════════════════════════╩════════════════════════╝

BENCHMARKING CODE

SETUP 1: Tabellen und Daten

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
);

SETUP 2: Master Execution Proc

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

SETUP 3: Kollisionserkennungsprozess

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

SETUP 4: Aufräumen (Alle Testprozesse löschen)

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);

SETUP 5: Testprozesse generieren

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];

TEST 1: Auf Kollisionen prüfen

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: Führen Sie Leistungstests durch

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;

Zu lösende Validierungsprobleme

Während der Schwerpunkt auf den Leistungstests einer singulären SQLCLR-UDF lag, wurden zwei Probleme, die zu Beginn besprochen wurden, nicht in die Tests einbezogen, sondern sollten im Idealfall untersucht werden, um festzustellen, welcher Ansatz alle Anforderungen erfüllt.

  1. Die Funktion wird zweimal pro Abfrage ausgeführt (einmal für die Importzeile und einmal für die aktuelle Zeile). Die bisherigen Tests haben die UDF in den Testabfragen nur einmal referenziert. Dieser Faktor ändert möglicherweise nichts an der Rangfolge der Optionen, sollte jedoch für alle Fälle nicht ignoriert werden.
  2. In einem inzwischen gelöschten Kommentar hatte Paul White erwähnt:

    Ein Nachteil beim Ersetzen HASHBYTESdurch eine CLR-Skalarfunktion ist, dass CLR-Funktionen den Batch-Modus nicht verwenden können, wohingegen dies möglich HASHBYTESist. Das könnte in Bezug auf die Leistung wichtig sein.

    Das ist also etwas zu beachten und erfordert eindeutig Tests. Wenn die SQLCLR-Optionen keine Vorteile gegenüber den integrierten bieten HASHBYTES, wird der Vorschlag von Solomon , vorhandene Hashes (für mindestens die größten Tabellen) in verwandten Tabellen zu erfassen , gewichtet .

Joe Obbish
quelle
6

Sie können wahrscheinlich die Leistung und möglicherweise die Skalierbarkeit aller .NET-Ansätze verbessern, indem Sie alle im Funktionsaufruf erstellten Objekte zusammenfassen und zwischenspeichern. ZB für Paul Whites obigen Code:

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 rät davon ab und versucht, die Verwendung statischer / gemeinsam genutzter Variablen zu verhindern. Sie können jedoch gemeinsam genutzte Variablen verwenden, wenn Sie diese als schreibgeschützt markieren. Was natürlich bedeutungslos ist, da Sie einfach eine einzelne Instanz eines veränderlichen Typs zuweisen können, wie z ConcurrentDictionary.

David Browne - Microsoft
quelle
Interessant ... ist dieser Thread sicher, wenn er immer und immer wieder dieselbe Instanz verwendet? Ich weiß, dass die verwalteten Hashes eine Clear()Methode haben, aber ich habe Spooky nicht so genau untersucht .
Solomon Rutzky
@ PaulWhite und David. Ich hätte etwas falsch machen können, oder es könnte ein Unterschied zwischen SHA256Managedund sein SpookyHashV2, aber ich habe es versucht und nicht viel, wenn überhaupt, Leistungsverbesserung festgestellt. Ich habe auch festgestellt, dass der ManagedThreadIdWert für alle SQLCLR-Referenzen in einer bestimmten Abfrage gleich ist. Ich habe mehrere Verweise auf dieselbe Funktion sowie einen Verweis auf eine andere Funktion getestet, wobei alle drei unterschiedliche Eingabewerte erhielten und unterschiedliche (aber erwartete) Rückgabewerte zurückgaben. Würde dies nicht die Chancen auf eine Rennsituation erhöhen? Um fair zu sein, in meinem Test habe ich keine gesehen.
Solomon Rutzky