Penulis Tamu :Andy Mallon (@AMtwo)
Jika Anda terbiasa mendukung database di balik Microsoft Dynamics CRM, Anda mungkin tahu bahwa itu bukan database dengan performa tercepat. Sejujurnya, itu seharusnya tidak mengejutkan-itu tidak dirancang untuk menjadi database yang sangat cepat. Ini dirancang untuk menjadi fleksibel basis data. Sebagian besar sistem Manajemen Hubungan Pelanggan (CRM) dirancang agar fleksibel sehingga dapat memenuhi kebutuhan banyak bisnis di banyak industri dengan kebutuhan bisnis yang sangat berbeda. Mereka menempatkan persyaratan tersebut di atas kinerja database. Itu mungkin bisnis yang cerdas, tapi saya bukan orang bisnis-saya orang database. Pengalaman saya dengan Dynamics CRM adalah ketika orang-orang datang kepada saya dan berkata
Andy, databasenya lambat
Satu kejadian baru-baru ini adalah dengan laporan yang gagal karena batas waktu kueri 5 menit. Dengan indeks yang tepat, kita seharusnya bisa mendapatkan beberapa ratus baris sangat cepat . Saya mendapatkan kueri dan beberapa contoh parameter, memasukkannya ke dalam Plan Explorer, dan menjalankannya beberapa kali di lingkungan Pengujian kami (saya melakukan semua ini di Uji–itu akan menjadi penting nanti). Saya ingin memastikan bahwa saya menjalankannya dengan cache hangat, sehingga saya dapat menggunakan "yang terbaik dari yang terburuk" untuk tolok ukur saya. Kuerinya adalah SELECT
yang sangat buruk dengan CTE, dan sekelompok bergabung. Sayangnya, saya tidak dapat memberikan kueri yang tepat, karena memiliki logika bisnis khusus pelanggan (Maaf!).
7 menit, 37 detik sama baiknya dengan yang didapat.
Langsung saja, ada banyak hal buruk yang terjadi di sini. 1,5 juta membaca adalah banyak sekali I/O. 457 detik untuk mengembalikan 200 baris lambat. Penaksir Kardinalitas mengharapkan 2 baris, bukan 200. Dan ada banyak penulisan–karena kueri ini hanya SELECT
pernyataan, ini berarti kita harus tumpah ke TempDb. Mungkin saya akan beruntung, dan dapat membuat indeks untuk menghilangkan pemindaian tabel dan mempercepat hal ini. Seperti apa rencananya?
Sepertinya apatosaurus, atau mungkin jerapah.
Tidak akan ada hit cepat
Izinkan saya berhenti sejenak untuk menjelaskan sesuatu tentang Dynamics CRM. Ini menggunakan tampilan. Ini menggunakan tampilan bersarang. Ini menggunakan tampilan bersarang untuk menegakkan keamanan tingkat baris. Dalam bahasa Dynamics, tampilan bertingkat yang menerapkan keamanan tingkat baris ini disebut "tampilan yang difilter". Setiap kueri dari aplikasi melewati tampilan yang difilter ini. Satu-satunya cara yang "didukung" untuk melakukan akses data adalah dengan menggunakan tampilan yang difilter ini.
Ingat saya mengatakan bahwa kueri ini merujuk pada banyak tabel? Yah, ini merujuk pada sekelompok tampilan yang difilter. Jadi kueri rumit yang saya berikan sebenarnya beberapa lapisan lebih rumit. Pada titik ini, saya mendapatkan secangkir kopi segar, dan beralih ke monitor yang lebih besar.
Cara yang bagus untuk memecahkan masalah adalah memulai dari awal. Saya memperbesar operator SELECT, dan mengikuti panah untuk melihat apa yang terjadi:
Bahkan pada monitor ultra-lebar 34" saya, saya harus mengutak-atik tampilan pengaturan agar rencana dapat melihat sebanyak ini. Plan Explorer dapat memutar rencana 90 derajat untuk membuat rencana "tinggi" muat di monitor lebar.
Lihat semua panggilan fungsi bernilai tabel itu! Diikuti segera oleh pertandingan hash yang sangat mahal. Rasa Spidey saya mulai tergelitik. Apa itu fn_GetMaxPrivilegeDepthMask
, dan mengapa disebut 30 kali? Saya yakin ini adalah masalah. Saat Anda melihat "Fungsi bernilai tabel" sebagai operator dalam sebuah rencana, itu sebenarnya adalah fungsi bernilai tabel multi-pernyataan . Jika itu adalah fungsi bernilai tabel sebaris, itu akan dimasukkan ke dalam rencana yang lebih besar, dan bukan menjadi kotak hitam. Fungsi bernilai tabel multi-pernyataan adalah jahat. Jangan gunakan mereka. Penaksir Kardinalitas tidak dapat membuat perkiraan yang akurat. Pengoptimal Kueri tidak dapat mengoptimalkannya dalam konteks kueri yang lebih besar. Dari perspektif kinerja, mereka tidak menskalakan.
Meskipun TVF ini adalah bagian kode yang out-of-the-box dari Dynamics CRM, Spidey Sense saya memberi tahu saya bahwa itu masalahnya. Lupakan pertanyaan besar yang tidak menyenangkan ini dengan rencana besar yang menakutkan. Mari masuk ke fungsi itu dan lihat apa yang terjadi:
create function [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returns @d table(PrivilegeDepthMask int) -- It is by design that we return a table with only one row and column as begin declare @UserId uniqueidentifier select @UserId = dbo.fn_FindUserGuid() declare @t table(depth int) -- from user roles insert into @t(depth) select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId) join Role r on (rp.RoleId = r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId = ur.RoleId and ur.SystemUserId = @UserId) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 -- from user's teams roles insert into @t(depth) select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId) join Role r on (rp.RoleId = r.ParentRootRoleId) join TeamRoles tr on (r.RoleId = tr.RoleId) join SystemUserPrincipals sup on (sup.PrincipalId = tr.TeamId and sup.SystemUserId = @UserId) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 insert into @d select max(depth) from @t return end GO
Fungsi ini mengikuti pola klasik di TVF multi-pernyataan:
- Deklarasikan variabel yang digunakan sebagai konstanta
- Menyisipkan ke dalam variabel tabel
- Kembalikan variabel tabel itu
Tidak ada yang mewah terjadi di sini. Kita dapat menulis ulang beberapa pernyataan ini sebagai satu SELECT
penyataan. Jika kita dapat menulisnya sebagai satu SELECT
pernyataan, kita dapat menulis ulang ini sebagai TVF sebaris.
Ayo kita lakukan
Jika tidak jelas, saya akan menulis ulang kode yang disediakan oleh vendor perangkat lunak. Saya belum pernah bertemu vendor perangkat lunak yang menganggap ini sebagai perilaku "didukung". Jika Anda mengubah kode aplikasi out-of-the-box, Anda sendirian. Microsoft tentu saja mempertimbangkan perilaku "tidak didukung" ini untuk Dynamics. Saya akan tetap melakukannya, karena saya menggunakan lingkungan pengujian dan saya tidak bermain-main dalam produksi. Menulis ulang fungsi ini hanya membutuhkan beberapa menit–jadi mengapa tidak mencobanya dan lihat apa yang terjadi? Inilah tampilan fungsi versi saya:
create function [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int) returns table -- It is by design that we return a table with only one row and column as RETURN -- from user roles select PrivilegeDepthMask = max(PrivilegeDepthMask) from ( select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId) join Role r on (rp.RoleId = r.ParentRootRoleId) join SystemUserRoles ur on (r.RoleId = ur.RoleId and ur.SystemUserId = dbo.fn_FindUserGuid()) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 UNION ALL -- from user's teams roles select --privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global) -- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global) -- do an AND with 0x0F ( =15) to get basic/local/deep/global max(rp.PrivilegeDepthMask % 0x0F) as PrivilegeDepthMask from PrivilegeBase priv join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId) join Role r on (rp.RoleId = r.ParentRootRoleId) join TeamRoles tr on (r.RoleId = tr.RoleId) join SystemUserPrincipals sup on (sup.PrincipalId = tr.TeamId and sup.SystemUserId = dbo.fn_FindUserGuid()) join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId) where potc.ObjectTypeCode = @ObjectTypeCode and priv.AccessRight & 0x01 = 1 )x GO
Saya kembali ke kueri pengujian asli saya, membuang cache, dan menjalankannya kembali beberapa kali. Ini dia paling lambat run time, saat menggunakan TVF versi saya:
Itu terlihat jauh lebih baik!
Ini masih bukan kueri yang paling efisien di dunia, tetapi cukup cepat–saya tidak perlu membuatnya lebih cepat. Kecuali… Saya harus memodifikasi kode Microsoft untuk mewujudkannya. Itu tidak ideal. Mari kita lihat paket lengkapnya dengan TVF baru:
Selamat tinggal apatosaurus, halo dispenser PEZ!
Ini masih rencana yang sangat buruk, tetapi jika Anda melihat pada awalnya, semua panggilan TVF kotak hitam itu hilang. Pencocokan hash yang sangat mahal telah hilang. SQL Server langsung bekerja tanpa hambatan besar panggilan TVF (pekerjaan di belakang TVF sekarang sejalan dengan SELECT
lainnya ):
Dampak gambaran besar
Di mana sebenarnya TVF ini digunakan? Hampir setiap tampilan yang difilter di Dynamics CRM menggunakan panggilan fungsi ini. Ada 246 tampilan yang difilter dan 206 di antaranya merujuk pada fungsi ini. Ini adalah fungsi penting sebagai bagian dari implementasi keamanan tingkat baris Dynamics. Hampir setiap kueri dari aplikasi ke database memanggil fungsi ini setidaknya sekali–biasanya beberapa kali. Ini adalah koin dua sisi:di satu sisi, memperbaiki fungsi ini kemungkinan akan bertindak sebagai dorongan turbo untuk seluruh aplikasi; di sisi lain, tidak ada cara bagi saya untuk melakukan tes regresi untuk semua yang menyentuh fungsi ini.
Tunggu sebentar–jika panggilan fungsi ini sangat penting bagi kinerja kami, dan sangat penting bagi Dynamics CRM, maka setiap orang yang menggunakan Dynamics mengalami hambatan kinerja ini. Kami membuka kasus dengan Microsoft, dan saya menelepon beberapa orang untuk mendapatkan tiket itu ke tim teknik yang bertanggung jawab atas kode ini. Dengan sedikit keberuntungan, versi fungsi yang diperbarui ini akan masuk ke dalam kotak (dan cloud) dalam rilis Dynamics CRM di masa mendatang.
Ini bukan satu-satunya TVF multi-pernyataan di Dynamics CRM–Saya membuat jenis perubahan yang sama pada fn_UserSharedAttributesAccess
untuk masalah kinerja lainnya. Dan masih banyak lagi TVF yang belum saya sentuh karena tidak menimbulkan masalah.
Pelajaran untuk semua orang, bahkan jika Anda tidak menggunakan Dynamics
Ulangi setelah saya:FUNGSI BERHARGA TABEL MULTI-PERNYATAAN ADALAH JAHAT!
Faktorkan ulang kode Anda untuk menghindari penggunaan TVF multi-pernyataan. Jika Anda mencoba menyetel kode, dan Anda melihat TVF multi-pernyataan, lihatlah dengan kritis. Anda tidak selalu dapat mengubah kode (atau mungkin melanggar kontrak dukungan Anda jika Anda melakukannya), tetapi jika Anda dapat mengubah kode, lakukanlah. Beri tahu vendor perangkat lunak Anda untuk berhenti menggunakan TVF multi-pernyataan. Jadikan dunia tempat yang lebih baik dengan menghilangkan beberapa fungsi buruk ini dari database Anda.