मास्टर / डिटेल टेबल के बीच हैश जुड़ने से बहुत कम कार्डिनिटी का अनुमान होता है


9

एक विस्तार तालिका में एक मास्टर तालिका में शामिल होने पर, मैं SQL सर्वर 2014 को बड़ी (विस्तार) तालिका के कार्डिनैलिटी अनुमान में शामिल होने के लिए कार्डिनैलिटी अनुमान का उपयोग करने के लिए कैसे प्रोत्साहित कर सकता हूं?

उदाहरण के लिए, 10K मास्टर पंक्तियों को 100K विस्तार पंक्तियों में शामिल करते समय, मैं SQL सर्वर को 100K पंक्तियों में शामिल होने का अनुमान लगाना चाहता हूं - विस्तार पंक्तियों की अनुमानित संख्या के समान। SQL सर्वर के अनुमानक को इस तथ्य का लाभ उठाने में मदद करने के लिए मुझे अपने प्रश्नों और / या तालिकाओं और / या अनुक्रमिकाओं की संरचना कैसे करनी चाहिए कि प्रत्येक विवरण पंक्ति में हमेशा एक समान मास्टर पंक्ति हो? (मतलब कि उनके बीच एक जुड़ाव कभी भी कार्डिनैलिटी के अनुमान को कम नहीं करता है।)

यहाँ अधिक विवरण है। हमारे डेटाबेस में तालिकाओं की एक मास्टर / डिटेल जोड़ी है: VisitTargetप्रत्येक बिक्री लेनदेन के लिए एक पंक्ति है, और VisitSaleप्रत्येक लेनदेन में प्रत्येक उत्पाद के लिए एक पंक्ति है। यह एक से कई संबंध है: 10 VisitSale पंक्तियों के औसत के लिए एक VisitTarget पंक्ति।

टेबल इस तरह दिखते हैं: (मैं इस प्रश्न के लिए केवल प्रासंगिक कॉलम को सरल बना रहा हूं)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

प्रदर्शन कारणों से, हमने आंशिक रूप SaleDateसे प्रत्येक तालिका तालिका पंक्तियों में मास्टर तालिका से सबसे आम फ़िल्टरिंग कॉलम (जैसे ) की प्रतिलिपि बनाकर असामान्य किया है , और फिर हमने दिनांक-फ़िल्टर्ड क्वेरीज़ को बेहतर समर्थन करने के लिए दोनों तालिकाओं पर अनुक्रमणिका को शामिल किया। दिनांक फ़िल्टर किए गए प्रश्नों को चलाने पर I / O को कम करने के लिए यह बहुत अच्छा काम करता है, लेकिन मुझे लगता है कि यह दृष्टिकोण मास्टर और डिटेल टेबल को एक साथ जोड़ते समय कार्डिनैलिटी आकलन की समस्या पैदा कर रहा है।

जब हम इन दो तालिकाओं से जुड़ते हैं, तो प्रश्न इस तरह दिखते हैं:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

डिटेल टेबल ( VisitSale) पर तिथि फ़िल्टर बेमानी है। डेट रेंज द्वारा फ़िल्टर किए गए प्रश्नों के लिए विस्तार तालिका में अनुक्रमिक I / O (उर्फ इंडेक्स सीक ऑपरेटर) को सक्षम करना है।

इस प्रकार के प्रश्नों की योजना इस प्रकार है:

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

एक ही समस्या के साथ एक क्वेरी की वास्तविक योजना यहां पाई जा सकती है

जैसा कि आप देख सकते हैं, जुड़ने के लिए कार्डिनिटी का अनुमान (चित्र में निचले-बाएं में टूलटिप) 4x से अधिक कम है: 2.1M वास्तविक बनाम 0.5M अनुमानित। यह प्रदर्शन समस्याओं का कारण बनता है (उदाहरण के लिए tempdb पर spilling), खासकर जब यह क्वेरी एक उपश्रेणी होती है जिसका उपयोग अधिक जटिल क्वेरी में किया जाता है।

लेकिन शामिल होने की प्रत्येक शाखा के लिए पंक्ति-गणना का अनुमान वास्तविक पंक्ति गणना के करीब है। सम्मिलित होने का शीर्ष आधा 100K वास्तविक बनाम 164K अनुमानित है। शामिल होने के निचले आधे हिस्से में 2.1M पंक्तियां वास्तविक बनाम 3.7M अनुमानित हैं। हैश बाल्टी वितरण भी अच्छा लगता है। ये अवलोकन मुझे सुझाव देते हैं कि आंकड़े प्रत्येक तालिका के लिए ठीक हैं, और समस्या यह है कि कार्डिनलिटी में शामिल होने का अनुमान है।

सबसे पहले मैंने सोचा था कि यह समस्या थी कि SQL सर्वर यह उम्मीद कर रहा है कि प्रत्येक तालिका में SaleDate कॉलम स्वतंत्र हैं, जबकि वास्तव में वे समान हैं। इसलिए मैंने बिक्री की तारीखों में शामिल होने की शर्त या WHERE क्लॉज, जैसे के लिए एक समानता तुलना जोड़ने की कोशिश की

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

या

WHERE vt.SaleDate = vs.SaleDate

यह काम नहीं किया। इसने कार्डिनैलिटी के अनुमानों को भी बदतर बना दिया! तो या तो SQL सर्वर उस समानता संकेत का उपयोग नहीं कर रहा है या कुछ और समस्या का मूल कारण है।

समस्या निवारण कैसे करें और उम्मीद करें कि इस कार्डिनैलिटी आकलन मुद्दे को कैसे ठीक किया जाए? मेरा लक्ष्य मास्टर / विस्तार में शामिल होने की कार्डिनैलिटी के लिए है जो कि जुड़ने के बड़े ("विस्तार तालिका") इनपुट के अनुमान के समान है।

अगर यह मायने रखता है, तो हम Windows Server पर SQL Server 2014 Enterprise SP2 CU8 का निर्माण 12.0.5557.0 से कर रहे हैं। कोई ट्रेस झंडे सक्षम नहीं हैं। डेटाबेस संगतता स्तर SQL सर्वर 2014 है। हम कई अलग-अलग SQL सर्वरों पर समान व्यवहार देखते हैं, इसलिए यह सर्वर-विशिष्ट समस्या होने की संभावना नहीं है।

SQL सर्वर 2014 कार्डिनैलिटी एस्टीमेटर में एक ऑप्टिमाइज़ेशन है जो ठीक वैसा ही व्यवहार है जैसा मैं देख रहा हूँ:

नए CE, हालांकि, एक सरल एल्गोरिथ्म का उपयोग करता है जो मानता है कि एक बड़ी तालिका और एक छोटी तालिका के बीच एक-से-कई सम्मिलित जुड़ाव है। यह मानता है कि बड़ी तालिका में प्रत्येक पंक्ति छोटी तालिका में ठीक एक पंक्ति से मेल खाती है। यह एल्गोरिथ्म बड़े इनपुट के अनुमानित आकार को कार्डिनैलिटी के रूप में लौटाता है।

आदर्श रूप से मुझे यह व्यवहार मिल सकता है, जहां शामिल होने के लिए कार्डिनिटी का अनुमान बड़ी तालिका के लिए अनुमान के समान होगा, भले ही मेरी "छोटी" तालिका अभी भी 100K पंक्तियों से अधिक वापस आ जाएगी!

जवाबों:


6

यह मानते हुए कि कोई सुधार आंकड़ों के कुछ कर या विरासत सीई का उपयोग करके प्राप्त किया जा सकता है, तो अपने समस्या को हल सबसे सरल तरीका है अपने को बदलने के लिए है INNER JOINएक करने के लिए LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

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

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

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