मेरी चयनित DISTINCT TOP N क्वेरी संपूर्ण तालिका को स्कैन क्यों करती है?


28

मैंने कुछ SELECT DISTINCT TOP Nप्रश्नों में भाग लिया है जो SQL सर्वर क्वेरी ऑप्टिमाइज़र द्वारा खराब रूप से अनुकूलित किए गए प्रतीत होते हैं। आइए एक तुच्छ उदाहरण पर विचार करके शुरू करें: दो वैकल्पिक मूल्यों के साथ एक लाख पंक्ति तालिका। मैं डेटा उत्पन्न करने के लिए GetNums फ़ंक्शन का उपयोग करूंगा :

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

निम्नलिखित प्रश्न के लिए:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL सर्वर तालिका के पहले डेटा पेज को स्कैन करके दो अलग-अलग मान पा सकता है, लेकिन यह इसके बजाय सभी डेटा को स्कैन करता है । SQL सर्वर तब तक स्कैन नहीं करता है जब तक कि वह अलग-अलग मानों की मांगी गई संख्या को नहीं खोज लेता है?

इस प्रश्न के लिए कृपया निम्नलिखित परीक्षण डेटा का उपयोग करें जिसमें 10 मिलियन पंक्तियाँ होती हैं जिनमें 10 अलग-अलग मान उत्पन्न होते हैं:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

एक संकुल सूचकांक वाली तालिका के उत्तर भी स्वीकार्य हैं:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

निम्न क्वेरी तालिका से सभी 10 मिलियन पंक्तियों को स्कैन करती है । मुझे ऐसी कोई चीज़ कैसे मिल सकती है जो संपूर्ण तालिका को स्कैन नहीं करती है? मैं SQL Server 2016 SP1 का उपयोग कर रहा हूं।

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

जवाबों:


30

तीन अलग-अलग ऑप्टिमाइज़र नियम हैं जो DISTINCTउपरोक्त क्वेरी में ऑपरेशन कर सकते हैं । निम्न क्वेरी एक त्रुटि फेंकती है जो बताती है कि सूची संपूर्ण है:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

एमएसजी 8622, स्तर 16, राज्य 1, पंक्ति 1

क्वेरी प्रोसेसर इस क्वेरी में परिभाषित संकेतों के कारण क्वेरी प्लान नहीं बना सकता है। किसी भी संकेत को निर्दिष्ट किए बिना और SET FORCEPLAN का उपयोग किए बिना क्वेरी को फिर से सबमिट करें।

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

हैश मैच ( फ्लो विशिष्ट ) ऑपरेटर इस समस्या को हल करने का एक तरीका है क्योंकि यह अवरुद्ध नहीं है। SQL सर्वर को स्कैन को रोकने में सक्षम होना चाहिए क्योंकि यह पर्याप्त भिन्न मान पाता है।

फ्लो डिस्टिक्ट लॉजिकल ऑपरेटर डुप्लिकेट को हटाते हुए इनपुट को स्कैन करता है। जबकि डिस्टिक्ट ऑपरेटर किसी भी आउटपुट को बनाने से पहले सभी इनपुट का उपभोग करता है, फ़्लो डिस्टिक्ट ऑपरेटर इनपुट के रूप में प्राप्त होने पर प्रत्येक पंक्ति को लौटाता है (जब तक कि वह पंक्ति डुप्लिकेट न हो, उस स्थिति में उसे छोड़ दिया जाता है)।

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

योजना मार्गदर्शिका प्राप्त करने का एक तरीका यहां है:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

एक योजना मार्गदर्शिका बनाने के लिए योजना को संभालें और उसका उपयोग करें:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

योजना मार्गदर्शिका केवल सटीक क्वेरी पाठ पर काम करती है, इसलिए इसे योजना गाइड से वापस कॉपी करें:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

डेटा रीसेट करें:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

लागू योजना गाइड के साथ क्वेरी के लिए एक क्वेरी प्लान प्राप्त करें :

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

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

इस समस्या पर हमला करने का एक तरीका पंक्ति लक्ष्य को कम करना है। यदि ऑप्टिमाइज़र के दृष्टिकोण से पंक्ति का लक्ष्य उन पंक्तियों की अलग-अलग गणना से कम है, जिन्हें हम संभवतः हैश मैच (अलग प्रवाह) प्राप्त करेंगे। इसे OPTIMIZE FORक्वेरी संकेत के साथ पूरा किया जा सकता है :

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

इस क्वेरी के लिए ऑप्टिमाइज़र एक योजना बनाता है जैसे कि क्वेरी को केवल पहली पंक्ति की आवश्यकता होती है लेकिन जब क्वेरी निष्पादित होती है तो यह पहले 10 पंक्तियों को वापस मिल जाती है। मेरी मशीन पर यह क्वेरी X_10_DISTINCT_HEAPसीपीयू समय के 250 एमएस और 2537 तार्किक रीड के साथ 299 एमएस में 892800 पंक्तियों को पूरा करती है।

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

इस समस्या पर हमला करने का एक अन्य तरीका यह है कि अनुमानित भिन्न मानों की संख्या को बढ़ाकर SQL सर्वर बेस टेबल से प्राप्त करने की अपेक्षा करता है। यह उम्मीद से ज्यादा कठिन था। एक नियतात्मक कार्य को लागू करने से संभवतः परिणामों की विशिष्ट संख्या में वृद्धि नहीं हो सकती है। यदि क्वेरी ऑप्टिमाइज़र उस गणितीय तथ्य से अवगत है (कुछ परीक्षण यह बताता है कि यह कम से कम हमारे उद्देश्यों के लिए है) तो नियतात्मक कार्य (जिसमें सभी स्ट्रिंग फ़ंक्शंस शामिल हैं) को लागू करने से अलग-अलग पंक्तियों की अनुमानित संख्या में वृद्धि नहीं होगी।

गैर नियतात्मक कार्यों में से कई या तो काम नहीं किया, के स्पष्ट विकल्प सहित NEWID()और RAND()। हालाँकि, LAG()इस क्वेरी के लिए ट्रिक करता है। क्वेरी ऑप्टिमाइज़र को उम्मीद है कि LAGअभिव्यक्ति के खिलाफ 10 मिलियन अलग-अलग मूल्य हैं जो एक हैश मैच (प्रवाह अलग) योजना को प्रोत्साहित करेंगे :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

मेरी मशीन पर यह क्वेरी 892800 पंक्तियों को स्कैन करती है X_10_DISTINCT_HEAPऔर 1165 ms में 1109 ms CPU समय और 2537 तार्किक रीड्स के साथ पूरा करती है, इसलिए यह LAG()काफी सापेक्ष ओवरहेड जोड़ता है। @Paul व्हाइट ने इस क्वेरी के लिए बैच मोड प्रोसेसिंग की कोशिश करने का सुझाव दिया। SQL Server 2016 पर हम बैच मोड प्रोसेसिंग को भी प्राप्त कर सकते हैं MAXDOP 1। एक पंक्ति स्टोर के लिए बैच मोड प्रसंस्करण प्राप्त करने का एक तरीका निम्न CCI में शामिल होना है:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

इस क्वेरी योजना में कोड परिणाम है ।

पॉल ने बताया कि मुझे उपयोग करने के लिए क्वेरी को बदलना पड़ा LAG(..., 1)क्योंकि LAG(..., 0)यह विंडो एग्रीगेट ऑप्टिमाइज़ेशन के लिए योग्य नहीं है। इस परिवर्तन ने बीता हुआ समय 520 ms और CPU समय को घटाकर 454 ms कर दिया।

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

एक अद्वितीय कॉलम (जैसे प्रश्न में क्लस्टर इंडेक्स उदाहरण) के साथ एक तालिका के खिलाफ हमारे पास बेहतर विकल्प हैं। उदाहरण के लिए हम ऑप्टिमाइज़र को एक ऐसी SUBSTRINGअभिव्यक्ति का उपयोग करके ट्रिक कर सकते हैं जो हमेशा एक खाली स्ट्रिंग लौटाती है। SQL सर्वर यह नहीं सोचता है कि SUBSTRINGअलग-अलग मानों की संख्या बदल जाएगी इसलिए यदि हम इसे एक अद्वितीय कॉलम, जैसे PK पर लागू करते हैं, तो अलग-अलग पंक्तियों की अनुमानित संख्या 10 मिलियन है। निम्न क्वेरी को हैश मैच (प्रवाह भिन्न) ऑपरेटर मिलता है:

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

मेरी मशीन पर यह क्वेरी 29000 X_10_DISTINCT_CIसीपीयू समय और 3011 तार्किक रीड के साथ 333 एमएस में पूर्ण और से 900000 पंक्तियों को स्कैन करती है।

सारांश में, क्वेरी ऑप्टिमाइज़र यह मानता है कि सभी पंक्तियों को तालिका से SELECT DISTINCT TOP Nप्रश्नों के लिए पढ़ा जाएगा जब N> = तालिका से अनुमानित अलग-अलग पंक्तियों की संख्या। हैश मैच (एग्रीगेट) ऑपरेटर की हैश मैच (फ्लो विशिष्ट) ऑपरेटर के समान लागत हो सकती है, लेकिन ऑप्टिमाइज़र हमेशा एग्रीगेट ऑपरेटर को चुनता है। यह अनावश्यक तार्किक रीड को जन्म दे सकता है जब टेबल स्कैन की शुरुआत के पास पर्याप्त भिन्न मान स्थित होते हैं। हैश मैच (फ्लो विशिष्ट) ऑपरेटर का उपयोग करके ऑप्टिमाइज़र को ट्रिक करने के दो तरीके हैं OPTIMIZE FORहिंट का उपयोग करके पंक्ति लक्ष्य को कम करना LAG()या SUBSTRINGएक अद्वितीय कॉलम का उपयोग करके अलग-अलग पंक्तियों की अनुमानित संख्या को बढ़ाना ।


12

आपने पहले ही अपने प्रश्नों का सही उत्तर दे दिया है।

मैं सिर्फ एक अवलोकन जोड़ना चाहता हूं कि वास्तव में सबसे प्रभावी तरीका पूरी मेज को स्कैन करना है - अगर इसे एक स्तंभ के रूप में व्यवस्थित किया जा सकता है 'हीप' :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

सरल प्रश्न:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

तब देता है:

निष्पादन योजना

तालिका 'X_10_DISTINCT_HEAP'। स्कैन गिनती 1,
 तार्किक पढ़ता है 0, शारीरिक पढ़ता है 0, पढ़ता-पढ़ता आगे 0, 
 लॉब लॉजिकल रीड 66 , लॉब फिजिकल रीड्स 0, लॉब रीड-फॉरवर्ड रीड्स 0।
तालिका 'X_10_DISTINCT_HEAP'। खंड 13 पढ़ता है, खंड 0 छोड़ दिया है।

 SQL सर्वर निष्पादन समय:
   सीपीयू समय = 0 एमएस, बीता हुआ समय = 11 एमएस।

हैश मैच (फ्लो डिस्टिंक्ट) वर्तमान में बैच मोड में निष्पादित नहीं कर सकता है। बैच से पंक्ति प्रसंस्करण के लिए (अदृश्य) महंगे संक्रमण के कारण इसका उपयोग करने वाले तरीके बहुत धीमे हैं। उदाहरण के लिए:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

देता है:

प्रवाह विचलित निष्पादन योजना

तालिका 'X_10_DISTINCT_HEAP'। स्कैन गिनती 1,
 तार्किक पढ़ता है 0, शारीरिक पढ़ता है 0, पढ़ता-पढ़ता आगे 0, 
 लॉब लॉजिकल रीड 20 , लॉब फिजिकल रीड्स 0, लॉब रीड-फॉरवर्ड रीड्स 0।
तालिका 'X_10_DISTINCT_HEAP'। खंड 4 को पढ़ता है , खंड 0 को छोड़ दिया गया है।

 SQL सर्वर निष्पादन समय:
   सीपीयू समय = 640 एमएस, बीता हुआ समय = 680 एमएस।

यह तब से धीमी है जब तालिका एक रोड़े की ढेर के रूप में व्यवस्थित होती है।


5

यहाँ एक पुनरावर्ती CTE का उपयोग करके बार-बार आंशिक स्कैन (समान स्कैन के समान लेकिन स्किप स्कैन के समान) का अनुकरण करने का प्रयास किया गया है। उद्देश्य - चूंकि हमारे पास कोई सूचकांक नहीं है (id)- टेबल पर सॉर्ट और कई स्कैन से बचने के लिए है।

यह कुछ पुनरावर्ती CTE प्रतिबंधों को बायपास करने के लिए कुछ चालें करता है:

  • TOPपुनरावर्ती भाग में कोई अनुमति नहीं है । हम एक उप-वर्ग का उपयोग करते हैं और ROW_NUMBER()इसके बजाय।
  • हमारे पास निरंतर भाग के कई संदर्भ नहीं हो सकते हैं LEFT JOINया NOT IN (SELECT id FROM cte)पुनरावर्ती भाग से उपयोग या उपयोग नहीं कर सकते हैं । बायपास करने के लिए, हम एक VARCHARस्ट्रिंग का निर्माण करते हैं जो सभी idमूल्यों को जमा करती है, STRING_AGGपदानुक्रम के समान या फिर इसके साथ तुलना करती है LIKE

एक ढेर के लिए (स्तंभ का नाम दिया गया है id) rextester.com पर टेस्ट -1

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

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

और जब टेबल को (सीआई पर unique_key) क्लस्टर किया जाता है , तो rextester.com पर टेस्ट -2

यह WHERE x.unique_key > ct.unique_keyकई स्कैन से बचने के लिए क्लस्टर इंडेक्स ( ) का उपयोग करता है:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

इस समाधान के साथ काफी सूक्ष्म प्रदर्शन मुद्दा है। यह Nth मान प्राप्त करने के बाद टेबल पर एक अतिरिक्त तलाश करना समाप्त करता है। तो अगर शीर्ष 10 के लिए 10 अलग-अलग मूल्य हैं, तो यह 11 वें मूल्य के लिए दिखेगा जो कि नहीं है। आप एक अतिरिक्त पूर्ण स्कैन के साथ समाप्त होते हैं और 10 मिलियन ROW_NUMBER () गणना वास्तव में जोड़ते हैं। मेरे पास यहां एक समाधान है जो मेरी मशीन पर क्वेरी 20X को गति देता है। तुम क्या सोचते हो? brentozar.com/pastetheplan/?id=SkDhAmFKe
3

2

पूर्णता के लिए, इस समस्या से निपटने का एक और तरीका है OUTER APPLY का उपयोग करना । हम OUTER APPLYप्रत्येक अलग मूल्य के लिए एक ऑपरेटर जोड़ सकते हैं जिसे हमें खोजने की आवश्यकता है। यह ypercube के पुनरावर्ती दृष्टिकोण की अवधारणा के समान है, लेकिन प्रभावी रूप से हाथ से लिखी गई पुनरावृत्ति है। एक फायदा यह है कि हम वर्कअराउंड के TOPबजाय व्युत्पन्न टेबल में उपयोग करने में सक्षम हैं ROW_NUMBER()। एक बड़ा नुकसान यह है कि क्वेरी टेक्स्ट Nबढ़ जाता है ।

यहाँ ढेर के खिलाफ क्वेरी के लिए एक कार्यान्वयन है:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

यहाँ उपरोक्त क्वेरी के लिए वास्तविक क्वेरी प्लान है। मेरी मशीन पर यह प्रश्न 713 एमएस में 625 एमएसयू सीपीयू समय और 12605 तार्किक रीड के साथ पूरा होता है। हमें प्रत्येक 100k पंक्तियों में एक नया विशिष्ट मान मिलता है, इसलिए मैं इस क्वेरी को लगभग 900000 * 10 * 0.5 = 4500000 पंक्तियों को स्कैन करने की अपेक्षा करूंगा। सिद्धांत रूप में इस क्वेरी को इस क्वेरी के तार्किक उत्तर को अन्य उत्तर से पांच गुना करना चाहिए:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

उस क्वेरी ने 2537 तार्किक रीड किए। 2537 * 5 = 12685 जो कि 12605 के काफी करीब है।

क्लस्टर इंडेक्स वाली तालिका के लिए हम बेहतर कर सकते हैं। इसका कारण यह है कि हम एक ही पंक्तियों को दो बार स्कैन करने से बचने के लिए व्युत्पन्न तालिका में अंतिम संकुल कुंजी मान में पारित कर सकते हैं। एक कार्यान्वयन:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

यहाँ उपरोक्त क्वेरी के लिए वास्तविक क्वेरी प्लान है। मेरी मशीन पर यह क्वेरी 154 ms में 140 ms CPU समय और 3203 तार्किक रीड के साथ पूरी होती है। यह OPTIMIZE FORसंकुल अनुक्रमणिका तालिका के विरूद्ध क्वेरी की तुलना में थोड़ी तेज़ी से चलता है । मुझे उम्मीद नहीं थी कि मैंने प्रदर्शन को अधिक सावधानी से मापने की कोशिश की। मेरी कार्यप्रणाली प्रत्येक क्वेरी को परिणाम सेट के बिना दस बार चलाने के लिए sys.dm_exec_sessionsऔर से और कुल संख्याओं को देखने के लिए थी sys.dm_exec_session_wait_stats। सत्र 56 APPLYक्वेरी था और सत्र 63 OPTIMIZE FORक्वेरी था ।

का आउटपुट sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

APPLYक्वेरी के लिए cpu_time और elapsed_time में स्पष्ट लाभ प्रतीत होता है ।

का आउटपुट sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

OPTIMIZE FORक्वेरी एक अतिरिक्त प्रतीक्षा प्रकार होता है, RESERVED_MEMORY_ALLOCATION_EXT । मैं वास्तव में इसका मतलब नहीं जानता। यह सिर्फ हैश मैच (प्रवाह अलग) ऑपरेटर में ओवरहेड का माप हो सकता है। किसी भी मामले में, शायद सीपीयू समय में 70 एमएस के अंतर के बारे में चिंता करने योग्य नहीं है।


1

मुझे लगता है कि आपके पास इसका जवाब है कि
यह पता करने का एक तरीका हो सकता है कि
मुझे पता है कि यह गड़बड़ लग रहा है, लेकिन निष्पादन योजना ने कहा कि शीर्ष 2 में 84% लागत थी

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

इस कोड ने मेरी मशीन पर 5 सेकंड का समय लिया। ऐसा लगता है कि टेबल चर में शामिल होने से ओवरहेड का थोड़ा सा जोड़ होता है। अंतिम क्वेरी में तालिका चर 892800 बार स्कैन किया गया था। उस क्वेरी में CPU समय का 1359 ms और बीता समय का 1374 ms लिया गया। निश्चित रूप से मेरी अपेक्षा से अधिक। तालिका चर में एक प्राथमिक कुंजी जोड़ना मदद करने के लिए लगता है, हालांकि मुझे यकीन नहीं है कि क्यों। अन्य संभावित अनुकूलन हो सकते हैं।
जो ओबिश
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.