स्पार्क एसक्यूएल। क्वेरी ऑप्टिमाइज़र के बारे में थोड़ा

सभी को नमस्कार। एक परिचय के रूप में, मैं आपको बताना चाहता हूं कि मैं ऐसे जीवन में कैसे आया।


बिग डेटा और स्पार्क के साथ मिलने से पहले, विशेष रूप से, मेरे पास बहुत कुछ था और अक्सर एसक्यूएल प्रश्नों का अनुकूलन करने के लिए, पहले MSSQL के लिए, फिर ओरेकल के लिए, और अब मैं स्पार्कक्यूएल में आया।


और अगर पहले से ही DBMS के लिए बहुत सारी अच्छी किताबें हैं जो कार्यप्रणाली और "पेन" का वर्णन करती हैं जिन्हें आप इष्टतम क्वेरी योजना प्राप्त करने के लिए मोड़ सकते हैं, तो मैंने स्पार्क के लिए ऐसी किताबें नहीं देखी हैं। मैं शुद्ध एसक्यूएल के बजाय RDD / Dataset एपीआई के माध्यम से काम करने से संबंधित अधिक लेखों और प्रथाओं के सेट के पार आया। मेरे लिए, एसक्यूएल अनुकूलन पर संदर्भ पुस्तकों में से एक जे। लुईस की पुस्तक, ओरेकल है। लागत अनुकूलन की मूल बातें। " मैंने अध्ययन की गहराई में कुछ इसी तरह की तलाश की। विशेष रूप से SparkSQL के अनुसंधान का विषय क्यों था, और अंतर्निहित एपीआई नहीं? तब दिलचस्पी उस परियोजना की सुविधाओं के कारण हुई, जिस पर मैं काम कर रहा हूं।




हमारे ग्राहकों में से एक के लिए, हमारी कंपनी एक डेटा वेयरहाउस विकसित कर रही है, जिसकी एक विस्तृत परत और प्रदर्शन मामलों का एक हिस्सा Hadoop क्लस्टर में है, और अंतिम प्रदर्शन मामले Oracle में हैं। इस परियोजना में एक व्यापक डेटा रूपांतरण परत शामिल है, जिसे स्पार्क पर लागू किया गया है। ईटीएल डेवलपर्स के विकास और कनेक्टिविटी को गति देने के लिए जो बिग डेटा तकनीकों की पेचीदगियों से परिचित नहीं हैं, लेकिन एसक्यूएल और ईटीएल टूल्स से परिचित हैं, एक ऐसा उपकरण विकसित किया गया है जो वैचारिक रूप से अन्य ईटीएल टूल्स की याद दिलाता है, उदाहरण के लिए, सूचनात्मक, और आपको बाद की पीढ़ी के साथ ईटीएल प्रक्रियाओं को नेत्रहीन रूप से डिजाइन करने की अनुमति देता है। स्पार्क के लिए कोड। एल्गोरिदम की जटिलता और बड़ी संख्या में परिवर्तनों के कारण, डेवलपर्स मुख्य रूप से स्पार्कक्यूएस प्रश्नों का उपयोग करते हैं।


यहीं से कहानी शुरू होती है, क्योंकि मुझे फॉर्म के प्रश्नों के बड़ी संख्या में उत्तर देना था "क्वेरी क्यों काम नहीं कर रही है / धीरे-धीरे काम कर रही है / ओरेकल की तरह काम नहीं कर रही है?" यह मेरे लिए सबसे दिलचस्प हिस्सा निकला: "यह धीरे-धीरे क्यों काम करता है?"। इसके अलावा, DBMS के विपरीत जिसके साथ मैंने पहले काम किया था, आप स्रोत कोड में जा सकते हैं और अपने सवालों का जवाब पा सकते हैं।


सीमाएँ और मान्यताएँ


स्पार्क 2.3.0 का उपयोग उदाहरणों को चलाने और स्रोत कोड का विश्लेषण करने के लिए किया जाता है।
यह माना जाता है कि पाठक स्पार्क वास्तुकला से परिचित है, और डीबीएमएस में से एक के लिए क्वेरी ऑप्टिमाइज़र के सामान्य सिद्धांत। कम से कम, वाक्यांश "क्वेरी प्लान" निश्चित रूप से आश्चर्यजनक नहीं होना चाहिए।


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


अध्ययन के लिए आगे बढ़ें


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


scala> spark.read.orc("/user/test/balance").createOrReplaceTempView("bal") scala> spark.read.orc("/user/test/customer").createOrReplaceTempView("cust") scala> val df = spark.sql(""" | select bal.account_rk, cust.full_name | from bal | join cust | on bal.party_rk = cust.party_rk | and bal.actual_date = cust.actual_date | where bal.actual_date = cast('2017-12-31' as date) | """) df: org.apache.spark.sql.DataFrame = [account_rk: decimal(38,18), full_name: string] scala> df.explain(true) 

SQL को पार्स करने और क्वेरी निष्पादन योजना के अनुकूलन के लिए जिम्मेदार मुख्य मॉड्यूल स्पार्क उत्प्रेरक है।


अनुरोध योजना (df.explain (true)) के विवरण में विस्तारित आउटपुट आपको उन सभी चरणों को ट्रैक करने की अनुमति देता है जिनके माध्यम से अनुरोध किया गया है:


  • पार्स लॉजिकल प्लान - SQL पार्स करने के बाद मिलता है। इस स्तर पर, अनुरोध के केवल वाक्यविन्यास शुद्धता की जाँच की जाती है।

 == Parsed Logical Plan == 'Project ['bal.account_rk, 'cust.full_name] +- 'Filter ('bal.actual_date = cast(2017-12-31 as date)) +- 'Join Inner, (('bal.party_rk = 'cust.party_rk) && ('bal.actual_date = 'cust.actual_date)) :- 'UnresolvedRelation `bal` +- 'UnresolvedRelation `cust` 

  • विश्लेषित तार्किक योजना - इस स्तर पर, प्रयुक्त संस्थाओं की संरचना के बारे में जानकारी जोड़ी जाती है, संरचना के पत्राचार और अनुरोधित विशेषताओं की जाँच की जाती है।

 == Analyzed Logical Plan == account_rk: decimal(38,18), full_name: string Project [account_rk#1, full_name#59] +- Filter (actual_date#27 = cast(2017-12-31 as date)) +- Join Inner, ((party_rk#18 = party_rk#57) && (actual_date#27 = actual_date#88)) :- SubqueryAlias bal : +- Relation[ACTUAL_END_DATE#0,ACCOUNT_RK#1,... 4 more fields] orc +- SubqueryAlias cust +- Relation[ACTUAL_END_DATE#56,PARTY_RK#57... 9 more fields] orc 

  • ऑप्टिमाइज्ड लॉजिकल प्लान हमारे लिए सबसे दिलचस्प है। इस स्तर पर, उपलब्ध अनुकूलन नियमों के आधार पर परिणामी क्वेरी ट्री को रूपांतरित किया जाता है।

 == Optimized Logical Plan == Project [account_rk#1, full_name#59] +- Join Inner, ((party_rk#18 = party_rk#57) && (actual_date#27 = actual_date#88)) :- Project [ACCOUNT_RK#1, PARTY_RK#18, ACTUAL_DATE#27] : +- Filter ((isnotnull(actual_date#27) && (actual_date#27 = 17531)) && isnotnull(party_rk#18)) : +- Relation[ACTUAL_END_DATE#0,ACCOUNT_RK#1,... 4 more fields] orc +- Project [PARTY_RK#57, FULL_NAME#59, ACTUAL_DATE#88] +- Filter ((isnotnull(actual_date#88) && isnotnull(party_rk#57)) && (actual_date#88 = 17531)) +- Relation[ACTUAL_END_DATE#56,PARTY_RK#57,... 9 more fields] orc 

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

 == Physical Plan == *(2) Project [account_rk#1, full_name#59] +- *(2) BroadcastHashJoin [party_rk#18, actual_date#27], [party_rk#57, actual_date#88], Inner, BuildRight :- *(2) Project [ACCOUNT_RK#1, PARTY_RK#18, ACTUAL_DATE#27] : +- *(2) Filter isnotnull(party_rk#18) : +- *(2) FileScan orc [ACCOUNT_RK#1,PARTY_RK#18,ACTUAL_DATE#27] Batched: false, Format: ORC, Location: InMemoryFileIndex[hdfs://cluster:8020/user/test/balance], PartitionCount: 1, PartitionFilters: [isnotnull(ACTUAL_DATE#27), (ACTUAL_DATE#27 = 17531)], PushedFilters: [IsNotNull(PARTY_RK)], ReadSchema: struct<ACCOUNT_RK:decimal(38,18),PARTY_RK:decimal(38,18)> +- BroadcastExchange HashedRelationBroadcastMode(List(input[0, decimal(38,18), true], input[2, date, true])) +- *(1) Project [PARTY_RK#57, FULL_NAME#59, ACTUAL_DATE#88] +- *(1) Filter isnotnull(party_rk#57) +- *(1) FileScan orc [PARTY_RK#57,FULL_NAME#59,ACTUAL_DATE#88] Batched: false, Format: ORC, Location: InMemoryFileIndex[hdfs://cluster:8020/user/test/customer], PartitionCount: 1, PartitionFilters: [isnotnull(ACTUAL_DATE#88), (ACTUAL_DATE#88 = 17531)], PushedFilters: [IsNotNull(PARTY_RK)], ReadSchema: struct<PARTY_RK:decimal(38,18),FULL_NAME:string> 

अनुकूलन और निष्पादन के निम्नलिखित चरण (उदाहरण के लिए, WholeStageCodegen) इस लेख के दायरे से परे हैं, लेकिन मास्टरींग स्पार्क स्क्ल में महान विवरण (साथ ही ऊपर वर्णित चरणों) में वर्णित हैं।


क्वेरी निष्पादन योजना को पढ़ना आमतौर पर "अंदर से" और "नीचे से ऊपर तक" होता है, अर्थात, सबसे नेस्टेड भागों को पहले निष्पादित किया जाता है, और धीरे-धीरे बहुत ऊपर स्थित अंतिम प्रक्षेपण के लिए अग्रिम होता है।


क्वेरी ऑप्टिमाइज़र के प्रकार


दो प्रकार के क्वेरी ऑप्टिमाइज़र को प्रतिष्ठित किया जा सकता है:


  • नियम-आधारित अनुकूलक (RBOs)।
  • ऑप्टिमाइज़र क्वेरी निष्पादन की लागत (लागत-आधारित ऑप्टिमाइज़र, CBO) के अनुमान के आधार पर।

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


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


अपाचे स्पार्क के लिए सीबीओ डिजाइन विनिर्देश के बारे में अधिक जानने के लिए, कृपया लिंक का पालन करें: विनिर्देश और कार्यान्वयन के लिए मुख्य JIRA कार्य


मौजूदा ऑप्टिमाइज़ेशन की पूरी श्रृंखला की खोज के लिए शुरुआती बिंदु ऑप्टिमाइज़र.काला कोड है।


यहाँ उपलब्ध अनुकूलन की एक लंबी सूची से एक छोटा सा अंश है:


 def batches: Seq[Batch] = { val operatorOptimizationRuleSet = Seq( // Operator push down PushProjectionThroughUnion, ReorderJoin, EliminateOuterJoin, PushPredicateThroughJoin, PushDownPredicate, LimitPushDown, ColumnPruning, InferFiltersFromConstraints, // Operator combine CollapseRepartition, CollapseProject, CollapseWindow, CombineFilters, CombineLimits, CombineUnions, // Constant folding and strength reduction NullPropagation, ConstantPropagation, ........ 

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


सीबीओ की एक विशेषता यह है कि सही संचालन के लिए इसे क्वेरी में उपयोग किए गए डेटा के आंकड़ों को जानना और संग्रहीत करना होगा - तालिका स्तंभों में डेटा वितरण के रिकॉर्ड, रिकॉर्ड आकार, हिस्टोग्राम्स की संख्या।


आँकड़ों को इकट्ठा करने के लिए, SQL कमांड ANALYZE TABLE का एक सेट ... COMPATE STATISTICS का उपयोग किया जाता है, इसके अलावा, सूचनाओं को संग्रहीत करने के लिए तालिकाओं के एक सेट की आवश्यकता होती है, API को बाह्यसंग्रह के माध्यम से प्रदान किया जाता है, और अधिक सटीक रूप से HiveExternatatalog के माध्यम से।


चूंकि CBO वर्तमान में डिफ़ॉल्ट रूप से अक्षम है, इसलिए मुख्य जोर RBO के उपलब्ध अनुकूलन और बारीकियों पर शोध करने पर लगाया जाएगा।


शामिल होने की रणनीति के प्रकार और विकल्प


अनुरोध को निष्पादित करने के लिए भौतिक योजना के गठन के चरण में, सम्मिलित रणनीति का चयन किया जाता है। वर्तमान में निम्नलिखित विकल्प स्पार्क में उपलब्ध हैं (आप SparkStrategies.scala में कोड से कोड सीखना शुरू कर सकते हैं)।


प्रसारण हैश में शामिल हों


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


   ,    ,     SQL      Oracle,   /*+ broadcast(t1, t2) */ 

सॉर्ट मर्ज ज्वाइन करें


स्पार्क.sql.join.preferSortMergeJoin डिफ़ॉल्ट रूप से चालू होने पर, इस विधि को डिफ़ॉल्ट रूप से लागू किया जाता है यदि जुड़ने के लिए कुंजी को क्रमबद्ध किया जा सकता है।
सुविधाओं में से, यह ध्यान दिया जा सकता है कि, पिछली विधि के विपरीत, ऑपरेशन करने के लिए कोड पीढ़ी का अनुकूलन केवल आंतरिक जुड़ाव के लिए उपलब्ध है।


शफ़ल हैश शामिल हों


यदि कुंजी को सॉर्ट नहीं किया जा सकता है, या डिफ़ॉल्ट सॉर्ट मर्ज में शामिल होने का चयन विकल्प अक्षम है, तो कैटलिस्ट एक फेरबदल हैश जॉइन को लागू करने की कोशिश करता है। सेटिंग्स की जाँच करने के अलावा, यह भी जाँच की जाती है कि स्पार्क के पास एक विभाजन के लिए एक स्थानीय हैश मैप बनाने के लिए पर्याप्त मेमोरी है ( स्पार्क्स की कुल संख्या स्पार्क.sql.shuffle.partitions सेट करके सेट की गई है)


ब्राडकास्टीनलोपजॉइन और कार्टेशियनप्रोडक्ट


ऐसे मामले में जहां कुंजी द्वारा प्रत्यक्ष तुलना की कोई संभावना नहीं है (उदाहरण के लिए, एक शर्त जैसी) या तालिकाओं में शामिल होने के लिए कोई कुंजी नहीं है, तालिकाओं के आकार के आधार पर, या तो इस प्रकार या कार्टेसियनप्रोडक्ट का चयन किया जाता है।


Join'ah में तालिकाओं को निर्दिष्ट करने का क्रम


किसी भी मामले में, सम्मिलित होने के लिए कुंजी द्वारा फेरबदल तालिकाओं की आवश्यकता होती है। इसलिए, इस समय, तालिकाओं को निर्दिष्ट करने का क्रम, विशेष रूप से एक पंक्ति में कई प्रदर्शन करने के मामले में, महत्वपूर्ण है (यदि आप एक बोर हैं, तो यदि CBO चालू नहीं है और JOIN_REORDER_ENABLED सेटिंग चालू नहीं है)।


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


फ़िल्टर स्थितियों का सकर्मक अनुप्रयोग


निम्नलिखित प्रश्न पर विचार करें:


 select bal.account_rk, cust.full_name from balance bal join customer cust on bal.party_rk = cust.party_rk and bal.actual_date = cust.actual_date where bal.actual_date = cast('2017-12-31' as date) 

यहां हम दो टेबल को कनेक्ट करते हैं जो उसी तरह से विभाजित होती हैं, वास्तविक_डेट फ़ील्ड के अनुसार और केवल बैलेंस टेबल के अनुसार विभाजन के लिए एक स्पष्ट फ़िल्टर लागू करें।


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


 == Optimized Logical Plan == Project [account_rk#1, full_name#59] +- Join Inner, ((party_rk#18 = party_rk#57) && (actual_date#27 = actual_date#88)) :- Project [ACCOUNT_RK#1, PARTY_RK#18, ACTUAL_DATE#27] : +- Filter ((isnotnull(actual_date#27) && (actual_date#27 = 17531)) && isnotnull(party_rk#18)) : +- Relation[,... 4 more fields] orc +- Project [PARTY_RK#57, FULL_NAME#59, ACTUAL_DATE#88] +- Filter (((actual_date#88 = 17531) && isnotnull(actual_date#88)) && isnotnull(party_rk#57)) +- Relation[,... 9 more fields] orc 

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


 == Optimized Logical Plan == Project [account_rk#1, full_name#59] +- Join LeftOuter, ((party_rk#18 = party_rk#57) && (actual_date#27 = actual_date#88)) :- Project [ACCOUNT_RK#1, PARTY_RK#18, ACTUAL_DATE#27] : +- Filter (isnotnull(actual_date#27) && (actual_date#27 = 17531)) : +- Relation[,... 4 more fields] orc +- Project [PARTY_RK#57, FULL_NAME#59, ACTUAL_DATE#88] +- Relation[,... 9 more fields] orc 

रूपांतरण टाइप करें


क्लाइंट प्रकार द्वारा फ़िल्टरिंग के साथ तालिका से चयन के एक सरल उदाहरण पर विचार करें, इस योजना में पार्टी_टाइप फ़ील्ड का प्रकार स्ट्रिंग है।


 select party_rk, full_name from cust where actual_date = cast('2017-12-31' as date) and party_type = 101 --   -- and party_type = '101' --     

और दो परिणामी योजनाओं की तुलना करें, पहला - जब हम गलत प्रकार को संदर्भित करते हैं (इंट के लिए एक अंतर्निहित कलाकार होगा), दूसरा - जब प्रकार योजना से मेल खाती है।


 PushedFilters: [IsNotNull(PARTY_TYPE)] //            . PushedFilters: [IsNotNull(PARTY_TYPE), EqualTo(PARTY_TYPE,101)] //             . 

तार के साथ तारीखों की तुलना के मामले में एक समान समस्या देखी जाती है, तार की तुलना करने के लिए एक फिल्टर होगा। एक उदाहरण:


 where OPER_DATE = '2017-12-31' Filter (isnotnull(oper_date#0) && (cast(oper_date#0 as string) = 2017-12-31) PushedFilters: [IsNotNull(OPER_DATE)] where OPER_DATE = cast('2017-12-31' as date) PushedFilters: [IsNotNull(OPER_DATE), EqualTo(OPER_DATE,2017-12-31)] 

मामले के लिए जब एक अंतर्निहित प्रकार रूपांतरण संभव है, उदाहरण के लिए, int -> दशमलव, अनुकूलक यह अपने आप करता है।


आगे का शोध


"नॉब्स" के बारे में बहुत सारी रोचक जानकारी जिनका उपयोग उत्प्रेरक को ठीक करने के लिए किया जा सकता है, साथ ही साथ ऑप्टिमाइज़र की संभावनाओं (वर्तमान और भविष्य) के बारे में, SQLConf.scala से प्राप्त किया जा सकता है।


विशेष रूप से, जैसा कि आप डिफ़ॉल्ट रूप से देख सकते हैं, लागत अनुकूलक अभी भी बंद है।


 val CBO_ENABLED = buildConf("spark.sql.cbo.enabled") .doc("Enables CBO for estimation of plan statistics when set true.") .booleanConf .createWithDefault(false) 

साथ ही इसके आश्रित अनुकूलन जुड़ने के साथ जुड़ते हैं।


 val JOIN_REORDER_ENABLED = buildConf("spark.sql.cbo.joinReorder.enabled") .doc("Enables join reorder in CBO.") .booleanConf .createWithDefault(false) 

या


 val STARSCHEMA_DETECTION = buildConf("spark.sql.cbo.starSchemaDetection") .doc("When true, it enables join reordering based on star schema detection. ") .booleanConf .createWithDefault(false) 

संक्षिप्त सारांश


मौजूदा अनुकूलन का केवल एक छोटा सा हिस्सा छुआ गया है, लागत अनुकूलन के साथ प्रयोग, जो क्वेरी रूपांतरण के लिए बहुत अधिक जगह दे सकते हैं, आगे हैं। इसके अलावा, एक अलग दिलचस्प सवाल यह है कि परियोजना के जीरा द्वारा जजों की पैरा-ऑर्क और फाइलों को पढ़ने के दौरान अनुकूलन के एक सेट की तुलना, यह समानता के बारे में है, लेकिन क्या यह वास्तव में ऐसा है?


इसके अलावा:


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

Source: https://habr.com/ru/post/hi417103/


All Articles