Beberapa diskusi menarik selalu berkembang seputar topik pemisahan string. Dalam dua posting blog sebelumnya, "Pisahkan string dengan cara yang benar – atau cara terbaik berikutnya" dan "Splitting Strings:A Follow-Up," Saya harap saya telah menunjukkan bahwa mengejar fungsi split T-SQL "berkinerja terbaik" tidak ada gunanya . Saat pemisahan benar-benar diperlukan, CLR selalu menang, dan opsi terbaik berikutnya dapat bervariasi tergantung pada tugas aktual yang dihadapi. Tetapi dalam posting tersebut saya mengisyaratkan bahwa pemisahan di sisi database mungkin tidak diperlukan sejak awal.
SQL Server 2008 memperkenalkan parameter bernilai tabel, cara untuk melewatkan "tabel" dari aplikasi ke prosedur tersimpan tanpa harus membangun dan mengurai string, membuat serial ke XML, atau menangani metodologi pemisahan ini. Jadi saya pikir saya akan memeriksa bagaimana metode ini dibandingkan dengan pemenang tes kami sebelumnya – karena ini mungkin merupakan opsi yang layak, apakah Anda dapat menggunakan CLR atau tidak. (Untuk alkitab utama tentang TVP, silakan lihat artikel komprehensif rekan SQL Server MVP Erland Sommarskog.)
Ujian
Untuk tes ini saya akan berpura-pura kita berurusan dengan satu set string versi. Bayangkan sebuah aplikasi C# yang melewati sekumpulan string ini (misalnya, yang telah dikumpulkan dari sekumpulan pengguna) dan kita perlu mencocokkan versi dengan sebuah tabel (misalnya, yang menunjukkan rilis layanan yang berlaku untuk kumpulan tertentu dari versi). Jelas aplikasi nyata akan memiliki lebih banyak kolom dari ini, tetapi hanya untuk membuat beberapa volume dan tetap menjaga tabel tetap kurus (saya juga menggunakan NVARCHAR di seluruh karena itulah yang dibutuhkan fungsi split CLR dan saya ingin menghilangkan ambiguitas karena konversi implisit) :
CREATE TABLE dbo.VersionStrings(left_post NVARCHAR(5), right_post NVARCHAR(5)); CREATE CLUSTERED INDEX x ON dbo.VersionStrings(left_post, right_post); ;WITH x AS ( SELECT lp = CONVERT(DECIMAL(4,3), RIGHT(RTRIM(s1.[object_id]), 3)/1000.0) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 ) INSERT dbo.VersionStrings ( left_post, right_post ) SELECT lp - CASE WHEN lp >= 0.9 THEN 0.1 ELSE 0 END, lp + (0.1 * CASE WHEN lp >= 0.9 THEN -1 ELSE 1 END) FROM x;
Sekarang setelah data berada di tempatnya, hal berikutnya yang perlu kita lakukan adalah membuat tipe tabel yang ditentukan pengguna yang dapat menampung serangkaian string. Jenis tabel awal untuk menampung string ini cukup sederhana:
CREATE TYPE dbo.VersionStringsTVP AS TABLE (VersionString NVARCHAR(5));
Kemudian kita memerlukan beberapa prosedur tersimpan untuk menerima daftar dari C#. Untuk mempermudah, sekali lagi, kami hanya akan menghitung sehingga kami dapat memastikan untuk melakukan pemindaian lengkap, dan kami akan mengabaikan hitungan dalam aplikasi:
CREATE PROCEDURE dbo.SplitTest_UsingCLR @list NVARCHAR(MAX) AS BEGIN SET NOCOUNT ON; SELECT c = COUNT(*) FROM dbo.VersionStrings AS v INNER JOIN dbo.SplitStrings_CLR(@list, N',') AS s ON s.Item BETWEEN v.left_post AND v.right_post; END GO CREATE PROCEDURE dbo.SplitTest_UsingTVP @list dbo.VersionStringsTVP READONLY AS BEGIN SET NOCOUNT ON; SELECT c = COUNT(*) FROM dbo.VersionStrings AS v INNER JOIN @list AS l ON l.VersionString BETWEEN v.left_post AND v.right_post; END GO
Perhatikan bahwa TVP yang diteruskan ke prosedur tersimpan harus ditandai sebagai READONLY – saat ini tidak ada cara untuk melakukan DML pada data seperti yang Anda lakukan untuk variabel tabel atau tabel temp. Namun, Erland telah mengajukan permintaan yang sangat populer agar Microsoft membuat parameter ini lebih fleksibel (dan banyak wawasan yang lebih dalam di balik argumennya di sini).
Keindahan di sini adalah bahwa SQL Server tidak lagi harus berurusan dengan pemisahan string sama sekali – baik dalam T-SQL maupun dalam menyerahkannya ke CLR – karena sudah dalam struktur yang ditetapkan di mana ia unggul.
Selanjutnya, aplikasi konsol C# yang melakukan hal berikut:
- Menerima angka sebagai argumen untuk menunjukkan berapa banyak elemen string yang harus didefinisikan
- Membuat string CSV dari elemen tersebut, menggunakan StringBuilder, untuk diteruskan ke prosedur tersimpan CLR
- Membuat DataTable dengan elemen yang sama untuk diteruskan ke prosedur tersimpan TVP
- Juga menguji overhead untuk mengonversi string CSV ke DataTable dan sebaliknya sebelum memanggil prosedur tersimpan yang sesuai
Kode untuk aplikasi C# terdapat di akhir artikel. Saya bisa mengeja C#, tapi saya sama sekali bukan seorang guru; Saya yakin ada ketidakefisienan yang dapat Anda temukan di sana yang mungkin membuat kinerja kode sedikit lebih baik. Namun perubahan seperti itu akan memengaruhi seluruh rangkaian pengujian dengan cara yang serupa.
Saya menjalankan aplikasi 10 kali menggunakan 100, 1.000, 2.500 dan 5.000 elemen. Hasilnya adalah sebagai berikut (ini menunjukkan durasi rata-rata, dalam detik, di 10 tes):
Selain Performa…
Selain perbedaan kinerja yang jelas, TVP memiliki keunggulan lain – jenis tabel jauh lebih sederhana untuk diterapkan daripada rakitan CLR, terutama di lingkungan di mana CLR dilarang karena alasan lain. Saya berharap hambatan terhadap CLR secara bertahap menghilang, dan alat baru membuat penerapan dan pemeliharaan tidak terlalu menyakitkan, tetapi saya ragu kemudahan penerapan awal untuk CLR akan lebih mudah daripada pendekatan asli.
Di sisi lain, di atas batasan hanya-baca, tipe tabel seperti tipe alias yang sulit dimodifikasi setelahnya. Jika Anda ingin mengubah ukuran kolom atau menambahkan kolom, tidak ada perintah ALTER TYPE, dan untuk menjatuhkan jenis dan membuatnya kembali, Anda harus terlebih dahulu menghapus referensi jenis dari semua prosedur yang menggunakannya . Jadi misalnya dalam kasus di atas jika kita perlu meningkatkan kolom VersionString ke NVARCHAR(32), kita harus membuat tipe dummy dan mengubah prosedur tersimpan (dan prosedur lain yang menggunakannya):
CREATE TYPE dbo.VersionStringsTVPCopy AS TABLE (VersionString NVARCHAR(32)); GO ALTER PROCEDURE dbo.SplitTest_UsingTVP @list dbo.VersionStringsTVPCopy READONLY AS ... GO DROP TYPE dbo.VersionStringsTVP; GO CREATE TYPE dbo.VersionStringsTVP AS TABLE (VersionString NVARCHAR(32)); GO ALTER PROCEDURE dbo.SplitTest_UsingTVP @list dbo.VersionStringsTVP READONLY AS ... GO DROP TYPE dbo.VersionStringsTVPCopy; GO
(Atau sebagai alternatif, hapus prosedur, hapus jenisnya, buat ulang jenisnya, dan buat ulang prosedurnya.)
Kesimpulan
Metode TVP secara konsisten mengungguli metode pemisahan CLR, dan dengan persentase yang lebih besar seiring dengan bertambahnya jumlah elemen. Bahkan menambahkan overhead untuk mengubah string CSV yang ada ke DataTable menghasilkan kinerja ujung-ke-ujung yang jauh lebih baik. Jadi saya harap, jika saya belum meyakinkan Anda untuk meninggalkan teknik pemisahan string T-SQL Anda demi CLR, saya telah mendesak Anda untuk mencoba parameter bernilai tabel. Seharusnya mudah untuk diuji meskipun saat ini Anda tidak menggunakan DataTable (atau yang setara).
Kode C# yang Digunakan Untuk Tes Ini
Seperti yang saya katakan, saya bukan guru C#, jadi mungkin ada banyak hal naif yang saya lakukan di sini, tetapi metodologinya harus cukup jelas.
using System; using System.IO; using System.Data; using System.Data.SqlClient; using System.Text; using System.Collections; namespace SplitTester { class SplitTester { static void Main(string[] args) { DataTable dt_pure = new DataTable(); dt_pure.Columns.Add("Item", typeof(string)); StringBuilder sb_pure = new StringBuilder(); Random r = new Random(); for (int i = 1; i <= Int32.Parse(args[0]); i++) { String x = r.NextDouble().ToString().Substring(0,5); sb_pure.Append(x).Append(","); dt_pure.Rows.Add(x); } using ( SqlConnection conn = new SqlConnection(@"Data Source=.; Trusted_Connection=yes;Initial Catalog=Splitter") ) { conn.Open(); // four cases: // (1) pass CSV string directly to CLR split procedure // (2) pass DataTable directly to TVP procedure // (3) serialize CSV string from DataTable and pass CSV to CLR procedure // (4) populate DataTable from CSV string and pass DataTable to TCP procedure // ********** (1) ********** // write(Environment.NewLine + "Starting (1)"); SqlCommand c1 = new SqlCommand("dbo.SplitTest_UsingCLR", conn); c1.CommandType = CommandType.StoredProcedure; c1.Parameters.AddWithValue("@list", sb_pure.ToString()); c1.ExecuteNonQuery(); c1.Dispose(); write("Finished (1)"); // ********** (2) ********** // write(Environment.NewLine + "Starting (2)"); SqlCommand c2 = new SqlCommand("dbo.SplitTest_UsingTVP", conn); c2.CommandType = CommandType.StoredProcedure; SqlParameter tvp1 = c2.Parameters.AddWithValue("@list", dt_pure); tvp1.SqlDbType = SqlDbType.Structured; c2.ExecuteNonQuery(); c2.Dispose(); write("Finished (2)"); // ********** (3) ********** // write(Environment.NewLine + "Starting (3)"); StringBuilder sb_fake = new StringBuilder(); foreach (DataRow dr in dt_pure.Rows) { sb_fake.Append(dr.ItemArray[0].ToString()).Append(","); } SqlCommand c3 = new SqlCommand("dbo.SplitTest_UsingCLR", conn); c3.CommandType = CommandType.StoredProcedure; c3.Parameters.AddWithValue("@list", sb_fake.ToString()); c3.ExecuteNonQuery(); c3.Dispose(); write("Finished (3)"); // ********** (4) ********** // write(Environment.NewLine + "Starting (4)"); DataTable dt_fake = new DataTable(); dt_fake.Columns.Add("Item", typeof(string)); string[] list = sb_pure.ToString().Split(','); for (int i = 0; i < list.Length; i++) { if (list[i].Length > 0) { dt_fake.Rows.Add(list[i]); } } SqlCommand c4 = new SqlCommand("dbo.SplitTest_UsingTVP", conn); c4.CommandType = CommandType.StoredProcedure; SqlParameter tvp2 = c4.Parameters.AddWithValue("@list", dt_fake); tvp2.SqlDbType = SqlDbType.Structured; c4.ExecuteNonQuery(); c4.Dispose(); write("Finished (4)"); } } static void write(string msg) { Console.WriteLine(msg + ": " + DateTime.UtcNow.ToString("HH:mm:ss.fffff")); } } }