O que é uma maneira escalável de simular HASHBYTES usando uma função escalar SQL CLR?

29

Como parte de nosso processo ETL, comparamos as linhas da preparação no banco de dados de relatórios para descobrir se alguma das colunas realmente mudou desde que os dados foram carregados pela última vez.

A comparação é baseada na chave exclusiva da tabela e em algum tipo de hash de todas as outras colunas. Atualmente, usamos HASHBYTESo SHA2_256algoritmo e descobrimos que ele não é dimensionado em servidores grandes se todos os threads de trabalho simultâneos estiverem chamando HASHBYTES.

A taxa de transferência medida em hashes por segundo não aumenta os 16 threads simultâneos ao testar em um servidor de 96 núcleos. Testo alterando o número de MAXDOP 8consultas simultâneas de 1 a 12. Os testes com MAXDOP 1mostraram o mesmo gargalo de escalabilidade.

Como solução alternativa, quero tentar uma solução SQL CLR. Aqui está minha tentativa de declarar os requisitos:

  • A função deve poder participar de consultas paralelas
  • A função deve ser determinística
  • A função deve receber uma entrada de uma NVARCHARou VARBINARYsequência (todas as colunas relevantes são concatenadas juntas)
  • O tamanho de entrada típico da sequência terá 100 - 20000 caracteres. 20000 não é um máximo
  • A chance de uma colisão de hash deve ser aproximadamente igual ou melhor que o algoritmo MD5. CHECKSUMnão funciona para nós porque há muitas colisões.
  • A função deve ser bem dimensionada em servidores grandes (a taxa de transferência por thread não deve diminuir significativamente à medida que o número de threads aumenta)

Por motivos de aplicação ™, suponha que eu não possa economizar o valor do hash para a tabela de relatórios. É um CCI que não suporta gatilhos ou colunas computadas (também existem outros problemas nos quais não quero entrar).

O que é uma maneira escalável de simular o HASHBYTESuso de uma função SQL CLR? Meu objetivo pode ser expresso como obter o maior número possível de hashes por segundo em um servidor grande, para que o desempenho também seja importante. Sou péssimo com o CLR, então não sei como fazer isso. Se isso motiva alguém a responder, pretendo adicionar uma recompensa a essa pergunta assim que possível. Abaixo está um exemplo de consulta que ilustra muito aproximadamente o 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 um pouco as coisas, provavelmente usarei algo como o seguinte para fazer comparações. Vou postar resultados com HASHBYTESna segunda-feira:

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
fonte

Respostas:

18

Como você está apenas procurando alterações, não precisa de uma função de hash criptográfico.

Você pode escolher entre um dos hashes não criptográficos mais rápidos da biblioteca Data.HashFunction de código aberto da Brandon Dahler, licenciada sob a licença MIT permissiva e aprovada pela OSI . SpookyHashé uma escolha popular.

Implementação de exemplo

Código fonte

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

A fonte fornece duas funções, uma para entradas de 8000 bytes ou menos, e uma versão LOB. A versão não LOB deve ser significativamente mais rápida.

Você pode envolver um binário LOB COMPRESSpara obtê-lo abaixo do limite de 8000 bytes, se isso resultar em desempenho. Como alternativa, você pode dividir o LOB em segmentos abaixo de 8000 bytes ou simplesmente reservar o uso HASHBYTESpara o caso LOB (uma vez que entradas mais longas escalam melhor).

Código pré-construído

Obviamente, você pode pegar o pacote e compilar tudo, mas eu construí os conjuntos abaixo para facilitar o teste rápido:

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

Funções 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

Um exemplo de uso dado os dados de amostra na pergunta:

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

Ao usar a versão LOB, o primeiro parâmetro deve ser convertido ou convertido em varbinary(max).

Plano de execução

plano


Safe Spooky

A biblioteca Data.HashFunction usa vários recursos de linguagem CLR que são considerados UNSAFEpelo SQL Server. É possível escrever um Spooky Hash básico compatível com o SAFEstatus. Um exemplo que eu escrevi baseado em Jon Hanna SpookilySharp está abaixo:

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

Paul White diz que a GoFundMonica
fonte
16

Não tenho certeza se o paralelismo será algum / significativamente melhor com o SQLCLR. No entanto, é realmente fácil testar, pois existe uma função hash na versão Free da biblioteca SQL # SQLCLR (que eu escrevi) chamada Util_HashBinary . Os algoritmos suportados são: MD5, SHA1, SHA256, SHA384 e SHA512.

Ele usa um VARBINARY(MAX)valor como entrada, portanto, você pode concatenar a versão da string de cada campo (como está fazendo atualmente) e depois converter para VARBINARY(MAX), ou pode ir diretamente VARBINARYpara cada coluna e concatenar os valores convertidos (isso pode ser mais rápido, pois você não está lidando com strings ou com a conversão extra de string para VARBINARY). Abaixo está um exemplo mostrando essas duas opções. Ele também mostra a HASHBYTESfunção para que você possa ver que os valores são os mesmos entre ela e o SQL # .Util_HashBinary .

Observe que os resultados do hash ao concatenar os VARBINARYvalores não corresponderão aos resultados do hash ao concatenar os NVARCHARvalores. Isso ocorre porque a forma binária do INTvalor "1" é 0x00000001, enquanto a forma UTF-16LE (ie NVARCHAR) do INTvalor de "1" (na forma binária, pois é nessa função que uma função de hash operará) é 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;

Você pode testar algo mais comparável ao Spooky não 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 o algoritmo SHA256 gerenciado incorporado ao .NET e não deve estar usando a biblioteca "bcrypt".

Além desse aspecto da pergunta, há algumas idéias adicionais que podem ajudar nesse processo:

Pensamento adicional nº 1 (pré-calcule os hashes, pelo menos alguns)

Você mencionou algumas coisas:

  1. comparamos as linhas da preparação em relação ao banco de dados de relatórios para descobrir se alguma das colunas realmente mudou desde que os dados foram carregados pela última vez.

    e:

  2. Não consigo salvar o valor do hash da tabela de relatórios. É um CCI que não suporta gatilhos ou colunas computadas

    e:

  3. as tabelas podem ser atualizadas fora do processo ETL

Parece que os dados nesta tabela de relatórios são estáveis ​​por um período de tempo e são modificados apenas por esse processo ETL.

Se nada mais modificar esta tabela, realmente não precisamos de uma exibição acionada ou indexada (afinal, eu pensei que você poderia).

Como você não pode modificar o esquema da tabela de relatórios, seria pelo menos possível criar uma tabela relacionada para conter o hash pré-calculado (e a hora UTC de quando foi calculada)? Isso permitiria que você tivesse um valor pré-calculado para comparar com a próxima vez, deixando apenas o valor recebido que requer o cálculo do hash de. Isso reduziria o número de chamadas para uma HASHBYTESou SQL#.Util_HashBinarypara metade. Você simplesmente se juntaria a esta tabela de hashes durante o processo de importação.

Você também criaria um procedimento armazenado separado que simplesmente atualiza os hashes desta tabela. Ele apenas atualiza os hashes de qualquer linha relacionada que foi alterada para atual e atualiza o carimbo de data e hora para essas linhas modificadas. Este processo pode / deve ser executado no final de qualquer outro processo que atualize esta tabela. Também pode ser agendado para execução de 30 a 60 minutos antes do início desse ETL (dependendo de quanto tempo leva para ser executado e quando algum desses outros processos pode ser executado). Pode até ser executado manualmente, se você suspeitar que possa haver linhas fora de sincronia.

Foi então observado que:

existem mais de 500 mesas

O fato de muitas tabelas dificultarem a existência de uma tabela extra para cada um para conter os hashes atuais, mas isso não é impossível, pois poderia ser script, pois seria um esquema padrão. O script precisaria considerar apenas o nome da tabela de origem e a descoberta das colunas PK da tabela de origem.

Ainda assim, independentemente de qual algoritmo de hash em última análise, prova ser o mais escalável, eu ainda altamente recomendar encontrar pelo menos algumas mesas (talvez há alguns que são muito maiores do que o resto das 500 mesas) ea criação de uma tabela relacionada à captura hashes atuais para que os valores "atuais" possam ser conhecidos antes do processo ETL. Mesmo a função mais rápida não pode ser executada sem nunca precisar chamá-la ;-).

Pensamento adicional # 2 (em VARBINARYvez de NVARCHAR)

Independentemente do SQLCLR vs interno HASHBYTES, eu ainda recomendaria a conversão direta para, VARBINARYpois isso deve ser mais rápido. Concatenar seqüências de caracteres não é muito eficiente. E isso além de converter valores não-string em strings em primeiro lugar, o que exige um esforço extra (suponho que a quantidade de esforço varie com base no tipo base: DATETIMEexigindo mais do que BIGINT), enquanto a conversão para VARBINARYsimplesmente fornece o valor subjacente (na maioria dos casos).

E, de fato, testando o mesmo conjunto de dados usado pelos outros testes e usando HASHBYTES(N'SHA2_256',...), mostrou um aumento de 23,415% no total de hashes calculados em um minuto. E esse aumento foi para fazer nada mais do que usar em VARBINARYvez de NVARCHAR! 😸 (consulte a resposta do wiki da comunidade para obter detalhes)

Pensamento adicional nº 3 (esteja atento aos parâmetros de entrada)

Testes adicionais mostraram que uma área que afeta o desempenho (sobre esse volume de execuções) são os parâmetros de entrada: quantos e quais tipos.

A função Util_HashBinary SQLCLR que está atualmente na minha biblioteca SQL # possui dois parâmetros de entrada: um VARBINARY(o valor para o hash) e outro NVARCHAR(o algoritmo a ser usado). Isto é devido ao meu espelhamento da assinatura da HASHBYTESfunção. No entanto, descobri que, se eu removesse o NVARCHARparâmetro e criasse uma função que apenas executasse o SHA256, o desempenho melhoraria bastante. Suponho que mesmo mudar o NVARCHARparâmetro para INTteria ajudado, mas também presumo que nem mesmo ter o INTparâmetro extra seja pelo menos um pouco mais rápido.

Além disso, SqlBytes.Valuepode ter um desempenho melhor que SqlBinary.Value.

Criei duas novas funções: Util_HashSHA256Binary e Util_HashSHA256Binary8k para este teste. Eles serão incluídos na próxima versão do SQL # (ainda não há data definida para isso).

Também descobri que a metodologia de teste poderia ser ligeiramente aprimorada, então atualizei o equipamento de teste na resposta do wiki da comunidade abaixo para incluir:

  1. pré-carregamento dos conjuntos SQLCLR para garantir que a sobrecarga do tempo de carregamento não incline os resultados.
  2. um procedimento de verificação para verificar colisões. Se houver alguma, ela exibe o número de linhas exclusivas / distintas e o número total de linhas. Isso permite determinar se o número de colisões (se houver) está além do limite para o caso de uso especificado. Alguns casos de uso podem permitir um pequeno número de colisões, outros podem não exigir nenhum. Uma função super rápida é inútil se não conseguir detectar alterações no nível de precisão desejado. Por exemplo, usando o equipamento de teste fornecido pelo OP, aumentei a contagem de linhas para 100k linhas (era originalmente 10k) e descobri que CHECKSUMregistrei mais de 9k colisões, o que é 9% (caramba).

Pensamento adicional nº 4 ( HASHBYTES+ SQLCLR juntos?)

Dependendo de onde está o gargalo, pode até ajudar a usar uma combinação de HASHBYTESUDF integrado e de SQLCLR para fazer o mesmo hash. Se as funções internas forem restritas de maneira diferente / separada das operações do SQLCLR, essa abordagem poderá realizar mais simultaneamente do que qualquer um HASHBYTESou o SQLCLR individualmente. Definitivamente vale a pena testar.

Pensamento adicional nº 5 (cache de objeto de hash?)

O cache do objeto do algoritmo de hash, conforme sugerido na resposta de David Browne, certamente parece interessante, então tentei e encontrei os dois pontos de interesse a seguir:

  1. Por qualquer motivo, não parece oferecer muito, se houver, melhoria no desempenho. Eu poderia ter feito algo incorretamente, mas aqui está o que eu tentei:

    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. O ManagedThreadIdvalor parece ser o mesmo para todas as referências SQLCLR em uma consulta específica. Testei várias referências à mesma função, bem como uma referência a uma função diferente, sendo que todas as três receberam valores de entrada diferentes e retornaram valores de retorno diferentes (mas esperados). Para ambas as funções de teste, a saída era uma sequência que incluía a representação ManagedThreadId, bem como uma representação, do resultado do hash. O ManagedThreadIdvalor era o mesmo para todas as referências UDF na consulta e em todas as linhas. Mas, o resultado do hash foi o mesmo para a mesma cadeia de entrada e diferente para diferentes cadeias de entrada.

    Embora eu não tenha visto resultados errôneos nos meus testes, isso não aumentaria as chances de uma condição de corrida? Se a chave do dicionário for a mesma para todos os objetos SQLCLR chamados em uma consulta específica, eles compartilharão o mesmo valor ou objeto armazenado para essa chave, certo? O ponto é que, mesmo que parecesse funcionar aqui (até certo ponto, novamente não parecia haver muito ganho de desempenho, mas funcionalmente nada quebrou), isso não me dá confiança de que essa abordagem funcione em outros cenários.

Solomon Rutzky
fonte
11

Esta não é uma resposta tradicional, mas achei que seria útil publicar referências de algumas das técnicas mencionadas até agora. Estou testando em um servidor de 96 núcleos com o SQL Server 2017 CU9.

Muitos problemas de escalabilidade são causados ​​por encadeamentos simultâneos disputando algum estado global. Por exemplo, considere a disputa clássica da página do PFS. Isso pode acontecer se muitos threads de trabalho precisarem modificar a mesma página na memória. À medida que o código se torna mais eficiente, ele pode solicitar a trava mais rapidamente. Isso aumenta a disputa. Simplificando, é mais provável que um código eficiente leve a problemas de escalabilidade, porque o estado global é disputado mais severamente. É menos provável que o código lento cause problemas de escalabilidade porque o estado global não é acessado com tanta frequência.

HASHBYTESA escalabilidade é parcialmente baseada no comprimento da sequência de entrada. Minha teoria foi por que isso ocorre é que o acesso a algum estado global é necessário quando a HASHBYTESfunção é chamada. O estado global fácil de observar é que uma página de memória precisa ser alocada por chamada em algumas versões do SQL Server. O mais difícil de observar é que há algum tipo de contenção no SO. Como resultado, se HASHBYTESfor chamado pelo código com menos frequência, a contenção diminuirá. Uma maneira de reduzir a taxa de HASHBYTESchamadas é aumentar a quantidade de trabalho de hash necessário por chamada. O trabalho de hash é parcialmente baseado no comprimento da sequência de entrada. Para reproduzir o problema de escalabilidade que vi no aplicativo, precisava alterar os dados da demonstração. Um cenário de pior caso razoável é uma tabela com 21BIGINTcolunas. A definição da tabela está incluída no código na parte inferior. Para reduzir o Local Factors ™, estou usando MAXDOP 1consultas simultâneas que operam em tabelas relativamente pequenas. Meu código de referência rápida está na parte inferior.

Observe que as funções retornam diferentes comprimentos de hash. MD5e SpookyHashambos são hashes de 128 bits, SHA256é um hash de 256 bits.

RESULTADOS ( NVARCHARvs VARBINARYconversão e concatenação)

Para verificar se a conversão e concatenação VARBINARYé realmente mais eficiente / com desempenho do que NVARCHARuma NVARCHARversão do RUN_HASHBYTES_SHA2_256procedimento armazenado foi criada a partir do mesmo modelo (consulte a "Etapa 5" na seção CÓDIGO DE BENCHMARKING abaixo). As únicas diferenças são:

  1. O nome do Procedimento Armazenado termina em _NVC
  2. BINARY(8)para a CASTfunção foi alterada para serNVARCHAR(15)
  3. 0x7C foi alterado para ser N'|'

Resultando em:

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

ao invés de:

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

A tabela abaixo contém o número de hashes realizados em 1 minuto. Os testes foram realizados em um servidor diferente do usado nos outros testes mencionados abaixo.

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

Observando apenas as médias, podemos calcular o benefício de mudar para VARBINARY:

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

Isso retorna:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

RESULTADOS (algoritmos de hash e implementações)

A tabela abaixo contém o número de hashes realizados em 1 minuto. Por exemplo, o uso CHECKSUMcom 84 consultas simultâneas resultou em mais de 2 bilhões de hashes antes do tempo acabar.

╔════════════════════╦════════════╦════════════╦════════════╗
      Function       12 threads  48 threads  84 threads 
╠════════════════════╬════════════╬════════════╬════════════╣
 CHECKSUM             281250000  1122440000  2040100000 
 HASHBYTES MD5         75940000   106190000   112750000 
 HASHBYTES SHA2_256    80210000   117080000   124790000 
 CLR Spooky           131250000   505700000   786150000 
 CLR SpookyLOB         17420000    27160000    31380000 
 SQL# MD5              17080000    26450000    29080000 
 SQL# SHA2_256         18370000    28860000    32590000 
 SQL# MD5 8k           24440000    30560000    32550000 
 SQL# SHA2_256 8k      87240000   159310000   155760000 
╚════════════════════╩════════════╩════════════╩════════════╝

Se você preferir ver os mesmos números medidos em termos de trabalho por segundo 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 
╚════════════════════╩════════════════════════════╩════════════════════════════╩════════════════════════════╝

Algumas reflexões rápidas sobre todos os métodos:

  • CHECKSUM: muito boa escalabilidade conforme o esperado
  • HASHBYTES: os problemas de escalabilidade incluem uma alocação de memória por chamada e uma grande quantidade de CPU gasta no sistema operacional
  • Spooky: surpreendentemente boa escalabilidade
  • Spooky LOB: o spinlock SOS_SELIST_SIZED_SLOCKgira fora de controle. Suspeito que esse seja um problema geral com a passagem de LOBs através das funções CLR, mas não tenho certeza
  • Util_HashBinary: parece que é atingido pelo mesmo spinlock. Eu não olhei para isso até agora, porque provavelmente não há muito que eu possa fazer sobre isso:

gire sua fechadura

  • Util_HashBinary 8k: resultados muito surpreendentes, não sei o que está acontecendo aqui

Resultados finais testados em um servidor menor:

╔═════════════════════════╦════════════════════════╦════════════════════════╗
     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 REFERÊNCIA

CONFIGURAÇÃO 1: Tabelas e dados

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

CONFIGURAÇÃO 2: Processo de execução principal

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

CONFIGURAÇÃO 3: Processo de detecção de colisão

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

CONFIGURAÇÃO 4: Limpeza (DROP All Procs 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);

CONFIGURAÇÃO 5: gerar procs de teste

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

TESTE 1: Verifique se há colisões

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;

TESTE 2: Executar testes de desempenho

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;

QUESTÕES DE VALIDAÇÃO A RESOLVER

Enquanto se concentra no teste de desempenho de uma UDF SQLCLR singular, duas questões discutidas no início não foram incorporadas aos testes, mas idealmente devem ser investigadas para determinar qual abordagem atende a todos os requisitos.

  1. A função será executada duas vezes por cada consulta (uma vez para a linha de importação e outra para a linha atual). Os testes até agora referenciaram o UDF apenas uma vez nas consultas de teste. Esse fator pode não alterar a classificação das opções, mas não deve ser ignorado, apenas por precaução.
  2. Em um comentário que já foi excluído, Paul White havia mencionado:

    Uma desvantagem de substituir HASHBYTESpor uma função escalar do CLR - parece que as funções do CLR não podem usar o modo em lote, enquanto HASHBYTESpodem. Isso pode ser importante, em termos de desempenho.

    Portanto, isso é algo a considerar e requer claramente testes. Se as opções do SQLCLR não fornecerem nenhum benefício sobre o interno HASHBYTES, isso adiciona peso à sugestão de Salomão de capturar hashes existentes (pelo menos para as maiores tabelas) nas tabelas relacionadas.

Joe Obbish
fonte
6

Provavelmente, você pode melhorar o desempenho e, talvez, a escalabilidade de todas as abordagens do .NET, agrupando e armazenando em cache quaisquer objetos criados na chamada de função. EG para o código de Paul White acima:

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

O SQL CLR desencoraja e tenta impedir o uso de variáveis ​​estáticas / compartilhadas, mas permitirá que você use variáveis ​​compartilhadas se as marcar como somente leitura. O que, é claro, não tem sentido, pois você pode apenas atribuir uma única instância de algum tipo mutável, como ConcurrentDictionary.

David Browne - Microsoft
fonte
interessante ... esse thread é seguro se estiver usando a mesma instância repetidamente? Eu sei que os hashes gerenciados têm um Clear()método, mas eu não olhei tão longe no Spooky.
Solomon Rutzky
@PaulWhite e David. Eu poderia ter feito algo errado, ou poderia haver uma diferença entre SHA256Managede SpookyHashV2, mas tentei isso e não vi muito, se houver, melhoria no desempenho. Também notei que o ManagedThreadIdvalor é o mesmo para todas as referências SQLCLR em uma consulta específica. Testei várias referências à mesma função, bem como uma referência a uma função diferente, sendo que todas as três receberam valores de entrada diferentes e retornaram valores de retorno diferentes (mas esperados). Isso não aumentaria as chances de uma condição de corrida? Para ser justo, no meu teste eu não vi nenhum.
Solomon Rutzky