Який масштабований спосіб імітувати HASHBYTES за допомогою скалярної функції SQL CLR?


29

У рамках нашого процесу ETL ми порівнюємо рядки від постановки до бази даних звітів, щоб з’ясувати, чи змінився якийсь із стовпців з моменту останнього завантаження даних.

Порівняння ґрунтується на унікальному ключі таблиці та певному хеші всіх інших стовпців. В даний час ми використовуємо HASHBYTESз SHA2_256алгоритмом і виявили , що він не масштабується на великих серверах , якщо кількість одночасних робочих потоків все покликання HASHBYTES.

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

Як вирішення, я хочу спробувати CLR-рішення SQL. Ось моя спроба викласти вимоги:

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

У програмі "Причини застосування" припустимо, що я не можу зберегти значення хеша для таблиці звітів. Це 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 від Брендона Далера, що має ліцензію згідно з дозволеною ліцензією MIT, затвердженою OSI . 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-версія повинна бути значно швидшою.

Можливо, ви зможете ввести бінарний код 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


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, тоді як UTF-16LE (тобто NVARCHAR) форма 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;

Ви можете протестувати щось більш порівнянне з Spooky, яке не є LOB, використовуючи:

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. процедура перевірки для перевірки на зіткнення. Якщо такі знайдені, він відображає кількість унікальних / чітких рядків та загальну кількість рядків. Це дозволяє визначити, чи кількість зіткнень (якщо такі є) перевищує межу для даного випадку використання. Деякі випадки використання можуть спричинити невелику кількість зіткнень, інші можуть вимагати жодного. Надшвидка функція марна, якщо вона не може виявити зміни до потрібного рівня точності. Наприклад, використовуючи тестовий джгут, наданий ОП, я збільшив кількість рядків до 100 к рядків (спочатку це було 10 к) і виявив, що CHECKSUMзареєстровано понад 9 к зіткнень, що становить 9% (вихід).

Додаткова думка №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код викликається рідше, тоді суперечка знижується. Один із способів зменшити швидкість HASHBYTESдзвінків - збільшити обсяг хеш-роботи, необхідний на один дзвінок. Робота хешування частково заснована на довжині вхідного рядка. Щоб відтворити проблему масштабованості, яку я бачив у програмі, мені потрібно було змінити демонстраційні дані. Доцільним найгіршим сценарієм є таблиця з 21BIGINTстовпчики. Визначення таблиці включено до коду внизу. Для зменшення локальних факторів ™ я використовую паралельні MAXDOP 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виходить з-під контролю. Я підозрюю, що це загальна проблема з проходженням LOB через функції 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: Очищення (DROP Усі тестові програми)

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.


цікаво ... чи безпечна ця нитка, якщо вона використовує один і той же екземпляр знову і знову? Я знаю, що в керованих хешах є Clear()метод, але я не заглядав так далеко в Spooky.
Соломон Руцький

@PaulWhite та David. Я міг би зробити щось не так, або це могла бути різниця між SHA256Managedі SpookyHashV2, але я спробував це і не побачив багато, якщо таке було, покращення продуктивності. Я також помітив, що ManagedThreadIdзначення однакове для всіх посилань SQLCLR у конкретному запиті. Я перевірив кілька посилань на одну і ту ж функцію, а також посилання на іншу функцію, всі 3 отримували різні вхідні значення та повертали різні (але очікувані) значення повернення. Чи не збільшилось би це шанси на стан гонки? Якщо чесно, то в моєму тесті я не бачив жодного.
Соломон Руцький
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.