Quelle est une façon évolutive de simuler des HASHBYTES à l'aide d'une fonction scalaire SQL CLR?


29

Dans le cadre de notre processus ETL, nous comparons les lignes du transfert par rapport à 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 HASHBYTESl' SHA2_256algorithme et avons 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 changeant le nombre de MAXDOP 8requêtes simultanées de 1 à 12. Les tests avec MAXDOP 1ont 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 NVARCHARou 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. CHECKSUMne 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 à l' HASHBYTESaide 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 HASHBYTESlundi:

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

Réponses:


18

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 la bibliothèque open-source Data.HashFunction de Brandon Dahler, sous licence permissive et approuvée OSI MIT . SpookyHashest 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 COMPRESSpour 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 HASHBYTESpour 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/365b265b476bf86754457fc9514b2300

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 prises UNSAFEen compte par SQL Server. Il est possible d'écrire un Hash Spooky de base compatible avec le SAFEstatut. Un exemple que j'ai écrit sur la base de SpookilySharp de Jon Hanna est ci-dessous:

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


16

Je ne sais pas si le parallélisme sera meilleur / significativement meilleur avec SQLCLR. Cependant, il est vraiment facile à 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 VARBINARY(MAX)valeur en entrée, vous pouvez donc concaténer la version chaîne de chaque champ (comme vous le faites actuellement) puis convertir en VARBINARY(MAX), ou vous pouvez aller directement à VARBINARYpour chaque colonne et concaténer les valeurs converties (cela pourrait être plus rapide car 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 HASHBYTESfonction afin 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 VARBINARYvaleurs ne correspondront pas aux résultats de hachage lors de la concaténation des NVARCHARvaleurs. Cela est dû au fait que la forme binaire de la INTvaleur "1" est 0x00000001, tandis que la forme UTF-16LE (c'est-à-dire NVARCHAR) de la INTvaleur "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 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.

    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 une certaine période et 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).

É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 le nombre d'appels à l'un HASHBYTESou à l'autre SQL#.Util_HashBinaryde moitié. 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 ce tableau. 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 programmé 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. Le script aurait juste besoin de 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 vivement de trouver au moins quelques tables (peut-être certaines sont BEAUCOUP plus grandes que le reste des 500 tables) et de configurer une table associée pour capturer 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 en premier lieu ;-).

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

Indépendamment de SQLCLR vs intégré HASHBYTES, je recommanderais toujours de convertir directement en VARBINARYcar cela devrait être plus rapide. Concaténer des chaînes n'est tout simplement pas terriblement efficace. Et , cela s'ajoute à la conversion de 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: DATETIMEnécessitant plus de BIGINT), tandis que la conversion en VARBINARYvous 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 utilisant a HASHBYTES(N'SHA2_256',...)montré une augmentation de 23,415% du nombre total de hachages calculé en une minute. Et cette augmentation était pour ne rien faire de plus que d'utiliser VARBINARYau lieu de NVARCHAR! 😸 (veuillez consulter la réponse du wiki de la communauté 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 (sur 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 HASHBYTESfonction. Cependant, j'ai trouvé que si je supprimais le NVARCHARparamètre et créais une fonction qui ne faisait que SHA256, les performances s'amélioraient assez bien. Je suppose que même changer le NVARCHARparamètre INTaurait aidé, mais je suppose aussi que ne pas avoir le INTparamètre supplémentaire est au moins légèrement plus rapide.

Peut également SqlBytes.Valueê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'OP, j'ai augmenté le nombre de lignes à 100 000 lignes (c'était à l'origine 10 000) et j'ai constaté que CHECKSUMplus de 9 000 collisions étaient enregistrées, ce qui représente 9% (yikes).

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

Selon l'endroit où se trouve le goulot d'étranglement, il peut même être utile d'utiliser une combinaison de HASHBYTESUDF intégré et SDF ULC 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 HASHBYTESSQLCLR ou 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 la réponse de David Browne semble certainement intéressante, alors 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 ManagedThreadIdvaleur 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 ManagedThreadIdune représentation de chaîne ainsi que le résultat du hachage. La ManagedThreadIdvaleur é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.


11

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.

HASHBYTESl'évolutivité est partiellement basée sur la longueur de la chaîne d'entrée. Ma théorie était de savoir pourquoi cela se produit est que l'accès à un état global est nécessaire lorsque la HASHBYTESfonction 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 HASHBYTESle code est appelé moins fréquemment, les conflits diminuent. Une façon de réduire le taux d' HASHBYTESappels 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 21BIGINTcolonnes. La définition de la table est incluse dans le code en bas. Pour réduire Local Factors ™, j'utilise des MAXDOP 1requêtes simultanées 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. MD5et SpookyHashsont les deux hachages 128 bits, SHA256est un hachage 256 bits.

RÉSULTATS ( NVARCHARvs VARBINARYconversion et concaténation)

Afin de voir si la conversion et la concaténation, VARBINARY est / vraiment plus efficace performante que NVARCHAR, une NVARCHARversion de la RUN_HASHBYTES_SHA2_256procédure stockée a été créée à partir du même modèle (voir « Etape 5 » dans BENCHMARKING CODE section 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 CASTfonction a été modifiée pour êtreNVARCHAR(15)
  3. 0x7C a été changé pour être 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 CHECKSUMavec 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: une évolutivité étonnamment bonne
  • Spooky LOB: le spinlock SOS_SELIST_SIZED_SLOCKtourne 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 spinlock. Je n'ai pas examiné cette question jusqu'à présent, car je ne peux probablement pas faire grand-chose à ce sujet:

faites tourner votre serrure

  • Util_HashBinary 8k: des 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érer des 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é intégrées aux tests, mais devraient idéalement être étudiées afin de déterminer quelle approche répond à toutes les 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 du remplacement HASHBYTESpar une fonction scalaire CLR - il semble que les fonctions CLR ne peuvent pas utiliser le mode batch alors qu'elles le HASHBYTESpeuvent. 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 à la fonction intégrée HASHBYTES, cela ajoute du poids à la suggestion de Salomon de capturer les hachages existants (pour au moins les plus grandes tables) dans des tables connexes.


6

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 attribuer une seule instance d'un type mutable, comme ConcurrentDictionary.


intéressant ... ce fil est-il sûr s'il utilise la même instance encore et encore? Je sais que les hachages gérés ont une Clear()méthode mais je n'ai pas regardé aussi loin dans Spooky.
Solomon Rutzky

@PaulWhite et David. J'aurais pu faire quelque chose de mal, ou cela pourrait être une différence entre SHA256Managedet SpookyHashV2, mais j'ai essayé cela et je n'ai pas vu beaucoup, voire aucune, d'amélioration des performances. J'ai également remarqué que la ManagedThreadIdvaleur est 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). Cela n'augmenterait-il pas les chances d'une condition de concurrence? Pour être juste, dans mon test, je n'en ai pas vu.
Solomon Rutzky
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.