Apa cara scalable untuk mensimulasikan HASHBYTES menggunakan fungsi skalar SQL CLR?

29

Sebagai bagian dari proses ETL kami, kami membandingkan baris dari pementasan terhadap database pelaporan untuk mencari tahu apakah ada kolom yang benar-benar berubah sejak data terakhir dimuat.

Perbandingan didasarkan pada kunci unik dari tabel dan beberapa jenis hash dari semua kolom lainnya. Kami saat ini menggunakan HASHBYTESdengan SHA2_256algoritma dan telah menemukan bahwa itu tidak skala pada server besar jika banyak utas pekerja bersamaan memanggilHASHBYTES .

Throughput yang diukur dalam hash per detik tidak meningkat melewati 16 utas bersamaan saat pengujian pada server 96 inti. Saya menguji dengan mengubah jumlah MAXDOP 8permintaan bersamaan dari 1 - 12. Pengujian denganMAXDOP 1 menunjukkan hambatan skalabilitas yang sama.

Sebagai solusinya saya ingin mencoba solusi SQL CLR. Ini adalah upaya saya untuk menyatakan persyaratan:

  • Fungsi harus dapat berpartisipasi dalam permintaan paralel
  • Fungsi harus deterministik
  • Fungsi harus mengambil input dari NVARCHARatau VARBINARYstring (semua kolom yang relevan digabungkan bersama-sama)
  • Panjang input tipikal dari string adalah 100 - 20000 karakter. 20000 bukan maks
  • Peluang tabrakan hash harus kira-kira sama atau lebih baik dari algoritma MD5. CHECKSUMtidak bekerja untuk kami karena ada terlalu banyak tabrakan.
  • Fungsi ini harus berskala baik pada server besar (throughput per utas tidak boleh berkurang secara signifikan karena jumlah utas meningkat)

Untuk Aplikasi Alasan ™, asumsikan bahwa saya tidak dapat menyimpan nilai hash untuk tabel pelaporan. Ini CCI yang tidak mendukung pemicu atau kolom yang dihitung (ada masalah lain yang juga tidak ingin saya bahas).

Apa cara scalable untuk mensimulasikan HASHBYTESmenggunakan fungsi SQL CLR? Tujuan saya dapat dinyatakan sebagai mendapatkan hash per detik sebanyak yang saya bisa di server besar, jadi kinerja juga penting. Saya buruk dengan CLR jadi saya tidak tahu bagaimana mencapainya. Jika itu memotivasi siapa pun untuk menjawab, saya berencana menambahkan hadiah untuk pertanyaan ini segera setelah saya mampu. Di bawah ini adalah contoh kueri yang secara kasar menggambarkan use case:

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

Untuk sedikit menyederhanakan, saya mungkin akan menggunakan sesuatu seperti yang berikut untuk pembandingan. Saya akan memposting hasil HASHBYTESpada hari Senin:

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);
Joe Obbish
sumber

Jawaban:

18

Karena Anda hanya mencari perubahan, Anda tidak memerlukan fungsi hash kriptografis.

Anda dapat memilih dari salah satu hash non-kriptografis yang lebih cepat di pustaka Data -source open-source oleh Brandon Dahler, dilisensikan di bawah lisensi MIT yang disetujui dan diizinkan oleh OSI . SpookyHashadalah pilihan yang populer.

Contoh implementasi

Kode sumber

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

Sumber menyediakan dua fungsi, satu untuk input 8000 byte atau kurang, dan versi LOB. Versi non-LOB harus lebih cepat secara signifikan.

Anda mungkin bisa memasukkan binary LOB COMPRESS untuk mendapatkannya di bawah batas 8000 byte, jika itu ternyata bermanfaat untuk kinerja. Atau, Anda dapat memecah LOB menjadi segmen sub-8000 byte, atau cukup mencadangkan penggunaanHASHBYTES untuk kasing LOB (karena skala input yang lebih panjang lebih baik).

Kode pra-dibangun

Anda jelas dapat mengambil paket untuk diri sendiri dan mengkompilasi semuanya, tetapi saya membuat rakitan di bawah ini untuk mempermudah pengujian cepat:

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

Fungsi 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

Pemakaian

Contoh penggunaan diberikan data sampel dalam pertanyaan:

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

Saat menggunakan versi LOB, parameter pertama harus dilemparkan atau dikonversi ke varbinary(max) .

Rencana eksekusi

rencana


Aman Seram

The Data.HashFunction perpustakaan menggunakan sejumlah fitur bahasa CLR yang dianggap UNSAFEoleh SQL Server. Dimungkinkan untuk menulis Hash Seram dasar yang kompatibel dengan SAFEstatus. Contoh yang saya tulis berdasarkan Jon Hanna's SpookilySharp adalah di bawah ini:

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

Paul White mengatakan GoFundMonica
sumber
16

Saya tidak yakin apakah paralelisme akan / secara signifikan lebih baik dengan SQLCLR. Namun, sangat mudah untuk menguji karena ada fungsi hash dalam versi gratis dari perpustakaan SQL # SQLCLR (yang saya tulis) disebut Util_HashBinary . Algoritma yang didukung adalah: MD5, SHA1, SHA256, SHA384, dan SHA512.

Dibutuhkan VARBINARY(MAX)nilai sebagai input, sehingga Anda dapat menyatukan versi string dari setiap bidang (seperti yang sedang Anda lakukan) dan kemudian mengonversi ke VARBINARY(MAX), atau Anda dapat langsung menuju VARBINARYke setiap kolom dan menggabungkan nilai yang dikonversi (ini mungkin lebih cepat karena Anda tidak berurusan dengan string atau konversi tambahan dari string ke VARBINARY). Di bawah ini adalah contoh yang menunjukkan kedua opsi ini. Ini juga menunjukkan HASHBYTESfungsi sehingga Anda dapat melihat bahwa nilainya sama antara itu dan SQL # .Util_HashBinary .

Harap dicatat bahwa hasil hash ketika menggabungkan VARBINARYnilai tidak akan cocok dengan hasil hash ketika menggabungkan NVARCHARnilai. Ini karena bentuk biner dari INTnilai "1" adalah 0x00000001, sedangkan bentuk UTF-16LE (yaitu NVARCHAR) dari INTnilai "1" (dalam bentuk biner karena itulah fungsi hashing akan beroperasi) adalah 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;

Anda dapat menguji sesuatu yang lebih sebanding dengan non-LOB Spooky menggunakan:

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

Catatan: Util_HashBinary menggunakan algoritma SHA256 yang dikelola yang dibangun ke dalam .NET, dan tidak boleh menggunakan perpustakaan "bcrypt".

Di luar aspek pertanyaan itu, ada beberapa pemikiran tambahan yang mungkin membantu proses ini:

Pemikiran Tambahan # 1 (pra-hitung hash, setidaknya beberapa)

Anda menyebutkan beberapa hal:

  1. kami membandingkan baris dari pementasan terhadap database pelaporan untuk mencari tahu apakah ada kolom yang benar-benar berubah sejak data terakhir dimuat.

    dan:

  2. Saya tidak bisa menyimpan nilai hash untuk tabel pelaporan. Ini CCI yang tidak mendukung pemicu atau kolom yang dihitung

    dan:

  3. tabel dapat diperbarui di luar proses ETL

Kedengarannya seperti data dalam tabel pelaporan ini stabil untuk jangka waktu tertentu, dan hanya dimodifikasi oleh proses ETL ini.

Jika tidak ada lagi yang memodifikasi tabel ini, maka kita benar-benar tidak memerlukan pemicu atau tampilan indeks setelah semua (saya awalnya berpikir bahwa Anda mungkin).

Karena Anda tidak dapat mengubah skema tabel pelaporan, apakah paling tidak dimungkinkan untuk membuat tabel terkait untuk berisi hash yang sudah dihitung sebelumnya (dan waktu UTC ketika dihitung)? Ini akan memungkinkan Anda untuk memiliki nilai pra-dihitung untuk dibandingkan dengan waktu berikutnya, hanya menyisakan nilai masuk yang mengharuskan menghitung hash. Ini akan mengurangi jumlah panggilan menjadi salah satu HASHBYTESatau SQL#.Util_HashBinarysetengahnya. Anda cukup bergabung ke tabel hash ini selama proses impor.

Anda juga akan membuat prosedur tersimpan terpisah yang hanya menyegarkan hash dari tabel ini. Itu hanya memperbarui hash dari setiap baris terkait yang telah berubah menjadi saat ini, dan memperbarui stempel waktu untuk baris yang dimodifikasi. Proc ini dapat / harus dijalankan pada akhir proses lain yang memperbarui tabel ini. Itu juga dapat dijadwalkan untuk berjalan 30 - 60 menit sebelum ETL ini dimulai (tergantung pada berapa lama waktu yang dibutuhkan untuk mengeksekusi, dan kapan salah satu dari proses lain ini berjalan). Itu bahkan dapat dieksekusi secara manual jika Anda pernah mencurigai ada baris yang tidak sinkron.

Kemudian dicatat bahwa:

ada lebih dari 500 tabel

Banyak tabel yang membuatnya lebih sulit untuk memiliki tabel tambahan per masing-masing untuk mengandung hash saat ini, tetapi ini bukan tidak mungkin karena dapat dituliskan karena itu akan menjadi skema standar. Scripting hanya perlu memperhitungkan nama tabel sumber dan penemuan tabel PK tabel sumber.

Namun, terlepas dari algoritma hash mana yang akhirnya terbukti paling terukur, saya masih sangat merekomendasikan menemukan setidaknya beberapa tabel (mungkin ada beberapa yang JAUH lebih besar daripada sisa 500 tabel) dan menyiapkan tabel terkait untuk menangkap hash saat ini sehingga nilai "current" dapat diketahui sebelum proses ETL. Bahkan fungsi tercepat tidak dapat bekerja lebih baik tanpa harus memanggilnya ;-).

Pemikiran Tambahan # 2 ( VARBINARYbukan NVARCHAR)

Terlepas dari SQLCLR vs built-in HASHBYTES, saya masih akan merekomendasikan konversi langsung VARBINARYkarena itu harus lebih cepat. String penggabungan tidak terlalu efisien. Dan , itu sebagai tambahan untuk mengubah nilai-nilai non-string menjadi string di tempat pertama, yang membutuhkan usaha ekstra (saya berasumsi jumlah usaha bervariasi berdasarkan pada tipe dasar: DATETIMEmembutuhkan lebih dari BIGINT), sedangkan mengkonversi dengan VARBINARYhanya memberi Anda nilai yang mendasarinya (umumnya).

Dan, pada kenyataannya, menguji dataset yang sama dengan tes lain yang digunakan, dan menggunakan HASHBYTES(N'SHA2_256',...), menunjukkan peningkatan total hash 23,415% dihitung dalam satu menit. Dan peningkatan itu adalah untuk melakukan tidak lebih dari menggunakan VARBINARYalih-alih NVARCHAR! 😸 (lihat jawaban wiki komunitas untuk detail)

Pemikiran Tambahan # 3 (perhatikan parameter input)

Pengujian lebih lanjut menunjukkan bahwa satu area yang memengaruhi kinerja (lebih dari volume eksekusi ini) adalah parameter input: berapa banyak dan jenis apa.

Fungsi Util_HashBinary SQLCLR yang saat ini ada di perpustakaan # SQL saya memiliki dua parameter input: satu VARBINARY(nilai untuk hash), dan satu NVARCHAR(algoritma untuk digunakan). Ini karena mirroring saya tanda tangan HASHBYTESfungsi. Namun, saya menemukan bahwa jika saya menghapus NVARCHARparameter dan membuat fungsi yang hanya melakukan SHA256, maka kinerja meningkat dengan cukup baik. Saya berasumsi bahwa bahkan beralih ke NVARCHARparameter INTakan membantu, tetapi saya juga berasumsi bahwa bahkan tidak memiliki INTparameter tambahan setidaknya sedikit lebih cepat.

Juga, SqlBytes.Valuemungkin berkinerja lebih baik daripada SqlBinary.Value.

Saya membuat dua fungsi baru: Util_HashSHA256Binary dan Util_HashSHA256Binary8k untuk pengujian ini. Ini akan dimasukkan dalam rilis SQL # berikutnya (belum ada tanggal yang ditetapkan untuk itu).

Saya juga menemukan bahwa metodologi pengujian bisa sedikit ditingkatkan, jadi saya memperbarui test harness di komunitas wiki jawaban di bawah ini untuk memasukkan:

  1. pra-pemuatan majelis SQLCLR untuk memastikan bahwa overhead waktu pemuatan tidak membuat miring hasil.
  2. prosedur verifikasi untuk memeriksa tabrakan. Jika ada yang ditemukan, ini akan menampilkan jumlah baris unik / berbeda dan jumlah total baris. Ini memungkinkan seseorang untuk menentukan apakah jumlah tabrakan (jika ada) di luar batas untuk kasus penggunaan yang diberikan. Beberapa kasus penggunaan mungkin memungkinkan sejumlah kecil tabrakan, yang lain mungkin tidak memerlukannya. Fungsi super cepat tidak berguna jika tidak dapat mendeteksi perubahan pada tingkat akurasi yang diinginkan. Sebagai contoh, dengan menggunakan test harness yang disediakan oleh OP, saya meningkatkan jumlah baris menjadi 100rb baris (awalnya 10rb) dan menemukan bahwa CHECKSUMterdaftar lebih dari 9r tabrakan, yaitu 9% (yikes).

Pemikiran Tambahan # 4 ( HASHBYTES+ SQLCLR bersama?)

Tergantung di mana bottleneck berada, bahkan mungkin membantu menggunakan kombinasi built-in HASHBYTESdan SQLCLR UDF untuk melakukan hash yang sama. Jika fungsi built-in dibatasi secara berbeda / terpisah dari operasi SQLCLR, maka pendekatan ini mungkin dapat mencapai lebih bersamaan daripada salah satu HASHBYTESatau SQLCLR secara individual. Ini pasti layak untuk diuji.

Pemikiran Tambahan # 5 (hashing caching objek?)

Caching objek algoritma hashing seperti yang disarankan dalam jawaban David Browne tentu tampak menarik, jadi saya mencobanya dan menemukan dua hal berikut yang menarik:

  1. Untuk alasan apa pun, sepertinya tidak memberikan banyak, jika ada, peningkatan kinerja. Saya bisa saja melakukan sesuatu yang salah, tetapi inilah yang saya coba:

    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. The ManagedThreadIdnilai tampaknya menjadi sama untuk semua referensi SQLCLR dalam pencarian tertentu. Saya menguji beberapa referensi ke fungsi yang sama, serta referensi ke fungsi yang berbeda, ketiganya diberi nilai input yang berbeda, dan mengembalikan nilai pengembalian yang berbeda (tetapi diharapkan). Untuk kedua fungsi tes, output adalah string yang menyertakan ManagedThreadIdserta representasi string dari hasil hash. The ManagedThreadIdnilai adalah sama untuk semua referensi UDF dalam query, dan di semua baris. Tapi, hasil hash sama untuk string input yang sama dan berbeda untuk string input yang berbeda.

    Sementara saya tidak melihat hasil yang salah dalam pengujian saya, tidakkah ini meningkatkan kemungkinan kondisi balapan? Jika kunci kamus sama untuk semua objek SQLCLR yang disebut dalam kueri tertentu, maka mereka akan berbagi nilai yang sama atau objek yang disimpan untuk kunci itu, bukan? Intinya adalah, walaupun dianggap bekerja di sini (pada tingkat tertentu, lagi-lagi tampaknya tidak ada banyak peningkatan kinerja, tetapi secara fungsional tidak ada yang rusak), yang tidak memberi saya kepercayaan diri bahwa pendekatan ini akan berhasil dalam skenario lain.

Solomon Rutzky
sumber
11

Ini bukan jawaban tradisional, tapi saya pikir akan sangat membantu untuk mengirim tolok ukur dari beberapa teknik yang disebutkan sejauh ini. Saya menguji pada server inti 96 dengan SQL Server 2017 CU9.

Banyak masalah skalabilitas disebabkan oleh utas bersamaan yang bersaing atas beberapa negara global. Sebagai contoh, pertimbangkan pertengkaran halaman PFS klasik. Ini bisa terjadi jika terlalu banyak utas pekerja perlu memodifikasi halaman yang sama dalam memori. Karena kode menjadi lebih efisien, ia dapat meminta kait lebih cepat. Itu meningkatkan pertikaian. Sederhananya, kode efisien lebih cenderung menyebabkan masalah skalabilitas karena negara global diperebutkan lebih parah. Kode lambat cenderung menyebabkan masalah skalabilitas karena negara global tidak sering diakses.

HASHBYTESskalabilitas sebagian didasarkan pada panjang string input. Teori saya adalah mengapa ini terjadi adalah bahwa akses ke beberapa negara global diperlukan ketika HASHBYTESfungsi dipanggil. Status global yang mudah diamati adalah halaman memori perlu dialokasikan per panggilan pada beberapa versi SQL Server. Yang lebih sulit untuk diamati adalah bahwa ada semacam pertikaian OS. Akibatnya, jika HASHBYTESdipanggil dengan kode lebih jarang maka pertikaian turun. Salah satu cara untuk mengurangi laju kolom. Definisi tabel termasuk dalam kode di bagian bawah. Untuk mengurangi Local Factors ™, saya menggunakan kueri bersamaan yang beroperasi pada tabel yang relatif kecil. Kode tolok ukur cepat saya ada di bagian bawah.HASHBYTES panggilan adalah dengan meningkatkan jumlah pekerjaan hashing yang dibutuhkan per panggilan. Pekerjaan hashing sebagian didasarkan pada panjang string input. Untuk mereproduksi masalah skalabilitas yang saya lihat di aplikasi saya perlu mengubah data demo. Skenario terburuk yang masuk akal adalah tabel dengan 21BIGINTMAXDOP 1

Perhatikan fungsi mengembalikan panjang hash yang berbeda. MD5dan SpookyHashkeduanya hash 128 bit, SHA256adalah hash 256 bit.

HASIL ( NVARCHARvs VARBINARYkonversi dan gabungan)

Untuk mengetahui apakah mengonversikan ke, dan menggabungkan, VARBINARYbenar-benar lebih efisien / berkinerja daripada NVARCHAR, NVARCHARversi RUN_HASHBYTES_SHA2_256prosedur yang tersimpan dibuat dari templat yang sama (lihat "Langkah 5" di bagian KODE BENCHMARKING CODE di bawah). Satu-satunya perbedaan adalah:

  1. Nama Prosedur yang Disimpan berakhir dengan _NVC
  2. BINARY(8)untuk CASTfungsi diubah menjadiNVARCHAR(15)
  3. 0x7C diubah menjadi N'|'

Yang menghasilkan:

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

dari pada:

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

Tabel di bawah ini berisi jumlah hash yang dilakukan dalam 1 menit. Tes dilakukan pada server yang berbeda dari yang digunakan untuk tes lain yang disebutkan di bawah ini.

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

Melihat rata-rata saja, kita dapat menghitung manfaat beralih ke VARBINARY:

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

Itu kembali:

IncreaseAmount:    2400000.0
IncreasePercentage:   23.415

HASIL (algoritme dan implementasi hash)

Tabel di bawah ini berisi jumlah hash yang dilakukan dalam 1 menit. Misalnya, menggunakan CHECKSUMdengan 84 kueri bersamaan menghasilkan lebih dari 2 miliar hash dilakukan sebelum waktu habis.

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

Jika Anda lebih suka melihat angka yang sama diukur dalam hal kerja per utas-detik:

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

Beberapa pemikiran cepat tentang semua metode:

  • CHECKSUM: skalabilitas yang sangat baik seperti yang diharapkan
  • HASHBYTES: masalah skalabilitas mencakup satu alokasi memori per panggilan dan sejumlah besar CPU yang dihabiskan di OS
  • Spooky: skalabilitas yang sangat baik
  • Spooky LOB: spinlock SOS_SELIST_SIZED_SLOCKberputar di luar kendali. Saya menduga ini adalah masalah umum dengan melewatkan LOB melalui fungsi CLR, tapi saya tidak yakin
  • Util_HashBinary: sepertinya akan terkena spinlock yang sama. Saya belum melihat sejauh ini karena mungkin tidak banyak yang bisa saya lakukan tentang hal ini:

putar kunci Anda

  • Util_HashBinary 8k: hasil yang sangat mengejutkan, tidak yakin apa yang terjadi di sini

Hasil akhir diuji pada server yang lebih kecil:

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

KODE BENCHMARKING

SETUP 1: Tabel dan Data

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

SETUP 2: Proc Master Eksekusi

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

SETUP 3: Proc Detection Collision

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

SETUP 4: Pembersihan (DROP Semua Procs Tes)

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

SETUP 5: Hasilkan Procs Tes

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

UJI 1: Periksa Tabrakan

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;

TEST 2: Jalankan Tes Kinerja

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;

MASALAH VALIDASI UNTUK MENYELESAIKAN

Sementara berfokus pada pengujian kinerja SQLCLR UDF tunggal, dua masalah yang dibahas sejak awal tidak dimasukkan ke dalam tes, tetapi idealnya harus diselidiki untuk menentukan pendekatan mana yang memenuhi semua persyaratan.

  1. Fungsi ini akan dieksekusi dua kali per setiap kueri (satu kali untuk baris impor, dan satu kali untuk baris saat ini). Tes sejauh ini hanya merujuk UDF satu kali dalam pertanyaan tes. Faktor ini mungkin tidak mengubah peringkat opsi, tetapi tidak boleh diabaikan, untuk berjaga-jaga.
  2. Dalam komentar yang telah dihapus, Paul White menyebutkan:

    Satu kelemahan dari penggantian HASHBYTESdengan fungsi skalar CLR - tampaknya fungsi CLR tidak dapat menggunakan mode batch sedangkan HASHBYTESbisa. Itu mungkin penting, kinerja-bijaksana.

    Jadi itu adalah sesuatu yang perlu dipertimbangkan, dan jelas membutuhkan pengujian. Jika opsi SQLCLR tidak memberikan manfaat apa pun atas built-in HASHBYTES, maka itu menambah bobot saran Solomon untuk menangkap hash yang ada (untuk setidaknya tabel terbesar) ke dalam tabel terkait.

Joe Obbish
sumber
6

Anda mungkin dapat meningkatkan kinerja, dan mungkin skalabilitas semua pendekatan .NET dengan menyatukan dan menyimpan semua objek yang dibuat dalam pemanggilan fungsi. EG untuk kode Paul White di atas:

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 mencegah dan mencoba untuk mencegah menggunakan variabel statis / bersama, tetapi itu akan membiarkan Anda menggunakan variabel bersama jika Anda menandai mereka sebagai hanya dibaca. Yang, tentu saja, tidak ada artinya karena Anda hanya dapat menetapkan satu contoh dari beberapa jenis yang bisa berubah, seperti ConcurrentDictionary.

David Browne - Microsoft
sumber
menarik ... apakah utas ini aman jika menggunakan instance yang sama berulang kali? Saya tahu bahwa hash yang dikelola memiliki Clear()metode tetapi saya belum melihat sejauh itu ke Spooky.
Solomon Rutzky
@ PaulWhite dan David. Saya bisa saja melakukan sesuatu yang salah, atau bisa jadi perbedaan antara SHA256Manageddan SpookyHashV2, tetapi saya mencoba ini dan tidak melihat banyak, jika ada, peningkatan kinerja. Saya juga memperhatikan bahwa ManagedThreadIdnilainya sama untuk semua referensi SQLCLR dalam permintaan tertentu. Saya menguji beberapa referensi ke fungsi yang sama, serta referensi ke fungsi yang berbeda, ketiganya diberi nilai input yang berbeda, dan mengembalikan nilai pengembalian yang berbeda (tetapi diharapkan). Bukankah ini meningkatkan peluang kondisi balapan? Agar adil, dalam pengujian saya, saya tidak melihat apa pun.
Solomon Rutzky