Qual è un modo scalabile per simulare HASHBYTES usando una funzione scalare di CLR SQL?


29

Come parte del nostro processo ETL, confrontiamo le righe dalla gestione temporanea con il database di report per capire se una delle colonne è stata effettivamente modificata dall'ultimo caricamento dei dati.

Il confronto si basa sulla chiave univoca della tabella e su un tipo di hash di tutte le altre colonne. Attualmente utilizziamo HASHBYTESl' SHA2_256algoritmo e abbiamo scoperto che non viene ridimensionato su server di grandi dimensioni se molti thread di lavoro simultanei chiamano tutti HASHBYTES.

Il throughput misurato in hash al secondo non aumenta oltre i 16 thread simultanei durante i test su un server a 96 core. Provo modificando il numero di MAXDOP 8query simultanee da 1 a 12. Il test con ha MAXDOP 1mostrato lo stesso collo di bottiglia della scalabilità.

Per ovviare al problema, voglio provare una soluzione CLR SQL. Ecco il mio tentativo di affermare i requisiti:

  • La funzione deve essere in grado di partecipare a query parallele
  • La funzione deve essere deterministica
  • La funzione deve accettare un input di un NVARCHARoVARBINARY stringa (tutte le colonne pertinenti sono concatenate insieme)
  • La dimensione di input tipica della stringa sarà lunga da 100 a 20000 caratteri. 20000 non è un max
  • La possibilità di una collisione dell'hash dovrebbe essere approssimativamente uguale o migliore dell'algoritmo MD5. CHECKSUMnon funziona per noi perché ci sono troppe collisioni.
  • La funzione deve scalare bene su server di grandi dimensioni (la velocità effettiva per thread non deve diminuire in modo significativo all'aumentare del numero di thread)

Per Application Reason ™, supponiamo che non riesca a salvare il valore dell'hash per la tabella di report. È un CCI che non supporta trigger o colonne calcolate (ci sono anche altri problemi in cui non voglio entrare).

Qual è un modo scalabile per simulare HASHBYTESusando una funzione CLR SQL? Il mio obiettivo può essere espresso come ottenere più hash al secondo che posso su un server di grandi dimensioni, quindi anche le prestazioni contano. Sono terribile con CLR, quindi non so come farlo. Se motiva qualcuno a rispondere, ho intenzione di aggiungere una generosità a questa domanda non appena sono in grado. Di seguito è una query di esempio che illustra in modo molto approssimativo il caso d'uso:

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

Per semplificare un po 'le cose, probabilmente userò qualcosa di simile al seguente per il benchmarking. Pubblicherò i risultati con HASHBYTESlunedì:

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

Risposte:


18

Dato che stai solo cercando modifiche, non hai bisogno di una funzione hash crittografica.

È possibile scegliere tra uno degli hash non crittografici più veloci nella libreria Data.HashFunction open-source di Brandon Dahler, concesso in licenza con la licenza MIT permissiva e approvata dall'OSI .SpookyHashè una scelta popolare.

Esempio di implementazione

Codice sorgente

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

La fonte fornisce due funzioni, una per input di 8000 byte o meno, e una versione LOB. La versione non LOB dovrebbe essere significativamente più veloce.

Potrebbe essere possibile racchiudere un file binario LOB COMPRESSper farlo rientrare nel limite di 8000 byte, se risulta utile per le prestazioni. In alternativa, è possibile suddividere il LOB in segmenti inferiori a 8000 byte o semplicemente riservare l'uso diHASHBYTES caso LOB (poiché gli input più lunghi si adattano meglio).

Codice precostruito

Puoi ovviamente prendere il pacchetto per te stesso e compilare tutto, ma ho creato i gruppi di seguito per semplificare i test rapidi:

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

Funzioni 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

uso

Un esempio di utilizzo dati i dati di esempio nella domanda:

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

Quando si utilizza la versione LOB, il primo parametro deve essere cast o convertito in varbinary(max) .

Progetto esecutivo

Piano


Sicuro Spettrale

La libreria Data.HashFunction utilizza una serie di funzionalità del linguaggio CLR considerate UNSAFEda SQL Server. È possibile scrivere uno Spooky Hash di base compatibile con lo SAFEstato. Un esempio che ho scritto basato su SpookilySharp di Jon Hanna è di seguito:

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


16

Non sono sicuro che il parallelismo sarà migliore / significativamente migliore con SQLCLR. Tuttavia, è davvero facile da testare poiché esiste una funzione hash nella versione gratuita della libreria SQL # SQLCLR (che ho scritto) chiamata Util_HashBinary . Gli algoritmi supportati sono: MD5, SHA1, SHA256, SHA384 e SHA512.

Prende un VARBINARY(MAX)valore come input, quindi puoi concatenare la versione di stringa di ciascun campo (come stai facendo attualmente) e poi convertire in VARBINARY(MAX), oppure puoi andare direttamente a VARBINARYper ogni colonna e concatenare i valori convertiti (questo potrebbe essere più veloce poiché non hai a che fare con stringhe o con la conversione extra da stringa a VARBINARY). Di seguito è riportato un esempio che mostra entrambe queste opzioni. Mostra anche la HASHBYTESfunzione in modo da poter vedere che i valori sono gli stessi tra esso e SQL # .Util_HashBinary .

Si noti che i risultati hash quando si concatenano i VARBINARYvalori non corrispondono ai risultati hash quando si concatenano i NVARCHARvalori. Questo perché la forma binaria del INTvalore "1" è 0x00000001, mentre la forma UTF-16LE (ovvero NVARCHAR) del INTvalore di "1" (in forma binaria poiché è su ciò che opererà una funzione di hashing) è 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;

Puoi testare qualcosa di più paragonabile allo Spooky non LOB usando:

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

Nota: Util_HashBinary utilizza l'algoritmo SHA256 gestito incorporato in .NET e non dovrebbe utilizzare la libreria "bcrypt".

Oltre quell'aspetto della domanda, ci sono alcuni pensieri aggiuntivi che potrebbero aiutare questo processo:

Pensiero aggiuntivo n. 1 (pre-calcola hash, almeno alcuni)

Hai citato alcune cose:

  1. confrontiamo le righe della gestione temporanea con il database di report per capire se una delle colonne è stata effettivamente modificata dall'ultimo caricamento dei dati.

    e:

  2. Non riesco a salvare il valore dell'hash per la tabella dei rapporti. È un CCI che non supporta trigger o colonne calcolate

    e:

  3. le tabelle possono essere aggiornate al di fuori del processo ETL

Sembra che i dati in questa tabella di report siano stabili per un periodo di tempo e vengano modificati solo da questo processo ETL.

Se nient'altro modifica questa tabella, allora non abbiamo davvero bisogno di un trigger o di una vista indicizzata (inizialmente pensavo che potresti).

Dal momento che non è possibile modificare lo schema della tabella di report, sarebbe almeno possibile creare una tabella correlata per contenere l'hash precalcolato (e l'ora UTC di quando è stata calcolata)? Ciò consentirebbe di avere un valore precalcolato da confrontare con la prossima volta, lasciando solo il valore in entrata che richiede il calcolo dell'hash di. Ciò ridurrebbe il numero di chiamate alla metà HASHBYTESo SQL#.Util_HashBinaryalla metà. Ti uniresti semplicemente a questa tabella di hash durante il processo di importazione.

Dovresti anche creare una procedura memorizzata separata che aggiorna semplicemente gli hash di questa tabella. Aggiorna solo gli hash di ogni riga correlata che è stata modificata per essere corrente e aggiorna il timestamp per quelle righe modificate. Questo proc può / dovrebbe essere eseguito alla fine di qualsiasi altro processo che aggiorna questa tabella. Può anche essere pianificato per l'esecuzione 30 - 60 minuti prima dell'avvio di questo ETL (a seconda di quanto tempo richiede l'esecuzione e quando uno di questi altri processi potrebbe essere eseguito). Può anche essere eseguito manualmente se si sospetta che potrebbero esserci righe non sincronizzate.

È stato quindi osservato che:

ci sono oltre 500 tavoli

Il fatto che molte tabelle rendano più difficile avere una tabella aggiuntiva per ognuna per contenere gli hash correnti, ma questo non è impossibile in quanto potrebbe essere scritto in quanto sarebbe uno schema standard. Lo script dovrebbe solo tenere conto del nome della tabella di origine e del rilevamento delle colonne PK della tabella di origine.

Tuttavia, indipendentemente da quale algoritmo di hash alla fine si rivela il più scalabile, consiglio vivamente di trovare almeno alcune tabelle (forse ce ne sono alcune che sono MOLTO più grandi del resto delle 500 tabelle) e di impostare una tabella correlata da catturare hash correnti in modo che i valori "attuali" possano essere conosciuti prima del processo ETL. Anche la funzione più veloce non può essere performante senza doverla chiamare in primo luogo ;-).

Pensiero aggiuntivo n. 2 ( VARBINARYanziché NVARCHAR)

Indipendentemente da SQLCLR vs integrato HASHBYTES, consiglierei comunque di convertirlo direttamente in VARBINARYcome dovrebbe essere più veloce. Concatenare le stringhe non è terribilmente efficiente. E questo è in aggiunta alla conversione di valori non stringa in stringhe in primo luogo, il che richiede uno sforzo extra (suppongo che la quantità di sforzo varia in base al tipo di base: DATETIMErichiede più di BIGINT), mentre la conversione in VARBINARYti dà semplicemente il valore sottostante (nella maggior parte dei casi).

E, infatti, testare lo stesso set di dati utilizzato dagli altri test e utilizzarlo ha HASHBYTES(N'SHA2_256',...)mostrato un aumento del 23,415% degli hash totali calcolati in un minuto. E quell'aumento era per non fare altro che usare VARBINARYinvece di NVARCHAR! 😸 (vedi la risposta della wiki della community per i dettagli)

Pensiero aggiuntivo n. 3 (prestare attenzione ai parametri di input)

Ulteriori test hanno dimostrato che un'area che influisce sulle prestazioni (su questo volume di esecuzioni) sono i parametri di input: quanti e quali tipi.

La funzione SQLCLR Util_HashBinary che è attualmente nella mia libreria SQL # ha due parametri di input: uno VARBINARY(il valore di hash) e uno NVARCHAR(l'algoritmo da utilizzare). Ciò è dovuto al mio mirroring della firma della HASHBYTESfunzione. Tuttavia, ho scoperto che se avessi rimosso il NVARCHARparametro e creato una funzione che eseguiva solo SHA256, le prestazioni sarebbero migliorate abbastanza bene. Presumo che anche il passaggio a un NVARCHARparametro INTavrebbe aiutato, ma presumo anche che nemmeno avere il INTparametro aggiuntivo sia almeno leggermente più veloce.

Inoltre, SqlBytes.Valuepotrebbe funzionare meglio di SqlBinary.Value.

Ho creato due nuove funzioni: Util_HashSHA256Binary e Util_HashSHA256Binary8k per questo test. Questi saranno inclusi nella prossima versione di SQL # (non è stata ancora fissata una data).

Ho anche scoperto che la metodologia di test potrebbe essere leggermente migliorata, quindi ho aggiornato il cablaggio di prova nella risposta wiki della comunità di seguito per includere:

  1. precaricamento degli assiemi SQLCLR per garantire che il sovraccarico del tempo di caricamento non distorca i risultati.
  2. una procedura di verifica per verificare le collisioni. Se ne vengono trovati, visualizza il numero di righe uniche / distinte e il numero totale di righe. Ciò consente di determinare se il numero di collisioni (se presenti) supera il limite per il caso d'uso indicato. Alcuni casi d'uso potrebbero consentire un numero limitato di collisioni, altri potrebbero non richiederne nessuno. Una funzione superveloce è inutile se non è in grado di rilevare le modifiche al livello di precisione desiderato. Ad esempio, utilizzando il cablaggio di prova fornito dall'OP, ho aumentato il conteggio delle righe a 100k righe (era originariamente 10k) e ho riscontrato che sono state CHECKSUMregistrate oltre 9k collisioni, pari al 9% (yikes).

Pensiero aggiuntivo n. 4 ( HASHBYTES+ SQLCLR insieme?)

A seconda di dove si trova il collo di bottiglia, potrebbe anche essere utile utilizzare una combinazione di HASHBYTESUDF SQLCLR e incorporati per eseguire lo stesso hash. Se le funzioni integrate sono vincolate in modo diverso / separatamente dalle operazioni SQLCLR, questo approccio potrebbe essere in grado di eseguire più simultaneamente rispetto a uno HASHBYTESo SQLCLR singolarmente. Vale sicuramente la pena testarlo.

Pensiero aggiuntivo n. 5 (memorizzazione nella cache degli oggetti con hashing?)

La memorizzazione nella cache dell'oggetto algoritmo di hashing come suggerito nella risposta di David Browne sembra certamente interessante, quindi l'ho provato e ho trovato i seguenti due punti di interesse:

  1. Per qualsiasi motivo, non sembra fornire molto, se del caso, miglioramenti delle prestazioni. Avrei potuto fare qualcosa di sbagliato, ma ecco cosa ho provato:

    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. Il ManagedThreadIdvalore sembra essere lo stesso per tutti i riferimenti SQLCLR in una query specifica. Ho testato più riferimenti alla stessa funzione, nonché un riferimento a una funzione diversa, a tutti e 3 sono stati dati valori di input diversi e sono stati restituiti valori di ritorno diversi (ma previsti). Per entrambe le funzioni di test, l'output era una stringa che includeva ManagedThreadIdanche una rappresentazione di stringa del risultato hash. Il ManagedThreadIdvalore era lo stesso per tutti i riferimenti UDF nella query e per tutte le righe. Ma il risultato hash è stato lo stesso per la stessa stringa di input e diverso per le diverse stringhe di input.

    Mentre non ho visto alcun risultato errato nei miei test, ciò non aumenterebbe le possibilità di una condizione di gara? Se la chiave del dizionario è la stessa per tutti gli oggetti SQLCLR chiamati in una query specifica, condivideranno lo stesso valore o oggetto archiviato per quella chiave, giusto? Il punto è che, anche se sembrava funzionare qui (in una certa misura, di nuovo non sembrava esserci molto guadagno in termini di prestazioni, ma funzionalmente nulla si rompeva), che non mi dà la fiducia che questo approccio funzionerà in altri scenari.


11

Questa non è una risposta tradizionale, ma ho pensato che sarebbe stato utile pubblicare parametri di riferimento di alcune delle tecniche menzionate finora. Sto testando su un server 96 core con SQL Server 2017 CU9.

Molti problemi di scalabilità sono causati da thread simultanei che contendono un certo stato globale. Ad esempio, considera la classica contesa di pagine PFS. Ciò può accadere se troppi thread di lavoro devono modificare la stessa pagina in memoria. Man mano che il codice diventa più efficiente, può richiedere più velocemente il latch. Ciò aumenta la contesa. Per dirla semplicemente, è più probabile che il codice efficiente porti a problemi di scalabilità perché lo stato globale è conteso in modo più severo. Il codice lento ha meno probabilità di causare problemi di scalabilità perché non è possibile accedere allo stato globale con la stessa frequenza.

HASHBYTESla scalabilità è parzialmente basata sulla lunghezza della stringa di input. La mia teoria era il motivo per cui questo si verifica è che l'accesso a qualche stato globale è necessario quando HASHBYTESviene chiamata la funzione. Lo stato globale facile da osservare è che una pagina di memoria deve essere allocata per chiamata su alcune versioni di SQL Server. Il più difficile da osservare è che esiste una sorta di contesa del sistema operativo. Di conseguenza, se HASHBYTESviene chiamato dal codice meno frequentemente, la contesa diminuisce. Un modo per ridurre il tasso di colonne. La definizione della tabella è inclusa nel codice in basso. Per ridurre Local Factors ™, sto usando query simultanee che operano su tabelle relativamente piccole. Il mio codice di riferimento rapido è in fondo.HASHBYTES chiamate è aumentare la quantità di lavoro di hashing necessario per chiamata. Il lavoro di hash si basa in parte sulla lunghezza della stringa di input. Per riprodurre il problema di scalabilità che ho visto nell'applicazione, avevo bisogno di cambiare i dati della demo. Uno scenario ragionevole peggiore è una tabella con 21BIGINTMAXDOP 1

Nota che le funzioni restituiscono diverse lunghezze di hash. MD5e SpookyHashsono entrambi hash a 128 bit, SHA256è un hash a 256 bit.

RISULTATI ( NVARCHARvs VARBINARYconversione e concatenazione)

Per vedere se la conversione e la concatenazione VARBINARYsono veramente più efficienti / performanti di NVARCHAR, una NVARCHARversione della RUN_HASHBYTES_SHA2_256procedura memorizzata è stata creata dallo stesso modello (vedere "Passaggio 5" nella sezione CODICE BENCHMARKING di seguito). Le uniche differenze sono:

  1. Il nome della procedura memorizzata termina con _NVC
  2. BINARY(8)per la CASTfunzione è stata cambiata per essereNVARCHAR(15)
  3. 0x7C è stato cambiato per essere N'|'

Con il risultato di:

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

invece di:

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

La tabella seguente contiene il numero di hash eseguiti in 1 minuto. I test sono stati eseguiti su un server diverso da quello utilizzato per gli altri test indicati di seguito.

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

Osservando solo le medie, possiamo calcolare il vantaggio del passaggio a VARBINARY:

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

Che ritorna:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

RISULTATI (algoritmi hash e implementazioni)

La tabella seguente contiene il numero di hash eseguiti in 1 minuto. Ad esempio, l'utilizzo CHECKSUMcon 84 query simultanee ha comportato l'esecuzione di oltre 2 miliardi di hash prima dello scadere del tempo.

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

Se preferisci vedere gli stessi numeri misurati in termini di lavoro al secondo thread:

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

Alcune considerazioni rapide su tutti i metodi:

  • CHECKSUM: ottima scalabilità come previsto
  • HASHBYTES: i problemi di scalabilità includono un'allocazione di memoria per chiamata e una grande quantità di CPU spesa nel sistema operativo
  • Spooky: sorprendentemente buona scalabilità
  • Spooky LOB: lo spinlock SOS_SELIST_SIZED_SLOCKgira fuori controllo. Ho il sospetto che questo sia un problema generale con il passaggio di LOB attraverso le funzioni CLR, ma non sono sicuro
  • Util_HashBinary: sembra che venga colpito dallo stesso spinlock. Finora non ci ho pensato perché probabilmente non c'è molto che posso fare al riguardo:

gira la serratura

  • Util_HashBinary 8k: risultati molto sorprendenti, non sono sicuro di cosa stia succedendo qui

Risultati finali testati su un server più piccolo:

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

CODICE DI BENCHMARKING

SETUP 1: Tabelle e dati

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: Proc. Esecuzione master

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: Proc. Rilevamento collisioni

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

SETUP 5: Genera test Procs

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: Verifica collisioni

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: eseguire test delle prestazioni

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;

PROBLEMI DI CONVALIDA DA RISOLVERE

Mentre ci si concentra sul test delle prestazioni di un singolare UDF SQLCLR, due questioni che sono state discusse all'inizio non sono state incorporate nei test, ma idealmente dovrebbero essere studiate al fine di determinare quale approccio soddisfa tutti i requisiti.

  1. La funzione verrà eseguita due volte per ogni query (una volta per la riga di importazione e una volta per la riga corrente). Finora i test hanno fatto riferimento all'UDF solo una volta nelle query di test. Questo fattore potrebbe non cambiare la classifica delle opzioni, ma non dovrebbe essere ignorato, per ogni evenienza.
  2. In un commento che è stato successivamente eliminato, Paul White aveva menzionato:

    Un aspetto negativo della sostituzione HASHBYTEScon una funzione scalare CLR: sembra che le funzioni CLR non possano usare la modalità batch mentre è HASHBYTESpossibile. Potrebbe essere importante, dal punto di vista delle prestazioni.

    Questo è qualcosa da considerare e richiede chiaramente dei test. Se le opzioni SQLCLR non offrono alcun vantaggio rispetto al built-in HASHBYTES, ciò aggiunge peso al suggerimento di Solomon di acquisire hash esistenti (almeno per le tabelle più grandi) in tabelle correlate.


6

Probabilmente è possibile migliorare le prestazioni e forse la scalabilità di tutti gli approcci .NET raggruppando e memorizzando nella cache tutti gli oggetti creati nella chiamata di funzione. EG per il codice di Paul White sopra:

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

CLR SQL scoraggia e tenta di impedire l'utilizzo di variabili statiche / condivise, ma ti consentirà di utilizzare le variabili condivise se le contrassegni come di sola lettura. Il che, ovviamente, non ha senso in quanto puoi semplicemente assegnare una singola istanza di un tipo mutabile, come ConcurrentDictionary.


interessante ... questo thread è sicuro se utilizza ripetutamente la stessa istanza? So che gli hash gestiti hanno un Clear()metodo, ma non ho studiato fino in fondo Spooky.
Solomon Rutzky,

@PaulWhite e David. Avrei potuto fare qualcosa di sbagliato, o potrebbe essere una differenza tra SHA256Managede SpookyHashV2, ma ci ho provato e non ho visto molti miglioramenti delle prestazioni. Ho anche notato che il ManagedThreadIdvalore è lo stesso per tutti i riferimenti SQLCLR in una query specifica. Ho testato più riferimenti alla stessa funzione, nonché un riferimento a una funzione diversa, a tutti e 3 sono stati dati valori di input diversi e sono stati restituiti valori di ritorno diversi (ma previsti). Ciò non aumenterebbe le possibilità di una condizione di gara? Ad essere sinceri, nel mio test non ne ho visto nessuno.
Solomon Rutzky,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.