¿Cuál es una forma escalable de simular HASHBYTES usando una función escalar SQL CLR?

29

Como parte de nuestro proceso ETL, comparamos las filas de la preparación con la base de datos de informes para determinar si alguna de las columnas realmente ha cambiado desde la última vez que se cargaron los datos.

La comparación se basa en la clave única de la tabla y en algún tipo de hash de todas las otras columnas. Actualmente lo utilizamos HASHBYTEScon el SHA2_256algoritmo y hemos descubierto que no se escala en servidores grandes si muchos hilos de trabajo concurrentes están llamando HASHBYTES.

El rendimiento medido en hashes por segundo no aumenta los últimos 16 subprocesos concurrentes cuando se prueba en un servidor de 96 núcleos. Pruebo cambiando el número de MAXDOP 8consultas simultáneas de 1 a 12. Las pruebas con MAXDOP 1mostraron el mismo cuello de botella de escalabilidad.

Como solución, quiero probar una solución SQL CLR. Aquí está mi intento de establecer los requisitos:

  • La función debe poder participar en consultas paralelas
  • La función debe ser determinista.
  • La función debe tomar una entrada de una cadena NVARCHARo VARBINARY(todas las columnas relevantes se concatenan juntas)
  • El tamaño de entrada típico de la cadena será de 100 a 20000 caracteres de longitud. 20000 no es un máximo
  • La posibilidad de una colisión de hash debe ser aproximadamente igual o mejor que el algoritmo MD5. CHECKSUMno funciona para nosotros porque hay demasiadas colisiones.
  • La función debe escalar bien en servidores grandes (el rendimiento por subproceso no debe disminuir significativamente a medida que aumenta el número de subprocesos)

Para Application Reasons ™, suponga que no puedo guardar el valor del hash para la tabla de informes. Es un CCI que no admite disparadores o columnas calculadas (también hay otros problemas en los que no quiero entrar).

¿Cuál es una forma escalable de simular HASHBYTESusando una función CLR de SQL? Mi objetivo se puede expresar como obtener tantos hashes por segundo como pueda en un servidor grande, por lo que el rendimiento también es importante. Soy terrible con CLR, así que no sé cómo lograr esto. Si motiva a alguien a responder, planeo agregar una recompensa a esta pregunta tan pronto como pueda. A continuación se muestra una consulta de ejemplo que ilustra más o menos el caso de uso:

DROP TABLE IF EXISTS #CHANGED_IDS;

SELECT stg.ID INTO #CHANGED_IDS
FROM (
    SELECT ID,
    CAST( HASHBYTES ('SHA2_256', 
        CAST(FK1 AS NVARCHAR(19)) + 
        CAST(FK2 AS NVARCHAR(19)) + 
        CAST(FK3 AS NVARCHAR(19)) + 
        CAST(FK4 AS NVARCHAR(19)) + 
        CAST(FK5 AS NVARCHAR(19)) + 
        CAST(FK6 AS NVARCHAR(19)) + 
        CAST(FK7 AS NVARCHAR(19)) + 
        CAST(FK8 AS NVARCHAR(19)) + 
        CAST(FK9 AS NVARCHAR(19)) + 
        CAST(FK10 AS NVARCHAR(19)) + 
        CAST(FK11 AS NVARCHAR(19)) + 
        CAST(FK12 AS NVARCHAR(19)) + 
        CAST(FK13 AS NVARCHAR(19)) + 
        CAST(FK14 AS NVARCHAR(19)) + 
        CAST(FK15 AS NVARCHAR(19)) + 
        CAST(STR1 AS NVARCHAR(500)) +
        CAST(STR2 AS NVARCHAR(500)) +
        CAST(STR3 AS NVARCHAR(500)) +
        CAST(STR4 AS NVARCHAR(500)) +
        CAST(STR5 AS NVARCHAR(500)) +
        CAST(COMP1 AS NVARCHAR(1)) + 
        CAST(COMP2 AS NVARCHAR(1)) + 
        CAST(COMP3 AS NVARCHAR(1)) + 
        CAST(COMP4 AS NVARCHAR(1)) + 
        CAST(COMP5 AS NVARCHAR(1)))
     AS BINARY(32)) HASH1
    FROM HB_TBL WITH (TABLOCK)
) stg
INNER JOIN (
    SELECT ID,
    CAST(HASHBYTES ('SHA2_256', 
        CAST(FK1 AS NVARCHAR(19)) + 
        CAST(FK2 AS NVARCHAR(19)) + 
        CAST(FK3 AS NVARCHAR(19)) + 
        CAST(FK4 AS NVARCHAR(19)) + 
        CAST(FK5 AS NVARCHAR(19)) + 
        CAST(FK6 AS NVARCHAR(19)) + 
        CAST(FK7 AS NVARCHAR(19)) + 
        CAST(FK8 AS NVARCHAR(19)) + 
        CAST(FK9 AS NVARCHAR(19)) + 
        CAST(FK10 AS NVARCHAR(19)) + 
        CAST(FK11 AS NVARCHAR(19)) + 
        CAST(FK12 AS NVARCHAR(19)) + 
        CAST(FK13 AS NVARCHAR(19)) + 
        CAST(FK14 AS NVARCHAR(19)) + 
        CAST(FK15 AS NVARCHAR(19)) + 
        CAST(STR1 AS NVARCHAR(500)) +
        CAST(STR2 AS NVARCHAR(500)) +
        CAST(STR3 AS NVARCHAR(500)) +
        CAST(STR4 AS NVARCHAR(500)) +
        CAST(STR5 AS NVARCHAR(500)) +
        CAST(COMP1 AS NVARCHAR(1)) + 
        CAST(COMP2 AS NVARCHAR(1)) + 
        CAST(COMP3 AS NVARCHAR(1)) + 
        CAST(COMP4 AS NVARCHAR(1)) + 
        CAST(COMP5 AS NVARCHAR(1)) )
 AS BINARY(32)) HASH1
    FROM HB_TBL_2 WITH (TABLOCK)
) rpt ON rpt.ID = stg.ID
WHERE rpt.HASH1 <> stg.HASH1
OPTION (MAXDOP 8);

Para simplificar un poco las cosas, probablemente usaré algo como lo siguiente para la evaluación comparativa. Publicaré resultados HASHBYTESel lunes:

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

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

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

Respuestas:

18

Como solo está buscando cambios, no necesita una función de cifrado hash.

Puede elegir uno de los hashes no criptográficos más rápidos en la biblioteca Data.HashFunction de código abierto de Brandon Dahler, con licencia bajo la licencia MIT permisiva y aprobada por OSI . SpookyHashEs una elección popular.

Implementación de ejemplo

Código fuente

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 fuente proporciona dos funciones, una para entradas de 8000 bytes o menos, y una versión LOB. La versión sin LOB debería ser significativamente más rápida.

Es posible que pueda ajustar un binario LOB COMPRESSpara obtenerlo por debajo del límite de 8000 bytes, si eso resulta útil para el rendimiento. Alternativamente, puede dividir el LOB en segmentos de menos de 8000 bytes, o simplemente reservar el uso HASHBYTESpara el caso LOB (ya que las entradas más largas se escalan mejor).

Código preconstruido

Obviamente, puede tomar el paquete usted mismo y compilar todo, pero construí los ensamblajes a continuación para facilitar las pruebas rápidas:

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

Funciones T-SQL

CREATE FUNCTION dbo.SpookyHash
(
    @Input varbinary(8000)
)
RETURNS binary(16)
WITH 
    RETURNS NULL ON NULL INPUT, 
    EXECUTE AS OWNER
AS EXTERNAL NAME Spooky.UserDefinedFunctions.SpookyHash;
GO
CREATE FUNCTION dbo.SpookyHashLOB
(
    @Input varbinary(max)
)
RETURNS binary(16)
WITH 
    RETURNS NULL ON NULL INPUT, 
    EXECUTE AS OWNER
AS EXTERNAL NAME Spooky.UserDefinedFunctions.SpookyHashLOB;
GO

Uso

Un ejemplo de uso dados los datos de muestra en la pregunta:

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

Cuando se usa la versión LOB, el primer parámetro se debe convertir o convertir varbinary(max).

Plan de ejecución

plan


Seguro espeluznante

La biblioteca Data.HashFunction utiliza una serie de características del lenguaje CLR que UNSAFESQL Server considera . Es posible escribir un Spooky Hash básico compatible con el SAFEestado. A continuación se muestra un ejemplo que escribí basado en SpookilySharp de Jon Hanna :

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

Paul White dice GoFundMonica
fuente
16

No estoy seguro si el paralelismo será / significativamente mejor con SQLCLR. Sin embargo, es realmente fácil de probar ya que hay una función hash en la versión gratuita de la biblioteca SQL # SQLCLR (que escribí) llamada Util_HashBinary . Los algoritmos compatibles son: MD5, SHA1, SHA256, SHA384 y SHA512.

Toma un VARBINARY(MAX)valor como entrada, por lo que puede concatenar la versión de cadena de cada campo (como lo está haciendo actualmente) y luego convertir a VARBINARY(MAX), o puede ir directamente a VARBINARYcada columna y concatenar los valores convertidos (esto podría ser más rápido ya que no está tratando con cadenas o la conversión adicional de cadena a VARBINARY). A continuación se muestra un ejemplo que muestra ambas opciones. También muestra la HASHBYTESfunción para que pueda ver que los valores son los mismos entre este y SQL # .Util_HashBinary .

Tenga en cuenta que los resultados del hash al concatenar los VARBINARYvalores no coincidirán con los resultados del hash al concatenar los NVARCHARvalores. Esto se debe a que la forma binaria del INTvalor "1" es 0x00000001, mientras que la forma UTF-16LE (es decir, la NVARCHARforma del INTvalor de "1" (en forma binaria, ya que eso es sobre lo que operará una función hash) es 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;

Puede probar algo más comparable al Spooky que no es LOB usando:

CREATE FUNCTION [SQL#].[Util_HashBinary8k]
(@Algorithm [nvarchar](50), @BaseData [varbinary](8000))
RETURNS [varbinary](8000) 
WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT
AS EXTERNAL NAME [SQL#].[UTILITY].[HashBinary];

Nota: Util_HashBinary usa el algoritmo SHA256 administrado que está integrado en .NET, y no debe usar la biblioteca "bcrypt".

Más allá de ese aspecto de la pregunta, hay algunas ideas adicionales que podrían ayudar a este proceso:

Pensamiento adicional n. ° 1 (calcular previamente hashes, al menos algunos)

Mencionaste algunas cosas:

  1. comparamos las filas de la preparación con la base de datos de informes para determinar si alguna de las columnas ha cambiado realmente desde la última vez que se cargaron los datos.

    y:

  2. No puedo guardar el valor del hash para la tabla de informes. Es un CCI que no admite disparadores ni columnas calculadas.

    y:

  3. las tablas se pueden actualizar fuera del proceso ETL

Parece que los datos en esta tabla de informes son estables durante un período de tiempo y solo se modifican mediante este proceso ETL.

Si nada más modifica esta tabla, entonces realmente no necesitamos un disparador o una vista indexada (originalmente pensé que podría).

Como no puede modificar el esquema de la tabla de informes, ¿al menos sería posible crear una tabla relacionada para contener el hash precalculado (y la hora UTC de cuándo se calculó)? Esto le permitiría tener un valor precalculado para comparar la próxima vez, dejando solo el valor entrante que requiere calcular el hash de. Esto reduciría la cantidad de llamadas a la mitad HASHBYTESo SQL#.Util_HashBinarya la mitad. Simplemente te unirías a esta tabla de hashes durante el proceso de importación.

También crearía un procedimiento almacenado separado que simplemente actualiza los hash de esta tabla. Simplemente actualiza los hashes de cualquier fila relacionada que haya cambiado para ser actual, y actualiza la marca de tiempo para esas filas modificadas. Este proceso puede / debe ejecutarse al final de cualquier otro proceso que actualice esta tabla. También se puede programar para que se ejecute entre 30 y 60 minutos antes de que se inicie este ETL (dependiendo de cuánto tiempo se tarde en ejecutar y cuándo se pueda ejecutar cualquiera de estos otros procesos). Incluso puede ejecutarse manualmente si sospecha que puede haber filas que no están sincronizadas.

Luego se observó que:

hay más de 500 mesas

El hecho de que muchas tablas hacen que sea más difícil tener una tabla adicional por cada una para contener los hashes actuales, pero esto no es imposible ya que podría ser un script ya que sería un esquema estándar. Las secuencias de comandos solo tendrían que tener en cuenta el nombre de la tabla fuente y el descubrimiento de las columnas PK de la tabla fuente.

Sin embargo, independientemente de qué algoritmo de hash en última instancia, resulta ser la más escalable, todavía altamente recomendaría para encontrar al menos un par de mesas (tal vez hay algunos que son mucho más grandes que el resto de las 500 mesas) y la creación de una tabla relacionada a la captura hashes actuales para que los valores "actuales" puedan conocerse antes del proceso ETL. Incluso la función más rápida no puede funcionar mejor sin tener que llamarla en primer lugar ;-).

Pensamiento adicional n. ° 2 (en VARBINARYlugar de NVARCHAR)

Independientemente de SQLCLR vs incorporado HASHBYTES, aún recomendaría convertir directamente a VARBINARYque debería ser más rápido. Concatenar cadenas simplemente no es terriblemente eficiente. Y , además de convertir valores que no son cadenas en cadenas en primer lugar, lo que requiere un esfuerzo adicional (supongo que la cantidad de esfuerzo varía según el tipo de base: DATETIMErequiere más de BIGINT), mientras que la conversión a VARBINARYsimplemente le da el valor subyacente (en la mayoría de los casos).

Y, de hecho, al probar el mismo conjunto de datos que las otras pruebas que se usaron, y al usar HASHBYTES(N'SHA2_256',...), mostraron un aumento del 23.415% en los hashes totales calculados en un minuto. ¡Y ese aumento fue por no hacer nada más que usar en VARBINARYlugar de NVARCHAR! 😸 (vea la respuesta de la wiki comunitaria para más detalles)

Pensamiento adicional n. ° 3 (tenga en cuenta los parámetros de entrada)

Las pruebas posteriores mostraron que un área que afecta el rendimiento (en este volumen de ejecuciones) son los parámetros de entrada: cuántos y de qué tipo (s).

La función Util_HashBinary SQLCLR que se encuentra actualmente en mi biblioteca SQL # tiene dos parámetros de entrada: uno VARBINARY(el valor de hash) y otro NVARCHAR(el algoritmo a usar). Esto se debe a que reflejo la firma de la HASHBYTESfunción. Sin embargo, descubrí que si eliminaba el NVARCHARparámetro y creaba una función que solo hacía SHA256, el rendimiento mejoraba bastante bien. Supongo que incluso cambiar el NVARCHARparámetro a INThabría ayudado, pero también asumo que ni siquiera tener el INTparámetro adicional es al menos un poco más rápido.

Además, SqlBytes.Valuepodría funcionar mejor que SqlBinary.Value.

Creé dos nuevas funciones: Util_HashSHA256Binary y Util_HashSHA256Binary8k para esta prueba. Estos se incluirán en la próxima versión de SQL # (todavía no se ha establecido una fecha).

También descubrí que la metodología de prueba podría mejorarse ligeramente, así que actualicé el arnés de prueba en la respuesta wiki de la comunidad a continuación para incluir:

  1. precarga de los ensamblajes SQLCLR para garantizar que la sobrecarga del tiempo de carga no sesgue los resultados.
  2. Un procedimiento de verificación para comprobar las colisiones. Si se encuentra alguno, muestra el número de filas únicas / distintas y el número total de filas. Esto permite determinar si el número de colisiones (si las hay) está más allá del límite para el caso de uso dado. Algunos casos de uso pueden permitir un pequeño número de colisiones, otros pueden no requerir ninguno. Una función súper rápida es inútil si no puede detectar cambios en el nivel de precisión deseado. Por ejemplo, utilizando el arnés de prueba proporcionado por el OP, aumenté el recuento de filas a 100k filas (originalmente era de 10k) y descubrí que se CHECKSUMregistraron más de 9k colisiones, que es del 9% (yikes).

Pensamiento adicional n. ° 4 (¿ HASHBYTES+ SQLCLR juntos?)

Dependiendo de dónde esté el cuello de botella, incluso podría ser útil usar una combinación de HASHBYTESUDF SQLCLR incorporado para hacer el mismo hash. Si las funciones integradas están restringidas de manera diferente / separada de las operaciones SQLCLR, entonces este enfoque podría ser capaz de lograr más concurrentemente que HASHBYTESSQLCLR o individualmente. Definitivamente vale la pena probarlo.

Pensamiento adicional n. ° 5 (almacenamiento en caché de objetos hash?)

El almacenamiento en caché del objeto del algoritmo de hash como se sugiere en la respuesta de David Browne ciertamente parece interesante, así que lo probé y encontré los siguientes dos puntos de interés:

  1. Por alguna razón, no parece proporcionar mucha mejora, si es que hay alguna, en el rendimiento. Podría haber hecho algo incorrectamente, pero esto es lo que probé:

    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. El ManagedThreadIdvalor parece ser el mismo para todas las referencias SQLCLR en una consulta particular. Probé varias referencias a la misma función, así como una referencia a una función diferente, a los 3 se les dieron valores de entrada diferentes y devolvieron valores de retorno diferentes (pero esperados). Para ambas funciones de prueba, la salida era una cadena que incluía la ManagedThreadIdrepresentación del resultado hash, así como una cadena. El ManagedThreadIdvalor era el mismo para todas las referencias UDF en la consulta y en todas las filas. Pero, el resultado hash fue el mismo para la misma cadena de entrada y diferente para diferentes cadenas de entrada.

    Si bien no vi ningún resultado erróneo en mis pruebas, ¿no aumentaría esto las posibilidades de una condición de carrera? Si la clave del diccionario es la misma para todos los objetos SQLCLR llamados en una consulta en particular, entonces compartirían el mismo valor u objeto almacenado para esa clave, ¿verdad? El punto es que, aunque parecía funcionar aquí (hasta cierto punto, nuevamente no parecía haber una gran ganancia de rendimiento, pero funcionalmente no se rompió nada), eso no me da la confianza de que este enfoque funcionará en otros escenarios.

Solomon Rutzky
fuente
11

Esta no es una respuesta tradicional, pero pensé que sería útil publicar puntos de referencia de algunas de las técnicas mencionadas hasta ahora. Estoy probando en un servidor de 96 núcleos con SQL Server 2017 CU9.

Muchos problemas de escalabilidad son causados ​​por hilos concurrentes que compiten por algún estado global. Por ejemplo, considere la contención clásica de la página PFS. Esto puede suceder si demasiados hilos de trabajo necesitan modificar la misma página en la memoria. A medida que el código se vuelve más eficiente, puede solicitar el enganche más rápido. Eso aumenta la contención. En pocas palabras, es más probable que un código eficiente conduzca a problemas de escalabilidad porque el estado global se enfrenta con mayor severidad. Es menos probable que el código lento cause problemas de escalabilidad porque no se accede al estado global con tanta frecuencia.

HASHBYTESla escalabilidad se basa parcialmente en la longitud de la cadena de entrada. Mi teoría era por qué esto ocurre es que se necesita acceso a algún estado global cuando HASHBYTESse llama a la función. El estado global fácil de observar es que una página de memoria debe asignarse por llamada en algunas versiones de SQL Server. La más difícil de observar es que hay algún tipo de contención del sistema operativo. Como resultado, si HASHBYTESel código lo invoca con menos frecuencia, la contención disminuye. Una forma de reducir la tasa de HASHBYTESllamadas es aumentar la cantidad de trabajo de hash necesario por llamada. El trabajo de hash se basa parcialmente en la longitud de la cadena de entrada. Para reproducir el problema de escalabilidad que vi en la aplicación, necesitaba cambiar los datos de demostración. Un peor escenario razonable es una tabla con 21BIGINTcolumnas La definición de la tabla se incluye en el código en la parte inferior. Para reducir Local Factors ™, estoy usando MAXDOP 1consultas concurrentes que operan en tablas relativamente pequeñas. Mi código de referencia rápido está en la parte inferior.

Tenga en cuenta que las funciones devuelven diferentes longitudes hash. MD5y SpookyHashson ambos hashes de 128 bits, SHA256es un hash de 256 bits.

RESULTADOS ( NVARCHARvs VARBINARYconversión y concatenación)

Para ver si la conversión y la concatenación VARBINARYes realmente más eficiente / eficiente que NVARCHAR, se creó una NVARCHARversión del RUN_HASHBYTES_SHA2_256procedimiento almacenado a partir de la misma plantilla (consulte el "Paso 5" en la sección CÓDIGO DE REFERENCIA a continuación). Las únicas diferencias son:

  1. El nombre del procedimiento almacenado termina en _NVC
  2. BINARY(8)para la CASTfunción se cambió para serNVARCHAR(15)
  3. 0x7C fue cambiado para ser N'|'

Resultando en:

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

en lugar de:

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

La siguiente tabla contiene el número de hashes realizados en 1 minuto. Las pruebas se realizaron en un servidor diferente al utilizado para las otras pruebas que se indican a continuación.

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

Mirando solo los promedios, podemos calcular el beneficio de cambiar a VARBINARY:

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

Eso vuelve:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

RESULTADOS (algoritmos hash e implementaciones)

La siguiente tabla contiene el número de hashes realizados en 1 minuto. Por ejemplo, el uso CHECKSUMcon 84 consultas simultáneas resultó en más de 2 mil millones de hashes realizados antes de que se agotara el tiempo.

╔════════════════════╦════════════╦════════════╦════════════╗
      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 prefiere ver los mismos números medidos en términos de trabajo por hilo-segundo:

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

Algunas reflexiones rápidas sobre todos los métodos:

  • CHECKSUM: muy buena escalabilidad como se esperaba
  • HASHBYTES: los problemas de escalabilidad incluyen una asignación de memoria por llamada y una gran cantidad de CPU gastada en el sistema operativo
  • Spooky: sorprendentemente buena escalabilidad
  • Spooky LOB: el spinlock SOS_SELIST_SIZED_SLOCKgira fuera de control. Sospecho que este es un problema general al pasar LOB a través de funciones CLR, pero no estoy seguro
  • Util_HashBinary: parece que es golpeado por el mismo spinlock. No he investigado esto hasta ahora porque probablemente no hay mucho que pueda hacer al respecto:

gira tu cerradura

  • Util_HashBinary 8k: resultados muy sorprendentes, no estoy seguro de lo que está pasando aquí

Resultados finales probados en un servidor más pequeño:

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

CÓDIGO DE REFERENCIA

CONFIGURACIÓN 1: Tablas y datos

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

CONFIGURACIÓN 2: Proceso de ejecución maestro

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

CONFIGURACIÓN 3: Proceso de detección de colisión

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

CONFIGURACIÓN 4: Limpieza (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);

CONFIGURACIÓN 5: Generar Procs de Prueba

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

PRUEBA 1: Verificar colisiones

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;

PRUEBA 2: ejecutar pruebas de rendimiento

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;

CUESTIONES DE VALIDACIÓN A RESOLVER

Al centrarse en las pruebas de rendimiento de un UDF SQLCLR singular, dos cuestiones que se discutieron desde el principio no se incorporaron a las pruebas, pero idealmente deberían investigarse para determinar qué enfoque cumple con todos los requisitos.

  1. La función se ejecutará dos veces por cada consulta (una vez para la fila de importación y otra para la fila actual). Las pruebas hasta ahora solo han hecho referencia al UDF una vez en las consultas de prueba. Es posible que este factor no cambie la clasificación de las opciones, pero no debe ignorarse, por si acaso.
  2. En un comentario que desde entonces ha sido eliminado, Paul White había mencionado:

    Una desventaja de reemplazar HASHBYTEScon una función escalar CLR: parece que las funciones CLR no pueden usar el modo por lotes, mientras que HASHBYTESsí. Eso podría ser importante, en cuanto al rendimiento.

    Eso es algo a considerar, y claramente requiere pruebas. Si las opciones SQLCLR no proporcionan ningún beneficio sobre el incorporado HASHBYTES, eso agrega peso a la sugerencia de Solomon de capturar hashes existentes (para al menos las tablas más grandes) en tablas relacionadas.

Joe Obbish
fuente
6

Probablemente pueda mejorar el rendimiento y quizás la escalabilidad de todos los enfoques .NET al agrupar y almacenar en caché cualquier objeto creado en la llamada a la función. EG para el código de Paul White arriba:

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 desalienta e intenta evitar el uso de variables estáticas / compartidas, pero le permitirá usar variables compartidas si las marca como de solo lectura. Lo cual, por supuesto, no tiene sentido, ya que puede asignar una sola instancia de algún tipo mutable, como ConcurrentDictionary.

David Browne - Microsoft
fuente
interesante ... ¿es seguro este hilo si usa la misma instancia una y otra vez? Sé que los hashes administrados tienen un Clear()método, pero no he investigado tanto en Spooky.
Solomon Rutzky
@PaulWhite y David. Podría haber hecho algo mal, o podría haber una diferencia entreSHA256Managed y SpookyHashV2, pero intenté esto y no vi mucha mejora, si es que hubo alguna, en el rendimiento. También noté que el ManagedThreadIdvalor es el mismo para todas las referencias SQLCLR en una consulta particular. Probé varias referencias a la misma función, así como una referencia a una función diferente, a los 3 se les dieron valores de entrada diferentes y devolvieron valores de retorno diferentes (pero esperados). ¿No aumentaría esto las posibilidades de una condición de carrera? Para ser justos, en mi prueba no vi ninguno.
Solomon Rutzky