代码之家  ›  专栏  ›  技术社区  ›  Der U

将表列合并为一个唯一值(作为哈希字节的输入)

  •  0
  • Der U  · 技术社区  · 1 年前

    目标

    我想将一行的四列合并为一个值,命名为 COMBINED_VALUE .

    这些严格的规则适用:

    • 按相同顺序具有相同列值的两行必须产生相同的结果 组合值
    • 没有两行可以产生相同的结果 组合值
    • 列的顺序有区别,尾随空格有区别,不可见字符有区别,任何区别都有区别
    • 所有列都可以为空,其中一列定义为 INT ,三列定义为 NVARCHAR(100)

    笔记

    我尝试的第一件事是 BINARY_CHECKSUM 。它失败了,导致不同行的值相同:

    SELECT
        a, b,
        binary_checksum_failing = BINARY_CHECKSUM(a,b)
    FROM
        (VALUES ('i01', CONVERT(DECIMAL(9,2), 100)),
                ('i01', CONVERT(DECIMAL(9,2), 10)),
                ('i01', CONVERT(DECIMAL(9,2),  1))) v(a, b)
    

    HASHBYTES 使用“SHA2_256”效果更好,但它需要一个准备充分的 组合值 作为输入。如果确保了这一点,它就有机会产生“非常”独特的哈希值。我发现连接列应该是可行的,但需要替换NULL并分隔列。

    在下面找到两段代码,显示我当前的进度和进一步的注释。第一部分显示了基于转换的解决方案 NVARCHAR ,第二部分使用 VARBINARY 示例行显示了边缘情况,并试图引发失败。

    问题

    • 你能让呈现的脚本失败吗?你能创建两个不同的行,产生相同的结果吗 组合值 通过验证时?
    • 有更好的办法吗 组合值 ? 有更快的方法吗?一般来说,最佳实践是什么?

    脚本#1 :

    DROP TABLE IF EXISTS #t
    
    CREATE TABLE #t
    (
        id          INT IDENTITY(1,1),  
        DESCRIPTION NVARCHAR(100), 
        number      INT,
        word1       NVARCHAR(100),
        word2       NVARCHAR(100),
        word3       NVARCHAR(100)
    )
    
    INSERT INTO #t(DESCRIPTION, number, word1, word2, word3)
    VALUES
     ('base row'                          , 1,        'aa',        'aa',     'aa') 
    ,('trailing space in different places', 1,        'aa',        'aa',    'aa ') 
    ,('trailing space in different places', 1,        'aa',       'aa ',     'aa') 
    ,('NULLs in different places'         , 1,       NULL,         'aa',     'aa') 
    ,('NULLs in different places'         , 1,        'aa',        NULL,     'aa') 
    ,('different rows, screwery with NULL', 1,        'aa',        'aa',     NULL) 
    ,('different rows, screwery with NULL', 1,        'aa',        'aa', '*NULL*') 
    ,('different rows, screwery with sep.', 1, 'aa'       , 'aa/SEP\aa',     'aa') 
    ,('different rows, screwery with sep.', 1, 'aa/SEP\aa',        'aa',     'aa') 
    ,('longer values'                     , 1111111111, 'abcdefghijklmn', 'abcdefghijklmn', 'abcdefghijklmn') 
    
    
    DECLARE @NULL_replacement NVARCHAR(10) = '*NULL*'
    DECLARE @separator        NVARCHAR(10) = '/SEP\'
    
    ;WITH cte AS
    (
        SELECT
            id, DESCRIPTION, number,
            word1, word2, word3,
            COMBINED_VALUE,
            valid_f
        FROM 
            #t
        OUTER APPLY
            (SELECT valid_f = IIF(COUNT(*) = 0, 1, 0)
             FROM (VALUES (word1), (word2), (word3)) v(v)
             JOIN (VALUES (@NULL_replacement), (@separator)) w (w)
                  ON v Like '%'+w+'%' COLLATE Czech_BIN2 )validator
        OUTER APPLY 
            (SELECT COMBINED_VALUE = CONCAT_WS(
                  @separator
                  ,COALESCE(CONVERT(NVARCHAR(100), number), @NULL_replacement) 
                  ,COALESCE(CONVERT(NVARCHAR(100),  word1), @NULL_replacement) 
                  ,COALESCE(CONVERT(NVARCHAR(100),  word2), @NULL_replacement) 
                  ,COALESCE(CONVERT(NVARCHAR(100),  word3), @NULL_replacement)) )combiner
    )
    SELECT
        id, DESCRIPTION, number,
        word1_encapsulated          = '>' + word1 + '<',
        word2_encapsulated          = '>' + word2 + '<',
        word3_encapsulated          = '>' + word3 + '<',
        COMBINED_VALUE_encapsulated = '>' + COMBINED_VALUE + '<',
        valid_f, repeats,
        COMBINED_VALUE_dlen = DATALENGTH(COMBINED_VALUE)
    FROM
        cte o
    OUTER APPLY
        (SELECT repeats = COUNT(*) 
         FROM cte i
         WHERE o.COMBINED_VALUE LIKE i.COMBINED_VALUE COLLATE Czech_BIN2 ) repeatfinder
    ORDER BY 
        id
    

    我认为上面的例子证明了我需要替换NULL并使用分隔符。如果这些列被连接起来,请参见第4行和第5行 CONCAT ,你会得到相同的输出。请参阅该列 DESCRIPTION 了解更多详情。

    NULL替换和分隔符

    • 防止不同的行形成相同的行 组合值
    • 必须禁止作为输入的一部分。这必须经过验证,输入必须经过消毒
    • 不太可能成为输入的一部分,应该是“罕见的”

    脚本#2

    在上述操作之后,我提出了一个基本相似的查询,它使用 VARBINARY 使输出更小,特别是对于数字数据。空替换和分隔符“非常罕见”。它们的ASCII码在ASCII表中标记为“未使用”。

    DECLARE 
        @separator_binary         VARBINARY(5) = CONVERT(VARBINARY(5), CHAR(129)+CHAR(141)+CHAR(143)+CHAR(144)+CHAR(157)),
        @NULL_replacement_binary VARBINARY(5) = CONVERT(VARBINARY(5), CHAR(157)+CHAR(144)+CHAR(143)+CHAR(141)+CHAR(129))
    
    DECLARE 
        @separator_varchar         VARCHAR(10) = CONVERT(VARCHAR(10), @separator_binary, 2),
        @NULL_replacement_varchar VARCHAR(10) = CONVERT(VARCHAR(10), @NULL_replacement_binary, 2)
    
    ;WITH cte AS
    (
        SELECT
            id, DESCRIPTION, number,
            word1, word2, word3,
            COMBINED_VALUE, 
            valid_f, word1_binary
        FROM 
            #t
        OUTER APPLY
            (SELECT 
                 number_binary = CONVERT(VARBINARY(8000), number),
                 word1_binary  = CONVERT(VARBINARY(8000), word1),
                 word2_binary  = CONVERT(VARBINARY(8000), word2),
                 word3_binary  = CONVERT(VARBINARY(8000), word3)) binaries
        OUTER APPLY
            (SELECT valid_f = IIF(COUNT(*) = 0, 1, 0)
             FROM (VALUES (CONVERT(VARCHAR(8000), number_binary, 2)),
                          (CONVERT(VARCHAR(8000), word1_binary , 2)),
                          (CONVERT(VARCHAR(8000), word2_binary , 2)),
                          (CONVERT(VARCHAR(8000), word3_binary , 2))) v(v)
             JOIN (VALUES (@separator_varchar       ),
                          (@NULL_replacement_varchar)) w(w) ON CHARINDEX(w, v COLLATE Latin1_General_BIN2) > 0) validator
        OUTER APPLY
            (SELECT 
                 COMBINED_VALUE = COALESCE(number_binary, @NULL_replacement_binary) 
                                  + @separator_binary + COALESCE(word1_binary , @NULL_replacement_binary) 
                                  + @separator_binary + COALESCE(word2_binary , @NULL_replacement_binary) 
                                  + @separator_binary + COALESCE(word3_binary , @NULL_replacement_binary)) combiner
        )
        SELECT
            id, DESCRIPTION, number,
            word1_encapsulated = '>' + word1 + '<',
            word2_encapsulated = '>' + word2 + '<',
            word3_encapsulated = '>' + word3 + '<',
            COMBINED_VALUE, valid_f, repeats,
            COMBINED_VALUE_dlen = DATALENGTH(COMBINED_VALUE),
            word1_varchar     = CONVERT(VARCHAR(8000), word1_binary, 2),
            separator_varchar = @separator_varchar
        FROM 
            cte o
        OUTER APPLY
            (SELECT repeats = COUNT(*) 
             FROM cte i
             WHERE o.COMBINED_VALUE = i.COMBINED_VALUE) repeatfinder
        ORDER BY 
            id
    
    2 回复  |  直到 1 年前
        1
  •  2
  •   T N    1 年前

    我可以建议对每一列进行二进制连接,包括以下组合:

    1. 空指示符 IIF(col IS NULL THEN 0x01, 0x00) ,
    2. 数据长度 ISNULL(CAST(DATALENGTH(col) AS VARBINARY(MAX)), 0x) ,以及
    3. 价值本身 ISNULL(CAST(col AS VARBINARY(MAX)), 0x) .

    对于非空列类型和固定长度列类型,可以分别选择性地省略空指示符和数据长度部分。

    这将明确地处理可空列和可变长度列的任何组合,您不需要担心数据中的分隔符。

    级联的结果可以被馈送到一个好的哈希函数中,如SHA-256。如果你每飞秒向数据库添加一百万个唯一行,那么SHA-256碰撞的几率大约是每100亿年1次。

    SELECT
        t.*,
        --C.CombinedValue,
        H.HashValue
    FROM #t t
    CROSS APPLY (
        SELECT 
            + IIF(number IS NULL, 0x01, 0x00)
              + ISNULL(CAST(DATALENGTH(number) AS VARBINARY(MAX)), 0x) -- Optional
              + ISNULL(CAST(number AS VARBINARY(MAX)), 0x)
            + IIF(word1 IS NULL, 0x01, 0x00)
              + ISNULL(CAST(DATALENGTH(word1) AS VARBINARY(MAX)), 0x)
              + ISNULL(CAST(word1 AS VARBINARY(MAX)), 0x)
            + IIF(word2 IS NULL, 0x01, 0x00)
              + ISNULL(CAST(DATALENGTH(word2) AS VARBINARY(MAX)), 0x)
              + ISNULL(CAST(word2 AS VARBINARY(MAX)), 0x)
            + IIF(word3 IS NULL, 0x01, 0x00)
              + ISNULL(CAST(DATALENGTH(word3) AS VARBINARY(MAX)), 0x)
              + ISNULL(CAST(word3 AS VARBINARY(MAX)), 0x)
            AS CombinedValue
    ) C
    CROSS APPLY (
        SELECT HASHBYTES('SHA2_256',C.CombinedValue) AS HashValue
    ) H
    ORDER BY T.id
    

    如果需要,可以很容易地重新排列上述内容以使用CTE。

    样本结果:

    身份证件 描述 单词1 单词2 单词3 哈希值
    1. 基排 1. aa aa aa 0xFB9299EC58E4DAC1465BD3025517F735DC140BFE1CB8EE1F627C610514DF2A2D
    2. 不同位置的尾随空间 1. aa aa aa 0x6E462C1EF5F7CD583F74F10594CF454C6EAD556D42564153C1F9B558ECB3C3A
    3. 不同位置的尾随空间 1. aa aa aa 0x862F08AC15AFEF147455038FF9F3AFEB95826530BFE3DA2C87EB8AFDAA572
    4. 不同地方的NULLs 1. 无效的 aa aa 0xBDF6691C3F2207B854251CBC527E9A1DAFF7282F68843F97178E47A2A91900
    5. 不同地方的NULLs 1. aa 无效的 aa 0xB53C88CA610E62159C942AF39D61165ECDB12F52E0B65D62A126BB69257AD316
    6. 不同的行,带NULL的螺旋 1. aa aa 无效的 0x86D4A0F8808C4691E121001E199AD69160283768824BE86723EE191FC5387A3
    7. 不同的行,带NULL的螺旋 1. aa aa 无效的 0x27CDC248AEDB67EA3FEE935FB28FFD6F8BCF144D0B177824DAEEA7D5F2C69D2F
    8. 不同的行,用seppe螺丝钉。 1. aa aa/SEP\aa aa 0x3CD630032648215DD6AFACE7098A3A3DA13D8180490B99046195F1B84CAE84
    9 不同的行,用seppe螺丝钉。 1. aa/SEP\aa aa aa 0x507C17CE4F560775AE6A0973A5BB33A8E30C52160927C7D682914446AC7ADC1E
    10 较长的值 1111111111 abcdefghijklmn abcdefghijklmn abcdefghijklmn 0x04DD3801CEBEC074D512AFDA327CDB1F05089E53EE2CD93AC72BC260145A95DC

    请参阅 this db<>fiddle 为了演示。

        2
  •  0
  •   Thorsten Kettner    1 年前

    据我所知,您可以将任何二进制值存储在NVARCHAR字符中。如果你有一个NVARCHAR2(100),并且你想要一个唯一表示该字符串的值,你需要与原始字符串一样多的字节,包括存储其长度的空间。当你最终得到尽可能多的空间时,以某种方式转换这个字符串是没有意义的,因为你没有经过一些加密。

    这意味着连接你的值是正确的做法。

    • 对于每个字符串:100个字符(用一些值填充,例如空白)加上3个字符,长度从0到100,一个类似999的代码表示NULL。
    • 对于使用尾随零的11个字符的整数。NULL的一些神奇数字,例如99999999999。

    在转换和格式化方面,SQL Server可能是最糟糕的DBMS,这有点笨拙。我认为这就是:

    SELECT
      LEFT(COALESCE(str1, '') + REPLICATE(' ', 100), 100) +
      RIGHT(REPLICATE('0', 3) + CAST(COALESCE(LEN(str1 + 'x') - 1, 999) AS NVARCHAR(3)), 3) +
      LEFT(COALESCE(str2, '') + REPLICATE(' ', 100), 100) +
      RIGHT(REPLICATE('0', 3) + CAST(COALESCE(LEN(str2 + 'x') - 1, 999) AS NVARCHAR(3)), 3) +
      LEFT(COALESCE(str3, '') + REPLICATE(' ', 100), 100) +
      RIGHT(REPLICATE('0', 3) + CAST(COALESCE(LEN(str3 + 'x') - 1, 999) AS NVARCHAR(3)), 3) +
      CASE WHEN int1 < 0 THEN '-' WHEN int1 > 0 THEN '+' ELSE ' ' END +
      COALESCE(RIGHT(REPLICATE('0', 11) + CAST(ABS(int1) AS NVARCHAR(11)), 11), '99999999999') AS code
    FROM mytable;
    

    演示: https://dbfiddle.uk/zR5QshZg