अतिथि लेखक:एंडी मॉलन (@AMtwo)
यदि आप Microsoft Dynamics CRM के पीछे डेटाबेस का समर्थन करने से परिचित हैं, तो आप शायद जानते हैं कि यह सबसे तेज़ प्रदर्शन करने वाला डेटाबेस नहीं है। ईमानदारी से, यह आश्चर्य की बात नहीं होनी चाहिए-यह एक चिल्लाते हुए तेज़ डेटाबेस होने के लिए डिज़ाइन नहीं किया गया है। इसे एक लचीला . होने के लिए डिज़ाइन किया गया है डेटाबेस। अधिकांश ग्राहक संबंध प्रबंधन (सीआरएम) सिस्टम को लचीले होने के लिए डिज़ाइन किया गया है ताकि वे कई उद्योगों में कई व्यवसायों की ज़रूरतों को पूरी तरह से अलग-अलग व्यावसायिक आवश्यकताओं के साथ पूरा कर सकें। उन्होंने उन आवश्यकताओं को डेटाबेस प्रदर्शन से आगे रखा। यह शायद स्मार्ट व्यवसाय है, लेकिन मैं एक व्यवसायी व्यक्ति नहीं हूं- मैं एक डेटाबेस व्यक्ति हूं। डायनेमिक्स सीआरएम के साथ मेरा अनुभव तब है जब लोग मेरे पास आते हैं और कहते हैं
एंडी, डेटाबेस धीमा है
हाल ही में एक घटना 5 मिनट के क्वेरी टाइमआउट के कारण विफल होने वाली रिपोर्ट के साथ हुई थी। उचित अनुक्रमणिका के साथ, हमें कुछ सौ पंक्तियाँ प्राप्त करने में सक्षम होना चाहिए वास्तव में तेज़ . मैंने क्वेरी और कुछ उदाहरण मापदंडों पर अपना हाथ रखा, इसे प्लान एक्सप्लोरर में गिरा दिया, और इसे हमारे टेस्ट वातावरण में कुछ बार चलाया (मैं यह सब टेस्ट में कर रहा हूं-यह बाद में महत्वपूर्ण होने वाला है)। मैं यह सुनिश्चित करना चाहता था कि मैं इसे एक गर्म कैश के साथ चला रहा हूं, ताकि मैं अपने बेंचमार्क के लिए "सबसे खराब में से सबसे अच्छा" का उपयोग कर सकूं। क्वेरी बहुत खराब थी SELECT
एक सीटीई के साथ, और जुड़ने का एक गुच्छा। दुर्भाग्य से, मैं सटीक क्वेरी प्रदान नहीं कर सकता, क्योंकि इसमें कुछ ग्राहक-विशिष्ट व्यावसायिक तर्क थे (क्षमा करें!)।
7 मिनट, 37 सेकंड जितना हो सके उतना अच्छा है।
बल्ले से ही, यहाँ बहुत बुरा चल रहा है। 1.5 मिलियन रीड्स बहुत सारे I/O की एक बिल्ली है। 200 पंक्तियों को वापस करने के लिए 457 सेकंड धीमा है। कार्डिनैलिटी एस्टीमेटर को 200 के बजाय 2 पंक्तियों की उम्मीद थी। और बहुत सारे लेखन थे-चूंकि यह क्वेरी केवल एक SELECT
है बयान, इसका मतलब है कि हमें TempDb पर जाना चाहिए। हो सकता है कि मैं भाग्यशाली हो जाऊं, और एक टेबल स्कैन को खत्म करने और इस चीज़ को गति देने के लिए एक इंडेक्स बनाने में सक्षम हो जाऊं। योजना कैसी दिखती है?
एपेटोसॉरस या शायद जिराफ जैसा दिखता है।
कोई त्वरित हिट नहीं होगी
डायनेमिक्स सीआरएम के बारे में कुछ समझाने के लिए मुझे एक पल के लिए रुकने दें। यह विचारों का उपयोग करता है। यह नेस्टेड दृश्यों का उपयोग करता है। यह पंक्ति-स्तरीय सुरक्षा को लागू करने के लिए नेस्टेड दृश्यों का उपयोग करता है। डायनेमिक्स की भाषा में, इन पंक्ति-स्तर-सुरक्षा-प्रवर्तित नेस्टेड दृश्यों को "फ़िल्टर किए गए दृश्य" कहा जाता है। एप्लिकेशन की प्रत्येक क्वेरी इन फ़िल्टर किए गए दृश्यों से होकर गुजरती है। डेटा एक्सेस करने का एकमात्र "समर्थित" तरीका इन फ़िल्टर किए गए दृश्यों का उपयोग करना है।
स्मरण करो मैंने कहा था कि यह प्रश्न तालिकाओं के एक समूह को संदर्भित कर रहा था? खैर, यह फ़िल्टर किए गए विचारों का एक समूह संदर्भित कर रहा है। तो मुझे जो जटिल प्रश्न सौंपा गया था वह वास्तव में कई परतें अधिक जटिल है। इस बिंदु पर, मुझे एक ताज़ा कप कॉफी मिली, और एक बड़े मॉनिटर पर स्विच किया गया।
समस्याओं को हल करने का एक शानदार तरीका शुरुआत से शुरू करना है। मैंने सेलेक्ट ऑपरेटर पर ज़ूम इन किया, और क्या हो रहा था यह देखने के लिए तीरों का अनुसरण किया:
यहां तक कि मेरे 34" अल्ट्रा-वाइड मॉनिटर पर भी, मुझे डिस्प्ले के साथ खिलवाड़ करना पड़ा इतना देखने के लिए योजना के लिए सेटिंग्स। विस्तृत मॉनिटर पर "लंबी" योजनाओं को फिट करने के लिए प्लान एक्सप्लोरर योजनाओं को 90 डिग्री घुमा सकता है।
उन सभी तालिका-मूल्यवान फ़ंक्शन कॉलों को देखें! इसके तुरंत बाद एक बहुत महंगा हैश मैच हुआ। माई स्पाइडी सेंस में झुनझुनी होने लगी। fn_GetMaxPrivilegeDepthMask
क्या है? , और इसे 30 बार क्यों बुलाया जा रहा है? मैं शर्त लगाता हूं कि यह एक समस्या है। जब आप किसी योजना में "तालिका-मूल्यवान फ़ंक्शन" को एक ऑपरेटर के रूप में देखते हैं, तो इसका वास्तव में मतलब है कि यह एक बहु-कथन तालिका-मूल्यवान फ़ंक्शन है . यदि यह एक इनलाइन तालिका-मूल्यवान फ़ंक्शन होता, तो इसे बड़ी योजना में शामिल किया जाता, न कि ब्लैक बॉक्स। बहु-कथन तालिका-मूल्यवान कार्य बुरे हैं। उनका प्रयोग न करें। कार्डिनैलिटी अनुमानक सटीक अनुमान लगाने में सक्षम नहीं है। क्वेरी ऑप्टिमाइज़र उन्हें बड़ी क्वेरी के संदर्भ में अनुकूलित करने में सक्षम नहीं है। प्रदर्शन के दृष्टिकोण से, वे बड़े नहीं होते हैं।
भले ही यह टीवीएफ डायनेमिक्स सीआरएम का एक आउट-ऑफ-द-बॉक्स कोड है, मेरा स्पाइडी सेंस मुझे बताता है कि यह समस्या है। एक बड़े डरावने प्लान के साथ इस बड़े भद्दे सवाल को भूल जाइए। आइए उस फ़ंक्शन में कदम रखें और देखें कि क्या हो रहा है:
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
यह फ़ंक्शन मल्टी-स्टेटमेंट TVF में एक क्लासिक पैटर्न का अनुसरण करता है:
- एक चर घोषित करें जो एक स्थिरांक के रूप में उपयोग किया जाता है
- टेबल वैरिएबल में डालें
- उस तालिका चर को वापस करें
यहाँ कुछ भी फैंसी नहीं चल रहा है। हम इन एकाधिक कथनों को एक SELECT
. के रूप में फिर से लिख सकते हैं बयान। अगर हम इसे सिंगल के रूप में लिख सकते हैं SELECT
बयान, हम इसे एक इनलाइन टीवीएफ के रूप में फिर से लिख सकते हैं।
चलो करते हैं
यदि यह स्पष्ट नहीं है, तो मैं एक सॉफ़्टवेयर विक्रेता द्वारा प्रदान किए गए कोड को फिर से लिखने वाला हूं। मैं ऐसे सॉफ़्टवेयर विक्रेता से कभी नहीं मिला जो इसे "समर्थित" व्यवहार मानता हो। यदि आप आउट-ऑफ़-द-बॉक्स एप्लिकेशन कोड बदलते हैं, तो आप स्वयं ही हैं। Microsoft निश्चित रूप से Dynamics के लिए इस "असमर्थित" व्यवहार को मानता है। मैं इसे वैसे भी करने जा रहा हूं, क्योंकि मैं परीक्षण वातावरण का उपयोग कर रहा हूं और मैं उत्पादन में नहीं खेल रहा हूं। इस फ़ंक्शन को फिर से लिखने में बस कुछ ही मिनट लगे-तो क्यों न इसे आज़माएं और देखें कि क्या होता है? यहाँ मेरे फ़ंक्शन का संस्करण कैसा दिखता है:
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
मैं अपनी मूल परीक्षण क्वेरी पर वापस गया, कैश को डंप कर दिया, और इसे कुछ बार फिर से चलाया। यह रहा सबसे धीमा टीवीएफ के मेरे संस्करण का उपयोग करते समय रन टाइम:
यह काफी बेहतर दिखता है!
यह अभी भी दुनिया में सबसे कुशल क्वेरी नहीं है, लेकिन यह काफी तेज़ है-मुझे इसे और तेज़ करने की आवश्यकता नहीं है। सिवाय ... मुझे ऐसा करने के लिए माइक्रोसॉफ्ट के कोड को संशोधित करना पड़ा। यह आदर्श नहीं है। आइए एक नजर डालते हैं नए टीवीएफ के साथ पूरे प्लान पर:
अलविदा एपेटोसॉरस, हैलो पीईजेड डिस्पेंसर!
यह अभी भी वास्तव में एक भयानक योजना है, लेकिन यदि आप शुरुआत को देखते हैं, तो वे सभी ब्लैक बॉक्स टीवीएफ कॉल समाप्त हो गए हैं। सुपर-महंगा हैश मैच चला गया है। SQL सर्वर TVF कॉल की उस बड़ी अड़चन के बिना काम करने के लिए नीचे आता है (TVF के पीछे का काम अब बाकी SELECT
के साथ इनलाइन है ):
बड़ा चित्र प्रभाव
यह टीवीएफ वास्तव में कहां उपयोग किया जाता है? Dynamics CRM में लगभग हर एक फ़िल्टर किया गया दृश्य इस फ़ंक्शन कॉल का उपयोग करता है। 246 फ़िल्टर किए गए दृश्य हैं और उनमें से 206 इस फ़ंक्शन को संदर्भित करते हैं। यह डायनेमिक्स पंक्ति-स्तरीय सुरक्षा कार्यान्वयन के भाग के रूप में एक महत्वपूर्ण कार्य है। आम तौर पर एप्लिकेशन से डेटाबेस तक की हर एक क्वेरी इस फ़ंक्शन को कम से कम एक बार-आमतौर पर कुछ बार कॉल करती है। यह एक दो-तरफा सिक्का है:एक तरफ, इस फ़ंक्शन को ठीक करना संभवतः पूरे एप्लिकेशन के लिए टर्बो बूस्ट के रूप में कार्य करेगा; दूसरी ओर, मेरे लिए इस फ़ंक्शन को छूने वाली हर चीज़ के लिए प्रतिगमन परीक्षण करने का कोई तरीका नहीं है।
एक सेकंड प्रतीक्षा करें-यदि यह फ़ंक्शन कॉल हमारे प्रदर्शन के लिए बहुत महत्वपूर्ण है, और डायनेमिक्स सीआरएम के लिए बहुत महत्वपूर्ण है, तो इसका मतलब यह है कि डायनेमिक्स का उपयोग करने वाला हर कोई इस प्रदर्शन की अड़चन को मार रहा है। हमने माइक्रोसॉफ्ट के साथ एक मामला खोला, और मैंने कुछ लोगों को इस कोड के लिए जिम्मेदार इंजीनियरिंग टीम के साथ टिकट टकराने के लिए बुलाया। थोड़े से भाग्य के साथ, फ़ंक्शन का यह अद्यतन संस्करण डायनेमिक्स CRM के भविष्य के रिलीज़ में इसे बॉक्स (और क्लाउड) में बना देगा।
डायनेमिक्स सीआरएम में यह एकमात्र मल्टी-स्टेटमेंट टीवीएफ नहीं है-मैंने fn_UserSharedAttributesAccess
में इसी प्रकार का परिवर्तन किया है। एक और प्रदर्शन मुद्दे के लिए। और भी टीवीएफ हैं जिन्हें मैंने छुआ नहीं है क्योंकि उन्होंने समस्याएं पैदा नहीं की हैं।
सभी के लिए एक सबक, भले ही आप Dynamics का उपयोग नहीं कर रहे हों
मेरे पीछे दोहराएँ:बहु-विवरण तालिका मूल्यवान कार्य बुरे हैं!
मल्टी-स्टेटमेंट टीवीएफ का उपयोग करने से बचने के लिए अपने कोड को फिर से फैक्टर करें। यदि आप कोड को ट्यून करने का प्रयास कर रहे हैं, और आपको एक मल्टी-स्टेटमेंट TVF दिखाई देता है, तो इसे गंभीर रूप से देखें। आप हमेशा कोड नहीं बदल सकते (या यदि आप ऐसा करते हैं तो यह आपके समर्थन अनुबंध का उल्लंघन हो सकता है), लेकिन यदि आप कोड बदल सकते हैं, तो इसे करें। अपने सॉफ़्टवेयर विक्रेता से कहें कि वह मल्टी-स्टेटमेंट TVF का उपयोग बंद कर दे। अपने डेटाबेस से इनमें से कुछ खराब कार्यों को हटाकर दुनिया को एक बेहतर जगह बनाएं।