Jaki jest skalowalny sposób symulacji HASHBYTES przy użyciu funkcji skalarnej SQL CLR?

29

W ramach naszego procesu ETL porównujemy wiersze z przemieszczania z bazą danych raportowania, aby dowiedzieć się, czy któraś z kolumn faktycznie się zmieniła od czasu ostatniego załadowania danych.

Porównanie opiera się na unikalnym kluczu tabeli i pewnego rodzaju skrócie wszystkich pozostałych kolumn. Obecnie używamy HASHBYTEStego SHA2_256algorytmu i stwierdziliśmy, że nie skaluje się on na dużych serwerach, jeśli wszystkie jednoczesne wątki robocze wywołują HASHBYTES.

Przepustowość mierzona w haszach na sekundę nie zwiększa się po 16 współbieżnych wątkach podczas testowania na serwerze z 96 rdzeniami. Testuję, zmieniając liczbę współbieżnych MAXDOP 8zapytań z 1–12. Testowanie przy użyciu MAXDOP 1tego samego wąskiego gardła skalowalności.

Jako obejście problemu chcę wypróbować rozwiązanie SQL CLR. Oto moja próba określenia wymagań:

  • Funkcja musi mieć możliwość uczestniczenia w równoległych zapytaniach
  • Funkcja musi być deterministyczna
  • Funkcja musi przyjmować dane wejściowe ciągu NVARCHARlub VARBINARYłańcucha (wszystkie odpowiednie kolumny są łączone razem)
  • Typowy rozmiar wejściowy ciągu będzie wynosił 100-20000 znaków. 20000 to nie maks
  • Szansa na kolizję skrótu powinna być mniej więcej równa lub większa niż algorytmu MD5. CHECKSUMnie działa dla nas, ponieważ jest zbyt wiele kolizji.
  • Ta funkcja musi dobrze skalować się na dużych serwerach (przepustowość na wątek nie powinna znacząco spadać wraz ze wzrostem liczby wątków)

W przypadku Application Reasons ™ załóż, że nie mogę zaoszczędzić wartości skrótu dla tabeli raportowania. To CCI, który nie obsługuje wyzwalaczy ani kolumn obliczeniowych (są też inne problemy, do których nie chcę wchodzić).

Jaki jest skalowalny sposób symulacji HASHBYTESprzy użyciu funkcji SQL CLR? Mój cel można wyrazić jako uzyskanie jak największej liczby skrótów na sekundę na dużym serwerze, więc wydajność również ma znaczenie. Jestem okropny z CLR, więc nie wiem, jak to osiągnąć. Jeśli motywuje to kogoś do odpowiedzi, planuję dodać nagrodę za to pytanie, jak tylko będę mógł. Poniżej znajduje się przykładowe zapytanie, które w przybliżeniu ilustruje przypadek użycia:

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

Aby trochę uprościć, prawdopodobnie użyję czegoś takiego jak poniższe do testów porównawczych. W HASHBYTESponiedziałek opublikuję wyniki :

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
źródło

Odpowiedzi:

18

Ponieważ szukasz tylko zmian, nie potrzebujesz kryptograficznej funkcji skrótu.

Możesz wybrać jeden z szybszych nieszyfrujących skrótów w bibliotece Data.HashFunction typu open source autorstwa Brandona Dahlera, licencjonowanej na permisywną i zatwierdzoną przez OSI licencję MIT . SpookyHashjest popularnym wyborem.

Przykładowa implementacja

Kod źródłowy

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

Źródło udostępnia dwie funkcje, jedną dla wejść o wielkości 8000 bajtów lub mniejszą oraz wersję LOB. Wersja inna niż LOB powinna być znacznie szybsza.

Możesz być w stanie owinąć plik binarny LOB, COMPRESSaby uzyskać go poniżej limitu 8000 bajtów, jeśli okaże się to opłacalne ze względu na wydajność. Alternatywnie, możesz podzielić LOB na segmenty poniżej 8000 bajtów lub po prostu zarezerwować użycie HASHBYTESdla przypadku LOB (ponieważ dłuższe dane wejściowe skalują się lepiej).

Wbudowany kod

Możesz oczywiście pobrać pakiet dla siebie i skompilować wszystko, ale zbudowałem poniższe zespoły, aby ułatwić szybkie testowanie:

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

Funkcje 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

Stosowanie

Przykładowe zastosowanie z podaniem przykładowych danych w pytaniu:

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

W przypadku korzystania z wersji LOB pierwszy parametr należy rzutować lub przekonwertować na varbinary(max).

Plan wykonania

plan


Bezpieczne straszne

Data.HashFunction biblioteka wykorzystuje szereg funkcji językowych CLR, które są uważane UNSAFEprzez SQL Server. Możliwe jest napisanie podstawowej Spooky Hash kompatybilnej ze SAFEstatusem. Przykład, który napisałem na podstawie SpookilySharp Jona Hanny jest poniżej:

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

Paul White mówi GoFundMonica
źródło
16

Nie jestem pewien, czy równoległość będzie / znacznie lepsza z SQLCLR. Jest to jednak naprawdę łatwe do przetestowania, ponieważ w bezpłatnej wersji biblioteki SQL # SQLCLR (którą napisałem) jest funkcja skrótu o nazwie Util_HashBinary . Obsługiwane algorytmy to: MD5, SHA1, SHA256, SHA384 i SHA512.

Pobiera VARBINARY(MAX)wartość jako dane wejściowe, więc możesz albo połączyć wersję łańcuchową każdego pola (jak obecnie), a następnie przekonwertować na VARBINARY(MAX), lub możesz przejść bezpośrednio do VARBINARYkażdej kolumny i połączyć skonwertowane wartości (może to być szybsze, ponieważ nie masz do czynienia z łańcuchami lub dodatkową konwersją z łańcucha na VARBINARY). Poniżej znajduje się przykład pokazujący obie te opcje. Pokazuje również HASHBYTESfunkcję, dzięki czemu widać, że wartości są takie same między nią a SQL # .Util_HashBinary .

Należy pamiętać, że wyniki mieszania podczas konkatenacji VARBINARYwartości nie będą zgodne z wynikami mieszania podczas konkatenacji NVARCHARwartości. Wynika to z faktu, że postać binarna INTwartości „1” to 0x00000001, zaś NVARCHARpostać UTF-16LE (tj. ) INTWartości „1” (w postaci binarnej, ponieważ na tym będzie działać funkcja haszująca) to 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;

Możesz przetestować coś bardziej porównywalnego do Spooky spoza LOB, używając:

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

Uwaga: Util_HashBinary wykorzystuje zarządzany algorytm SHA256 wbudowany w .NET i nie powinien używać biblioteki „bcrypt”.

Poza tym aspektem pytania istnieją dodatkowe przemyślenia, które mogą pomóc w tym procesie:

Dodatkowa myśl nr 1 (pre-oblicz skróty, przynajmniej niektóre)

Wspomniałeś o kilku rzeczach:

  1. porównujemy wiersze pomostowe z bazą danych raportowania, aby dowiedzieć się, czy którakolwiek z kolumn rzeczywiście się zmieniła od czasu ostatniego załadowania danych.

    i:

  2. Nie mogę zapisać wartości skrótu dla tabeli raportowania. To CCI, który nie obsługuje wyzwalaczy ani kolumn obliczeniowych

    i:

  3. tabele można aktualizować poza procesem ETL

Wygląda na to, że dane w tej tabeli raportowania są stabilne przez pewien czas i są modyfikowane tylko przez ten proces ETL.

Jeśli nic więcej nie modyfikuje tej tabeli, to tak naprawdę nie potrzebujemy wyzwalacza ani widoku indeksowanego (początkowo myślałem, że możesz).

Ponieważ nie można zmodyfikować schematu tabeli raportowania, czy możliwe byłoby przynajmniej utworzenie powiązanej tabeli zawierającej wstępnie obliczony skrót (i czas UTC, kiedy został obliczony)? To pozwoli ci mieć wstępnie obliczoną wartość do porównania następnym razem, pozostawiając tylko wartość przychodzącą, która wymaga obliczenia wartości skrótu. Zmniejszyłoby to liczbę połączeń do jednej HASHBYTESlub SQL#.Util_HashBinaryo połowę. Po prostu dołączysz do tej tabeli skrótów podczas procesu importowania.

Utworzyłbyś także oddzielną procedurę składowaną, która po prostu odświeża skróty tej tabeli. Po prostu aktualizuje skróty każdego pokrewnego wiersza, który zmienił się na bieżący, i aktualizuje znacznik czasu dla tych zmodyfikowanych wierszy. Ten proces może / powinien zostać wykonany na końcu każdego innego procesu, który aktualizuje tę tabelę. Można również zaplanować jego uruchomienie na 30–60 minut przed rozpoczęciem ETL (w zależności od tego, ile czasu zajmuje wykonanie i kiedy dowolny z tych innych procesów może zostać uruchomiony). Można go nawet wykonać ręcznie, jeśli podejrzewasz, że wiersze mogą być niezsynchronizowane.

Następnie zauważono, że:

istnieje ponad 500 stolików

Tak wiele tabel utrudnia posiadanie dodatkowej tabeli dla każdej z nich, która zawierałaby bieżące wartości mieszające, ale nie jest to niemożliwe, ponieważ można je skrypty, ponieważ byłby to standardowy schemat. Skrypty będą musiały jedynie uwzględniać nazwę tabeli źródłowej i wykrywanie kolumn PK tabeli źródłowej.

Mimo to, niezależnie od tego, który algorytm skrótu ostatecznie okaże się najbardziej skalowalny, nadal bardzo polecam znalezienie co najmniej kilku tabel (być może niektóre są DUŻO większe niż reszta z 500 tabel) i skonfigurowanie powiązanej tabeli do przechwytywania bieżące wartości skrótu, dzięki czemu wartości „bieżące” można poznać przed procesem ETL. Nawet najszybsza funkcja nie jest w stanie działać lepiej, nigdy nie wywołując jej w pierwszej kolejności ;-).

Dodatkowa myśl # 2 ( VARBINARYzamiast NVARCHAR)

Niezależnie od SQLCLR vs wbudowany HASHBYTES, nadal zalecałbym bezpośrednią konwersję, VARBINARYponieważ powinno to być szybsze. Łączenie łańcuchów nie jest po prostu zbyt wydajne. I to w dodatku do konwertowania wartości nie łańcuchowych na łańcuchy, co wymaga dodatkowego wysiłku (zakładam, że ilość wysiłku różni się w zależności od typu podstawowego: DATETIMEwymaga więcej niż BIGINT), podczas gdy konwersja do VARBINARYpo prostu daje podstawową wartość (w większości przypadków).

W rzeczywistości testowanie tego samego zestawu danych, którego używano i przy użyciu innych testów HASHBYTES(N'SHA2_256',...), wykazało wzrost sumy mieszania o 23,315% obliczony w ciągu jednej minuty. I ten wzrost był na nic więcej niż używanie VARBINARYzamiast NVARCHAR! 😸 ( szczegółowe informacje znajdują się w odpowiedzi na wiki społeczności )

Dodatkowa myśl nr 3 (pamiętaj o parametrach wejściowych)

Dalsze testy wykazały, że jednym obszarem, który wpływa na wydajność (w stosunku do tej liczby wykonań) są parametry wejściowe: ile i jaki typ (typy).

Funkcja Util_HashBinary SQLCLR, która jest obecnie w mojej bibliotece SQL #, ma dwa parametry wejściowe: jeden VARBINARY(wartość skrótu) i jeden NVARCHAR(algorytm do użycia). Wynika to z mojego lustrzanego podpisu HASHBYTESfunkcji. Odkryłem jednak, że jeśli usunę NVARCHARparametr i utworzę funkcję, która tylko robi SHA256, wtedy wydajność całkiem się poprawi. Zakładam, że nawet zmiana NVARCHARparametru INTpomogłaby, ale zakładam również, że nawet brak dodatkowego INTparametru jest co najmniej nieco szybszy.

Ponadto SqlBytes.Valuemoże działać lepiej niż SqlBinary.Value.

Do tych testów stworzyłem dwie nowe funkcje: Util_HashSHA256Binary i Util_HashSHA256Binary8k . Zostaną one uwzględnione w następnej wersji SQL # (jeszcze nie ustalono daty).

Odkryłem również, że metodologię testowania można nieco ulepszyć, dlatego zaktualizowałem wiązkę testową w odpowiedzi na wiki społeczności poniżej, aby uwzględnić:

  1. wstępne ładowanie zestawów SQLCLR, aby upewnić się, że narzut czasu ładowania nie wypacza wyników.
  2. procedura weryfikacji w celu sprawdzenia kolizji. Jeśli zostaną znalezione, wyświetla liczbę unikalnych / odrębnych wierszy i całkowitą liczbę wierszy. Pozwala to ustalić, czy liczba kolizji (jeśli występują) przekracza limit dla danego przypadku użycia. Niektóre przypadki użycia mogą dopuszczać niewielką liczbę kolizji, inne mogą nie wymagać żadnych. Superszybka funkcja jest bezużyteczna, jeśli nie może wykryć zmian pożądanego poziomu dokładności. Na przykład, używając uprzęży testowej dostarczonej przez OP, zwiększyłem liczbę wierszy do 100 tys. Rzędów (pierwotnie było to 10 tys.) I stwierdziłem, że CHECKSUMzarejestrowałem ponad 9 tys. Kolizji, co stanowi 9% (ryczałt).

Dodatkowa myśl # 4 ( HASHBYTES+ SQLCLR razem?)

W zależności od tego, gdzie jest wąskie gardło, może nawet pomóc użycie kombinacji wbudowanej HASHBYTESi SQLCLR UDF do wykonania tego samego skrótu. Jeśli wbudowane funkcje są ograniczone inaczej / osobno od operacji SQLCLR, wówczas to podejście może być w stanie osiągnąć więcej współbieżnie niż w przypadku HASHBYTESSQLCLR indywidualnie. Zdecydowanie warto to przetestować.

Dodatkowa myśl # 5 (buforowanie obiektów mieszających?)

Buforowanie obiektu algorytmu mieszającego, zgodnie z sugestią Davida Browne'a, z pewnością wydaje się interesujące, więc wypróbowałem go i znalazłem następujące dwa punkty zainteresowania:

  1. Z jakiegokolwiek powodu wydaje się, że nie zapewnia znacznej poprawy wydajności. Mogłem zrobić coś niepoprawnie, ale oto, co próbowałem:

    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. ManagedThreadIdWartość wydaje się być taka sama dla wszystkich odniesień SQLCLR w danym zapytaniu. Przetestowałem wiele odniesień do tej samej funkcji, a także odwołanie do innej funkcji, wszystkie 3 mają różne wartości wejściowe i zwracają różne (ale oczekiwane) wartości zwracane. W przypadku obu funkcji testowych wynikiem był ciąg zawierający zarówno ciąg znaków, ManagedThreadIdjak i reprezentację wyniku mieszania. ManagedThreadIdWartość była taka sama dla wszystkich odniesień UDF w zapytaniu, a we wszystkich rzędach. Ale wynik mieszania był taki sam dla tego samego ciągu wejściowego i różny dla różnych ciągów wejściowych.

    Chociaż podczas testów nie widziałem żadnych błędnych wyników, czy nie zwiększyłoby to szans na warunki wyścigowe? Jeśli klucz słownika jest taki sam dla wszystkich obiektów SQLCLR wywoływanych w danym zapytaniu, to będą one dzielić tę samą wartość lub obiekt przechowywany dla tego klucza, prawda? Chodzi o to, że nawet wydawało mi się, że to tutaj działa (do pewnego stopnia, znowu nie wydawało się, że osiągnięto duży wzrost wydajności, ale funkcjonalnie nic się nie zepsuło), co nie daje mi pewności, że takie podejście zadziała w innych scenariuszach.

Solomon Rutzky
źródło
11

To nie jest tradycyjna odpowiedź, ale pomyślałem, że pomocne byłoby opublikowanie testów porównawczych niektórych z wspomnianych do tej pory technik. Testuję na 96-rdzeniowym serwerze z SQL Server 2017 CU9.

Wiele problemów ze skalowalnością jest powodowanych przez współbieżne wątki rywalizujące o pewien stan globalny. Rozważmy na przykład klasyczną rywalizację strony PFS. Może się to zdarzyć, jeśli zbyt wiele wątków roboczych musi zmodyfikować tę samą stronę w pamięci. Ponieważ kod staje się bardziej wydajny, może wymagać szybszego zatrzaśnięcia. To zwiększa sprzeczkę. Mówiąc prościej, efektywny kod może prowadzić do problemów ze skalowalnością, ponieważ stan globalny jest poważniej zagrożony. Powolny kod rzadziej powoduje problemy ze skalowalnością, ponieważ stan globalny nie jest uzyskiwany tak często.

HASHBYTESskalowalność jest częściowo oparta na długości ciągu wejściowego. Moja teoria głosiła, że ​​tak się dzieje, że po HASHBYTESwywołaniu funkcji potrzebny jest dostęp do pewnego stanu globalnego . Łatwy do zaobserwowania stan globalny polega na przydzieleniu strony pamięci dla każdego połączenia w niektórych wersjach programu SQL Server. Trudniejsze do zaobserwowania jest to, że istnieje pewien rodzaj sprzeczności systemu operacyjnego. W rezultacie, jeśli HASHBYTESjest wywoływany przez kod rzadziej, rywalizacja spada. Jednym ze sposobów zmniejszenia liczby HASHBYTESpołączeń jest zwiększenie ilości haszowania wymaganego na każde połączenie. Praca mieszania jest częściowo oparta na długości ciągu wejściowego. Aby odtworzyć problem skalowalności, który widziałem w aplikacji, musiałem zmienić dane demo. Rozsądnym najgorszym scenariuszem jest tabela z 21BIGINTkolumny. Definicja tabeli zawarta jest w kodzie na dole. Aby zmniejszyć Local Factors ™, używam współbieżnych MAXDOP 1zapytań, które działają na stosunkowo małych tabelach. Mój kod szybkiego testu porównawczego znajduje się na dole.

Uwaga: funkcje zwracają różne długości skrótów. MD5i SpookyHashoba mają 128- SHA256bitowy skrót , to 256-bitowy skrót.

WYNIKI ( NVARCHARvs VARBINARYkonwersja i konkatenacja)

Aby przekonać się, czy konwersja i konkatenacja VARBINARYjest naprawdę wydajniejsza / wydajniejsza niż NVARCHAR, NVARCHARwersja RUN_HASHBYTES_SHA2_256procedury składowanej została utworzona z tego samego szablonu (patrz „Krok 5” w sekcji KOD BENKMARKINGOWY poniżej). Jedyne różnice to:

  1. Nazwa procedury składowanej kończy się na _NVC
  2. BINARY(8)ponieważ CASTfunkcja została zmieniona naNVARCHAR(15)
  3. 0x7C został zmieniony na N'|'

Wynikające z:

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

zamiast:

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

Poniższa tabela zawiera liczbę skrótów wykonanych w ciągu 1 minuty. Testy zostały przeprowadzone na innym serwerze niż ten, który został wykorzystany w innych testach wymienionych poniżej.

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

Patrząc tylko na średnie, możemy obliczyć korzyść z przejścia na VARBINARY:

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

To zwraca:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

WYNIKI (algorytmy i implementacje skrótu)

Poniższa tabela zawiera liczbę skrótów wykonanych w ciągu 1 minuty. Na przykład użycie CHECKSUM84 równoczesnych zapytań spowodowało wykonanie ponad 2 miliardów skrótów przed upływem czasu.

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

Jeśli wolisz widzieć te same liczby mierzone pod względem pracy na sekundę wątku:

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

Kilka szybkich przemyśleń na temat wszystkich metod:

  • CHECKSUM: bardzo dobra skalowalność zgodnie z oczekiwaniami
  • HASHBYTES: problemy ze skalowalnością obejmują jeden przydział pamięci na połączenie i dużą ilość procesora spędzonego w systemie operacyjnym
  • Spooky: zaskakująco dobra skalowalność
  • Spooky LOB: blokada SOS_SELIST_SIZED_SLOCKobraca się spod kontroli. Podejrzewam, że jest to ogólny problem z przekazywaniem obiektów LOB przez funkcje CLR, ale nie jestem pewien
  • Util_HashBinary: wygląda na to, że został trafiony przez tę samą blokadę. Do tej pory nie analizowałem tego, ponieważ prawdopodobnie nie mogę wiele z tym zrobić:

zakręć zamkiem

  • Util_HashBinary 8k: bardzo zaskakujące wyniki, nie jestem pewien, co się tutaj dzieje

Ostateczne wyniki przetestowane na mniejszym serwerze:

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

KOD BENCHMARKINGOWY

USTAWIENIA 1: Tabele i dane

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

USTAWIENIA 3: Wykrywanie kolizji Proc

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: Cleanup (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);

USTAWIENIA 5: Generowanie wyników testu

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: Sprawdź, czy nie ma kolizji

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: Uruchom testy wydajności

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;

KWESTIE WALIDACJI DO ROZWIĄZANIA

Koncentrując się na testowaniu wydajności pojedynczej SQLCLR UDF, dwie kwestie, które zostały wcześniej omówione, nie zostały włączone do testów, ale najlepiej powinny zostać zbadane w celu ustalenia, które podejście spełnia wszystkie wymagania.

  1. Funkcja będzie wykonywana dwukrotnie dla każdego zapytania (raz dla wiersza importu i raz dla bieżącego wiersza). Dotychczasowe testy odwoływały się do UDF tylko raz w zapytaniach testowych. Ten czynnik może nie zmienić rankingu opcji, ale na wszelki wypadek nie powinien być ignorowany.
  2. W komentarzu, który został usunięty, Paul White wspomniał:

    Wadą zastąpienia HASHBYTESfunkcji skalarnej CLR - wydaje się, że funkcje CLR nie mogą korzystać z trybu wsadowego, podczas gdy HASHBYTESmogą. To może być ważne pod względem wydajności.

    Jest to więc kwestia do rozważenia i wyraźnie wymaga testowania. Jeśli opcje SQLCLR nie zapewniają żadnej korzyści w porównaniu z wbudowanym HASHBYTES, to zwiększa wagę sugestii Solomona o przechwytywaniu istniejących skrótów (dla co najmniej największych tabel) do powiązanych tabel.

Joe Obbish
źródło
6

Prawdopodobnie możesz poprawić wydajność i być może skalowalność wszystkich podejść .NET, łącząc i buforując wszystkie obiekty utworzone w wywołaniu funkcji. EG dla powyższego kodu Paula White'a:

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 odradza i próbuje zapobiegać używaniu zmiennych statycznych / współdzielonych, ale pozwoli ci używać zmiennych współdzielonych, jeśli oznaczysz je jako tylko do odczytu. Co oczywiście nie ma znaczenia, ponieważ można po prostu przypisać jedną instancję jakiegoś typu zmiennego, na przykład ConcurrentDictionary.

David Browne - Microsoft
źródło
ciekawe ... czy ten wątek jest bezpieczny, jeśli używa tej samej instancji w kółko? Wiem, że zarządzane skróty mają Clear()metodę, ale nie zajrzałem tak daleko w Spooky.
Solomon Rutzky
@PaulWhite and David. Mogłem zrobić coś złego, czy może to być różnica między SHA256Manageda SpookyHashV2, ale to próbowałem i nie zobaczyć wiele, jeśli w ogóle, poprawę wydajności. Zauważyłem również, że ManagedThreadIdwartość jest taka sama dla wszystkich referencji SQLCLR w określonym zapytaniu. Testowałem wiele odniesień do tej samej funkcji, a także odwołanie do innej funkcji, wszystkie 3 mają różne wartości wejściowe i zwracają różne (ale oczekiwane) wartości zwracane. Czy nie zwiększyłoby to szans na warunki wyścigowe? Szczerze mówiąc, w moim teście nie widziałem żadnego.
Solomon Rutzky