एक से अधिक INSERT कथन


119

मैं 1000 INSERT बयानों का उपयोग करने के बीच एक प्रदर्शन तुलना चला रहा हूं:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0)
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1)
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999)

.. 1000 मानों के साथ एकल INSERT विवरण का उपयोग कर रहा है:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
VALUES 
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0),
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1),
...
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999)

मेरे बड़े आश्चर्य के लिए, परिणाम मेरे विचार से विपरीत हैं:

  • 1000 INSERT बयान: 290 मिसे।
  • 1 1000 वाल्ट्स के साथ INSERT स्टेटमेंट: 2800 मिसे।

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

क्या यह वाजिब हो सकता है या किसी तरह समझाया जा सकता है? कैसे, माना जाता है कि तेजी से विधि 10 गुना (!) में खराब प्रदर्शन करती है?

धन्यवाद।

संपादित करें: दोनों के लिए निष्पादन योजनाएं संलग्न करना: परीक्षा योजना


1
ये साफ-सुथरे परीक्षण हैं, कुछ भी समानांतर नहीं चल रहा है, कोई दोहराया डेटा नहीं है (प्रत्येक प्रश्न अलग-अलग डेटा के साथ है, ज़ाहिर है, साधारण कैशिंग से बचने के लिए)
बोरका

1
क्या इसमें कोई ट्रिगर शामिल है?
एके

2
मैंने मूल्यों पर 1000 की सीमा पार करने के लिए एक कार्यक्रम को टीवीपी में बदल दिया और एक बड़ा प्रदर्शन हासिल किया। मैं एक तुलना चलाऊंगा।
पापाराज़ो

1
प्रासंगिक: simple-talk.com/sql/performance/…
२३

जवाबों:


126

जोड़: SQL सर्वर 2012 इस क्षेत्र में कुछ बेहतर प्रदर्शन दिखाता है, लेकिन नीचे दिए गए विशिष्ट मुद्दों से निपटने के लिए प्रतीत नहीं होता है। यह SQL सर्वर 2012 के बाद अगले प्रमुख संस्करण में स्पष्ट रूप से तय किया जाना चाहिए !

आपकी योजना से पता चलता है कि एकल आवेषण पैरामीटर वाली प्रक्रियाओं (संभवतः ऑटो मानकीकृत) का उपयोग कर रहे हैं, इसलिए इन के लिए पार्स / संकलन समय न्यूनतम होना चाहिए।

मैंने सोचा कि मैं इसे थोड़ा और देखूंगा, हालांकि एक लूप सेट किया है ( स्क्रिप्ट ) और VALUESक्लॉज़ की संख्या को समायोजित करने और संकलन समय को रिकॉर्ड करने की कोशिश की गई ।

मैंने तब प्रति संकलन औसत समय प्राप्त करने के लिए पंक्तियों की संख्या से संकलन समय को विभाजित किया। परिणाम नीचे हैं

ग्राफ़

250 तक VALUES खंडों संकलन समय / संख्याओं की संख्या एक मामूली प्रवृत्ति है, लेकिन कुछ भी नाटकीय नहीं है।

ग्राफ़

लेकिन फिर अचानक बदलाव आ जाता है।

डेटा का वह भाग नीचे दिखाया गया है।

+------+----------------+-------------+---------------+---------------+
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows |
+------+----------------+-------------+---------------+---------------+
|  245 |            528 |          41 |          2400 | 0.167346939   |
|  246 |            528 |          40 |          2416 | 0.162601626   |
|  247 |            528 |          38 |          2416 | 0.153846154   |
|  248 |            528 |          39 |          2432 | 0.157258065   |
|  249 |            528 |          39 |          2432 | 0.156626506   |
|  250 |            528 |          40 |          2448 | 0.16          |
|  251 |            400 |         273 |          3488 | 1.087649402   |
|  252 |            400 |         274 |          3496 | 1.087301587   |
|  253 |            400 |         282 |          3520 | 1.114624506   |
|  254 |            408 |         279 |          3544 | 1.098425197   |
|  255 |            408 |         290 |          3552 | 1.137254902   |
+------+----------------+-------------+---------------+---------------+

कैश्ड प्लान का आकार, जो रैखिक रूप से अचानक बढ़ रहा था, लेकिन CompileTime 7 गुना और CompileMemory शूट बढ़ाता है। यह योजना के बीच का कट ऑफ पॉइंट है जो एक ऑटो पैरामीरिडेड एक (1,000 मापदंडों के साथ) एक गैर पैरामीरिजेड के साथ है। इसके बाद, यह रैखिक रूप से कम कुशल (एक निश्चित समय में संसाधित मूल्य खंडों की संख्या के संदर्भ में) लगता है।

यकीन नहीं होता कि ऐसा क्यों होना चाहिए। संभवतः जब यह विशिष्ट शाब्दिक मूल्यों के लिए एक योजना को संकलित कर रहा है तो उसे कुछ गतिविधि करनी चाहिए जो रैखिक रूप से पैमाने पर नहीं होती है (जैसे कि छंटाई)।

यह कैश्ड क्वेरी योजना के आकार को प्रभावित करने के लिए नहीं लगता है जब मैंने एक क्वेरी पूरी तरह से डुप्लिकेट पंक्तियों से मिलकर बनाई थी और न ही स्थिरांक तालिका के उत्पादन के क्रम को प्रभावित करता है (और जैसा कि आप एक ढेर समय में डाल रहे हैं छँटाई में बिताए गए वैसे भी निरर्थक होगा भले ही यह किया हो)।

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

योजना

मैंने डिबगर में इसे देखने की कोशिश की, लेकिन SQL Server 2008 के मेरे संस्करण के लिए सार्वजनिक प्रतीक उपलब्ध नहीं हैं, इसलिए इसके बजाय मुझे UNION ALLSQL सर्वर 2005 में इसके बराबर निर्माण को देखना पड़ा ।

एक विशिष्ट स्टैक ट्रेस नीचे है

sqlservr.exe!FastDBCSToUnicode()  + 0xac bytes  
sqlservr.exe!nls_sqlhilo()  + 0x35 bytes    
sqlservr.exe!CXVariant::CmpCompareStr()  + 0x2b bytes   
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare()  + 0x18 bytes  
sqlservr.exe!CXVariant::CmpCompare()  + 0x11f67d bytes  
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion()  + 0xe2 bytes   
sqlservr.exe!CConstraintProp::PcnstrUnion()  + 0x35e bytes  
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive()  + 0x11a bytes    
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler()  + 0x18f bytes    
sqlservr.exe!CLogOpArg::DeriveGroupProperties()  + 0xa9 bytes   
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties()  + 0x40 bytes    
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x18a bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!CQuery::PqoBuild()  + 0x3cb bytes  
sqlservr.exe!CStmtQuery::InitQuery()  + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal()  + 0xf0 bytes   
sqlservr.exe!CStmtDML::Init()  + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep()  + 0x176 bytes   
sqlservr.exe!CSQLSource::FCompile()  + 0x741 bytes  
sqlservr.exe!CSQLSource::FCompWrapper()  + 0x922be bytes    
sqlservr.exe!CSQLSource::Transform()  + 0x120431 bytes  
sqlservr.exe!CSQLSource::Compile()  + 0x2ff bytes   

तो स्टैक ट्रेस में नामों को छोड़ना स्ट्रिंग्स की तुलना में बहुत समय बिताना प्रतीत होता है।

यह KB आलेख इंगित करता है कि क्वेरी प्रसंस्करण DeriveNormalizedGroupPropertiesके सामान्यीकरण चरण कहा जाता है के साथ जुड़ा हुआ है

इस चरण को अब बाध्यकारी या बीजगणित कहा जाता है और यह पिछले पार्स चरण से अभिव्यक्ति पार्स ट्री आउटपुट लेता है और इस मामले में अनुकूलन (तुच्छ योजना अनुकूलन) के लिए आगे बढ़ने के लिए एक अल्जीब्रीड अभिव्यक्ति ट्री (क्वेरी प्रोसेसर ट्री) को आउटपुट करता है [रेफ]

मैंने एक और प्रयोग ( स्क्रिप्ट ) की कोशिश की, जो मूल परीक्षण को फिर से चलाने के लिए था, लेकिन तीन अलग-अलग मामलों को देख रहा था।

  1. प्रथम नाम और अंतिम नाम स्ट्रिंग्स की लंबाई 10 वर्ण बिना डुप्लिकेट के।
  2. प्रथम नाम और अंतिम नाम स्ट्रिंग्स की लंबाई 50 अक्षर जिसमें कोई डुप्लिकेट नहीं है।
  3. सभी डुप्लिकेट के साथ लंबाई 10 अक्षर का पहला नाम और अंतिम नाम।

ग्राफ़

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

संपादित करें

एक जगह जहां यह जानकारी लीवरेज की गई है, यहां @Lieven द्वारा दिखाई गई है

SELECT * 
FROM (VALUES ('Lieven1', 1),
             ('Lieven2', 2),
             ('Lieven3', 3))Test (name, ID)
ORDER BY name, 1/ (ID - ID) 

क्योंकि संकलन के समय में यह निर्धारित किया जा सकता है कि Nameस्तंभ की कोई डुप्लिकेट नहीं है, यह 1/ (ID - ID)रन टाइम के दौरान द्वितीयक अभिव्यक्ति द्वारा आदेश देने वाली स्कीप को छोड़ देता है (योजना में छंटनी का केवल एक ORDER BYकॉलम होता है) और शून्य त्रुटि से विभाजित नहीं किया जाता है। यदि डुप्लिकेट तालिका में जोड़े जाते हैं तो सॉर्ट ऑपरेटर कॉलम द्वारा दो ऑर्डर दिखाता है और अपेक्षित त्रुटि उठाई जाती है।


6
आपके पास जो मैजिक नंबर है वह है NumberOfRows / ColumnCount = 250. अपनी क्वेरी को केवल तीन कॉलम का उपयोग करने के लिए बदलें और परिवर्तन 333 पर होगा। मैजिक नंबर 1000 कैश्ड प्लान में उपयोग किए जाने वाले अधिकतम मापदंडों की तरह हो सकता है। यह <ParameterList>एक <ConstantScan><Values><Row>सूची के साथ एक से अधिक के साथ एक योजना उत्पन्न करने के लिए "आसान" हो जाता है ।
मिकेल एरिकसन

1
@ मायिकेलरिक्सन - सहमत। 1000 मानों के साथ 250 पंक्ति एक को 251 पंक्ति में ऑटो पैरामीटर किया जाता है ताकि ऐसा न हो कि अंतर प्रतीत हो। हालांकि यकीन नहीं हो रहा है। शायद यह डुप्लिकेट या कुछ और जब यह है के लिए देख शाब्दिक मूल्यों को छाँटने में समय व्यतीत करता है।
मार्टिन स्मिथ

1
यह एक बहुत ही पागल मुद्दा है, मैं बस इससे दुखी हो गया। यह एक महान जवाब है धन्यवाद
प्यार नहीं किया

1
@MikaelEriksson क्या आपका मतलब है जादू नंबर NumberOfRows * ColumnCount = 1000 है?
पापाराज़ो

1
@ बलम - हाँ। जब तत्वों की कुल संख्या 1000 से अधिक है (NumberOfRows * ColumnCount) <ConstantScan><Values><Row>इसके बजाय उपयोग करने के लिए क्वेरी योजना बदल गई <ParameterList>
मिकेल एरिकसन

23

यह बहुत आश्चर्य की बात नहीं है: छोटे सम्मिलित करने के लिए निष्पादन योजना एक बार गणना की जाती है, और फिर 1000 बार पुन: उपयोग की जाती है। योजना को तैयार करना और तैयार करना त्वरित है, क्योंकि इसमें केवल चार मान होते हैं। दूसरी ओर, 1000-पंक्ति की योजना को 4000 मानों (या 4000 मापदंडों से निपटने की जरूरत है, यदि आपने अपने C # परीक्षणों को मानकीकृत किया है)। यह एसक्यूएल सर्वर के लिए 999 राउंडट्रिप्स को समाप्त करके आपके द्वारा प्राप्त की गई बचत को आसानी से खा सकता है, खासकर अगर आपका नेटवर्क बहुत धीमा नहीं है।


9

समस्या को क्वेरी को संकलित करने में लगने वाले समय के साथ संभवतः करना है।

यदि आप आवेषण में तेजी लाना चाहते हैं, तो आपको वास्तव में उन्हें लेन-देन करने की आवश्यकता है:

BEGIN TRAN;
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0);
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1);
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999);
COMMIT TRAN;

C # से, आप एक टेबल वैल्यू पैरामीटर का उपयोग करने पर भी विचार कर सकते हैं। एक ही बैच में कई आदेशों को जारी करना, उन्हें अर्धविराम के साथ अलग करना, एक और दृष्टिकोण है जो मदद भी करेगा।


1
पुन: "एक ही बैच में कई कमांड जारी करना": जो थोड़ी मदद करता है, लेकिन बहुत कुछ नहीं। लेकिन मैं निश्चित रूप से एक ट्रांसक्शन में लपेटने के अन्य दो विकल्पों से सहमत हूं (क्या वास्तव में ट्रांस काम करता है या इसे सिर्फ TRAN होना चाहिए?) या एक टीवीपी का उपयोग कर।
सोलोमन रटज़की

1

मैं एक ऐसी ही स्थिति में दौड़ा, जिसमें एक तालिका को C ++ प्रोग्राम (MFC / ODBC) के साथ कई 100k पंक्तियों के साथ बदलने की कोशिश की गई।

चूंकि इस ऑपरेशन में बहुत लंबा समय लगा, इसलिए मैंने कई आवेषणों को एक ( MSSQL सीमा के कारण 1000 तक ) में जोड़ दिया । मेरा अनुमान है कि बहुत सारे सिंगल स्टेटमेंट यहाँ वर्णित के समान एक ओवरहेड बनाएंगे ।

हालांकि, यह पता चला है कि रूपांतरण वास्तव में काफी लंबा था:

        Method 1       Method 2     Method 3 
        Single Insert  Multi Insert Joined Inserts
Rows    1000           1000         1000
Insert  390 ms         765 ms       270 ms
per Row 0.390 ms       0.765 ms     0.27 ms

तो, CDatabase को 1000 सिंगल कॉल :: प्रत्येक INSERT स्टेटमेंट (विधि 1) के साथ ExecuteSql लगभग दो बार CDatabase के लिए एक कॉल के रूप में तेजी से कर रहे हैं :: ExecuteSql 1000 मूल्य ट्यूपल्स (विधि 2) के साथ एक बहु-पंक्ति INS कथन के साथ।

अद्यतन: तो, अगली चीज़ जो मैंने कोशिश की थी वह 1000 अलग-अलग INSERT कथनों को एक स्ट्रिंग में बाँधने और सर्वर को निष्पादित करने की है (विधि 3)। यह पता चला है कि यह विधि 1 की तुलना में थोड़ा तेज है।

संपादित करें: मैं Microsoft SQL सर्वर एक्सप्रेस संस्करण (64-बिट) v10.0.2531.0 का उपयोग कर रहा हूं

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