web-dev-qa-db-fra.com

Qu'est-ce qu'un moyen évolutif de simuler des HASHBYTES à l'aide d'une fonction scalaire SQL CLR?

Dans le cadre de notre processus ETL, nous comparons les lignes du transfert avec la base de données de rapports pour déterminer si l'une des colonnes a réellement changé depuis le dernier chargement des données.

La comparaison est basée sur la clé unique de la table et une sorte de hachage de toutes les autres colonnes. Nous utilisons actuellement HASHBYTES avec le SHA2_256 algorithme et ont constaté qu'il ne s'adapte pas aux grands serveurs si de nombreux threads de travail simultanés appellent tous HASHBYTES.

Le débit mesuré en hachages par seconde n'augmente pas les 16 derniers threads simultanés lors des tests sur un serveur à 96 cœurs. Je teste en modifiant le nombre de _ MAXDOP 8 requêtes de 1 à 12. Test avec MAXDOP 1 a montré le même goulot d'étranglement d'évolutivité.

Comme solution de contournement, je veux essayer une solution SQL CLR. Voici ma tentative d'énoncer les exigences:

  • La fonction doit pouvoir participer à des requêtes parallèles
  • La fonction doit être déterministe
  • La fonction doit prendre une entrée d'une chaîne NVARCHAR ou VARBINARY (toutes les colonnes pertinentes sont concaténées ensemble)
  • La taille d'entrée typique de la chaîne sera de 100 à 20000 caractères. 20000 n'est pas un maximum
  • Le risque de collision de hachage doit être à peu près égal ou supérieur à l'algorithme MD5. CHECKSUM ne fonctionne pas pour nous car il y a trop de collisions.
  • La fonction doit bien évoluer sur les grands serveurs (le débit par thread ne doit pas diminuer de manière significative à mesure que le nombre de threads augmente)

Pour Application Reasons ™, supposez que je ne peux pas économiser la valeur du hachage pour le tableau de rapport. C'est une CCI qui ne prend pas en charge les déclencheurs ou les colonnes calculées (il y a aussi d'autres problèmes que je ne veux pas aborder).

Qu'est-ce qu'un moyen évolutif de simuler HASHBYTES à l'aide d'une fonction SQL CLR? Mon objectif peut être exprimé en obtenant autant de hachages par seconde que je peux sur un grand serveur, donc les performances sont également importantes. Je suis terrible avec CLR donc je ne sais pas comment y arriver. Si cela motive quelqu'un à répondre, je prévois d'ajouter une prime à cette question dès que j'en serai capable. Voici un exemple de requête qui illustre très approximativement le cas d'utilisation:

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

Pour simplifier un peu les choses, je vais probablement utiliser quelque chose comme ce qui suit pour l'analyse comparative. Je publierai les résultats avec HASHBYTES lundi:

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

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

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

Puisque vous recherchez simplement des changements, vous n'avez pas besoin d'une fonction de hachage cryptographique.

Vous pouvez choisir parmi l'un des hachages non cryptographiques les plus rapides de l'open source bibliothèque Data.HashFunction par Brandon Dahler, sous licence permissive et approuvée par OSI licence MIT . SpookyHash est un choix populaire.

Exemple d'implémentation

Code source

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 source fournit deux fonctions, une pour les entrées de 8 000 octets ou moins et une version LOB. La version non LOB devrait être beaucoup plus rapide.

Vous pourrez peut-être envelopper un binaire LOB dans COMPRESS pour le mettre sous la limite de 8000 octets, si cela s'avère utile pour les performances. Alternativement, vous pouvez diviser le LOB en segments inférieurs à 8 000 octets, ou simplement réserver l'utilisation de HASHBYTES pour le cas du LOB (car les entrées plus longues évoluent mieux).

Code préconstruit

Vous pouvez évidemment récupérer le package pour vous-même et tout compiler, mais j'ai construit les assemblages ci-dessous pour faciliter les tests rapides:

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

Fonctions 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

Usage

Un exemple d'utilisation étant donné les exemples de données dans la question:

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

Lorsque vous utilisez la version LOB, le premier paramètre doit être converti ou converti en varbinary(max).

Plan d'exécution

plan


Safe Spooky

La bibliothèque Data.HashFunction utilise un certain nombre de fonctionnalités du langage CLR qui sont considérées comme UNSAFE par SQL Server. Il est possible d'écrire un Hash Spooky de base compatible avec le statut SAFE. Un exemple que j'ai écrit sur la base de Jon Hanna's SpookilySharp est ci-dessous:

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

21
Paul White 9

Je ne sais pas si le parallélisme sera meilleur/significativement meilleur avec SQLCLR. Cependant, il est vraiment facile de tester car il existe une fonction de hachage dans la version gratuite de la bibliothèque SQL # SQLCLR (que j'ai écrite) appelée Util_HashBinary . Les algorithmes pris en charge sont: MD5, SHA1, SHA256, SHA384 et SHA512.

Il prend une valeur VARBINARY(MAX) en entrée, vous pouvez donc concaténer la version chaîne de chaque champ (comme vous le faites actuellement), puis la convertir en VARBINARY(MAX), ou vous pouvez aller directement à VARBINARY pour chaque colonne et concaténer les valeurs converties (cela pourrait être plus rapide puisque vous ne traitez pas avec des chaînes ou la conversion supplémentaire de chaîne en VARBINARY). Voici un exemple montrant ces deux options. Il montre également la fonction HASHBYTES pour que vous puissiez voir que les valeurs sont les mêmes entre elle et SQL # .Util_HashBinary .

Veuillez noter que les résultats de hachage lors de la concaténation des valeurs VARBINARY ne correspondront pas aux résultats de hachage lors de la concaténation des valeurs NVARCHAR. Cela est dû au fait que la forme binaire de la valeur INT "1" est 0x00000001, tandis que la forme UTF-16LE (c'est-à-dire NVARCHAR) de la valeur INT de "1" ( sous forme binaire puisque c'est sur cela qu'une fonction de hachage fonctionnera) est 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;

Vous pouvez tester quelque chose de plus comparable au Spooky non-LOB en utilisant:

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

Remarque: Util_HashBinary utilise l'algorithme SHA256 géré intégré à .NET et ne doit pas utiliser la bibliothèque "bcrypt".

Au-delà de cet aspect de la question, d'autres réflexions pourraient aider ce processus:

Pensée supplémentaire # 1 (pré-calculer les hachages, au moins certains)

Vous avez mentionné quelques éléments:

  1. nous comparons les lignes de la mise en attente avec la base de données de rapports pour déterminer si l'une des colonnes a réellement changé depuis le dernier chargement des données.

    et:

  2. Je ne peux pas enregistrer la valeur du hachage pour le tableau de rapport. C'est une CCI qui ne prend pas en charge les déclencheurs ou les colonnes calculées

    et:

  3. les tables peuvent être mises à jour en dehors du processus ETL

Il semble que les données de ce tableau de rapport soient stables pendant un certain temps et qu'elles ne soient modifiées que par ce processus ETL.

Si rien d'autre ne modifie ce tableau, alors nous n'avons vraiment pas besoin d'un déclencheur ou d'une vue indexée (je pensais à l'origine que vous pourriez le faire).

Étant donné que vous ne pouvez pas modifier le schéma de la table de génération de rapports, serait-il au moins possible de créer une table associée pour contenir le hachage précalculé (et l'heure UTC du moment où il a été calculé)? Cela vous permettrait d'avoir une valeur pré-calculée à comparer avec la prochaine fois, ne laissant que la valeur entrante qui nécessite le calcul du hachage de. Cela réduirait de moitié le nombre d'appels à HASHBYTES ou SQL#.Util_HashBinary. Vous vous joindriez simplement à cette table de hachage pendant le processus d'importation.

Vous devez également créer une procédure stockée distincte qui actualise simplement les hachages de cette table. Il met simplement à jour les hachages de toute ligne associée qui a changé pour être actuelle et met à jour l'horodatage de ces lignes modifiées. Ce proc peut/doit être exécuté à la fin de tout autre processus qui met à jour ce tableau. Il peut également être planifié pour s'exécuter 30 à 60 minutes avant le démarrage de cet ETL (en fonction du temps qu'il faut pour s'exécuter et du moment où l'un de ces autres processus peut s'exécuter). Il peut même être exécuté manuellement si vous pensez que des lignes ne sont pas synchronisées.

Il a ensuite été noté que:

il y a plus de 500 tables

Ce nombre de tables rend plus difficile d'avoir une table supplémentaire pour chacune contenant les hachages actuels, mais ce n'est pas impossible car elle pourrait être scriptée car ce serait un schéma standard. Les scripts devraient simplement prendre en compte le nom de la table source et la découverte des colonnes PK de la table source.

Pourtant, quel que soit l'algorithme de hachage qui se révèle être le plus évolutif, je recommande toujours fortement de trouver au moins quelques tables (peut-être que certaines sont BEAUCOUP plus grandes que le reste des 500 tables ) et la mise en place d'une table associée pour capturer les hachages actuels afin que les valeurs "actuelles" puissent être connues avant le processus ETL. Même la fonction la plus rapide ne peut pas être plus performante sans avoir à l'appeler au départ ;-).

Pensée supplémentaire # 2 (VARBINARY au lieu de NVARCHAR)

Indépendamment de SQLCLR vs HASHBYTES intégré, je recommanderais toujours de convertir directement en VARBINARY car cela devrait être plus rapide. Concaténer des chaînes n'est tout simplement pas terriblement efficace. Et, c'est en plus de convertir des valeurs non-chaîne en chaînes en premier lieu, ce qui nécessite un effort supplémentaire (je suppose que la quantité d'effort varie en fonction du type de base: DATETIME nécessitant plus de BIGINT), alors que la conversion en VARBINARY vous donne simplement la valeur sous-jacente (dans la plupart des cas).

Et, en fait, le test du même ensemble de données que les autres tests utilisés et l'utilisation de HASHBYTES(N'SHA2_256',...), ont montré une augmentation de 23,415% du total des hachages calculés en une minute. Et cette augmentation était pour ne rien faire de plus que d'utiliser VARBINARY au lieu de NVARCHAR! ???? (veuillez consulter réponse wiki communautaire pour plus de détails)

Pensée supplémentaire # 3 (soyez conscient des paramètres d'entrée)

Des tests supplémentaires ont montré qu'un domaine qui affecte les performances (par rapport à ce volume d'exécutions) est les paramètres d'entrée: combien et quel (s) type (s).

La fonction Util_HashBinary SQLCLR qui se trouve actuellement dans ma bibliothèque SQL # a deux paramètres d'entrée: un VARBINARY (la valeur à hacher), et un NVARCHAR (l'algorithme à utiliser). Cela est dû à ma mise en miroir de la signature de la fonction HASHBYTES. Cependant, j'ai trouvé que si je supprimais le paramètre NVARCHAR et créais une fonction qui ne faisait que SHA256, les performances s'amélioraient assez bien. Je suppose que même changer le paramètre NVARCHAR en INT aurait aidé, mais je suppose aussi que ne pas avoir le paramètre supplémentaire INT est au moins légèrement plus rapide.

De plus, SqlBytes.Value Pourrait être plus performant que SqlBinary.Value.

J'ai créé deux nouvelles fonctions: Util_HashSHA256Binary et Util_HashSHA256Binary8k pour ce test. Ceux-ci seront inclus dans la prochaine version de SQL # (aucune date n'a encore été fixée pour cela).

J'ai également constaté que la méthodologie de test pourrait être légèrement améliorée, j'ai donc mis à jour le faisceau de test dans la réponse wiki communautaire ci-dessous pour inclure:

  1. préchargement des assemblages SQLCLR pour garantir que le temps de chargement ne faussera pas les résultats.
  2. une procédure de vérification pour vérifier les collisions. S'il en trouve, il affiche le nombre de lignes uniques/distinctes et le nombre total de lignes. Cela permet de déterminer si le nombre de collisions (s'il y en a) dépasse la limite pour le cas d'utilisation donné. Certains cas d'utilisation peuvent autoriser un petit nombre de collisions, d'autres peuvent n'en nécessiter aucune. Une fonction ultra-rapide est inutile si elle ne peut pas détecter les changements au niveau de précision souhaité. Par exemple, en utilisant le faisceau de test fourni par l'O.P., j'ai augmenté le nombre de lignes à 100 000 lignes (c'était à l'origine 10 000) et j'ai constaté que CHECKSUM enregistré plus de 9 000 collisions, soit 9% (yikes).

Pensée supplémentaire # 4 (HASHBYTES + SQLCLR ensemble?)

Selon l'emplacement du goulot d'étranglement, il peut même être utile d'utiliser une combinaison de HASHBYTES et d'un UDF SQLCLR intégrés pour faire le même hachage. Si les fonctions intégrées sont contraintes différemment/séparément des opérations SQLCLR, cette approche peut être en mesure d'accomplir plus simultanément que HASHBYTES ou SQLCLR individuellement. Cela vaut vraiment la peine d'être testé.

Pensée supplémentaire # 5 (mise en cache d'objets de hachage?)

La mise en cache de l'objet algorithme de hachage comme suggéré dans réponse de David Browne semble certainement intéressante, donc je l'ai essayé et j'ai trouvé les deux points d'intérêt suivants:

  1. Pour quelque raison que ce soit, il ne semble pas apporter beaucoup, voire aucune, d’amélioration des performances. J'aurais pu faire quelque chose de mal, mais voici ce que j'ai essayé:

    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. La valeur ManagedThreadId semble être la même pour toutes les références SQLCLR dans une requête particulière. J'ai testé plusieurs références à la même fonction, ainsi qu'une référence à une fonction différente, toutes les 3 recevant des valeurs d'entrée différentes et retournant des valeurs de retour différentes (mais attendues). Pour les deux fonctions de test, la sortie était une chaîne qui comprenait le ManagedThreadId ainsi qu'une représentation sous forme de chaîne du résultat de hachage. La valeur ManagedThreadId était la même pour toutes les références UDF dans la requête et sur toutes les lignes. Mais, le résultat du hachage était le même pour la même chaîne d'entrée et différent pour différentes chaînes d'entrée.

    Bien que je n'ai vu aucun résultat erroné dans mes tests, cela n'augmenterait-il pas les chances d'une condition de course? Si la clé du dictionnaire est la même pour tous les objets SQLCLR appelés dans une requête particulière, alors ils partageraient la même valeur ou l'objet stocké pour cette clé, non? Le fait étant que même si cela semblait fonctionner ici (dans une certaine mesure, il ne semblait pas y avoir de gain de performances important, mais rien ne fonctionnait), cela ne me donne pas confiance que cette approche fonctionnerait dans d'autres scénarios.

17
Solomon Rutzky

Ce n'est pas une réponse traditionnelle, mais j'ai pensé qu'il serait utile de publier des références de certaines des techniques mentionnées jusqu'à présent. Je teste sur un serveur 96 cœurs avec SQL Server 2017 CU9.

De nombreux problèmes d'évolutivité sont causés par des threads simultanés rivalisant sur un état global. Par exemple, considérez la contention de page PFS classique. Cela peut se produire si trop de threads de travail doivent modifier la même page en mémoire. À mesure que le code devient plus efficace, il peut demander le verrou plus rapidement. Cela augmente les conflits. Pour le dire simplement, un code efficace est plus susceptible de conduire à des problèmes d'évolutivité car l'état global est plus sévèrement contesté. Le code lent est moins susceptible de provoquer des problèmes d'évolutivité car l'état global n'est pas consulté aussi fréquemment.

L'évolutivité de HASHBYTES est partiellement basée sur la longueur de la chaîne d'entrée. Ma théorie était de savoir pourquoi cela se produit, c'est que l'accès à un état global est nécessaire lorsque la fonction HASHBYTES est appelée. L'état global facile à observer est qu'une page mémoire doit être allouée par appel sur certaines versions de SQL Server. Le plus difficile à observer est qu'il existe une sorte de conflit de système d'exploitation. Par conséquent, si HASHBYTES est appelé moins fréquemment par le code, les conflits diminuent. Une façon de réduire le taux d'appels HASHBYTES consiste à augmenter la quantité de travail de hachage nécessaire par appel. Le travail de hachage est partiellement basé sur la longueur de la chaîne d'entrée. Pour reproduire le problème d'évolutivité que j'ai vu dans l'application, j'avais besoin de modifier les données de démonstration. Un pire scénario raisonnable est un tableau avec 21 colonnes BIGINT. La définition de la table est incluse dans le code en bas. Pour réduire Local Factors ™, j'utilise des requêtes simultanées MAXDOP 1 Qui fonctionnent sur des tables relativement petites. Mon code de référence rapide est en bas.

Notez que les fonctions renvoient différentes longueurs de hachage. MD5 Et SpookyHash sont tous deux des hachages de 128 bits, SHA256 Est un hachage de 256 bits.

RÉSULTATS (NVARCHAR vs VARBINARY conversion et concaténation)

Afin de voir si la conversion et la concaténation de VARBINARY est vraiment plus efficace/performante que NVARCHAR, une version NVARCHAR de la procédure stockée RUN_HASHBYTES_SHA2_256 A été créé à partir du même modèle (voir "Étape 5" dans la section CODE DE RÉFÉRENCE ci-dessous). Les seules différences sont:

  1. Le nom de la procédure stockée se termine par _NVC
  2. BINARY(8) pour la fonction CAST a été changé en NVARCHAR(15)
  3. 0x7C A été modifié pour devenir N'|'

Résultant en:

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

au lieu de:

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

Le tableau ci-dessous contient le nombre de hachages effectués en 1 minute. Les tests ont été effectués sur un serveur différent de celui utilisé pour les autres tests indiqués ci-dessous.

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

En ne regardant que les moyennes, nous pouvons calculer l'avantage de passer à VARBINARY:

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

Cela revient:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

RÉSULTATS (algorithmes de hachage et implémentations)

Le tableau ci-dessous contient le nombre de hachages effectués en 1 minute. Par exemple, l'utilisation de CHECKSUM avec 84 requêtes simultanées a entraîné plus de 2 milliards de hachages avant l'expiration du délai.

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

Si vous préférez voir les mêmes nombres mesurés en termes de travail par seconde de 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 ║
╚════════════════════╩════════════════════════════╩════════════════════════════╩════════════════════════════╝

Quelques réflexions rapides sur toutes les méthodes:

  • CHECKSUM: très bonne évolutivité comme prévu
  • HASHBYTES: les problèmes d'évolutivité incluent une allocation de mémoire par appel et une grande quantité de CPU dépensée dans le système d'exploitation
  • Spooky: évolutivité étonnamment bonne
  • Spooky LOB: Le verrou tournant SOS_SELIST_SIZED_SLOCK Tourne hors de contrôle. Je soupçonne que c'est un problème général avec le passage des LOB via les fonctions CLR, mais je ne suis pas sûr
  • Util_HashBinary: On dirait qu'il est touché par le même verrou tournant. Je n'ai pas examiné cette question jusqu'à présent, car je ne peux probablement pas faire grand-chose à ce sujet:

spin your lock

  • Util_HashBinary 8k: Résultats très surprenants, je ne sais pas ce qui se passe ici

Résultats finaux testés sur un serveur plus petit:

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

CODE DE RÉFÉRENCE

CONFIGURATION 1: Tableaux et données

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

CONFIGURATION 3: Processus de détection de collision

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

CONFIGURATION 5: Génération de processus de test

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: Vérifier les collisions

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: exécuter des tests de performances

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;

QUESTIONS DE VALIDATION À RÉSOUDRE

Tout en se concentrant sur les tests de performance d'un FDU SQLCLR singulier, deux questions qui ont été discutées au début n'ont pas été incorporées dans les tests, mais devraient idéalement être étudiées afin de déterminer quelle approche répond à tous de la exigences.

  1. La fonction sera exécutée deux fois pour chaque requête (une fois pour la ligne d'importation et une fois pour la ligne actuelle). Jusqu'à présent, les tests n'ont référencé l'UDF qu'une seule fois dans les requêtes de test. Ce facteur peut ne pas changer le classement des options, mais il ne doit pas être ignoré, juste au cas où.
  2. Dans un commentaire qui a depuis été supprimé, Paul White avait mentionné:

    Un inconvénient de remplacer HASHBYTES par une fonction scalaire CLR - il semble que les fonctions CLR ne peuvent pas utiliser le mode batch alors que HASHBYTES le peut. Cela pourrait être important, en termes de performances.

    C'est donc quelque chose à considérer et qui nécessite clairement des tests. Si les options SQLCLR n'offrent aucun avantage par rapport au HASHBYTES intégré, cela ajoute du poids à suggestion de Salomon de capture des hachages existants (pour au moins les plus grandes tables) dans des tables connexes .

12
Joe Obbish

Vous pouvez probablement améliorer les performances et peut-être l'évolutivité de toutes les approches .NET en regroupant et en mettant en cache tous les objets créés dans l'appel de fonction. EG pour le code de Paul White ci-dessus:

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 décourage et essaie d'empêcher l'utilisation de variables statiques/partagées, mais il vous permettra d'utiliser des variables partagées si vous les marquez en lecture seule. Ce qui, bien sûr, n'a pas de sens car vous pouvez simplement affecter une seule instance d'un type mutable, comme ConcurrentDictionary.

7