शामिल होने के बजाय समुच्चय द्वारा "ToTime" ढूँढना
मैं वास्तव में एक जंगली क्वेरी साझा करना चाहता हूं जो केवल 1 तार्किक पढ़ने के साथ तालिका का 1 स्कैन लेता है। तुलना करके, पृष्ठ पर सबसे अच्छा अन्य उत्तर, साइमन किंग्स्टन की क्वेरी, 2 स्कैन लेती है।
डेटा के एक बहुत बड़े सेट (17,408 इनपुट पंक्तियों, 8,193 परिणाम पंक्तियों का उत्पादन) पर यह CPU 574 और समय 2645 लेता है, जबकि साइमन किंग्स्टन की क्वेरी CPU 63,820 और समय 37,108 लेती है।
यह संभव है कि अनुक्रमणिका के साथ पृष्ठ पर अन्य प्रश्न कई गुना बेहतर प्रदर्शन कर सकते हैं, लेकिन केवल क्वेरी को फिर से लिखकर 111x CPU सुधार और 14x गति सुधार प्राप्त करना मेरे लिए दिलचस्प है।
(कृपया ध्यान दें:मेरा मतलब साइमन किंग्स्टन या किसी और के लिए बिल्कुल भी अनादर नहीं है; मैं इस प्रश्न को इतनी अच्छी तरह से समझने के लिए अपने विचार के बारे में उत्साहित हूं। उनकी क्वेरी मुझसे बेहतर है क्योंकि इसका प्रदर्शन बहुत है और यह वास्तव में समझने योग्य और रखरखाव योग्य है , मेरे विपरीत।)
यहाँ असंभव क्वेरी है। समझना मुश्किल है। लिखना कठिन था। लेकिन यह कमाल है। :)पी>
WITH Ranks AS (
SELECT
T = Dense_Rank() OVER (ORDER BY Time, Num),
N = Dense_Rank() OVER (PARTITION BY Name ORDER BY Time, Num),
*
FROM
#Data D
CROSS JOIN (
VALUES (1), (2)
) X (Num)
), Items AS (
SELECT
FromTime = Min(Time),
ToTime = Max(Time),
Name = IsNull(Min(CASE WHEN Num = 2 THEN Name END), Min(Name)),
I = IsNull(Min(CASE WHEN Num = 2 THEN T - N END), Min(T - N)),
MinNum = Min(Num)
FROM
Ranks
GROUP BY
T / 2
)
SELECT
FromTime = Min(FromTime),
ToTime = CASE WHEN MinNum = 2 THEN NULL ELSE Max(ToTime) END,
Name
FROM Items
GROUP BY
I, Name, MinNum
ORDER BY
FromTime
नोट:इसके लिए SQL 2008 या बाद के संस्करण की आवश्यकता है। इसे SQL 2005 में काम करने के लिए, VALUES क्लॉज को चुनें 1 UNION सभी SELECT 2
में बदलें ।
अपडेट की गई क्वेरी
इसके बारे में थोड़ा सोचने के बाद, मुझे एहसास हुआ कि मैं एक ही समय में दो अलग-अलग तार्किक कार्यों को पूरा कर रहा था, और इसने क्वेरी को अनावश्यक रूप से जटिल बना दिया:1) मध्यवर्ती पंक्तियों को हटा दें जिनका अंतिम समाधान पर कोई असर नहीं पड़ता है (पंक्तियां जो शुरू नहीं होती हैं) एक नया कार्य) और 2) अगली पंक्ति से "ToTime" मान खींचें। #1 पहले . प्रदर्शन करके #2, क्वेरी सरल है और लगभग आधे CPU के साथ काम करती है!
तो यहाँ सरलीकृत क्वेरी है जो पहले, उन पंक्तियों को काट देती है जिनकी हमें परवाह नहीं है, फिर जॉइन के बजाय समुच्चय का उपयोग करके ToTime मान प्राप्त करता है। हां, इसमें 2 के बजाय 3 विंडोिंग फ़ंक्शन हैं, लेकिन अंततः कम पंक्तियों के कारण (उनकी छंटाई करने के बाद जिनकी हमें परवाह नहीं है) इसके पास करने के लिए कम काम है:
WITH Ranks AS (
SELECT
Grp =
Row_Number() OVER (ORDER BY Time)
- Row_Number() OVER (PARTITION BY Name ORDER BY Time),
[Time], Name
FROM #Data D
), Ranges AS (
SELECT
Result = Row_Number() OVER (ORDER BY Min(R.[Time]), X.Num) / 2,
[Time] = Min(R.[Time]),
R.Name, X.Num
FROM
Ranks R
CROSS JOIN (VALUES (1), (2)) X (Num)
GROUP BY
R.Name, R.Grp, X.Num
)
SELECT
FromTime = Min([Time]),
ToTime = CASE WHEN Count(*) = 1 THEN NULL ELSE Max([Time]) END,
Name = IsNull(Min(CASE WHEN Num = 2 THEN Name ELSE NULL END), Min(Name))
FROM Ranges R
WHERE Result > 0
GROUP BY Result
ORDER BY FromTime;
इस अद्यतन क्वेरी में वही सभी मुद्दे हैं जो मैंने अपने स्पष्टीकरण में प्रस्तुत किए हैं, हालांकि, उन्हें हल करना आसान है क्योंकि मैं अतिरिक्त अनावश्यक पंक्तियों से निपट नहीं रहा हूं। मैं यह भी देखता हूं कि Row_Number() / 2
0 का मान मुझे बाहर करना पड़ा, और मुझे यकीन नहीं है कि मैंने इसे पूर्व क्वेरी से क्यों नहीं निकाला, लेकिन किसी भी मामले में यह पूरी तरह से काम करता है और आश्चर्यजनक रूप से तेज़ है!
बाह्य रूप से साफ-सुथरी चीजों को लागू करें
अंत में, यहां मूल रूप से साइमन किंग्स्टन की क्वेरी के समान एक संस्करण है जो मुझे लगता है कि सिंटैक्स को समझना आसान है।
SELECT
FromTime = Min(D.Time),
X.ToTime,
D.Name
FROM
#Data D
OUTER APPLY (
SELECT TOP 1 ToTime = D2.[Time]
FROM #Data D2
WHERE
D.[Time] < D2.[Time]
AND D.[Name] <> D2.[Name]
ORDER BY D2.[Time]
) X
GROUP BY
X.ToTime,
D.Name
ORDER BY
FromTime;
यदि आप बड़े डेटा सेट पर प्रदर्शन तुलना करना चाहते हैं तो यहां सेटअप स्क्रिप्ट है:
CREATE TABLE #Data (
RecordId int,
[Time] int,
Name varchar(10)
);
INSERT #Data VALUES
(1, 10, 'Running'),
(2, 18, 'Running'),
(3, 21, 'Running'),
(4, 29, 'Walking'),
(5, 33, 'Walking'),
(6, 57, 'Running'),
(7, 66, 'Running'),
(8, 77, 'Running'),
(9, 81, 'Walking'),
(10, 89, 'Running'),
(11, 93, 'Walking'),
(12, 99, 'Running'),
(13, 107, 'Running'),
(14, 113, 'Walking'),
(15, 124, 'Walking'),
(16, 155, 'Walking'),
(17, 178, 'Running');
GO
insert #data select recordid + (select max(recordid) from #data), time + (select max(time) +25 from #data), name from #data
GO 10
स्पष्टीकरण
यहाँ मेरी क्वेरी के पीछे मूल विचार है।
-
एक स्विच का प्रतिनिधित्व करने वाले समय को दो आसन्न पंक्तियों में दिखाना होता है, एक पिछली गतिविधि को समाप्त करने के लिए, और दूसरा अगली गतिविधि शुरू करने के लिए। इसका प्राकृतिक समाधान एक जुड़ाव है ताकि एक आउटपुट पंक्ति अपनी पंक्ति (शुरुआती समय के लिए) से खींच सके और अगला बदला पंक्ति (अंतिम समय के लिए)।
-
हालांकि, मेरी क्वेरी
क्रॉस जॉइन (VALUES (1), (2))
के साथ पंक्ति को दो बार दोहराकर दो अलग-अलग पंक्तियों में अंतिम समय प्रदर्शित करने की आवश्यकता को पूरा करती है। . अब हमारे पास हमारी सभी पंक्तियों की नकल है। विचार यह है कि कॉलम में गणना करने के लिए जॉइन का उपयोग करने के बजाय, हम प्रत्येक वांछित जोड़ी पंक्तियों को एक में संक्षिप्त करने के लिए एकत्रीकरण के कुछ रूपों का उपयोग करेंगे। -
अगला कार्य प्रत्येक डुप्लिकेट पंक्ति को ठीक से विभाजित करना है ताकि एक उदाहरण पिछली जोड़ी के साथ और एक अगली जोड़ी के साथ चला जाए। यह टी कॉलम, एक
ROW_NUMBER()
. के साथ पूरा किया जाता हैसमय
. द्वारा आदेशित , और फिर 2 से विभाजित (हालांकि मैंने इसे बदल दिया है, समरूपता के लिए DENSE_RANK() करते हैं क्योंकि इस मामले में यह ROW_NUMBER के समान मान देता है)। दक्षता के लिए मैंने अगले चरण में विभाजन किया ताकि पंक्ति संख्या को दूसरी गणना में पुन:उपयोग किया जा सके (पढ़ते रहें)। चूंकि पंक्ति संख्या 1 से शुरू होती है, और 2 से भाग देने पर परोक्ष रूप से int में परिवर्तित हो जाता है, इसका अनुक्रम0 1 1 2 2 3 3 4 4 ...
उत्पन्न करने का प्रभाव पड़ता है। जिसका वांछित परिणाम है:इस गणना मूल्य के आधार पर समूहित करके, क्योंकि हमनेNum
. द्वारा भी आदेश दिया है पंक्ति संख्या में, हमने अब यह पूरा कर लिया है कि पहले के बाद के सभी सेट में "पूर्व" पंक्ति से एक संख्या =2 और "अगली" पंक्ति से एक संख्या =1 शामिल है। -
अगला मुश्किल काम उन पंक्तियों को खत्म करने का तरीका ढूंढ रहा है जिनकी हम परवाह नहीं करते हैं और किसी तरह ब्लॉक के प्रारंभ समय को ब्लॉक के अंत समय के समान पंक्ति में संक्षिप्त कर देते हैं। हम जो चाहते हैं वह यह है कि दौड़ने या चलने के प्रत्येक असतत सेट को अपना नंबर दिया जाए ताकि हम इसके द्वारा समूह बना सकें।
DENSE_RANK()
एक प्राकृतिक समाधान है, लेकिन एक समस्या यह है कि यहORDER BY
के प्रत्येक मान पर ध्यान देता है क्लॉज--हमारे पासDENSE_RANK() OVER (प्रीऑर्डर बाय टाइम ऑर्डर बाय नेम)
करने के लिए सिंटैक्स नहीं है। ताकिसमय
रैंक
. का कारण नहीं बनता हैनाम
. में प्रत्येक परिवर्तन को छोड़कर परिवर्तन की गणना . कुछ सोचने के बाद मुझे एहसास हुआ कि मैं इत्ज़िक बेन-गण के समूहीकृत द्वीप समाधान के पीछे के तर्क से थोड़ा पीछे हट सकता हूं, और मुझे पता चला कि पंक्तियों की रैंकसमय
द्वारा क्रमबद्ध है ,Name
. द्वारा विभाजित पंक्तियों की रैंक से घटाया गया औरसमय
. द्वारा आदेशित किया गया , एक मान प्राप्त करेगा जो एक ही समूह में प्रत्येक पंक्ति के लिए समान था लेकिन अन्य समूहों से अलग था। सामान्य समूहीकृत द्वीप तकनीक दो परिकलित मान बनाने के लिए है जो दोनों लॉकस्टेप में पंक्तियों के साथ चढ़ते हैं जैसे कि4 5 6
और1 2 3
, कि जब घटाया जाए तो वही मान प्राप्त होगा (इस उदाहरण के मामले में3 3 3
4 - 1
. के परिणाम के रूप में ,5 - 2
, और6 - 3
) नोट:मैंने शुरुआत मेंROW_NUMBER()
. से शुरुआत की थी मेरेN
. के लिए गणना लेकिन यह काम नहीं कर रहा था। सही उत्तर थाDENSE_RANK()
हालांकि मुझे यह कहते हुए खेद है कि मुझे याद नहीं है कि मैंने उस समय यह निष्कर्ष क्यों निकाला, और मुझे इसका पता लगाने के लिए फिर से गोता लगाना होगा। लेकिन वैसे भी,T-N
. यही है गणना करता है:एक संख्या जिसे एक स्थिति (या तो दौड़ना या चलना) के प्रत्येक "द्वीप" को अलग करने के लिए समूहीकृत किया जा सकता है। -
लेकिन यह अंत नहीं था क्योंकि कुछ झुर्रियाँ हैं। सबसे पहले, प्रत्येक समूह में "अगली" पंक्ति में
नाम
. के लिए गलत मान हैं ,एन
, औरटी
. हम प्रत्येक समूह से,Num =2
. से मान का चयन करके इसे प्राप्त करते हैं पंक्ति जब यह मौजूद है (लेकिन यदि ऐसा नहीं है, तो हम शेष मान का उपयोग करते हैं)। यहCASE WHEN NUM =2 THEN x END
. जैसे एक्सप्रेशन देता है :यह गलत "अगली" पंक्ति मानों को ठीक से हटा देगा। -
कुछ प्रयोग के बाद, मुझे एहसास हुआ कि
T - N
. द्वारा समूहित करना पर्याप्त नहीं था अपने आप में, क्योंकि चलने वाले समूहों और दौड़ने वाले समूहों दोनों का एक ही परिकलित मान हो सकता है (17 तक प्रदान किए गए मेरे नमूना डेटा के मामले में, दोT - N
हैं। 6 के मान)। लेकिन बसनाम
. के आधार पर समूह बनाना साथ ही इस समस्या का समाधान भी करता है। "रनिंग" या "वॉकिंग" के किसी भी समूह में विपरीत प्रकार के बीच के मानों की संख्या समान नहीं होगी। अर्थात्, चूंकि पहला समूह "रनिंग" से शुरू होता है, और अगले "रनिंग" समूह से पहले दो "वॉकिंग" पंक्तियाँ हस्तक्षेप करती हैं, तो N का मानT
उस अगले "रनिंग" समूह में। मुझे अभी एहसास हुआ कि इस बारे में सोचने का एक तरीका यह है किT - N
गणना वर्तमान पंक्ति से पहले पंक्तियों की संख्या की गणना करती है जो समान मान "रनिंग" या "वॉकिंग" से संबंधित नहीं हैं। कुछ विचार यह दिखाएंगे कि यह सच है:यदि हम तीसरे "रनिंग" समूह पर आगे बढ़ते हैं, तो यह "चलने" समूह को अलग करने के आधार पर केवल तीसरा समूह है, इसलिए इसमें अलग-अलग संख्या में हस्तक्षेप करने वाली पंक्तियां आ रही हैं इससे पहले, और उच्च स्थिति से शुरू होने के कारण, यह इतना अधिक है कि मानों को दोहराया नहीं जा सकता है। -
अंत में, चूंकि हमारे अंतिम समूह में केवल एक पंक्ति है (कोई समाप्ति समय नहीं है और हमें एक
NULL
प्रदर्शित करने की आवश्यकता है। इसके बजाय) मुझे एक गणना में फेंकना पड़ा जिसका उपयोग यह निर्धारित करने के लिए किया जा सकता है कि हमारे पास अंतिम समय था या नहीं। यहMin(Num)
. के साथ पूरा किया जाता है अभिव्यक्ति और फिर अंत में यह पता लगाना कि जब न्यूनतम (संख्या) 2 थी (जिसका अर्थ है कि हमारे पास "अगली" पंक्ति नहीं थी) तो एकNULL
प्रदर्शित करेंMax(ToTime)
. के बजाय मूल्य।
मुझे आशा है कि यह स्पष्टीकरण लोगों के लिए कुछ उपयोगी है। मुझे नहीं पता कि मेरी "पंक्ति-गुणा" तकनीक आम तौर पर उपयोगी होगी और उत्पादन वातावरण में अधिकांश SQL क्वेरी लेखकों के लिए लागू होगी क्योंकि इसे समझने में कठिनाई और रखरखाव की कठिनाई यह निश्चित रूप से आने वाले अगले व्यक्ति को पेश करेगी कोड (प्रतिक्रिया शायद "यह पृथ्वी पर क्या कर रहा है!?!" इसके बाद एक त्वरित "फिर से लिखने का समय!")।
यदि आपने इसे इतनी दूर कर लिया है तो मैं आपके समय के लिए और मुझे अविश्वसनीय रूप से मजेदार-एसक्यूएल-पहेली-भूमि में अपने छोटे से भ्रमण में शामिल करने के लिए धन्यवाद देता हूं।
इसे स्वयं देखें
ए.के.ए. "प्रीऑर्डर बाय" सिम्युलेट करना:
एक आखिरी नोट। यह देखने के लिए कि कैसे T - N
काम करता है - और यह देखते हुए कि मेरी विधि के इस भाग का उपयोग आमतौर पर SQL समुदाय पर लागू नहीं हो सकता है - नमूना डेटा की पहली 17 पंक्तियों के विरुद्ध निम्न क्वेरी चलाएँ:
WITH Ranks AS (
SELECT
T = Dense_Rank() OVER (ORDER BY Time),
N = Dense_Rank() OVER (PARTITION BY Name ORDER BY Time),
*
FROM
#Data D
)
SELECT
*,
T - N
FROM Ranks
ORDER BY
[Time];
यह पैदावार:
RecordId Time Name T N T - N
----------- ---- ---------- ---- ---- -----
1 10 Running 1 1 0
2 18 Running 2 2 0
3 21 Running 3 3 0
4 29 Walking 4 1 3
5 33 Walking 5 2 3
6 57 Running 6 4 2
7 66 Running 7 5 2
8 77 Running 8 6 2
9 81 Walking 9 3 6
10 89 Running 10 7 3
11 93 Walking 11 4 7
12 99 Running 12 8 4
13 107 Running 13 9 4
14 113 Walking 14 5 9
15 124 Walking 15 6 9
16 155 Walking 16 7 9
17 178 Running 17 10 7
महत्वपूर्ण हिस्सा यह है कि "चलना" या "चलना" के प्रत्येक समूह का T - N
के लिए समान मान है जो समान नाम वाले किसी भी अन्य समूह से अलग है।
प्रदर्शन
मैं अपनी क्वेरी के अन्य लोगों की तुलना में तेज़ होने के बारे में बात नहीं करना चाहता। हालांकि, यह देखते हुए कि अंतर कितना हड़ताली है (जब कोई अनुक्रमणिका नहीं है) मैं संख्याओं को तालिका प्रारूप में दिखाना चाहता था। यह एक अच्छी तकनीक है जब इस तरह के पंक्ति-से-पंक्ति सहसंबंध के उच्च प्रदर्शन की आवश्यकता होती है।
प्रत्येक क्वेरी के चलने से पहले, मैंने DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;
. मैंने समांतरता के समय-पतन प्रभावों को दूर करने के लिए प्रत्येक क्वेरी के लिए MAXDOP को 1 पर सेट किया है। मैंने प्रत्येक परिणाम को क्लाइंट को वापस करने के बजाय चर में सेट किया ताकि केवल प्रदर्शन को मापने के लिए और क्लाइंट डेटा ट्रांसमिशन को नहीं। सभी प्रश्नों को समान ORDER BY खंड दिए गए थे। सभी परीक्षणों में 17,408 इनपुट पंक्तियों का उपयोग किया गया और 8,193 परिणाम पंक्तियाँ मिलीं।
निम्नलिखित लोगों/कारणों के लिए कोई परिणाम प्रदर्शित नहीं होते हैं:
RichardTheKiwi *Could not test--query needs updating*
ypercube *No SQL 2012 environment yet :)*
Tim S *Did not complete tests within 5 minutes*
बिना किसी अनुक्रमणिका के:
CPU Duration Reads Writes
----------- ----------- ----------- -----------
ErikE 344 344 99 0
Simon Kingston 68672 69582 549203 49
इंडेक्स के साथ अद्वितीय क्लस्टर इंडेक्स बनाएं CI_#डेटा ऑन #डेटा (समय);
:
CPU Duration Reads Writes
----------- ----------- ----------- -----------
ErikE 328 336 99 0
Simon Kingston 70391 71291 549203 49 * basically not worse
अनुक्रमणिका के साथ अद्वितीय क्लस्टर अनुक्रमणिका बनाएं CI_#डेटा पर #डेटा (समय, नाम);
:
CPU Duration Reads Writes
----------- ----------- ----------- -----------
ErikE 375 414 359 0 * IO WINNER
Simon Kingston 172 189 38273 0 * CPU WINNER
तो कहानी का नैतिक है:
उचित अनुक्रमणिका क्वेरी विज़ार्ड से अधिक महत्वपूर्ण हैं
उपयुक्त इंडेक्स के साथ, साइमन किंग्स्टन का संस्करण समग्र रूप से जीतता है, खासकर जब क्वेरी जटिलता/रखरखाव सहित।
इस पाठ पर ध्यान दो! 38k पढ़ता है वास्तव में बहुत से नहीं हैं, और साइमन किंग्स्टन का संस्करण मेरे रूप में आधे समय में चला। मेरी क्वेरी की गति में वृद्धि पूरी तरह से टेबल पर कोई इंडेक्स नहीं होने के कारण थी, और सहवर्ती विनाशकारी लागत ने किसी भी प्रश्न को शामिल होने की आवश्यकता के लिए दिया (जो मेरा नहीं था):एक पूर्ण टेबल स्कैन हैश मैच इसके प्रदर्शन को मार रहा है। एक इंडेक्स के साथ, उनकी क्वेरी क्लस्टर इंडेक्स सीक (उर्फ बुकमार्क लुकअप) के साथ नेस्टेड लूप करने में सक्षम थी, जिसने चीजों को वास्तव में बना दिया। तेज़।
यह दिलचस्प है कि अकेले टाइम पर एक क्लस्टर इंडेक्स पर्याप्त नहीं था। भले ही टाइम्स अद्वितीय थे, जिसका अर्थ है कि प्रति बार केवल एक नाम आया था, फिर भी इसे ठीक से उपयोग करने के लिए नाम को इंडेक्स का हिस्सा बनने की आवश्यकता थी।
1 सेकंड से कम समय में डेटा से भरे होने पर क्लस्टर्ड इंडेक्स को तालिका में जोड़ना! अपनी अनुक्रमणिका की उपेक्षा न करें।