SQL CLR 스칼라 함수를 사용하여 HASHBYTES를 시뮬레이션하는 확장 가능한 방법은 무엇입니까?


29

ETL 프로세스의 일부로, 스테이징의 행과보고 데이터베이스를 비교하여 데이터가 마지막으로로드 된 이후 실제로 변경된 열이 있는지 확인합니다.

비교는 테이블의 고유 키와 다른 모든 열의 해시에 기반합니다. 우리는 현재 사용 HASHBYTESSHA2_256알고리즘과 동시 작업자 스레드가 모두 호출하는 경우는 대형 서버에서 확장되지 않는 것으로 나타났습니다 HASHBYTES.

초당 해시로 측정 된 처리량은 96 코어 서버에서 테스트 할 때 16 개의 동시 스레드를 지나치지 않습니다. 동시 MAXDOP 8쿼리 수를 1-12 에서 변경하여 테스트했습니다 . 테스트 MAXDOP 1결과 동일한 확장 성 병목 현상 이 나타났습니다.

해결 방법으로 SQL CLR 솔루션을 시도하고 싶습니다. 요구 사항을 설명하려는 나의 시도는 다음과 같습니다.

  • 이 함수는 병렬 쿼리에 참여할 수 있어야합니다
  • 이 기능은 결정 론적이어야합니다
  • 함수는 NVARCHAR또는 VARBINARY문자열을 입력해야 합니다 (모든 관련 열이 함께 연결됨)
  • 문자열의 일반적인 입력 크기는 100-20000 자입니다. 20000은 최대 값이 아닙니다
  • 해시 충돌 가능성은 MD5 알고리즘과 대략 같거나 같아야합니다. CHECKSUM충돌이 너무 많아서 작동하지 않습니다.
  • 이 기능은 대규모 서버에서 제대로 확장되어야합니다 (스레드 수가 증가함에 따라 스레드 당 처리량이 크게 줄어들지 않아야 함)

Application Reasons ™의 경우보고 테이블의 해시 값을 저장할 수 없다고 가정하십시오. 트리거 또는 계산 열을 지원하지 않는 CCI입니다 (다른 문제도 있고 싶지 않습니다).

HASHBYTESSQL 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

변경 사항 만 찾고 있으므로 암호화 해시 기능이 필요하지 않습니다.

Brandon Dahler 의 공개 소스 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 버전은 훨씬 빨라야합니다.

LOB 2 진을 랩핑 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 언어 기능의 수를 사용하는 UNSAFESQL Server가 있습니다. SAFE상태 와 호환되는 기본 Spooky Hash를 작성할 수 있습니다 . Jon Hanna의 SpookilySharp를 기반으로 작성한 예제 는 다음과 같습니다.

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


16

병렬 처리가 SQLCLR을 사용하여 훨씬 더 나은지 확실하지 않습니다. 그러나 Util_HashBinary 라는 SQL # SQLCLR 라이브러리 의 무료 버전에 해시 함수가 있기 때문에 테스트하기가 정말 쉽습니다 . 지원되는 알고리즘은 MD5, SHA1, SHA256, SHA384 및 SHA512입니다.

그것은 소요 VARBINARY(MAX)당신이 (당신이 현재하고있는대로) 중 각 필드의 문자열 버전을 연결 한 다음에 변환 할 수 있도록 입력으로 값을 VARBINARY(MAX), 또는 당신이 직접 갈 수있는 VARBINARY빠른 이후 (이 될 수를 각 열에 대해 변환 된 값을 연결 문자열을 다루거나 문자열에서 VARBINARY) 로의 추가 변환을 처리하지 않습니다 . 아래는이 두 옵션을 모두 보여주는 예입니다. 또한 HASHBYTES함수와 SQL # .Util_HashBinary 값이 동일한 것을 확인할 수 있는 함수를 보여줍니다 .

해시 결과가 합치 할 때주의하시기 바랍니다 VARBINARY합치 할 때 해시 결과가 일치하지 않습니다 값을 NVARCHAR값을. 의 바이너리 형식 때문이다 INT가 UTF-16LE (즉, 동안 값 "1"가 0x00000001 인 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;

다음을 사용하여 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 계산 시간)를 포함 할 관련 테이블을 만들 수 있습니까? 이렇게하면 다음에 비교할 미리 계산 된 값을 가질 수 있으며 해시 계산에 필요한 입력 값만 남겨 둡니다. 이 중 하나를 호출 수를 줄일 HASHBYTES또는 SQL#.Util_HashBinary반으로. 가져 오기 프로세스 중에이 해시 테이블에 조인하면됩니다.

또한이 테이블의 해시를 단순히 새로 고치는 별도의 스토어드 프로 시저를 작성합니다. 현재로 변경된 모든 관련 행의 해시를 업데이트하고 수정 된 행의 타임 스탬프를 업데이트합니다. 이 proc는이 테이블을 업데이트하는 다른 프로세스의 끝에서 실행될 수 있습니다. 이 ETL이 시작되기 30 분에서 60 분 전에 실행되도록 예약 할 수도 있습니다 (실행하는 데 걸리는 시간 및 이러한 다른 프로세스가 실행될 수있는 시간에 따라 다름). 동기화되지 않은 행이있을 것으로 의심되는 경우 수동으로 실행할 수도 있습니다.

그런 다음에

500 개가 넘는 테이블이 있습니다

많은 테이블은 현재 해시를 포함하기 위해 각 테이블마다 여분의 테이블을 갖는 것이 더 어려워 지지만 표준 스키마이기 때문에 스크립팅 될 수 있으므로 불가능하지 않습니다. 스크립팅은 소스 테이블 이름과 소스 테이블 PK 열 발견을 설명하기 만하면됩니다.

그러나 관계없이의 해시 알고리즘은 궁극적으로 가장 확장 성이있는 것으로 판명, 난 여전히 매우 적어도 몇 테이블 (아마도 500 개 테이블의 나머지 부분보다 훨씬 큰 일부가) 발견하고 캡처 관련 테이블을 설정하는 것이 좋습니다 현재 해시이므로 "현재"값을 ETL 프로세스 전에 알 수 있습니다. 가장 빠른 기능조차도 처음부터 그것을 호출 할 필요가 없습니다. ;-).

추가 생각 # 2 ( VARBINARY대신 NVARCHAR)

에 관계없이 SQLCLR의 내장 대 HASHBYTES, 난 아직 직접 변환하는 것이 좋습니다 VARBINARY그 같이 한다 빠르다. 문자열을 연결하는 것은 그리 효율적이지 않습니다. 그리고 그것은 문자열이 아닌 값을 문자열로 변환하는 것 외에도 추가 노력이 필요합니다 (노력은 기본 유형에 따라 다릅니다 : DATETIME이상을 요구합니다 BIGINT). 그러나 변환하면 VARBINARY단순히 기본 값을 제공합니다 (대부분의 경우에).

실제로 다른 테스트에서 사용한 것과 동일한 데이터 집합을 테스트 HASHBYTES(N'SHA2_256',...)하고을 사용 하면 1 분 동안 계산 된 총 해시가 23.415 % 증가했습니다. 그리고 그 증가는 VARBINARY대신에 사용하는 것 이상의 아무것도하지 않았기 때문 입니다 NVARCHAR! 😸 (자세한 내용은 커뮤니티 위키 답변 참조)

추가 생각 # 3 (입력 매개 변수에 유의)

추가 테스트를 통해 성능에 영향을 미치는 한 영역 (이 실행 횟수에 걸쳐)이 입력 매개 변수 (얼마나 많은 유형 및 유형) 인 것으로 나타났습니다.

현재 SQL # 라이브러리에 있는 Util_HashBinary SQLCLR 함수에는 두 가지 입력 매개 변수가 있습니다. 하나 VARBINARY는 해시 값이고 다른 하나 NVARCHAR는 사용할 알고리즘입니다. 이것은 HASHBYTES함수 의 서명을 반영하기 때문 입니다. 그러나 NVARCHAR매개 변수를 제거하고 SHA256 만 수행하는 함수를 만들면 성능이 크게 향상되었습니다. NVARCHAR매개 변수를 전환해도 INT도움이 될 것이라고 가정하지만 추가 INT매개 변수가 없어도 약간 더 빠르다고 가정합니다.

또한 SqlBytes.Value보다 성능이 우수 할 수 있습니다 SqlBinary.Value.

이 테스트를 위해 Util_HashSHA256BinaryUtil_HashSHA256Binary8k 라는 두 가지 새로운 기능을 만들었습니다 . 이것들은 다음 SQL # 릴리스에 포함될 것입니다 (아직 날짜가 설정되지 않았습니다).

또한 테스트 방법론이 약간 개선 될 수 있다는 것을 알았으므로 아래 커뮤니티 위키 답변에서 테스트 하네스를 업데이트하여 다음을 포함 시켰습니다.

  1. 로드 시간 오버 헤드로 인해 결과가 왜곡되지 않도록 SQLCLR 어셈블리를 사전로드합니다.
  2. 충돌을 확인하기위한 검증 절차. 발견 된 경우 고유 / 개별 행 수와 총 행 수를 표시합니다. 이를 통해 충돌 횟수 (있는 경우)가 주어진 사용 사례의 한계를 초과하는지 여부를 확인할 수 있습니다. 일부 유스 케이스는 적은 수의 충돌을 허용하고 다른 유스 케이스는 필요하지 않을 수 있습니다. 원하는 정확도 수준의 변화를 감지 할 수 없으면 초고속 기능은 쓸모가 없습니다. 예를 들어, OP가 제공 한 테스트 하네스를 사용하여 행 수를 100k 행 (원래 10k)으로 늘리고 CHECKSUM9k (충돌) 인 9k 충돌 을 등록했습니다.

추가 생각 # 4 ( HASHBYTES+ SQLCLR?)

병목 현상의 위치에 따라 내장 HASHBYTES및 SQLCLR UDF 조합을 사용 하여 동일한 해시를 수행하는 데 도움이 될 수도 있습니다 . 내장 함수가 SQLCLR 조작과 다르게 / 별도로 제한되는 경우,이 방법은 HASHBYTES개별적으로 또는 SQLCLR 보다 더 동시에 수행 할 수 있습니다 . 테스트 할 가치가 있습니다.

추가 생각 # 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 참조에 대한 동일하게 나타납니다. 동일한 함수에 대한 여러 참조와 다른 함수에 대한 참조를 테스트했습니다 .3 개 모두 다른 입력 값이 주어지고 다른 (그러나 예상되는) 반환 값을 반환합니다. 두 테스트 함수 모두 출력 ManagedThreadId결과는 해시 결과의 문자열 표현과 문자열 표현 이 포함 된 문자열이었습니다 . ManagedThreadId값은 쿼리의 모든 UDF 참조에 대한 동일하고 모든 행에 걸쳐. 그러나 해시 결과는 동일한 입력 문자열에서 동일하고 다른 입력 문자열에서 다릅니다.

    테스트에서 잘못된 결과를 보지 못했지만 이것이 경쟁 조건의 가능성을 증가시키지 않습니까? 사전의 키가 특정 쿼리에서 호출 된 모든 SQLCLR 객체에 대해 동일한 경우 해당 키에 대해 저장된 동일한 값 또는 객체를 공유하고 있습니까? 요점은 심지어 여기에서 작동하는 것처럼 보였지만 (다시 한 번 더 많은 성능 향상이 없었지만 기능적으로 아무런 문제가 없었습니다),이 접근법은 다른 시나리오에서 작동 할 것이라는 확신을주지 못합니다.


11

이것은 전통적인 대답은 아니지만 지금까지 언급 한 일부 기술의 벤치 마크를 게시하는 것이 도움이 될 것이라고 생각했습니다. SQL Server 2017 CU9를 사용하여 96 코어 서버에서 테스트하고 있습니다.

많은 확장 성 문제는 일부 전역 상태에서 경합하는 동시 스레드로 인해 발생합니다. 예를 들어, 고전적인 PFS 페이지 경합을 고려하십시오. 너무 많은 작업자 스레드가 메모리에서 동일한 페이지를 수정해야하는 경우에 발생할 수 있습니다. 코드의 효율성이 높아지면 래치를 더 빨리 요청할 수 있습니다. 그것은 경합을 증가시킵니다. 간단히 말해서, 효율적인 상태의 코드는 글로벌 상태가 더 심각하게 경쟁하기 때문에 확장 성 문제로 이어질 가능성이 높습니다. 글로벌 상태에 자주 액세스하지 않기 때문에 느린 코드는 확장 성 문제를 일으킬 가능성이 적습니다.

HASHBYTES확장 성은 입력 문자열의 길이를 부분적으로 기반으로합니다. 내 이론은 이것이 발생하는 이유에 대한 것인데 HASHBYTES함수가 호출 될 때 일부 전역 상태에 대한 액세스가 필요하다는 것 입니다. 관찰하기 쉬운 전역 상태는 일부 SQL Server 버전에서 호출 당 메모리 페이지를 할당해야한다는 것입니다. 관찰하기 어려운 것은 일종의 OS 경합이 있다는 것입니다. 결과적으로 HASHBYTES코드에서 덜 자주 호출 하면 경합이 중단됩니다. HASHBYTES통화 속도를 줄이는 한 가지 방법은 통화 당 필요한 해싱 작업량을 늘리는 것입니다. 해싱 작업은 입력 문자열의 길이를 부분적으로 기반으로합니다. 응용 프로그램에서 본 확장 성 문제를 재현하기 위해 데모 데이터를 변경해야했습니다. 합리적인 최악의 시나리오는 21 인 테이블입니다.BIGINT열. 테이블 정의는 하단의 코드에 포함되어 있습니다. Local Factors ™를 줄이기 위해 MAXDOP 1상대적으로 작은 테이블에서 작동 하는 동시 쿼리를 사용하고 있습니다. 빠른 벤치 마크 코드가 맨 아래에 있습니다.

함수는 다른 해시 길이를 반환합니다. MD5SpookyHash모두 128 비트 해시하며, SHA256256 비트 해시이다.

(결과 NVARCHARVARBINARY변환 및 연결)

로 변환하고 연결하는 VARBINARY것이 실제로보다 효율적이고 성능이 좋은지 확인하기 위해 저장 프로 시저 NVARCHARNVARCHAR버전이 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 개의 동시 쿼리를 사용 하면 시간이 다 떨어지기 전에 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: 확장 성 문제에는 호출 당 하나의 메모리 할당 및 OS에서 소비되는 대량의 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 : 정리 (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;

확인 문제 확인

단일 SQLCLR UDF의 성능 테스트에 중점을 두면서 초기에 논의 된 두 가지 문제는 테스트에 포함되지 않았지만 모든 요구 사항을 충족시키는 접근 방식을 결정하기 위해서는 이상적으로 조사해야합니다 .

  1. 함수는 각 쿼리마다 두 번 실행됩니다 (가져 오기 행에 대해 한 번, 현재 행에 대해 한 번). 지금까지의 테스트는 테스트 쿼리에서 UDF를 한 번만 참조했습니다. 이 요소는 옵션의 순위를 변경하지 않을 수 있지만 만일을 위해 무시해서는 안됩니다.
  2. 이후 삭제 된 의견에서 Paul White는 다음과 같이 언급했습니다.

    HASHBYTESCLR 스칼라 함수 로 대체 하는 한 가지 단점은 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는 그다지 살펴 보지 않았습니다.
Solomon Rutzky

@PaulWhite과 David. 나는 완료 뭔가 잘못했습니다 수, 또는 될 수있는 차이를 사이 SHA256ManagedSpookyHashV2, 그러나 나는이 시도하고있는 경우, 많은 성능 향상을 보지 못했다. 또한 ManagedThreadId특정 쿼리의 모든 SQLCLR 참조에 대해 값이 동일 하다는 것을 알았습니다 . 동일한 함수에 대한 여러 참조와 다른 함수에 대한 참조를 테스트했습니다 .3 개 모두 다른 입력 값이 주어지고 다른 (그러나 예상되는) 반환 값을 반환합니다. 이것이 경쟁 조건의 가능성을 증가시키지 않습니까? 공평하게, 내 시험에서 나는 아무것도 보지 못했다.
솔로몬 루츠 키
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.