इस बारे में ब्लॉगिंग करने के बाद कि फ़िल्टर्ड इंडेक्स अधिक शक्तिशाली कैसे हो सकते हैं, और हाल ही में कैसे मजबूर पैरामीटराइजेशन द्वारा उन्हें बेकार किया जा सकता है, मैं फ़िल्टर किए गए इंडेक्स/पैरामीटराइजेशन विषय पर फिर से जा रहा हूं। हाल ही में काम पर एक बहुत ही सरल समाधान आया, और मुझे साझा करना पड़ा।
निम्नलिखित उदाहरण लें, जहां हमारे पास ऑर्डर की एक तालिका वाला बिक्री डेटाबेस है। कभी-कभी हम केवल अभी तक भेजे जाने वाले आदेशों की एक सूची (या गिनती) चाहते हैं - जो समय के साथ, (उम्मीद है!) समग्र तालिका के एक छोटे और छोटे प्रतिशत का प्रतिनिधित्व करते हैं:
CREATE DATABASE Sales; GO USE Sales; GO -- simplified, obviously: CREATE TABLE dbo.Orders ( OrderID int IDENTITY(1,1) PRIMARY KEY, OrderDate datetime NOT NULL, filler char(500) NOT NULL DEFAULT '', IsShipped bit NOT NULL DEFAULT 0 ); GO -- let's put some data in there; 7,000 shipped orders, and 50 unshipped: INSERT dbo.Orders(OrderDate, IsShipped) -- random dates over two years SELECT TOP (7000) DATEADD(DAY, ABS(object_id % 730), '20171101'), 1 FROM sys.all_columns UNION ALL -- random dates from this month SELECT TOP (50) DATEADD(DAY, ABS(object_id % 30), '20191201'), 0 FROM sys.all_columns;
इस परिदृश्य में इस तरह एक फ़िल्टर्ड इंडेक्स बनाने के लिए यह समझ में आता है (जो उन अनशिप ऑर्डर पर प्राप्त करने की कोशिश कर रहे किसी भी प्रश्न का त्वरित काम करता है):
CREATE INDEX ix_OrdersNotShipped ON dbo.Orders(IsShipped, OrderDate) WHERE IsShipped = 0;
फ़िल्टर किए गए इंडेक्स का उपयोग कैसे करता है यह देखने के लिए हम इस तरह एक त्वरित क्वेरी चला सकते हैं:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
निष्पादन योजना काफी सरल है, लेकिन बेजोड़ इंडेक्स के बारे में एक चेतावनी है:
चेतावनी का नाम थोड़ा भ्रामक है - अनुकूलक अंततः सूचकांक का उपयोग करने में सक्षम था, लेकिन यह सुझाव दे रहा है कि यह मापदंडों के बिना "बेहतर" होगा (जिसका हमने स्पष्ट रूप से उपयोग नहीं किया), भले ही बयान ऐसा लगता है कि यह पैरामीटरयुक्त था:
यदि आप वास्तव में चाहते हैं, तो आप वास्तविक प्रदर्शन में कोई अंतर नहीं होने के साथ चेतावनी को समाप्त कर सकते हैं (यह सिर्फ कॉस्मेटिक होगा)। एक तरीका है शून्य-प्रभाव वाले विधेय को जोड़ना, जैसे AND (1 > 0)
:
SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
एक और (शायद अधिक सामान्य) OPTION (RECOMPILE)
जोड़ना है :
SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
इन दोनों विकल्पों से एक ही योजना मिलती है (बिना किसी चेतावनी के खोज):
अब तक सब ठीक है; हमारे फ़िल्टर्ड इंडेक्स का उपयोग किया जा रहा है (उम्मीद के मुताबिक)। ये एकमात्र चाल नहीं हैं, बिल्कुल; अन्य लोगों के लिए नीचे दी गई टिप्पणियां देखें जिन्हें पाठकों ने पहले ही सबमिट कर दिया है।
फिर, जटिलता
चूंकि डेटाबेस बड़ी संख्या में तदर्थ प्रश्नों के अधीन है, इसलिए कोई व्यक्ति जबरन पैरामीटरकरण चालू करता है, संकलन को कम करने और योजना कैश को प्रदूषित करने से कम और एकल-उपयोग वाली योजनाओं को समाप्त करने का प्रयास करता है:
ALTER DATABASE Sales SET PARAMETERIZATION FORCED;
अब हमारी मूल क्वेरी फ़िल्टर्ड इंडेक्स का उपयोग नहीं कर सकती है; यह संकुल अनुक्रमणिका को स्कैन करने के लिए बाध्य है:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
बेजोड़ इंडेक्स के बारे में चेतावनी वापस आती है, और हमें अवशिष्ट I/O के बारे में नई चेतावनियां मिलती हैं। ध्यान दें कि कथन पैरामीटरयुक्त है, लेकिन यह थोड़ा अलग दिखता है:
यह डिज़ाइन द्वारा है, क्योंकि मजबूर पैरामीटरकरण का पूरा उद्देश्य इस तरह के प्रश्नों को पैरामीटर करना है। लेकिन यह हमारे फ़िल्टर किए गए इंडेक्स के उद्देश्य को विफल कर देता है, क्योंकि यह विधेय में एक मान का समर्थन करने के लिए है, न कि एक पैरामीटर जो बदल सकता है।
टॉमफूलरी
अतिरिक्त विधेय का उपयोग करने वाली हमारी "ट्रिक" क्वेरी भी फ़िल्टर किए गए इंडेक्स का उपयोग करने में असमर्थ है, और बूट करने के लिए थोड़ी अधिक जटिल योजना के साथ समाप्त होती है:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
विकल्प (पुनः संकलित)
इस मामले में सामान्य प्रतिक्रिया, जैसे पहले चेतावनी को हटाने के साथ, OPTION (RECOMPILE)
जोड़ना है बयान को। यह काम करता है, और एक कुशल खोज के लिए फ़िल्टर किए गए अनुक्रमणिका को चुनने की अनुमति देता है…
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
...लेकिन OPTION (RECOMPILE)
adding जोड़ना और क्वेरी के प्रत्येक निष्पादन के खिलाफ इस अतिरिक्त संकलन को हिट करना हमेशा उच्च-मात्रा वाले वातावरण में स्वीकार्य नहीं होगा (विशेषकर यदि वे पहले से ही सीपीयू-बाउंड हैं)।
संकेत
किसी ने पुन:संकलन की लागत से बचने के लिए फ़िल्टर किए गए इंडेक्स को स्पष्ट रूप से संकेत देने का सुझाव दिया। सामान्य तौर पर, यह बल्कि भंगुर होता है, क्योंकि यह कोड के बाहर रहने वाले सूचकांक पर निर्भर करता है; मैं इसका उपयोग अंतिम उपाय के रूप में करता हूं। इस मामले में यह वैसे भी मान्य नहीं है। जब पैरामीटरकरण नियम अनुकूलक को फ़िल्टर किए गए अनुक्रमणिका को स्वचालित रूप से चुनने से रोकते हैं, तो वे आपको इसे मैन्युअल रूप से चुनने से भी रोकते हैं। एक सामान्य FORCESEEK
. के साथ एक ही समस्या संकेत:
SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0; SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;
दोनों यह त्रुटि उत्पन्न करते हैं:
Msg 8622, Level 16, State 1क्वेरी प्रोसेसर इस क्वेरी में परिभाषित संकेतों के कारण क्वेरी प्लान नहीं बना सका। बिना किसी संकेत के और SET FORCEPLAN का उपयोग किए बिना क्वेरी को फिर से सबमिट करें।
और यह समझ में आता है, क्योंकि यह जानने का कोई तरीका नहीं है कि IsShipped
के लिए अज्ञात मान पैरामीटर फ़िल्टर किए गए इंडेक्स से मेल खाएगा (या किसी इंडेक्स पर सीक ऑपरेशन को सपोर्ट करेगा)।
डायनामिक SQL?
मैंने सुझाव दिया कि आप डायनेमिक एसक्यूएल का उपयोग कर सकते हैं, कम से कम केवल उस रीकंपाइल हिट का भुगतान करने के लिए जब आप जानते हैं कि आप छोटे इंडेक्स को हिट करना चाहते हैं:
DECLARE @IsShipped bit = 0; DECLARE @sql nvarchar(max) = N'SELECT dynsql = OrderID, OrderDate FROM dbo.Orders' + CASE WHEN @IsShipped IS NOT NULL THEN N' WHERE IsShipped = @IsShipped' ELSE N'' END + CASE WHEN @IsShipped = 0 THEN N' OPTION (RECOMPILE)' ELSE N'' END; EXEC sys.sp_executesql @sql, N'@IsShipped bit', @IsShipped;
यह ऊपर के समान ही कुशल योजना की ओर जाता है। अगर आपने वेरिएबल को @IsShipped = 1
. में बदल दिया है , तो आपको अधिक महंगा क्लस्टर इंडेक्स स्कैन मिलता है जिसकी आपको अपेक्षा करनी चाहिए:
लेकिन कोई भी इस तरह के किनारे के मामले में गतिशील एसक्यूएल का उपयोग करना पसंद नहीं करता है - यह कोड को पढ़ने और बनाए रखने के लिए कठिन बनाता है, और भले ही यह कोड एप्लिकेशन में बाहर हो, फिर भी यह अतिरिक्त तर्क है जिसे वहां जोड़ना होगा, इसे वांछनीय से कम बनाना ।
कुछ आसान
हमने एक योजना मार्गदर्शिका को लागू करने के बारे में संक्षेप में बात की, जो निश्चित रूप से सरल नहीं है, लेकिन फिर एक सहयोगी ने सुझाव दिया कि आप संग्रहीत प्रक्रिया, दृश्य, या इनलाइन तालिका-मूल्यवान फ़ंक्शन के अंदर पैरामीटरयुक्त कथन को "छिपा" कर ऑप्टिमाइज़र को मूर्ख बना सकते हैं। यह इतना आसान था, मुझे विश्वास नहीं था कि यह काम करेगा।
लेकिन फिर मैंने कोशिश की:
CREATE PROCEDURE dbo.GetUnshippedOrders AS BEGIN SET NOCOUNT ON; SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; END GO CREATE VIEW dbo.vUnshippedOrders AS SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; GO CREATE FUNCTION dbo.fnUnshippedOrders() RETURNS TABLE AS RETURN (SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0); GO
ये तीनों प्रश्न फ़िल्टर किए गए अनुक्रमणिका के विरुद्ध प्रभावी खोज करते हैं:
EXEC dbo.GetUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();
निष्कर्ष
मुझे आश्चर्य हुआ कि यह इतना प्रभावी था। बेशक, इसके लिए आपको एप्लिकेशन को बदलना होगा; यदि आप किसी संग्रहीत कार्यविधि को कॉल करने के लिए ऐप कोड नहीं बदल सकते हैं या दृश्य या फ़ंक्शन को संदर्भित कर सकते हैं (या यहां तक कि OPTION (RECOMPILE)
जोड़ें) ), आपको अन्य विकल्पों की तलाश करते रहना होगा। लेकिन अगर आप एप्लिकेशन कोड को बदल सकते हैं, तो विधेय को किसी अन्य मॉड्यूल में भरना एक रास्ता हो सकता है।