Itu adalah hari Selasa setiap bulan – Anda tahu, hari dimana pesta blok blogger yang dikenal sebagai T-SQL Tuesday terjadi. Bulan ini dipandu oleh Russ Thomas (@SQLJudo), dan topiknya adalah, "Memanggil Semua Tuner dan Gear Head." Saya akan menangani masalah yang berhubungan dengan kinerja di sini, meskipun saya mohon maaf bahwa itu mungkin tidak sepenuhnya sejalan dengan pedoman yang ditetapkan Russ dalam undangannya (saya tidak akan menggunakan petunjuk, tanda jejak, atau panduan rencana) .
Di SQLBits minggu lalu, saya memberikan presentasi tentang pemicu, dan teman baik saya dan sesama MVP Erland Sommarskog kebetulan hadir. Pada satu titik saya menyarankan bahwa sebelum membuat pemicu baru di tabel, Anda harus memeriksa untuk melihat apakah ada pemicu yang sudah ada, dan pertimbangkan untuk menggabungkan logika daripada menambahkan pemicu tambahan. Alasan saya terutama untuk pemeliharaan kode, tetapi juga untuk kinerja. Erland bertanya apakah saya pernah menguji untuk melihat apakah ada overhead tambahan dalam mengaktifkan beberapa pemicu untuk tindakan yang sama, dan saya harus mengakui bahwa, tidak, saya tidak melakukan sesuatu yang ekstensif. Jadi saya akan melakukannya sekarang.
Di AdventureWorks2014, saya membuat satu set tabel sederhana yang pada dasarnya mewakili sys.all_objects
(~2.700 baris) dan sys.all_columns
(~ 9.500 baris). Saya ingin mengukur efek beban kerja dari berbagai pendekatan untuk memperbarui kedua tabel – pada dasarnya Anda memiliki pengguna yang memperbarui tabel kolom, dan Anda menggunakan pemicu untuk memperbarui kolom yang berbeda di tabel yang sama, dan beberapa kolom di tabel objek.
- T1:Dasar :Asumsikan bahwa Anda dapat mengontrol semua akses data melalui prosedur tersimpan; dalam hal ini, pembaruan terhadap kedua tabel dapat dilakukan secara langsung, tanpa memerlukan pemicu. (Ini tidak praktis di dunia nyata, karena Anda tidak dapat secara andal melarang akses langsung ke tabel.)
- T2:Pemicu tunggal terhadap tabel lain :Asumsikan bahwa Anda dapat mengontrol pernyataan pembaruan terhadap tabel yang terpengaruh dan menambahkan kolom lain, tetapi pembaruan ke tabel sekunder perlu diterapkan dengan pemicu. Kami akan memperbarui ketiga kolom dengan satu pernyataan.
- T3:Pemicu tunggal terhadap kedua tabel :Dalam hal ini, kami memiliki pemicu dengan dua pernyataan, satu yang memperbarui kolom lainnya di tabel yang terpengaruh, dan satu yang memperbarui ketiga kolom di tabel sekunder.
- T4:Pemicu tunggal terhadap kedua tabel :Seperti T3, tetapi kali ini, kami memiliki pemicu dengan empat pernyataan, satu yang memperbarui kolom lainnya di tabel yang terpengaruh, dan pernyataan untuk setiap kolom yang diperbarui di tabel sekunder. Ini mungkin cara penanganannya jika persyaratan ditambahkan dari waktu ke waktu dan pernyataan terpisah dianggap lebih aman dalam hal pengujian regresi.
- T5:Dua pemicu :Satu pemicu hanya memperbarui tabel yang terpengaruh; yang lain menggunakan satu pernyataan untuk memperbarui tiga kolom di tabel sekunder. Ini mungkin cara yang dilakukan jika pemicu lain tidak diketahui atau jika modifikasi dilarang.
- T6:Empat pemicu :Satu pemicu hanya memperbarui tabel yang terpengaruh; tiga lainnya memperbarui setiap kolom di tabel sekunder. Sekali lagi, ini mungkin cara yang dilakukan jika Anda tidak tahu ada pemicu lain, atau jika Anda takut menyentuh pemicu lain karena masalah regresi.
Berikut adalah sumber data yang kami tangani:
-- sys.all_objects: SELECT * INTO dbo.src FROM sys.all_objects; CREATE UNIQUE CLUSTERED INDEX x ON dbo.src([object_id]); GO -- sys.all_columns: SELECT * INTO dbo.tr1 FROM sys.all_columns; CREATE UNIQUE CLUSTERED INDEX x ON dbo.tr1([object_id], column_id); -- repeat 5 times: tr2, tr3, tr4, tr5, tr6
Sekarang, untuk masing-masing dari 6 pengujian, kami akan menjalankan pembaruan kami 1.000 kali, dan mengukur lamanya waktu
T1:Dasar
Ini adalah skenario di mana kita cukup beruntung untuk menghindari pemicu (sekali lagi, tidak terlalu realistis). Dalam hal ini, kami akan mengukur pembacaan dan durasi batch ini. Saya memasukkan /*real*/
ke dalam teks kueri sehingga saya dapat dengan mudah menarik statistik hanya untuk pernyataan ini, dan bukan pernyataan apa pun dari dalam pemicu, karena pada akhirnya metrik digabungkan ke pernyataan yang memanggil pemicu. Perhatikan juga bahwa pembaruan aktual yang saya buat tidak terlalu masuk akal, jadi abaikan bahwa saya sedang mengatur susunan ke nama server/instance dan principal_id
objek ke session_id
sesi saat ini .
UPDATE /*real*/ dbo.tr1 SET name += N'', collation_name = @@SERVERNAME WHERE name LIKE '%s%'; UPDATE /*real*/ s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID FROM dbo.src AS s INNER JOIN dbo.tr1 AS t ON s.[object_id] = t.[object_id] WHERE t.name LIKE '%s%'; GO 1000
T2:Pemicu Tunggal
Untuk ini kita memerlukan pemicu sederhana berikut, yang hanya memperbarui dbo.src
:
CREATE TRIGGER dbo.tr_tr2 ON dbo.tr2 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = SUSER_ID() FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Kemudian batch kami hanya perlu memperbarui dua kolom di tabel utama:
UPDATE /*real*/ dbo.tr2 SET name += N'', collation_name = @@SERVERNAME WHERE name LIKE '%s%'; GO 1000
T3:Pemicu tunggal terhadap kedua tabel
Untuk pengujian ini, pemicu kami terlihat seperti ini:
CREATE TRIGGER dbo.tr_tr3 ON dbo.tr3 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr3 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Dan sekarang kumpulan yang kami uji hanya perlu memperbarui kolom asli di tabel utama; yang lainnya ditangani oleh pemicu:
UPDATE /*real*/ dbo.tr3 SET name += N'' WHERE name LIKE '%s%'; GO 1000
T4:Pemicu tunggal terhadap kedua tabel
Ini seperti T3, tetapi sekarang pemicunya memiliki empat pernyataan:
CREATE TRIGGER dbo.tr_tr4 ON dbo.tr4 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr4 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; UPDATE s SET modify_date = GETDATE() FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; UPDATE s SET is_ms_shipped = 0 FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; UPDATE s SET principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Batch tes tidak berubah:
UPDATE /*real*/ dbo.tr4 SET name += N'' WHERE name LIKE '%s%'; GO 1000
T5:Dua pemicu
Di sini kita memiliki satu pemicu untuk memperbarui tabel utama, dan satu pemicu untuk memperbarui tabel sekunder:
CREATE TRIGGER dbo.tr_tr5_1 ON dbo.tr5 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr5 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr5_2 ON dbo.tr5 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Kumpulan tes sekali lagi sangat mendasar:
UPDATE /*real*/ dbo.tr5 SET name += N'' WHERE name LIKE '%s%'; GO 1000
T6:Empat pemicu
Kali ini kami memiliki pemicu untuk setiap kolom yang terpengaruh; satu di tabel utama, dan tiga di tabel sekunder.
CREATE TRIGGER dbo.tr_tr6_1 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE t SET collation_name = @@SERVERNAME FROM dbo.tr6 AS t INNER JOIN inserted AS i ON t.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr6_2 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET modify_date = GETDATE() FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr6_3 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET is_ms_shipped = 0 FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO CREATE TRIGGER dbo.tr_tr6_4 ON dbo.tr6 AFTER UPDATE AS BEGIN SET NOCOUNT ON; UPDATE s SET principal_id = @@SPID FROM dbo.src AS s INNER JOIN inserted AS i ON s.[object_id] = i.[object_id]; END GO
Dan kumpulan tes:
UPDATE /*real*/ dbo.tr6 SET name += N'' WHERE name LIKE '%s%'; GO 1000
Mengukur dampak beban kerja
Akhirnya, saya menulis kueri sederhana terhadap sys.dm_exec_query_stats
untuk mengukur pembacaan dan durasi untuk setiap tes:
SELECT [cmd] = SUBSTRING(t.text, CHARINDEX(N'U', t.text), 23), avg_elapsed_time = total_elapsed_time / execution_count * 1.0, total_logical_reads FROM sys.dm_exec_query_stats AS s CROSS APPLY sys.dm_exec_sql_text(s.sql_handle) AS t WHERE t.text LIKE N'%UPDATE /*real*/%' ORDER BY cmd;
Hasil
Saya menjalankan tes 10 kali, mengumpulkan hasilnya, dan rata-rata semuanya. Begini cara rusaknya:
Uji/Batch | Durasi Rata-rata (mikrodetik) | Total Dibaca (8K halaman) |
---|---|---|
T1 :UPDATE /*real*/ dbo.tr1 … | 22.608 | 205,134 |
T2 :UPDATE /*real*/ dbo.tr2 … | 32,749 | 11,331,628 |
T3 :UPDATE /*real*/ dbo.tr3 … | 72.899 | 22.838.308 |
T4 :UPDATE /*real*/ dbo.tr4 … | 78.372 | 44.463.275 |
T5 :UPDATE /*real*/ dbo.tr5 … | 88,563 | 41.514.778 |
T6 :UPDATE /*real*/ dbo.tr6 … | 127.079 | 100.330.753 |
Dan ini adalah representasi grafis dari durasinya:
Kesimpulan
Jelas bahwa, dalam kasus ini, ada beberapa overhead substansial untuk setiap pemicu yang dipanggil – semua kumpulan ini pada akhirnya memengaruhi jumlah baris yang sama, tetapi dalam beberapa kasus, baris yang sama disentuh beberapa kali. Saya mungkin akan melakukan pengujian lanjutan lebih lanjut untuk mengukur perbedaan ketika baris yang sama tidak pernah disentuh lebih dari sekali – skema yang lebih rumit, mungkin, di mana 5 atau 10 tabel lain harus disentuh setiap kali, dan pernyataan yang berbeda ini bisa jadi dalam satu pemicu atau dalam beberapa. Dugaan saya adalah bahwa perbedaan overhead akan lebih didorong oleh hal-hal seperti konkurensi dan jumlah baris yang terpengaruh daripada oleh overhead pemicu itu sendiri – tetapi kita akan lihat.
Ingin mencoba demo sendiri? Unduh skripnya di sini.