उपयोगकर्ता परिभाषित फ़ंक्शन के साथ अनुकूलन मुद्दा


26

मुझे यह समझने में समस्या है कि SQL सर्वर तालिका में प्रत्येक मूल्य के लिए उपयोगकर्ता परिभाषित फ़ंक्शन को कॉल करने का निर्णय क्यों लेता है, भले ही केवल एक पंक्ति प्राप्त की जानी चाहिए। वास्तविक एसक्यूएल बहुत अधिक जटिल है, लेकिन मैं इस समस्या को कम करने में सक्षम था:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

इस क्वेरी के लिए, SQL सर्वर उत्पाद तालिका में मौजूद हर एक मान के लिए GetGroupCode फ़ंक्शन को कॉल करने का निर्णय लेता है, भले ही ORDERLINE से लौटी पंक्तियों का अनुमान और वास्तविक संख्या 1 है (यह प्राथमिक कुंजी है):

क्वेरी योजना

पंक्ति एक्सप्लोरर को दिखाने वाले प्लान एक्सप्लोरर में एक ही योजना:

योजना अन्वेषक टेबल्स:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

स्कैन के लिए उपयोग किया जा रहा सूचकांक है:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

फ़ंक्शन वास्तव में थोड़ा अधिक जटिल है, लेकिन एक ही बात इस तरह से एक डमी मल्टी-स्टेटमेंट फ़ंक्शन के साथ होती है:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

मैं शीर्ष 1 उत्पाद लाने के लिए SQL सर्वर को मजबूर करके प्रदर्शन को "ठीक" करने में सक्षम था, हालांकि 1 अधिकतम है जो कभी भी हो सकता है:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

फिर योजना का आकार भी कुछ बदल जाता है जिसकी मुझे उम्मीद थी कि यह मूल रूप से होगा:

शीर्ष के साथ क्वेरी योजना

हालांकि मैं यह भी कहता हूं कि अनुक्रमणिका PRODUCT_FACTORY क्लस्टर अनुक्रमणिका PRODUCT_PK से छोटी है, लेकिन इसका प्रभाव पड़ेगा, लेकिन यहां तक ​​कि PRODUCT_PK का उपयोग करने के लिए प्रश्न को बाध्य करने के बावजूद, योजना अभी भी मूल है, फ़ंक्शन के लिए 6655 कॉल के साथ।

अगर मैं ORDERHDR को पूरी तरह से छोड़ देता हूं, तो योजना पहले ORDERLINE और PRODUCT के बीच नेस्टेड लूप से शुरू होती है, और फ़ंक्शन को केवल एक बार कहा जाता है।

मैं यह समझना चाहूंगा कि इसका क्या कारण हो सकता है क्योंकि सभी ऑपरेशन प्राथमिक कुंजियों का उपयोग करके किए जाते हैं और इसे कैसे ठीक किया जाए यदि यह अधिक जटिल क्वेरी में होता है जो इसे आसानी से हल नहीं किया जा सकता है।

संपादित करें: तालिका विवरण बनाएं:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

जवाबों:


30

आपके द्वारा की जाने वाली योजना के तीन मुख्य तकनीकी कारण हैं:

  1. आशावादी लागत के ढांचे में गैर-इनलाइन कार्यों के लिए कोई वास्तविक समर्थन नहीं है। यह फ़ंक्शन परिभाषा के अंदर देखने का कोई प्रयास नहीं करता है कि यह कितना महंगा हो सकता है, यह सिर्फ एक बहुत ही कम निश्चित लागत प्रदान करता है, और यह अनुमान लगाता है कि फ़ंक्शन प्रत्येक बार आउटपुट की 1 पंक्ति का उत्पादन करेगा। इन दोनों मॉडलिंग मान्यताओं बहुत अक्सर पूरी तरह से असुरक्षित हैं। 2014 में स्थिति में थोड़ा सुधार हुआ है, क्योंकि नए कार्डिनैलिटी अनुमानक को सक्षम किया गया है क्योंकि निर्धारित 100-पंक्ति अनुमान को 100-पंक्ति अनुमान के साथ बदल दिया गया है। हालांकि, गैर-इनलाइन कार्यों की सामग्री की लागत के लिए अभी भी कोई समर्थन नहीं है।
  2. SQL सर्वर शुरू में जुड़ जाता है और एक एकल आंतरिक n-ary तार्किक जुड़ता है। यह बाद में शामिल होने के आदेशों के बारे में आशावादी कारण में मदद करता है। एकल एन-आर्य को उम्मीदवार में शामिल होने का विस्तार करते हुए शामिल होने के आदेश बाद में आते हैं, और मोटे तौर पर अनुमान के आधार पर होता है। उदाहरण के लिए, आंतरिक जुड़ाव बाहरी जोड़ से पहले आते हैं, छोटे तालिकाओं और बड़े तालिकाओं से पहले चयनात्मक जोड़ और कम चयनात्मक जुड़ाव, और इसी तरह।
  3. जब SQL सर्वर कॉस्ट-बेस्ड ऑप्टिमाइज़ेशन करता है, तो यह कम-लागत वाले प्रश्नों के अनुकूलन में बहुत अधिक खर्च करने की संभावना को कम करने के लिए वैकल्पिक चरणों में प्रयास को विभाजित करता है। तीन मुख्य चरण हैं, खोज 0, खोज 1, और खोज 2. प्रत्येक चरण में प्रवेश की स्थिति है, और बाद में चरण पहले वाले लोगों की तुलना में अधिक आशावादी खोज सक्षम करते हैं। आपकी क्वेरी न्यूनतम-सक्षम खोज चरण के लिए अर्हता प्राप्त करने के लिए होती है, चरण 0. एक कम पर्याप्त लागत योजना वहां पाई जाती है जो बाद के चरणों में प्रवेश नहीं करती है।

यूडीएफ लागू करने के लिए सौंपे गए छोटे कार्डिनैलिटी अनुमान को देखते हुए, एन-एरी विस्तार हेयूरिस्ट्स में शामिल हो जाता है दुर्भाग्य से इसे पहले की तुलना में आप चाहें तो पेड़ में बदल सकते हैं।

क्वेरी कम से कम तीन जुड़ने (लागू होता है) के आधार पर खोज 0 अनुकूलन के लिए भी योग्य है। अंतिम भौतिक योजना आपको अजीब-सी दिखने वाली स्कैन के साथ मिलती है, जो कि हेयुरिस्टली-डिडक्टेड ज्वाइन ऑर्डर पर आधारित है। यह काफी कम लागत वाली है कि अनुकूलक "पर्याप्त अच्छा" योजना पर विचार करता है। यूडीएफ के लिए कम लागत का अनुमान और कार्डिनैलिटी इस शुरुआती समापन में योगदान देता है।

खोज 0 (जिसे लेन-देन प्रसंस्करण चरण के रूप में भी जाना जाता है) निम्न-कार्डिनलिटी ओएलटीपी-प्रकार के प्रश्नों को लक्षित करता है, अंतिम योजना के साथ जो आमतौर पर नेस्टेड लूप जॉइन करते हैं। इससे भी महत्वपूर्ण बात, खोज 0 ऑप्टिमाइज़र की अन्वेषण क्षमताओं का एक अपेक्षाकृत छोटा सबसेट है। इस सबसेट में शामिल होने (नियम PullApplyOverJoin) पर क्वेरी ट्री को लागू करने वाले पुल को शामिल नहीं किया गया है । यह ठीक वैसा ही है जैसा कि परीक्षण मामले में UDF को जॉइंट्स के ऊपर लागू करने के लिए, ऑपरेशन के अनुक्रम में अंतिम रूप में प्रकट करने के लिए आवश्यक है (जैसा कि यह था)।

एक ऐसा मुद्दा भी है जहां ऑप्टिमाइज़र भोले-भाले नोज्ड लूप्स के साथ जुड़ने का निर्णय ले सकता है (जॉइन स्वयं पर प्रेडिक्टेट करें) और एक सहसंबंधित अनुक्रमित ज्वाइन (लागू करें) जहाँ परस्पर संबंधित डेडिकेट इंडेक्स की तलाश में शामिल होने के लिए अंदर की तरफ लगाया जाता है। उत्तरार्द्ध आमतौर पर वांछित योजना आकार है, लेकिन अनुकूलक दोनों की खोज करने में सक्षम है। गलत लागत और कार्डिनैलिटी अनुमानों के साथ, यह गैर-लागू एनएल में शामिल हो सकता है, जैसा कि प्रस्तुत योजनाओं (स्कैन को समझाते हुए) में किया गया है।

इसलिए, कई सामान्य ऑप्टिमाइज़र सुविधाओं से जुड़े कई परस्पर विरोधी कारण हैं जो सामान्य रूप से अत्यधिक संसाधनों का उपयोग किए बिना कम समय में अच्छी योजनाओं को खोजने के लिए अच्छी तरह से काम करते हैं। किसी भी कारण से बचने के लिए नमूना क्वेरी के लिए 'अपेक्षित' योजना आकृति तैयार करने के लिए पर्याप्त है, यहां तक ​​कि खाली टेबल के साथ:

खोज 0 अक्षम के साथ खाली तालिकाओं पर योजना बनाएं

खोज 0 योजना चयन, प्रारंभिक ऑप्टिमाइज़र समाप्ति से बचने के लिए या यूडीएफ की लागत में सुधार (इसके लिए SQL सर्वर 2014 सीई मॉडल में सीमित वृद्धि से अलग) का कोई समर्थित तरीका नहीं है। यह योजना गाइड, मैनुअल क्वेरी फिर से लिखना ( TOP (1)विचार सहित या अस्थायी अस्थायी तालिकाओं का उपयोग करना) और गैर-इनलाइन कार्यों की तरह खराब लागत वाले 'ब्लैक बॉक्स' (एक क्यूओ दृष्टिकोण से) को छोड़ देता है।

पुनर्लेखन CROSS APPLYके रूप में OUTER APPLYभी काम कर सकते हैं, के रूप में यह वर्तमान में प्रारंभिक चरण में शामिल होने-टूट काम से कुछ से बचाता है, लेकिन आप मूल प्रश्न अर्थ विज्ञान संरक्षित करने के लिए सावधान रहना होगा (जैसे किसी भी खारिज NULL-extended पंक्तियों को पेश किया जा सकता है, अनुकूलक एक करने के लिए वापस गिर के बिना पार लागू)। आपको इस बारे में जागरूक होने की आवश्यकता है कि यह व्यवहार स्थिर रहने की गारंटी नहीं है, इसलिए आपको हर बार जब आप पैच करते हैं या SQL सर्वर को अपग्रेड करते हैं, तो आपको ऐसे किसी भी देखे गए व्यवहार को फिर से याद करना होगा।

कुल मिलाकर, आपके लिए सही समाधान विभिन्न प्रकार के कारकों पर निर्भर करता है जिन्हें हम आपके लिए नहीं आंक सकते हैं। हालांकि, मैं आपको उन समाधानों पर विचार करने के लिए प्रोत्साहित करता हूं जो भविष्य में हमेशा काम करने की गारंटी देते हैं, और यह कि जहां भी संभव हो, आशावादी के साथ काम करते हैं।


24

ऐसा लगता है कि यह ऑप्टिमाइज़र द्वारा लागत आधारित निर्णय है, लेकिन एक बुरा है।

यदि आप PRODUCT में 50000 पंक्तियों को जोड़ते हैं तो ऑप्टिमाइज़र को लगता है कि स्कैन बहुत काम का है और आपको UDF को तीन सॉक्स और एक कॉल के साथ एक योजना देता है।

उत्पाद में मुझे 6655 पंक्तियों के लिए योजना मिलती है

यहाँ छवि विवरण दर्ज करें

उत्पाद में 50000 पंक्तियों के साथ मुझे यह योजना मिलती है।

यहाँ छवि विवरण दर्ज करें

मुझे लगता है कि यूडीएफ को बुलाने की लागत को कम करके आंका गया है।

एक वर्कअराउंड जो इस मामले में ठीक काम करता है, वह है यूडीएफ के खिलाफ बाहरी उपयोग के लिए क्वेरी को बदलना। मुझे अच्छी योजना मिलती है, चाहे तालिका PRODUCT में कितनी ही पंक्तियाँ हों।

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

यहाँ छवि विवरण दर्ज करें

आपके मामले में सबसे अच्छा समाधान संभवत: उन मूल्यों को प्राप्त करना है जो आपको एक अस्थायी तालिका में चाहिए और फिर UDF पर लागू क्रॉस के साथ अस्थायी तालिका को क्वेरी करें। इस तरह आप सुनिश्चित हैं कि यूडीएफ को आवश्यकता से अधिक निष्पादित नहीं किया जाएगा।

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

अस्थायी तालिका के लिए बने रहने के बजाय आप top()UDF के कॉल करने से पहले जॉइन से परिणाम का मूल्यांकन करने के लिए SQL सर्वर को बाध्य करने के लिए एक व्युत्पन्न तालिका में उपयोग कर सकते हैं । बस टॉप सर्वर में वास्तव में उच्च संख्या का उपयोग करें SQL सर्वर को क्वेरी के उस भाग के लिए अपनी पंक्तियों को गिनने से पहले उस पर जाना चाहिए और UDF का उपयोग करना चाहिए।

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

यहाँ छवि विवरण दर्ज करें

मैं यह समझना चाहूंगा कि इसका क्या कारण हो सकता है क्योंकि सभी ऑपरेशन प्राथमिक कुंजियों का उपयोग करके किए जाते हैं और इसे कैसे ठीक किया जाए यदि यह अधिक जटिल क्वेरी में होता है जो इसे आसानी से हल नहीं किया जा सकता है।

मैं वास्तव में इसका जवाब नहीं दे सकता, लेकिन मैंने सोचा कि मुझे जो भी पता है उसे साझा करना चाहिए। मुझे नहीं पता है कि PRODUCT तालिका का स्कैन क्यों माना जाता है। ऐसे मामले हो सकते हैं जहां ऐसा करना सबसे अच्छी बात है और ऐसे सामान हैं जिनके बारे में आशावादी यूडीएफ के साथ कैसा व्यवहार करते हैं, इसके बारे में मुझे नहीं पता है।

एक अतिरिक्त अवलोकन यह था कि आपकी क्वेरी को SQL सर्वर 2014 में नए कार्डिनैलिटी अनुमानक के साथ एक अच्छी योजना मिलती है। ऐसा इसलिए है क्योंकि UDF में प्रत्येक कॉल के लिए पंक्तियों की अनुमानित संख्या 1 के बजाय 100 है क्योंकि यह SQL सर्वर 2012 और उससे पहले है। लेकिन यह अभी भी स्कैन संस्करण और योजना के तलाश संस्करण के बीच एक ही लागत आधारित निर्णय करेगा। PRODUCT में 500 (497 मेरे मामले में) वाली पंक्तियों के साथ आपको SQL Server 2014 में भी योजना का स्कैन संस्करण मिल जाएगा।


2
एसक्यूएल बिट्स में किसी तरह से एडम मैकहानिक
जेम्स जेड
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.