मैं एक स्केलर UDF को एक क्वेरी में सिर्फ एक बार मूल्यांकन करने के लिए कैसे मजबूर कर सकता हूं?


12

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

UDF स्वयं कॉल करने के लिए बहुत महंगा है, लेकिन सिद्धांत रूप में प्रश्नों को ऑप्टिमाइज़र द्वारा तार्किक रूप से इस तरह से लागू किया जा सकता है कि फ़ंक्शन को केवल एक बार गणना करने की आवश्यकता होती है। मैंने इस प्रश्न के लिए एक बहुत ही सरल उदाहरण प्रस्तुत किया है। मेरी मशीन पर अमल करने के लिए निम्नलिखित क्वेरी में 6152 ms लगते हैं:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

क्वेरी प्लान में फ़िल्टर ऑपरेटर बताता है कि फ़ंक्शन का मूल्यांकन प्रत्येक पंक्ति के लिए एक बार किया गया था:

क्वेरी प्लान 1

डीडीएल और डेटा प्रस्तुतिकरण:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

GO

DROP TABLE IF EXISTS dbo.X_100_INTEGERS;

CREATE TABLE dbo.X_100_INTEGERS (ID INT NOT NULL);

-- insert 100 integers from 1 - 100
WITH
    L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
    L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO dbo.X_100_INTEGERS WITH (TABLOCK)
SELECT n FROM Nums WHERE n <= 100;

यहां उपरोक्त उदाहरण के लिए एक db फिडल लिंक है , हालांकि कोड को वहां निष्पादित करने में लगभग 18 सेकंड लगते हैं।

कुछ मामलों में मैं फ़ंक्शन के कोड को संपादित करने में सक्षम नहीं हो सकता क्योंकि यह एक विक्रेता द्वारा आपूर्ति की जाती है। अन्य मामलों में मैं बदलाव करने में सक्षम हूं। मैं एक स्केलर UDF को एक क्वेरी में सिर्फ एक बार मूल्यांकन करने के लिए कैसे मजबूर कर सकता हूं?

जवाबों:


17

अंततः, क्वेरी में केवल एक बार एक स्केलर UDF का मूल्यांकन करने के लिए SQL सर्वर को मजबूर करना संभव नहीं है। हालाँकि, कुछ कदम हैं जिन्हें प्रोत्साहित करने के लिए यह कदम उठाया जा सकता है। परीक्षण के साथ मेरा मानना ​​है कि आप SQL सर्वर के वर्तमान संस्करण के साथ काम करने वाले कुछ प्राप्त कर सकते हैं, लेकिन यह संभव है कि भविष्य के परिवर्तनों के लिए आपको अपने कोड को फिर से देखना होगा।

यदि कोड को संपादित करना संभव है तो कोशिश करने के लिए एक अच्छी बात यह है कि यदि संभव हो तो फ़ंक्शन नियतात्मक बनाना चाहिए। पॉल व्हाइट यहां बताते हैं कि फ़ंक्शन को SCHEMABINDINGविकल्प के साथ बनाया जाना चाहिए और फ़ंक्शन कोड स्वयं को नियतात्मक होना चाहिए।

निम्नलिखित परिवर्तन करने के बाद:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

प्रश्न से प्रश्न 64 एमएस में निष्पादित किया जाता है:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

क्वेरी प्लान में अब फ़िल्टर ऑपरेटर नहीं है:

क्वेरी प्लान 1

यह सुनिश्चित करने के लिए कि यह केवल एक बार निष्पादित होता है जब हम SQL Server 2016 में जारी नए sysinos_exec_function_stats DMV का उपयोग कर सकते हैं :

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');

एक जारी करने वाले ALTERसमारोह के खिलाफ रीसेट कर देगा execution_countउस वस्तु के लिए। उपरोक्त क्वेरी 1 का अर्थ है कि फ़ंक्शन केवल एक बार निष्पादित किया गया था।

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

WITH cte (UDF_VALUE) AS
(
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

DISTINCTफ़िल्टर ऑपरेटर से छुटकारा पाने के लिए सुपरफ़्लस को जोड़ा गया था। योजना आशाजनक लग रही है:

क्वेरी प्लान 2

उसके आधार पर, किसी को उम्मीद होगी कि यूडीएफ एक बार मूल्यांकन किया जाएगा और नेस्टेड लूप में बाहरी तालिका के रूप में उपयोग किया जाएगा। हालाँकि, क्वेरी को मेरी मशीन पर चलाने के लिए 6446 ms लगते हैं। sys.dm_exec_function_statsसमारोह के अनुसार 100 बार निष्पादित किया गया था। यह कैसे संभव है? " कम्प्यूट स्केलर्स, एक्सप्रेशंस एंड एक्ज़ीक्यूशन प्लान परफॉर्मेंस " में, पॉल व्हाइट बताते हैं कि कंप्यूट स्केलर ऑपरेटर को टाल दिया जा सकता है:

अधिक बार नहीं, एक गणना स्केलर केवल एक अभिव्यक्ति को परिभाषित करता है; वास्तविक गणना को तब तक के लिए स्थगित कर दिया जाता है जब तक कि निष्पादन योजना में कुछ बाद में परिणाम की आवश्यकता होती है।

इस क्वेरी के लिए ऐसा लगता है कि UDF कॉल को तब तक के लिए स्थगित कर दिया गया था, जब तक कि उसे 100 बार मूल्यांकन नहीं किया गया था।

दिलचस्प है, UTEF के साथ SCHEMABINDINGमूल प्रश्न में परिभाषित नहीं होने पर CTE उदाहरण मेरी मशीन पर 71 एमएस में निष्पादित होता है । फ़ंक्शन को केवल एक बार निष्पादित किया जाता है जब क्वेरी चलाया जाता है। यहाँ उस के लिए क्वेरी योजना है:

क्वेरी प्लान 3

यह स्पष्ट नहीं है कि गणना स्केलर को स्थगित क्यों नहीं किया गया है। ऐसा इसलिए हो सकता है क्योंकि फ़ंक्शन का नॉनडेटर्मिज्म ऑपरेटरों के पुनर्व्यवस्थापन को सीमित करता है जो क्वेरी ऑप्टिमाइज़र कर सकता है।

एक वैकल्पिक तरीका यह है कि सीटीई में एक छोटी तालिका को जोड़ा जाए और उस तालिका में एकमात्र पंक्ति को क्वेरी करें। कोई भी छोटा टेबल करेगा, लेकिन निम्नलिखित का उपयोग करें:

CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);

INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);

क्वेरी तब बन जाती है:

WITH cte (UDF_VALUE) AS
(       
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
    FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

dbo.X_ONE_ROW_TABLEअनुकूलक के लिए अनिश्चितता को जोड़ता है। यदि तालिका में शून्य पंक्तियाँ हैं तो CTE 0 पंक्तियों को लौटा देगा। किसी भी स्थिति में, ऑप्टिमाइज़र यह गारंटी नहीं दे सकता कि UDF निर्धारक नहीं होने पर CTE एक पंक्ति में वापस आ जाएगी, इसलिए ऐसा लगता है कि UDF का मूल्यांकन जॉइन से पहले किया जाएगा। मुझे उम्मीद है कि ऑप्टिमाइज़र स्कैन dbo.X_ONE_ROW_TABLEकरने के लिए, एक पंक्ति का अधिकतम मूल्य प्राप्त करने के लिए एक स्ट्रीम एग्रीगेट का उपयोग करेगा (जिसे फ़ंक्शन का मूल्यांकन करने की आवश्यकता है), और उपयोग करने के लिए कि नेस्टेड लूप के लिए बाहरी तालिका dbo.X_100_INTEGERSमुख्य क्वेरी में शामिल हो । । ऐसा होता प्रतीत होता है :

क्वेरी प्लान 4

मेरे मशीन पर लगभग 110 ms में क्वेरी निष्पादित होती है और UDF का मूल्यांकन केवल एक बार के अनुसार किया जाता है sys.dm_exec_function_stats। यह कहना गलत नहीं होगा कि क्वेरी ऑप्टिमाइज़र को केवल एक बार यूडीएफ का मूल्यांकन करने के लिए मजबूर किया जाता है। हालांकि, एक आशावादी पुनर्लेखन की कल्पना करना कठिन है जो कि यूडीएफ के आसपास की सीमाओं और लागत स्केलर लागत की गणना के साथ कम लागत वाले प्रश्न को जन्म देगा।

सारांश में, नियतात्मक कार्यों के लिए (जिसमें SCHEMABINDINGविकल्प शामिल होना चाहिए ) क्वेरी को यथासंभव सरल तरीके से लिखने का प्रयास करें। यदि SQL Server 2016 या बाद के संस्करण पर, पुष्टि करें कि फ़ंक्शन का उपयोग करते समय केवल निष्पादित किया गया था sys.dm_exec_function_stats। निष्पादन की योजना उस संबंध में भ्रामक हो सकती है।

SQL सर्वर द्वारा निर्धारक नहीं होने वाले कार्यों के लिए, SCHEMABINDINGविकल्प की कमी के साथ कुछ भी शामिल है , एक दृष्टिकोण UDF को सावधानी से तैयार किए गए CTE या व्युत्पन्न तालिका में रखना है। इसके लिए थोड़ी देखभाल की आवश्यकता होती है लेकिन एक ही सीटीई नियतात्मक और नॉनडेर्मिनिस्टिक दोनों कार्यों के लिए काम कर सकता है।

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