मुझे पहली बार याद है जब मैंने रेल 'ActiveRecord. यह एक रहस्योद्घाटन था। यह 2005 में वापस आ गया था और मैं एक PHP ऐप के लिए एसक्यूएल प्रश्नों को हाथ से कोड कर रहा था। अचानक, डेटाबेस का उपयोग करना एक थकाऊ काम से आसान हो गया और - मैं कहने की हिम्मत करता हूं - मज़ा।
...फिर मैंने प्रदर्शन के मुद्दों पर ध्यान देना शुरू कर दिया।
ActiveRecord स्वयं धीमा नहीं था। मैंने अभी उन प्रश्नों पर ध्यान देना बंद कर दिया था जो वास्तव में चलाए जा रहे थे। और यह पता चला है, रेल सीआरयूडी ऐप्स में उपयोग किए जाने वाले कुछ सबसे बेवकूफ डेटाबेस प्रश्न बड़े डेटासेट तक स्केलिंग में डिफ़ॉल्ट रूप से काफी खराब हैं।
इस लेख में हम तीन सबसे बड़े दोषियों पर चर्चा करने जा रहे हैं। लेकिन पहले, आइए इस बारे में बात करते हैं कि आप कैसे बता सकते हैं कि आपके डीबी प्रश्न अच्छी तरह से बढ़ रहे हैं या नहीं।
प्रदर्शन मापना
यदि आपके पास पर्याप्त छोटा डेटासेट है तो प्रत्येक डीबी क्वेरी प्रदर्शनकारी है। तो वास्तव में प्रदर्शन के लिए महसूस करने के लिए, हमें उत्पादन-आकार के डेटाबेस के खिलाफ बेंचमार्क करने की आवश्यकता है। हमारे उदाहरणों में, हम faults
. नामक तालिका का उपयोग करने जा रहे हैं लगभग 22,000 रिकॉर्ड के साथ।
हम पोस्टग्रेज का उपयोग कर रहे हैं। पोस्टग्रेज में, जिस तरह से आप प्रदर्शन को मापते हैं वह है explain
. का उपयोग करना . उदाहरण के लिए:
# explain (analyze) select * from faults where id = 1;
QUERY PLAN
--------------------------------------------------------------------------------------------------
Index Scan using faults_pkey on faults (cost=0.29..8.30 rows=1 width=1855) (actual time=0.556..0.556 rows=0 loops=1)
Index Cond: (id = 1)
Total runtime: 0.626 ms
यह क्वेरी (cost=0.29..8.30 rows=1 width=1855)
को निष्पादित करने की अनुमानित लागत दोनों को दर्शाता है और इसे निष्पादित करने में लगने वाला वास्तविक समय (actual time=0.556..0.556 rows=0 loops=1)
यदि आप अधिक पठनीय प्रारूप पसंद करते हैं, तो आप पोस्टग्रेज़ को परिणामों को YAML में प्रिंट करने के लिए कह सकते हैं।
# explain (analyze, format yaml) select * from faults where id = 1;
QUERY PLAN
--------------------------------------
- Plan: +
Node Type: "Index Scan" +
Scan Direction: "Forward" +
Index Name: "faults_pkey" +
Relation Name: "faults" +
Alias: "faults" +
Startup Cost: 0.29 +
Total Cost: 8.30 +
Plan Rows: 1 +
Plan Width: 1855 +
Actual Startup Time: 0.008 +
Actual Total Time: 0.008 +
Actual Rows: 0 +
Actual Loops: 1 +
Index Cond: "(id = 1)" +
Rows Removed by Index Recheck: 0+
Triggers: +
Total Runtime: 0.036
(1 row)
अभी के लिए हम केवल "योजना पंक्तियों" और "वास्तविक पंक्तियों" पर ध्यान केंद्रित करने जा रहे हैं।
- योजना पंक्तियों सबसे खराब स्थिति में, आपकी क्वेरी का जवाब देने के लिए डीबी को कितनी पंक्तियों में लूप करना होगा
- वास्तविक पंक्तियां जब इसने क्वेरी को निष्पादित किया, तो डीबी ने वास्तव में कितनी पंक्तियों को लूप किया?
यदि "प्लान रो" 1 है, जैसा कि यह ऊपर है, तो क्वेरी शायद अच्छी तरह से स्केल होने वाली है। अगर "प्लान रो" डेटाबेस में पंक्तियों की संख्या के बराबर है, तो इसका मतलब है कि क्वेरी "फुल टेबल स्कैन" करने वाली है और अच्छी तरह से स्केल नहीं होने वाली है।
अब जब आप जानते हैं कि क्वेरी प्रदर्शन को कैसे मापना है, तो आइए कुछ सामान्य रेल मुहावरों को देखें और देखें कि वे कैसे ढेर हो जाते हैं।
गिनती
रेल के विचारों में इस तरह के कोड को देखना वास्तव में आम है:
Total Faults <%= Fault.count %>
इसका परिणाम SQL में होता है जो कुछ इस तरह दिखता है:
select count(*) from faults;
आइए explain
में प्लग इन करें और देखो क्या होता है।
# explain (analyze, format yaml) select count(*) from faults;
QUERY PLAN
--------------------------------------
- Plan: +
Node Type: "Aggregate" +
Strategy: "Plain" +
Startup Cost: 1840.31 +
Total Cost: 1840.32 +
Plan Rows: 1 +
Plan Width: 0 +
Actual Startup Time: 24.477 +
Actual Total Time: 24.477 +
Actual Rows: 1 +
Actual Loops: 1 +
Plans: +
- Node Type: "Seq Scan" +
Parent Relationship: "Outer"+
Relation Name: "faults" +
Alias: "faults" +
Startup Cost: 0.00 +
Total Cost: 1784.65 +
Plan Rows: 22265 +
Plan Width: 0 +
Actual Startup Time: 0.311 +
Actual Total Time: 22.839 +
Actual Rows: 22265 +
Actual Loops: 1 +
Triggers: +
Total Runtime: 24.555
(1 row)
वाह! हमारी साधारण गणना क्वेरी 22,265 पंक्तियों में लूपिंग कर रही है - संपूर्ण तालिका! पोस्टग्रेज में, काउंट हमेशा पूरे रिकॉर्ड सेट पर लूप करता है।
आप where
. जोड़कर सेट किए गए रिकॉर्ड के आकार को कम कर सकते हैं क्वेरी के लिए शर्तें। आपकी आवश्यकताओं के आधार पर, जहां प्रदर्शन स्वीकार्य है, वहां आपको आकार काफी कम मिल सकता है।
इस समस्या को हल करने का एकमात्र अन्य तरीका है कि आप अपने गणना मूल्यों को कैश करें। यदि आप इसे सेट करते हैं तो रेल आपके लिए यह कर सकती है:
belongs_to :project, :counter_cache => true
एक अन्य विकल्प यह देखने के लिए उपलब्ध है कि क्या क्वेरी कोई रिकॉर्ड लौटाती है। Users.count > 0
. के बजाय , कोशिश करें Users.exists?
. परिणामी क्वेरी बहुत अधिक प्रदर्शनकारी है। (इसे मेरी ओर इंगित करने के लिए पाठक गेरी शॉ का धन्यवाद।)
सॉर्टिंग
सूचकांक पृष्ठ। लगभग हर ऐप में कम से कम एक होता है। आप डेटाबेस से नवीनतम 20 रिकॉर्ड खींचते हैं और उन्हें प्रदर्शित करते हैं। क्या आसान हो सकता है?
रिकॉर्ड लोड करने के लिए कोड कुछ इस तरह दिख सकता है:
@faults = Fault.order(created_at: :desc)
इसके लिए sql इस तरह दिखता है:
select * from faults order by created_at desc;
तो चलिए इसका विश्लेषण करते हैं:
# explain (analyze, format yaml) select * from faults order by created_at desc;
QUERY PLAN
--------------------------------------
- Plan: +
Node Type: "Sort" +
Startup Cost: 39162.46 +
Total Cost: 39218.12 +
Plan Rows: 22265 +
Plan Width: 1855 +
Actual Startup Time: 75.928 +
Actual Total Time: 86.460 +
Actual Rows: 22265 +
Actual Loops: 1 +
Sort Key: +
- "created_at" +
Sort Method: "external merge" +
Sort Space Used: 10752 +
Sort Space Type: "Disk" +
Plans: +
- Node Type: "Seq Scan" +
Parent Relationship: "Outer"+
Relation Name: "faults" +
Alias: "faults" +
Startup Cost: 0.00 +
Total Cost: 1784.65 +
Plan Rows: 22265 +
Plan Width: 1855 +
Actual Startup Time: 0.004 +
Actual Total Time: 4.653 +
Actual Rows: 22265 +
Actual Loops: 1 +
Triggers: +
Total Runtime: 102.288
(1 row)
यहां हम देखते हैं कि जब भी आप इस क्वेरी को करते हैं तो डीबी सभी 22,265 पंक्तियों को सॉर्ट कर रहा है। नो ब्यूनो!
डिफ़ॉल्ट रूप से, आपके SQL में प्रत्येक "ऑर्डर बाय" क्लॉज वास्तविक समय में रिकॉर्ड सेट को ठीक उसी समय सॉर्ट करने का कारण बनता है। कोई कैशिंग नहीं है। आपको बचाने के लिए कोई जादू नहीं।
समाधान अनुक्रमणिका का उपयोग करना है। इस तरह के साधारण मामलों के लिए, create_at कॉलम में सॉर्ट किए गए इंडेक्स को जोड़ने से क्वेरी काफी तेज हो जाएगी।
अपने रेल प्रवास में आप डाल सकते हैं:
class AddIndexToFaultCreatedAt < ActiveRecord::Migration
def change
add_index(:faults, :created_at)
end
end
जो निम्न SQL चलाता है:
CREATE INDEX index_faults_on_created_at ON faults USING btree (created_at);
वहाँ बहुत अंत में, (created_at)
सॉर्ट ऑर्डर निर्दिष्ट करता है। डिफ़ॉल्ट रूप से यह आरोही है।
अब, यदि हम अपनी सॉर्ट क्वेरी को फिर से चलाते हैं, तो हम देखते हैं कि इसमें अब सॉर्टिंग चरण शामिल नहीं है। यह केवल इंडेक्स से प्री-सॉर्ट किए गए डेटा को पढ़ता है।
# explain (analyze, format yaml) select * from faults order by created_at desc;
QUERY PLAN
----------------------------------------------
- Plan: +
Node Type: "Index Scan" +
Scan Direction: "Backward" +
Index Name: "index_faults_on_created_at"+
Relation Name: "faults" +
Alias: "faults" +
Startup Cost: 0.29 +
Total Cost: 5288.04 +
Plan Rows: 22265 +
Plan Width: 1855 +
Actual Startup Time: 0.023 +
Actual Total Time: 8.778 +
Actual Rows: 22265 +
Actual Loops: 1 +
Triggers: +
Total Runtime: 10.080
(1 row)
यदि आप एक से अधिक स्तंभों के आधार पर क्रमित कर रहे हैं, तो आपको एक अनुक्रमणिका बनानी होगी जो अनेक स्तंभों द्वारा भी क्रमित हो। रेल प्रवास में जो दिखता है वह यहां दिया गया है:
add_index(:faults, [:priority, :created_at], order: {priority: :asc, created_at: :desc)
जैसे ही आप अधिक जटिल प्रश्न करना शुरू करते हैं, उन्हें explain
. के माध्यम से चलाना एक अच्छा विचार है . इसे जल्दी और अक्सर करें। आप पा सकते हैं कि क्वेरी में कुछ सरल परिवर्तन ने पोस्टग्रेज़ के लिए अनुक्रमणिका को सॉर्ट करने के लिए उपयोग करना असंभव बना दिया है।
सीमाएं और ऑफसेट
हमारे अनुक्रमणिका पृष्ठों में हम शायद ही कभी डेटाबेस में प्रत्येक आइटम को शामिल करते हैं। इसके बजाय हम एक बार में केवल 10 या 30 या 50 आइटम दिखाते हुए, पेजिनेट करते हैं। ऐसा करने का सबसे आम तरीका है limit
. का उपयोग करना और offset
साथ में। रेल में ऐसा दिखता है:
Fault.limit(10).offset(100)
यह SQL उत्पन्न करता है जो इस तरह दिखता है:
select * from faults limit 10 offset 100;
अब अगर हम एक्सप्लेन चलाते हैं, तो हमें कुछ अजीब दिखाई देता है। स्कैन की गई पंक्तियों की संख्या 110 है, जो सीमा और ऑफसेट के बराबर है।
# explain (analyze, format yaml) select * from faults limit 10 offset 100;
QUERY PLAN
--------------------------------------
- Plan: +
Node Type: "Limit" +
...
Plans: +
- Node Type: "Seq Scan" +
Actual Rows: 110 +
...
यदि आप ऑफ़सेट को 10,000 में बदलते हैं, तो आप देखेंगे कि स्कैन की गई पंक्तियों की संख्या बढ़कर 10010 हो जाती है, और क्वेरी 64x धीमी हो जाती है।
# explain (analyze, format yaml) select * from faults limit 10 offset 10000;
QUERY PLAN
--------------------------------------
- Plan: +
Node Type: "Limit" +
...
Plans: +
- Node Type: "Seq Scan" +
Actual Rows: 10010 +
...
यह एक परेशान करने वाले निष्कर्ष की ओर ले जाता है:पृष्ठांकन करते समय, बाद के पृष्ठ पहले के पृष्ठों की तुलना में लोड होने में धीमे होते हैं। यदि हम ऊपर के उदाहरण में प्रति पृष्ठ 100 आइटम मानते हैं, तो पृष्ठ 100 पृष्ठ 1 से 13x धीमा है।
तो तुम क्या करते हो?
सच कहूँ तो, मैं एक सही समाधान नहीं खोज पाया। मैं यह देखकर शुरू करूंगा कि क्या मैं डेटासेट के आकार को कम कर सकता हूं, इसलिए मेरे पास शुरू करने के लिए 100 या 1000 पेज नहीं थे।
यदि आप अपने रिकॉर्ड सेट को कम करने में असमर्थ हैं, तो आपकी सबसे अच्छी शर्त यह हो सकती है कि आप ऑफ़सेट/सीमा को जहाँ क्लॉज़ से बदल दें।
# You could use a date range
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)
# ...or even an id range
Fault.where("id > ? and id < ?", 100, 200)
निष्कर्ष
मुझे उम्मीद है कि इस लेख ने आपको आश्वस्त किया है कि आपको वास्तव में अपने डीबी प्रश्नों के साथ संभावित प्रदर्शन समस्याओं को खोजने के लिए पोस्टग्रेज व्याख्या फ़ंक्शन का लाभ उठाना चाहिए। यहां तक कि सबसे सरल प्रश्न भी प्रमुख प्रदर्शन समस्याओं का कारण बन सकते हैं, इसलिए यह जांच करने के लिए भुगतान करता है। :)पी>