Bulan lalu, saya membahas teka-teki yang melibatkan pencocokan setiap baris dari satu meja dengan pertandingan terdekat dari meja lain. Saya mendapatkan teka-teki ini dari Karen Ly, Analis Pendapatan Tetap Jr di RBC. Saya membahas dua solusi relasional utama yang menggabungkan operator APPLY dengan subkueri berbasis TOP. Solusi 1 selalu memiliki penskalaan kuadrat. Solusi 2 cukup baik ketika dilengkapi dengan indeks pendukung yang baik, tetapi tanpa indeks tersebut juga memiliki skala kuadrat. Dalam artikel ini saya membahas solusi berulang, yang meskipun umumnya tidak disukai oleh pro SQL, memberikan penskalaan yang jauh lebih baik dalam kasus kami bahkan tanpa pengindeksan yang optimal.
Tantangan
Sebagai pengingat cepat, tantangan kami melibatkan tabel yang disebut T1 dan T2, yang Anda buat dengan kode berikut:
SET NOCOUNT AKTIF; JIKA DB_ID('testdb') NULL CREATE DATABASE testdb; PERGI GUNAKAN testdb; DROP TABLE JIKA ADA dbo.T1, dbo.T2; CREATE TABLE dbo.T1 ( keycol INT NOT NULL IDENTITY CONSTRAINT PK_T1 PRIMARY KEY, val INT NOT NULL, othercols BINARY(100) NOT NULL CONSTRAINT DFT_T1_col1 DEFAULT(0xAA) ); CREATE TABLE dbo.T2 ( keycol INT NOT NULL IDENTITY CONSTRAINT PK_T2 PRIMARY KEY, val INT NOT NULL, othercols BINARY(100) NOT NULL CONSTRAINT DFT_T2_col1 DEFAULT(0xBB) );
Anda kemudian menggunakan kode berikut untuk mengisi tabel dengan kumpulan kecil data sampel untuk memeriksa kebenaran solusi Anda:
TRUNCATE TABLE dbo.T1; TRUNCATE TABEL dbo.T2; MASUKKAN KE dbo.T1 (val) NILAI(1),(1),(3),(3),(5),(8),(13),(16),(18),(20),( 21); MASUKKAN KE dbo.T2 (val) NILAI(2),(2),(7),(3),(3),(11),(11),(13),(17),(19);Ingat tantangannya adalah untuk mencocokkan setiap baris dari T1 baris dari T2 di mana perbedaan mutlak antara T2.val dan T1.val adalah yang terendah. Dalam kasus ikatan, Anda seharusnya menggunakan val ascending, keycol urutan menaik sebagai tiebreaker.
Inilah hasil yang diinginkan untuk data sampel yang diberikan:
keycol1 val1 othercols1 keycol2 val2 othercols2 ----------- ----------- ---------- --------- -- ----------- ---------- 1 1 0xAA 1 2 0xBB 2 1 0xAA 1 2 0xBB 3 3 0xAA 4 3 0xBB 4 3 0xAA 4 3 0xBB 5 5 0xAA 4 3 0xBB 6 8 0xAA 3 7 0xBB 7 13 0xAA 8 13 0xBB 8 16 0xAA 9 17 0xBB 9 18 0xAA 9 17 0xBB 10 20 0xAA 10 19 0xBB 11 21 0xAA 10 19 0xBBUntuk memeriksa kinerja solusi Anda, Anda memerlukan kumpulan data sampel yang lebih besar. Anda terlebih dahulu membuat fungsi pembantu GetNums, yang menghasilkan urutan bilangan bulat dalam rentang yang diminta, menggunakan kode berikut:
DROP FUNCTION JIKA ADA dbo.GetNums; GO CREATE OR ALTER FUNCTION dbo.GetNums(@low AS BIGINT, @high AS BIGINT) MENGEMBALIKAN TABEL SEBAGAI RETURN DENGAN L0 AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)), L1 AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B), L2 AS (PILIH 1 AS c FROM L1 AS A CROSS JOIN L1 AS B), L3 AS (PILIH 1 AS c FROM L2 AS A CROSS JOIN L2 AS B), L4 AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B), L5 AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B), Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL) ) AS rownum FROM L5) SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n FROM Nums ORDER BY rownum; PERGIAnda kemudian mengisi T1 dan T2 menggunakan kode berikut, menyesuaikan parameter yang menunjukkan jumlah baris dan nilai maksimum berdasarkan kebutuhan Anda:
MENYATAKAN @numrowsT1 SEBAGAI INT =1000000, @maxvalT1 SEBAGAI INT =10000000, @numrowsT2 SEBAGAI INT =1000000, @maxvalT2 SEBAGAI INT =10000000; TRUNCATE TABEL dbo.T1; TRUNCATE TABEL dbo.T2; INSERT INTO dbo.T1 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT1 + 1 AS val FROM dbo.GetNums(1, @numrowsT1) AS Nums; INSERT INTO dbo.T2 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT2 + 1 AS val FROM dbo.GetNums(1, @numrowsT2) AS Nums;Dalam contoh ini, Anda mengisi tabel dengan masing-masing 1.000.000 baris, dengan nilai dalam rentang 1 – 10.000.000 di kolom val (kepadatan rendah).
Solusi 3, menggunakan kursor dan variabel tabel berbasis disk
Solusi iteratif yang efisien untuk tantangan kecocokan terdekat kami didasarkan pada algoritme yang mirip dengan algoritme Gabung gabung. Idenya adalah untuk menerapkan hanya satu pass berurutan terhadap setiap tabel menggunakan kursor, mengevaluasi elemen urutan dan tiebreak di setiap putaran untuk memutuskan sisi mana yang akan maju, dan mencocokkan baris di sepanjang jalan.
Pass yang dipesan terhadap setiap tabel pasti akan mendapat manfaat dari indeks pendukung, tetapi implikasi dari tidak memilikinya adalah bahwa penyortiran eksplisit akan terjadi. Ini berarti bahwa bagian penyortiran akan menimbulkan n log n penskalaan, tetapi itu jauh lebih ringan daripada penskalaan kuadrat yang Anda dapatkan dari Solusi 2 dalam keadaan serupa.
Juga, kinerja Solusi 1 dan 2 dipengaruhi oleh kepadatan kolom val. Dengan kepadatan yang lebih tinggi, rencana tersebut menerapkan lebih sedikit rebind. Sebaliknya, karena solusi iteratif hanya melakukan satu lintasan terhadap setiap masukan, kepadatan kolom val bukanlah faktor yang mempengaruhi kinerja.
Gunakan kode berikut untuk membuat indeks pendukung:
BUAT INDEKS idx_val_key PADA dbo.T1(val, keycol) INCLUDE(othercols); BUAT INDEKS idx_val_key PADA dbo.T2(val, keycol) INCLUDE(othercols);Pastikan Anda menguji solusi dengan dan tanpa indeks ini.
Berikut kode lengkap untuk Solusi 3:
SET NOCOUNT AKTIF; MULAI TRANS; MENYATAKAN @keycol1 SEBAGAI INT, @val1 SEBAGAI INT, @othercols1 SEBAGAI BINARY(100), @keycol2 SEBAGAI INT, @val2 SEBAGAI INT, @othercols2 SEBAGAI BINARY(100), @prevkeycol2 SEBAGAI INT, @prevval2 SEBAGAI INT, @prevothercols2 SEBAGAI BINARY(100), @C1 SEBAGAI KURSOR, @C2 SEBAGAI KURSOR, @C1fetch_status SEBAGAI INT, @C2fetch_status SEBAGAI INT; MENYATAKAN @Result SEBAGAI TABEL ( keycol1 INT NOT NULL PRIMARY KEY, val1 INT NOT NULL, othercols1 BINARY(100) NOT NULL, keycol2 INT NULL, val2 INT NULL, othercols2 BINARY(100) NULL ); SET @C1 =CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT keycol, val, othercols FROM dbo.T1 ORDER BY val, keycol; SET @C2 =CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT keycol, val, othercols FROM dbo.T2 ORDER BY val, keycol; BUKA @C1; BUKA @C2; TAMBAHKAN BERIKUTNYA DARI @C2 KE @keycol2, @val2, @othercols2; SET @C2fetch_status =@@fetch_status; PILIH @prevkeycol2 =@keycol2, @prevval2 =@val2, @prevothercols2 =@othercols2; TAMBAHKAN BERIKUTNYA DARI @C1 KE @keycol1, @val1, @othercols1; SET @C1fetch_status =@@fetch_status; WHILE @C1fetch_status =0 BEGIN JIKA @val1 <=@val2 ATAU @C2fetch_status <> 0 BEGIN JIKA ABS(@val1 - @val2)@prevval2 PILIH @prevkeycol2 =@keycol2, @prevval2 =@val2, @prevothercols2 =@othercols2; TAMBAHKAN BERIKUTNYA DARI @C2 KE @keycol2, @val2, @othercols2; SET @C2fetch_status =@@fetch_status; AKHIR; AKHIR; SELECT keycol1, val1, SUBSTRING(othercols1, 1, 1) AS othercols1, keycol2, val2, SUBSTRING(othercols2, 1, 1) AS othercols2 FROM @Result; KOMITKAN TRANS; Kode menggunakan variabel tabel yang disebut @Result untuk menyimpan kecocokan dan akhirnya mengembalikannya dengan menanyakan variabel tabel. Perhatikan bahwa kode melakukan pekerjaan dalam satu transaksi untuk mengurangi pencatatan.
Kode menggunakan variabel kursor yang disebut @C1 dan @C2 untuk beralih melalui baris di T1 dan T2, masing-masing, dalam kedua kasus yang diurutkan berdasarkan val, keycol. Variabel lokal digunakan untuk menyimpan nilai baris saat ini dari setiap kursor (@keycol1, @val1 dan @othercols1 untuk @C1, dan @keycol2, @val2 dan @othercols2 untuk @C2). Variabel lokal tambahan menyimpan nilai baris sebelumnya dari @C2 (@prevkeycol2, @prevval2 dan @prevothercols2). Variabel @C1fetch_status dan @C2fetch_status menyimpan status pengambilan terakhir dari masing-masing kursor.
Setelah mendeklarasikan dan membuka kedua kursor, kode mengambil baris dari setiap kursor ke variabel lokal masing-masing, dan awalnya menyimpan nilai baris saat ini dari @C2 juga di variabel baris sebelumnya. Kode kemudian memasuki loop yang terus berjalan saat pengambilan terakhir dari @C1 berhasil (@C1fetch_status =0). Tubuh loop menerapkan kode semu berikut di setiap putaran:
Jika @val1 <=@val2 atau mencapai akhir @C2 Mulai Jika perbedaan mutlak antara @val1 dan @val2 kurang dari antara @val1 dan @prevval2 Tambahkan baris ke @Result dengan nilai baris saat ini dari @C1 dan baris saat ini nilai dari @C2 Lain Tambahkan baris ke @Hasil dengan nilai baris saat ini dari @C1 dan nilai baris sebelumnya dari @C2 Ambil baris berikutnya dari @C1 Akhiri Lain jika pengambilan terakhir dari @C2 berhasil Mulai Jika @val2> @prevval2 Setel variabel yang ditahan Nilai baris @C2 sebelumnya ke nilai variabel baris saat ini Ambil baris berikutnya dari @C2 AkhirKode kemudian hanya menanyakan variabel tabel @Result untuk mengembalikan semua kecocokan.
Menggunakan kumpulan besar data sampel (1.000.000 baris di setiap tabel), dengan pengindeksan optimal, solusi ini membutuhkan waktu 38 detik untuk diselesaikan di sistem saya, dan melakukan 28.240 pembacaan logis. Tentu saja, penskalaan solusi ini kemudian linier. Tanpa pengindeksan yang optimal, butuh 40 detik untuk menyelesaikannya (hanya 2 detik ekstra!), Dan melakukan 29.519 pembacaan logis. Bagian pengurutan dalam solusi ini memiliki n log n penskalaan.
Solusi 4, menggunakan kursor dan variabel tabel yang dioptimalkan memori
Dalam upaya meningkatkan kinerja pendekatan berulang, satu hal yang dapat Anda coba adalah mengganti penggunaan variabel tabel berbasis disk dengan variabel yang dioptimalkan memori. Karena solusinya melibatkan penulisan 1.000.000 baris ke variabel tabel, ini dapat menghasilkan peningkatan yang tidak dapat diabaikan.
Pertama, Anda perlu mengaktifkan In-Memory OLTP di database dengan membuat filegroup yang ditandai sebagai CONTAINS MEMORY_OPTIMIZED_DATA, dan di dalamnya ada wadah yang menunjuk ke folder di sistem file. Dengan asumsi Anda membuat folder induk yang disebut C:\IMOLTP\, gunakan kode berikut untuk menerapkan dua langkah ini:
ALTER DATABASE testdb ADD FILEGROUP testdb_MO BERISI MEMORY_OPTIMIZED_DATA; ALTER DATABASE testdb ADD FILE ( NAME =testdb_dir, FILENAME ='C:\IMOLTP\testdb_dir' ) KE FILEGROUP testdb_MO;Langkah selanjutnya adalah membuat tipe tabel yang dioptimalkan memori sebagai template untuk variabel tabel kita dengan menjalankan kode berikut:
DROP TYPE JIKA ADA dbo.TYPE_closestmatch; GO CREATE TYPE dbo.TYPE_closestmatch SEBAGAI TABEL ( keycol1 INT NOT NULL PRIMARY KEY NONCLUSTERED, val1 INT NOT NULL, othercols1 BINARY(100) NOT NULL, keycol2 INT NULL, val2 INT NULL, othercols2 BINARY =_OPTIMMENULLIZED WITH (100) ON );Kemudian alih-alih deklarasi asli dari variabel tabel @Result, Anda akan menggunakan kode berikut:
MENYATAKAN @Result SEBAGAI dbo.TYPE_closestmatch;Berikut kode solusi lengkapnya:
SET NOCOUNT AKTIF; GUNAKAN testdb; MULAI TRANS; MENYATAKAN @keycol1 SEBAGAI INT, @val1 SEBAGAI INT, @othercols1 SEBAGAI BINARY(100), @keycol2 SEBAGAI INT, @val2 SEBAGAI INT, @othercols2 SEBAGAI BINARY(100), @prevkeycol2 SEBAGAI INT, @prevval2 SEBAGAI INT, @prevothercols2 SEBAGAI BINARY(100), @C1 SEBAGAI KURSOR, @C2 SEBAGAI KURSOR, @C1fetch_status SEBAGAI INT, @C2fetch_status SEBAGAI INT; MENYATAKAN @Hasil SEBAGAI dbo.TYPE_closestmatch; SET @C1 =CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT keycol, val, othercols FROM dbo.T1 ORDER BY val, keycol; SET @C2 =CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT keycol, val, othercols FROM dbo.T2 ORDER BY val, keycol; BUKA @C1; BUKA @C2; TAMBAHKAN BERIKUTNYA DARI @C2 KE @keycol2, @val2, @othercols2; SET @C2fetch_status =@@fetch_status; PILIH @prevkeycol2 =@keycol2, @prevval2 =@val2, @prevothercols2 =@othercols2; TAMBAHKAN BERIKUTNYA DARI @C1 KE @keycol1, @val1, @othercols1; SET @C1fetch_status =@@fetch_status; WHILE @C1fetch_status =0 BEGIN JIKA @val1 <=@val2 ATAU @C2fetch_status <> 0 BEGIN JIKA ABS(@val1 - @val2)@prevval2 PILIH @prevkeycol2 =@keycol2, @prevval2 =@val2, @prevothercols2 =@othercols2; TAMBAHKAN BERIKUTNYA DARI @C2 KE @keycol2, @val2, @othercols2; SET @C2fetch_status =@@fetch_status; AKHIR; AKHIR; SELECT keycol1, val1, SUBSTRING(othercols1, 1, 1) AS othercols1, keycol2, val2, SUBSTRING(othercols2, 1, 1) AS othercols2 FROM @Result; KOMITKAN TRANS; Dengan pengindeksan optimal di tempat, solusi ini membutuhkan waktu 27 detik untuk diselesaikan di mesin saya (dibandingkan dengan 38 detik dengan variabel tabel berbasis disk), dan tanpa pengindeksan optimal, butuh 29 detik untuk menyelesaikannya (dibandingkan dengan 40 detik). Itu hampir 30 persen pengurangan waktu berjalan.
Solusi 5, menggunakan SQL CLR
Cara lain untuk lebih meningkatkan kinerja pendekatan iteratif adalah dengan mengimplementasikan solusi menggunakan SQL CLR, mengingat bahwa sebagian besar overhead solusi T-SQL disebabkan oleh inefisiensi pengambilan kursor dan pengulangan di T-SQL.
Berikut kode solusi lengkap yang mengimplementasikan algoritme yang sama yang saya gunakan di Solusi 3 dan 4 dengan C#, menggunakan objek SqlDataReader alih-alih kursor T-SQL:
menggunakan Sistem; menggunakan System.Data; menggunakan System.Data.SqlClient; menggunakan System.Data.SqlTypes; menggunakan Microsoft.SqlServer.Server; public partial class ClosestMatch { [SqlProcedure] public static void GetClosestMatches() { menggunakan (SqlConnection conn =new SqlConnection("data source=MyServer\\MyInstance;Database=testdb;Trusted_Connection=True;MultipleActiveResultSets=true;")) { SqlCommand =new SqlCommand(); SqlCommand comm2 =SqlCommand baru(); comm1.Koneksi =samb; comm2.Koneksi =samb; comm1.CommandText ="PILIH keycol, val, othercols FROM dbo.T1 ORDER BY val, keycol;"; comm2.CommandText ="PILIH keycol, val, othercols FROM dbo.T2 ORDER BY val, keycol;"; SqlMetaData[] kolom =SqlMetaData[6] baru; kolom[0] =new SqlMetaData("keycol1", SqlDbType.Int); kolom[1] =new SqlMetaData("val1", SqlDbType.Int); kolom[2] =new SqlMetaData("othercols1", SqlDbType.Binary, 100); kolom[3] =new SqlMetaData("keycol2", SqlDbType.Int); kolom[4] =new SqlMetaData("val2", SqlDbType.Int); kolom[5] =new SqlMetaData("othercols2", SqlDbType.Binary, 100); SqlDataRecord record =new SqlDataRecord(kolom); SqlContext.Pipe.SendResultsStart(rekam); samb.Buka(); SqlDataReader reader1 =comm1.ExecuteReader(); SqlDataReader reader2 =comm2.ExecuteReader(); SqlInt32 keycol1 =SqlInt32.Null; SqlInt32 val1 =SqlInt32.Null; SqlBinary othercols1 =SqlBinary.Null; SqlInt32 keycol2 =SqlInt32.Null; SqlInt32 val2 =SqlInt32.Null; SqlBinary othercols2 =SqlBinary.Null; SqlInt32 prevkeycol2 =SqlInt32.Null; SqlInt32 prevval2 =SqlInt32.Null; SqlBinary prevothercols2 =SqlBinary.Null; Boolean reader2foundrow =reader2.Read(); if (reader2foundrow) { keycol2 =reader2.GetSqlInt32(0); val2 =reader2.GetSqlInt32(1); othercols2 =reader2.GetSqlBinary(2); prevkeycol2 =keycol2; prevval2 =val2; prevothercols2 =kolom lain2; } Boolean reader1foundrow =reader1.Read(); if (reader1foundrow) { keycol1 =reader1.GetSqlInt32(0); val1 =reader1.GetSqlInt32(1); othercols1 =reader1.GetSqlBinary(2); } while (reader1foundrow) { if (val1 <=val2 || !reader2foundrow) { if (Math.Abs((int)(val1 - val2))prevval2) { prevkeycol2 =keycol2; prevval2 =val2; prevothercols2 =kolom lain2; } reader2foundrow =reader2.Read(); if (reader2foundrow) { keycol2 =reader2.GetSqlInt32(0); val2 =reader2.GetSqlInt32(1); othercols2 =reader2.GetSqlBinary(2); } } } SqlContext.Pipe.SendResultsEnd(); } } } Untuk terhubung ke database, Anda biasanya menggunakan opsi "koneksi konteks=true" alih-alih string koneksi penuh. Sayangnya, opsi ini tidak tersedia saat Anda perlu bekerja dengan beberapa kumpulan hasil aktif. Solusi kami mengemulasi pekerjaan paralel dengan dua kursor menggunakan dua objek SqlDataReader, dan oleh karena itu Anda memerlukan string koneksi penuh, dengan opsi MultipleActiveResultSets=true. Berikut string koneksi lengkapnya:
"sumber data=MyServer\\MyInstance;Database=testdb;Trusted_Connection=True;MultipleActiveResultSets=true;"Tentu saja dalam kasus Anda, Anda perlu mengganti MyServer\\MyInstance dengan nama server dan instance Anda (jika relevan).
Selain itu, fakta bahwa Anda tidak menggunakan "koneksi konteks=true" melainkan string koneksi eksplisit berarti bahwa Majelis memerlukan akses ke sumber daya eksternal dan karenanya dapat dipercaya. Biasanya, Anda akan mencapai ini dengan menandatanganinya dengan sertifikat atau kunci asimetris yang memiliki login yang sesuai dengan izin yang tepat, atau memasukkannya ke daftar putih menggunakan prosedur sp_add_trusted_assembly. Demi kesederhanaan, saya akan mengatur opsi database TRUSTWORTHY ke ON, dan menentukan set izin EXTERNAL_ACCESS saat membuat perakitan. Kode berikut menyebarkan solusi dalam database:
EXEC sys.sp_configure 'lanjutan', 1; KONFIGURASI ULANG; EXEC sys.sp_configure 'clr diaktifkan', 1; EXEC sys.sp_configure 'clr ketat keamanan', 0; KONFIGURASI ULANG; EXEC sys.sp_configure 'lanjutan', 0; KONFIGURASI ULANG; ALTER DATABASE testdb SET TRUSTWORTHY ON; GUNAKAN testdb; JATUH PROC JIKA ADA dbo.GetClosestMatches; DROP ASSEMBLY JIKA ADA ClosestMatch; BUAT PERAKITAN ClosestMatch DARI 'C:\ClosestMatch\ClosestMatch\bin\Debug\ClosestMatch.dll' DENGAN PERMISSION_SET =EXTERNAL_ACCESS; BUAT PROSEDUR dbo.GetClosestMatches SEBAGAI NAMA EKSTERNAL ClosestMatch.ClosestMatch.GetClosestMatches;Kode mengaktifkan CLR dalam instance, menonaktifkan opsi keamanan ketat CLR, menyetel opsi database TRUSTWORTHY ke ON, membuat perakitan, dan membuat prosedur GetClosestMatches.
Gunakan kode berikut untuk menguji prosedur tersimpan:
EXEC dbo.GetClosestMatches;Solusi CLR membutuhkan waktu 8 detik untuk diselesaikan di sistem saya dengan pengindeksan optimal, dan 9 detik tanpa pengindeksan. Itu peningkatan kinerja yang cukup mengesankan dibandingkan dengan semua solusi lain — baik relasional maupun iteratif.
Kesimpulan
Solusi berulang biasanya tidak disukai di komunitas SQL karena tidak mengikuti model relasional. Kenyataannya adalah terkadang Anda tidak dapat menciptakan solusi relasional yang berkinerja baik dan kinerja adalah prioritas. Menggunakan pendekatan berulang, Anda tidak terbatas pada algoritme yang dapat diakses oleh pengoptimal SQL Server, tetapi dapat mengimplementasikan algoritme apa pun yang Anda suka. Seperti yang ditunjukkan dalam artikel ini, menggunakan algoritme mirip penggabungan, Anda dapat menyelesaikan tugas dengan satu pass berurutan terhadap setiap input. Menggunakan kursor T-SQL dan variabel tabel berbasis disk, Anda mendapatkan kinerja dan penskalaan yang wajar. Anda dapat meningkatkan kinerja sekitar 30 persen dengan beralih ke variabel tabel yang dioptimalkan memori, dan secara signifikan lebih banyak lagi dengan menggunakan SQL CLR.