Что такое масштабируемый способ имитации хэш-битов с помощью скалярной функции SQL CLR?

29

В рамках нашего процесса ETL мы сравниваем строки от подготовки к базе данных отчетов, чтобы выяснить, действительно ли какой-либо из столбцов изменился с момента последней загрузки данных.

Сравнение основано на уникальном ключе таблицы и некотором хешировании всех остальных столбцов. В настоящее время мы используем HASHBYTESэтот SHA2_256алгоритм и обнаружили, что он не масштабируется на больших серверах, если все параллельные рабочие потоки вызывают все HASHBYTES.

Пропускная способность, измеренная в хэшах в секунду, не увеличивается за последние 16 одновременных потоков при тестировании на 96-ядерном сервере. Я тестирую, меняя число одновременных MAXDOP 8запросов с 1 на 12. Тестирование с MAXDOP 1использованием одного и того же узкого места масштабируемости.

В качестве обходного пути я хочу попробовать решение SQL CLR. Вот моя попытка сформулировать требования:

  • Функция должна быть в состоянии участвовать в параллельных запросах
  • Функция должна быть детерминированной
  • Функция должна принимать ввод строки NVARCHARили VARBINARY(все соответствующие столбцы объединяются)
  • Типичный размер ввода строки будет 100 - 20000 символов. 20000 не макс
  • Вероятность коллизии хеша должна быть примерно равна или лучше, чем алгоритм MD5. CHECKSUMне работает для нас, потому что слишком много столкновений.
  • Функция должна хорошо масштабироваться на больших серверах (пропускная способность на поток не должна значительно уменьшаться при увеличении количества потоков)

Для Application Reasons ™ предположим, что я не могу сохранить значение хэша для таблицы отчетов. Это CCI, который не поддерживает триггеры или вычисляемые столбцы (есть и другие проблемы, в которые я не хочу вдаваться).

Что такое масштабируемый способ моделирования HASHBYTESс использованием функции SQL CLR? Моя цель может быть выражена в получении как можно большего числа хэшей в секунду на большом сервере, поэтому производительность также имеет значение. Я ужасен с CLR, поэтому я не знаю, как этого добиться. Если это побудит кого-либо ответить, я планирую добавить вознаграждение к этому вопросу, как только смогу. Ниже приведен пример запроса, который очень приблизительно иллюстрирует вариант использования:

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

Чтобы немного упростить ситуацию, я, вероятно, буду использовать что-то вроде следующего для бенчмаркинга. Я опубликую результаты HASHBYTESв понедельник:

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);
Джо Оббиш
источник

Ответы:

18

Поскольку вы просто ищете изменения, вам не нужна криптографическая хеш-функция.

Вы можете выбрать один из более быстрых некриптографических хэшей в библиотеке Data.HashFunction с открытым исходным кодом от Брэндона Далера, лицензированной по разрешающей и одобренной OSI лицензии MIT . SpookyHashэто популярный выбор.

Пример реализации

Исходный код

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

Источник предоставляет две функции: одну для входных данных объемом 8000 байт или меньше и версию LOB. Версия без LOB должна быть значительно быстрее.

Возможно, вы сможете обернуть двоичный файл большого объекта, COMPRESSчтобы получить его под пределом в 8000 байт, если это окажется полезным для производительности. В качестве альтернативы, вы можете разбить LOB на сегменты размером менее 8000 байтов или просто зарезервировать использование HASHBYTESдля случая LOB (поскольку более длинные входы масштабируются лучше).

Готовый код

Очевидно, что вы можете взять пакет для себя и скомпилировать все, но я собрал сборки ниже, чтобы облегчить быстрое тестирование:

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

Функции 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

использование

Пример использования приведен для примера данных в вопросе:

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

При использовании LOB-версии первый параметр должен быть приведен или преобразован в varbinary(max).

План выполнения

план


Сейф Жуткий

Библиотека Data.HashFunction использует ряд функций языка CLR, которые рассматриваются UNSAFESQL Server. Можно написать основной Spooky Hash, совместимый со SAFEстатусом. Пример, который я написал на основе SpookilySharp Джона Ханны ниже:

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

Пол Уайт говорит, что GoFundMonica
источник
16

Я не уверен, будет ли параллелизм хоть немного / значительно лучше с SQLCLR. Однако это действительно легко протестировать, поскольку в бесплатной версии библиотеки SQL # SQLCLR (которую я написал) есть хеш-функция Util_HashBinary . Поддерживаются следующие алгоритмы: MD5, SHA1, SHA256, SHA384 и SHA512.

Он принимает VARBINARY(MAX)значение в качестве входного, поэтому вы можете либо объединить строковую версию каждого поля (как вы это делаете в настоящее время), а затем преобразовать в VARBINARY(MAX), или вы можете перейти непосредственно к VARBINARYкаждому столбцу и объединить преобразованные значения (это может быть быстрее, так как Вы не имеете дело со строками или дополнительным преобразованием из строки в VARBINARY). Ниже приведен пример, показывающий обе эти опции. Также показана HASHBYTESфункция, чтобы вы могли видеть, что значения одинаковы между ней и SQL # .Util_HashBinary .

Обратите внимание, что результаты хеширования при объединении VARBINARYзначений не будут совпадать с результатами хеширования при объединении NVARCHARзначений. Это связано с тем, что двоичная форма INTзначения «1» равна 0x00000001, в то время как NVARCHARформа UTF-16LE (т. Е. ) INTЗначения «1» (в двоичной форме, поскольку именно для этого будет работать хэш-функция) равна 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;

Вы можете протестировать что-то более сопоставимое с не-LOB Spooky, используя:

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

Примечание. Util_HashBinary использует управляемый алгоритм SHA256, встроенный в .NET, и не должен использовать библиотеку «bcrypt».

Помимо этого аспекта вопроса, есть некоторые дополнительные мысли, которые могут помочь этому процессу:

Дополнительная мысль № 1 (предварительно рассчитайте хэши, хотя бы некоторые)

Вы упомянули несколько вещей:

  1. мы сравниваем строки от подготовки к базе данных отчетов, чтобы выяснить, изменился ли какой-либо из столбцов с момента последней загрузки данных.

    а также:

  2. Я не могу сохранить значение хэша для таблицы отчетов. Это CCI, который не поддерживает триггеры или вычисляемые столбцы

    а также:

  3. таблицы могут быть обновлены вне процесса ETL

Похоже, что данные в этой таблице отчетов стабильны в течение определенного периода времени и изменяются только этим процессом ETL.

Если ничто иное не изменяет эту таблицу, тогда нам действительно не нужен триггер или индексированное представление (я изначально думал, что вы можете).

Поскольку вы не можете изменить схему таблицы отчетов, возможно ли будет по крайней мере создать связанную таблицу, содержащую предварительно вычисленный хэш (и время UTC, когда она была рассчитана)? Это позволит вам предварительно рассчитать значение для сравнения со следующим разом, оставив только входящее значение, которое требует вычисления хеша. Это уменьшит количество звонков до половины HASHBYTESили SQL#.Util_HashBinaryнаполовину. Вы просто присоединитесь к этой таблице хэшей в процессе импорта.

Вы также создали бы отдельную хранимую процедуру, которая просто обновляет хэши этой таблицы. Он просто обновляет хэши любой связанной строки, которая изменилась на текущую, и обновляет временную метку для этих измененных строк. Этот процесс может / должен быть выполнен в конце любого другого процесса, который обновляет эту таблицу. Можно также запланировать запуск за 30–60 минут до запуска ETL (в зависимости от того, сколько времени потребуется для выполнения и когда может выполняться любой из этих других процессов). Это может даже быть выполнено вручную, если вы когда-либо подозреваете, что могут быть строки, которые не синхронизированы.

Затем было отмечено, что:

более 500 столов

Из-за того, что многие таблицы действительно усложняют создание дополнительной таблицы для каждой из них, чтобы содержать текущие хэши, но это не невозможно, так как это может быть записано в сценарии, поскольку это будет стандартная схема. Сценарии просто должны учитывать имя исходной таблицы и обнаружение столбцов столбцов PK исходной таблицы.

Тем не менее, независимо от того, какой алгоритм хеширования в конечном итоге окажется наиболее масштабируемым, я все равно настоятельно рекомендую найти хотя бы несколько таблиц (возможно, некоторые из них НАМНОГО больше, чем остальные из 500 таблиц) и настроить связанную таблицу для захвата текущие хэши, так что «текущие» значения могут быть известны до процесса ETL. Даже самая быстрая функция не может обойтись, даже не вызывая ее ;-).

Дополнительная мысль № 2 ( VARBINARYвместо NVARCHAR)

Независимо от встроенного SQLCLR HASHBYTES, я все равно рекомендовал бы конвертировать напрямую, VARBINARYпоскольку это должно быть быстрее. Объединение строк просто не очень эффективно. И это в дополнение к преобразованию нестроковых значений в строки в первую очередь, что требует дополнительных усилий (я предполагаю, что количество усилий варьируется в зависимости от базового типа: DATETIMEтребуется больше, чем BIGINT), тогда как преобразование в VARBINARYпросто дает вам базовое значение (в большинстве случаев).

И, фактически, тестирование того же набора данных, которое использовались и другие тесты HASHBYTES(N'SHA2_256',...), показало увеличение общего количества хэшей, вычисленных за одну минуту, на 23,415%. И это увеличение было сделано только для того, чтобы использовать VARBINARYвместо NVARCHAR! 😸 (подробности см. В вики-сообществе )

Дополнительная мысль № 3 (помните о входных параметрах)

Дальнейшее тестирование показало, что одной областью, которая влияет на производительность (в этом объеме выполнения), являются входные параметры: сколько и какой тип (типы).

Функция Util_HashBinary SQLCLR, которая в настоящее время находится в моей библиотеке SQL #, имеет два входных параметра: один VARBINARY(значение для хэша) и один NVARCHAR(используемый алгоритм). Это связано с моим отражением подписи HASHBYTESфункции. Однако я обнаружил, что если я удалил NVARCHARпараметр и создал функцию, которая выполняла только SHA256, то производительность значительно улучшилась. Я предполагаю, что даже переключение NVARCHARпараметра на INTпомогло бы, но я также предполагаю, что даже отсутствие дополнительного INTпараметра, по крайней мере, немного быстрее.

Кроме того, SqlBytes.Valueможет работать лучше, чем SqlBinary.Value.

Я создал две новые функции: Util_HashSHA256Binary и Util_HashSHA256Binary8k для этого тестирования. Они будут включены в следующий выпуск SQL # (дата для этого пока не установлена).

Я также обнаружил, что методология тестирования может быть немного улучшена, поэтому я обновил тестовую систему в приведенном ниже ответе вики сообщества:

  1. предварительная загрузка сборок SQLCLR, чтобы гарантировать, что накладные расходы времени загрузки не искажают результаты.
  2. процедура проверки на наличие столкновений. Если они найдены, отображается количество уникальных / отличных строк и общее количество строк. Это позволяет определить, превышает ли количество коллизий (если они есть) предел для данного варианта использования. Некоторые варианты использования могут допускать небольшое количество столкновений, другие могут не требовать их. Сверхбыстрая функция бесполезна, если она не может обнаружить изменения до желаемого уровня точности. Например, используя тестовый жгут, предоставленный OP, я увеличил количество строк до 100 тыс. Строк (изначально оно было 10 тыс.) И обнаружил, что CHECKSUMзарегистрировано более 9 тыс. Столкновений, что составляет 9% (yikes).

Дополнительная мысль № 4 ( HASHBYTES+ SQLCLR вместе?)

В зависимости от того, где находится узкое место, это может даже помочь использовать комбинацию встроенного HASHBYTESи SQLCLR UDF для того же хеша. Если встроенные функции ограничены по-другому / отдельно от операций SQLCLR, то этот подход может быть в состоянии выполнить более одновременно, чем один HASHBYTESили SQLCLR по отдельности. Это определенно стоит проверить.

Дополнительная мысль № 5 (кеширование объектов хэширования?)

Кэширование объекта алгоритма хеширования, как было предложено в ответе Дэвида Брауна, безусловно, кажется интересным, поэтому я попробовал его и обнаружил следующие два момента:

  1. По какой-то причине он, по-видимому, не обеспечивает значительного улучшения производительности. Я мог бы сделать что-то неправильно, но вот что я попробовал:

    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. ManagedThreadIdЗначение представляется одинаковой для всех ссылок SQLCLR в конкретном запросе. Я протестировал несколько ссылок на одну и ту же функцию, а также ссылку на другую функцию, причем все 3 получили разные входные значения и возвращали разные (но ожидаемые) возвращаемые значения. Для обеих тестовых функций выводом была строка, включающая в ManagedThreadIdсебя как строковое представление результата хеширования. Это ManagedThreadIdзначение было одинаковым для всех ссылок UDF в запросе и для всех строк. Но результат хеширования был одинаковым для одной входной строки и разным для разных входных строк.

    Хотя я не видел ошибочных результатов в своем тестировании, не увеличит ли это шансы на состояние гонки? Если ключ словаря одинаков для всех объектов SQLCLR, вызываемых в конкретном запросе, тогда они будут использовать одно и то же значение или объект, сохраненный для этого ключа, верно? Дело в том, что даже мысль о том, что она работает здесь (в некоторой степени, опять же, похоже, не было большого прироста производительности, но функционально ничего не сломалось), не дает мне уверенности, что этот подход будет работать в других сценариях.

Соломон Руцкий
источник
11

Это не традиционный ответ, но я подумал, что было бы полезно опубликовать тесты некоторых методов, упомянутых до сих пор. Я тестирую на 96-ядерном сервере с SQL Server 2017 CU9.

Многие проблемы с масштабируемостью вызваны одновременными потоками, конкурирующими в каком-либо глобальном состоянии Например, рассмотрим классический конфликт страниц PFS. Это может произойти, если слишком много рабочих потоков должны изменить одну и ту же страницу в памяти. Поскольку код становится более эффективным, он может запросить блокировку быстрее. Это увеличивает раздор. Проще говоря, эффективный код с большей вероятностью приведет к проблемам с масштабируемостью, поскольку глобальное состояние более жесткое. Медленный код с меньшей вероятностью вызовет проблемы с масштабируемостью, поскольку к глобальному состоянию не обращаются так часто.

HASHBYTESмасштабируемость частично зависит от длины входной строки. Моя теория состояла в том, почему это происходит, что при HASHBYTESвызове функции необходим доступ к некоторому глобальному состоянию . Простое глобальное состояние, которое нужно наблюдать, - это то, что страница памяти должна быть выделена на вызов в некоторых версиях SQL Server. Труднее заметить, что существует какая-то конкуренция ОС. В результате, если HASHBYTESкод вызывается реже, конкуренция снижается. Один из способов снизить скорость столбцов. Определение таблицы включено в код внизу. Чтобы уменьшить Local Factors ™, я использую параллельные запросы, которые работают с относительно небольшими таблицами. Мой быстрый тестовый код находится внизу.HASHBYTES вызовов является увеличение объема хэширования, необходимого для одного вызова. Хеширование частично зависит от длины входной строки. Чтобы воспроизвести проблему масштабируемости, которую я увидел в приложении, мне нужно было изменить демонстрационные данные. Разумный сценарий наихудшего случая - таблица с 21BIGINTMAXDOP 1

Обратите внимание, что функции возвращают разные длины хеша. MD5и SpookyHashоба 128-битных хэша, SHA256это 256-битный хэш.

РЕЗУЛЬТАТЫ ( NVARCHARпротив VARBINARYпреобразования и объединения)

Для того , чтобы увидеть , если преобразование в и конкатенации, VARBINARYдействительно более эффективным / производительный , чем NVARCHAR, NVARCHARверсия RUN_HASHBYTES_SHA2_256хранимой процедуры была создана из того же шаблона (см «Шаг 5» в СРАВНИТЕЛЬНОЙ КОДЕ разделе ниже). Единственные различия:

  1. Имя хранимой процедуры заканчивается на _NVC
  2. BINARY(8)для CASTфункции была изменена наNVARCHAR(15)
  3. 0x7C был изменен, чтобы быть N'|'

В результате чего:

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

вместо того:

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

Таблица ниже содержит количество хэшей, выполненных за 1 минуту. Тесты проводились на другом сервере, чем использовались для других тестов, указанных ниже.

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

Рассматривая только средние значения, мы можем рассчитать пользу от перехода на VARBINARY:

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

Это возвращает:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

РЕЗУЛЬТАТЫ (хеш-алгоритмы и реализации)

Таблица ниже содержит количество хэшей, выполненных за 1 минуту. Например, использование CHECKSUM84 одновременных запросов привело к выполнению более 2 миллиардов хэшей до истечения времени.

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

Если вы предпочитаете видеть одни и те же числа, измеренные в единицах работы в секунду потока:

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

Несколько быстрых мыслей обо всех методах:

  • CHECKSUM: очень хорошая масштабируемость, как и ожидалось
  • HASHBYTES: проблемы масштабируемости включают в себя одно выделение памяти на вызов и большое количество процессоров, затрачиваемых в ОС
  • Spooky: удивительно хорошая масштабируемость
  • Spooky LOB: спин-блокировка SOS_SELIST_SIZED_SLOCKвыходит из-под контроля. Я подозреваю, что это общая проблема с передачей больших объектов через функции CLR, но я не уверен
  • Util_HashBinary: похоже, что его ударил тот же спинлок. Я не изучал это до сих пор, потому что, вероятно, я мало что могу с этим поделать:

закрутить замок

  • Util_HashBinary 8k: очень удивительные результаты, не уверен, что здесь происходит

Окончательные результаты проверены на меньшем сервере:

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

ЭТАЛОННЫЙ КОД

НАСТРОЙКА 1: таблицы и данные

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

НАСТРОЙКА 2: Мастер выполнения

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

НАСТРОЙКА 3: Процесс обнаружения столкновений

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

НАСТРОЙКА 4: Очистка (УДАЛИТЬ все тестовые процедуры)

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

НАСТРОЙКА 5: Генерация тестовых процедур

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

ТЕСТ 1: Проверка на столкновения

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;

ТЕСТ 2: Выполнить тесты производительности

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;

ВОПРОСЫ ВАЛИДАЦИИ ДЛЯ РАЗРЕШЕНИЯ

Сосредоточив внимание на тестировании производительности единственного UDF SQLCLR, две проблемы, которые обсуждались на ранних этапах, не были включены в тесты, но в идеале их следует изучить, чтобы определить, какой подход отвечает всем требованиям.

  1. Функция будет выполняться дважды для каждого запроса (один раз для строки импорта и один раз для текущей строки). До сих пор тесты ссылались на UDF только один раз в тестовых запросах. Этот фактор может не изменить рейтинг опций, но его не следует игнорировать, на всякий случай.
  2. В комментарии, который с тех пор был удален, Пол Уайт упомянул:

    Один недостаток замены HASHBYTESскалярной функцией CLR - кажется, что функции CLR не могут использовать пакетный режим, тогда как HASHBYTESмогут. Это может быть важно с точки зрения производительности.

    Так что это то, что нужно учитывать, и, безусловно, требует тестирования. Если параметры SQLCLR не дают каких-либо преимуществ по сравнению со встроенными HASHBYTES, то это добавляет веса предложению Соломона о захвате существующих хэшей (по крайней мере для самых больших таблиц) в связанные таблицы.

Джо Оббиш
источник
6

Вероятно, вы можете улучшить производительность и, возможно, масштабируемость всех подходов .NET путем объединения и кэширования любых объектов, созданных в вызове функции. Например, код Пола Уайта выше:

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 препятствует и пытается предотвратить использование статических / общих переменных, но позволит вам использовать общие переменные, если вы пометите их как доступные только для чтения. Что, конечно, бессмысленно, так как вы можете просто назначить один экземпляр некоторого изменяемого типа, например ConcurrentDictionary.

Дэвид Браун - Microsoft
источник
интересно ... безопасен ли этот поток, если он использует один и тот же экземпляр снова и снова? Я знаю, что у управляемых хэшей есть Clear()метод, но я не заглядывал так далеко в Spooky.
Соломон Руцкий
@PaulWhite и Дэвид. Я мог бы сделать что-то не так, или это могло бы быть различием между SHA256Managedи SpookyHashV2, но я попробовал это и не видел большого, если вообще, улучшения производительности. Я также заметил, что ManagedThreadIdзначение одинаково для всех ссылок SQLCLR в конкретном запросе. Я протестировал несколько ссылок на одну и ту же функцию, а также ссылку на другую функцию, причем все 3 получили разные входные значения и возвращали разные (но ожидаемые) возвращаемые значения. Не увеличит ли это шансы на состояние гонки? Если честно, в моем тесте я не видел ни одного.
Соломон Руцкий