使用SQL CLR标量函数模拟HASHBYTES的可伸缩方法是什么?


29

作为ETL流程的一部分,我们将暂存中的行与报表数据库进行比较,以找出自从上次加载数据以来是否实际更改了任何列。

比较是基于表的唯一键和所有其他列的某种哈希值。我们目前使用HASHBYTESSHA2_256算法,并且发现如果许多并发工作线程都在调用,则该算法无法在大型服务器上扩展HASHBYTES

在96台核心服务器上进行测试时,以每秒哈希数衡量的吞吐量不会增加超过16个并发线程。我通过将并发MAXDOP 8查询数从1 更改为12进行测试。测试MAXDOP 1显示了相同的可伸缩性瓶颈。

作为一种解决方法,我想尝试一个SQL CLR解决方案。这是我尝试陈述要求的尝试:

  • 该功能必须能够参与并行查询
  • 函数必须是确定性的
  • 该函数必须接受NVARCHARVARBINARY字符串的输入(所有相关列都串联在一起)
  • 字符串的典型输入大小为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);

Answers:


18

由于您只是在寻找更改,因此不需要加密哈希函数。

您可以从Brandon Dahler 的开放源代码Data.HashFunction库中选择一种较快的非加密哈希值,该哈希值已获得MITOS的许可并获得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个字节以下的段,或者仅保留HASHBYTESLOB情况下的使用(因为较长的输入可以更好地扩展)。

预建代码

您显然可以自己获取软件包并编译所有内容,但是我在下面构建了程序集,以使快速测试变得更加容易:

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语言功能UNSAFE由SQL Server。可以编写与SAFE状态兼容的基本Spooky Hash 。我根据乔恩·汉纳(Jon Hanna)的SpookilySharp编写的示例如下:

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


16

我不确定使用SQLCLR并行性是否会更好/更好。但是,测试真的很容易,因为SQL# SQLCLR库的免费版本(我编写)中有一个哈希函数,称为Util_HashBinary。支持的算法为:MD5,SHA1,SHA256,SHA384和SHA512。

它以一个VARBINARY(MAX)值作为输入,因此您可以连接每个字段的字符串版本(如您当前所做的那样),然后转换为VARBINARY(MAX),或者您可以直接访问VARBINARY每个列并连接转换后的值(这可能会更快,因为您无需处理字符串或从string到的额外转换VARBINARY。下面是显示这两个选项的示例。它还显示了该HASHBYTES函数,因此您可以看到它与SQL#.Util_HashBinary之间的值相同。

请注意,连接VARBINARY值时的哈希结果与NVARCHAR值连接时的哈希结果不匹配。这是因为INT值“ 1” 的二进制形式是0x00000001,而值“ 1”的UTF-16LE(即NVARCHAR)形式INT(因为是哈希函数将对之进行操作,所以是二进制形式)是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使用内置于.NET中的托管SHA256算法,因此不应使用“ bcrypt”库。

除了问题的这一方面之外,还有一些其他想法可能有助于此过程:

附加思想#1(至少计算一些哈希值)

您提到了几件事:

  1. 我们将暂存中的行与报表数据库进行比较,以找出自从上次加载数据以来是否有任何列实际发生了更改。

    和:

  2. 我无法保存报表的哈希值。这是不支持触发器或计算列的CCI

    和:

  3. 可以在ETL流程之外更新表

听起来此报告表中的数据在一段时间内是稳定的,并且只能通过此ETL流程进行修改。

如果没有其他修改该表的方法,那么我们实际上根本不需要触发器或索引视图(我本来以为可以)。

由于您无法修改报告表的架构,因此至少可以创建一个相关表来包含预先计算的哈希值(以及计算该哈希表的UTC时间)吗?这样一来,您便可以预先计算出要与下一次进行比较的值,而只留下需要计算其哈希值的传入值。这将减少呼叫次数HASHBYTESSQL#.Util_HashBinary减少一半。您只需在导入过程中加入此哈希表即可。

您还将创建一个单独的存储过程,该存储过程仅刷新此表的哈希值。它只是更新已更改为当前的任何相关行的哈希,并更新那些修改后的行的时间戳。可以/应该在更新此表的任何其他过程结束时执行此proc。还可以安排它在此ETL开始之前30到60分钟运行(取决于执行所需的时间以及任何其他进程何时运行)。如果您怀疑行可能不同步,甚至可以手动执行。

随后指出:

有超过500张桌子

这么多的表确实使每个表都有一个额外的表来包含当前哈希值变得更加困难,但这并不是不可能的,因为可以将其编写为脚本,因为它是标准架构。该脚本只需要考虑源表名称和源表PK列的发现。

尽管如此,无论哪种散列算法最终被证明是最具扩展性的,我仍然强烈建议至少找到几个表(也许有些表比其他500个表大得多)并建立一个相关的表来捕获当前哈希值,因此可以在ETL处理之前知道“当前”值。甚至最快的函数也永远无法胜过永远不必首先调用它的;-)。

附加思想#2(VARBINARY代替NVARCHAR

不管SQLCLR HASHBYTES还是内置的,我仍然建议直接转换为,VARBINARY因为这样应该更快。串联字符串并不是十分有效。并且,除了首先将非字符串值转换为字符串外,这还需要额外的工作(我假设工作量会根据基本类型而有所不同:DATETIME需要更多BIGINT),而转换为则VARBINARY仅会为您提供基础值(绝大多数情况下)。

而且,实际上,测试与其他测试使用的相同数据集,并使用HASHBYTES(N'SHA2_256',...),显示在一分钟内计算出的总哈希增加了23.415%。而增加的原因就是不使用VARBINARY而不是NVARCHAR!😸(请参阅社区Wiki答案以了解详细信息)

附加思考3(请注意输入参数)

进一步的测试表明,影响性能(在此执行量范围内)的一个领域是输入参数:多少个和什么类型。

我的SQL#库中当前存在的Util_HashBinary SQLCLR函数具有两个输入参数:一个VARBINARY(用于哈希的值)和一个NVARCHAR(使用的算法)。这是由于我镜像了HASHBYTES函数的签名。但是,我发现如果删除NVARCHAR参数并创建一个仅执行SHA256的函数,则性能会得到很好的改善。我认为,即使切换NVARCHAR参数INT会有所帮助,但我也认为,即使没有额外的INT参数是至少稍微快。

此外,效果SqlBytes.Value可能会比更好SqlBinary.Value

我为此测试创建了两个新函数:Util_HashSHA256BinaryUtil_HashSHA256Binary8k。这些将包含在SQL#的下一版本中(尚未设置日期)。

我还发现可以稍微改善测试方法,因此我在下面的社区Wiki答案中更新了测试工具,其中包括:

  1. 预加载SQLCLR程序集,以确保加载时间开销不会扭曲结果。
  2. 验证程序以检查冲突。如果找到任何内容,它将显示唯一/不同的行数和总行数。这样就可以确定冲突次数(如果有)是否超出给定用例的限制。一些用例可能只允许少量冲突,而另一些用例可能不需要。如果无法检测到所需精度水平的变化,则超快速功能将无用。例如,使用OP提供的测试工具,我将行数增加到100k行(最初是10k),发现CHECKSUM记录的碰撞超过9k,即9%(相似)。

附加思想4(HASHBYTES+ + SQLCLR在一起)

根据瓶颈所在的位置,甚至可以结合使用内置HASHBYTES和SQLCLR UDF来执行相同的哈希。如果内置函数与SQLCLR操作的约束不同/分开,则与单独使用SQLCLR或单独使用SQLCLR相比,此方法可能能够同时执行更多操作HASHBYTES。绝对值得测试。

附加想法5(哈希对象缓存?)

正如David Browne的答案中所建议的那样,对哈希算法对象进行缓存似乎很有趣,因此我尝试了一下,发现了以下两点:

  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引用,该值似乎都相同。我测试了对同一个函数的多个引用,以及对一个不同函数的引用,所有这三个函数都被赋予了不同的输入值,并返回了不同的(但期望的)返回值。对于这两个测试函数,输出是一个字符串,其中包括ManagedThreadId哈希结果的以及字符串表示形式。该ManagedThreadId查询中所有行的所有UDF引用的值均相同。但是,对于相同的输入字符串,散列结果相同,而对于不同的输入字符串,散列结果不同。

    虽然我在测试中没有看到任何错误的结果,但这是否会增加比赛条件的机会?如果在特定查询中调用的所有SQLCLR对象的字典键均相同,则它们将共享为该键存储的相同值或对象,对吗?关键是,甚至认为它似乎可以在这里工作(在某种程度上,似乎又没有太大的性能提升,但是在功能上没有什么坏处),这让我没有信心这种方法可以在其他情况下使用。


11

这不是传统的答案,但我认为发布迄今为止提到的某些技术的基准会有所帮助。我正在使用SQL Server 2017 CU9在96核心服务器上进行测试。

许多可伸缩性问题是由争用某个全局状态的并发线程引起的。例如,考虑经典的PFS页面争用。如果太多的工作线程需要修改内存中的同一页,则会发生这种情况。随着代码变得更有效率,它可能会更快地请求锁存器。这增加了争论。简而言之,高效的代码更有可能导致可伸缩性问题,因为全局状态的竞争更为严峻。较慢的代码不太可能引起可伸缩性问题,因为不会频繁访问全局状态。

HASHBYTES可伸缩性部分基于输入字符串的长度。我的理论是为什么会发生这种情况,就是在HASHBYTES调用函数时需要访问某些全局状态。易于观察的全局状态是,在某些版本的SQL Server上,每个调用都需要分配一个内存页。较难观察到的是存在某种操作系统争用。结果,如果HASHBYTES该代码被调用的频率降低,则争用将减少。降低HASHBYTES调用率的一种方法是增加每个调用所需的哈希工作量。散列工作部分基于输入字符串的长度。为了重现我在应用程序中看到的可伸缩性问题,我需要更改演示数据。一个合理的最坏情况是一个包含21个表BIGINT列。该表的定义包含在底部的代码中。为了减少Local Factors™,我使用MAXDOP 1在较小表上运行的并发查询。我的快速基准测试代码位于底部。

请注意,这些函数返回不同的哈希长度。MD5并且SpookyHash都是128位哈希,SHA256是256位哈希。

结果(NVARCHARvs VARBINARY转换和串联)

为了查看转换和连接VARBINARY是否真的比/高效/高效NVARCHAR,使用相同的模板创建NVARCHARRUN_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分钟内执行的哈希数。例如,CHECKSUM与84个并发查询一起使用会导致在时间用完之前执行超过20亿次哈希处理。

╔════════════════════╦════════════╦════════════╦════════════╗
      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:可伸缩性问题包括每个调用一个内存分配以及操作系统中花费的大量CPU
  • Spooky:令人惊讶的良好扩展性
  • Spooky LOB:自旋锁SOS_SELIST_SIZED_SLOCK失去控制。我怀疑这是通过CLR函数传递LOB的普遍问题,但我不确定
  • 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;

解决的验证问题

在着重于单个SQLCLR UDF的性能测试时,前面没有讨论的两个问题并未纳入测试中,但理想情况下应进行调查,以确定哪种方法满足所有要求。

  1. 该函数将对每个查询执行两次(对于导入行一次,对于当前行一次)。到目前为止,这些测试在测试查询中仅引用了UDF一次。这个因素可能不会改变选项的排名,但是以防万一,它不应被忽略。
  2. 保罗·怀特(Paul White)在后来被删除的评论中提到:

    HASHBYTES用CLR标量函数代替的一个缺点-似乎CLR函数不能使用批处理模式,而HASHBYTES可以使用批处理模式。就性能而言,这可能很重要。

    因此,这是要考虑的事情,显然需要进行测试。如果SQLCLR选项没有对内置函数提供任何好处HASHBYTES,则将所罗门建议的捕获现有哈希(至少对于最大的表)捕获到相关表中的建议增加了分量。


6

通过池化和缓存在函数调用中创建的任何对象,您可能可以提高所有.NET方法的性能以及可伸缩性。上面的Paul White的代码的EG:

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个都被赋予了不同的输入值,并返回了不同的(但期望的)返回值。这不会增加比赛条件的机会吗?公平地说,在测试中我没有看到任何东西。
所罗门·鲁兹基
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.