ऐपसिग्नल में हम डेवलपर्स को एप्लिकेशन के प्रदर्शन में मदद करते हैं। हम बड़ी संख्या में ऐसे ऐप्स की निगरानी कर रहे हैं जो अरबों अनुरोध भेजते हैं। हमने सोचा कि हम रूबी और प्रदर्शन के बारे में कुछ ब्लॉग पोस्ट के साथ भी कुछ मदद कर सकते हैं। N+1 क्वेरीज़ समस्या रेल अनुप्रयोगों में एक सामान्य एंटीपैटर्न है।
रेल के ActiveRecord जैसे बहुत से ओआरएम में आलसी लोडिंग होती है जिससे आप पूछताछ संघों को तब तक स्थगित कर सकते हैं जब तक उनकी आवश्यकता न हो। यह इस निर्णय को दृश्य में लोड करके इस बारे में निहित होने की अनुमति देता है कि किन संघों को लोड करने की आवश्यकता है।
N+1 क्वेरी की समस्या एक आम है, लेकिन आमतौर पर स्पॉट करना आसान है, प्रदर्शन एंटीपैटर्न जिसके परिणामस्वरूप प्रत्येक एसोसिएशन के लिए एक क्वेरी चलती है, जो डेटाबेस से बड़ी संख्या में एसोसिएशन को क्वेरी करते समय ओवरहेड का कारण बनती है।
<ब्लॉकक्वॉट>वैसे, यदि आप इस लेख को पसंद करते हैं, तो हमने रूबी (ऑन रेल्स) के प्रदर्शन के बारे में और भी बहुत कुछ लिखा है, हमारी रूबी प्रदर्शन निगरानी चेकलिस्ट देखें।
ActiveRecord में आलसी लोडिंग
ActiveRecord संबंधों के साथ काम करना आसान बनाने के लिए अंतर्निहित आलसी लोडिंग का उपयोग करता है। आइए वेबशॉप उदाहरण पर विचार करें, जहां प्रत्येक उत्पाद वेरिएंट की कोई भी संख्या हो सकती है जिसमें उत्पाद का रंग या आकार होता है, उदाहरण के लिए।
# app/models/product.rb
class Product < ActiveRecord::Base
has_many :variants
end
ProductsController#show
. में , उत्पादों में से एक के लिए विवरण दृश्य, हम Product.find(params[:id])
का उपयोग करेंगे उत्पाद प्राप्त करने के लिए और इसे @product
. को असाइन करें चर।
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
end
end
इस कार्रवाई को ध्यान में रखते हुए, हम variants
पर कॉल करके उत्पाद के वेरिएंट पर लूप करेंगे @product
. पर विधि नियंत्रक से हमें प्राप्त चर।
# app/views/products/show.html.erb
<h1><%= @product.title %></h1>
<ul>
<%= @product.variants.each do |variant| %>
<li><%= variant.name %></li>
<% end %>
</ul>
@product.variants
. पर कॉल करके दृश्य में, हमारे लिए लूप ओवर करने के लिए वेरिएंट प्राप्त करने के लिए रेल डेटाबेस से पूछताछ करेगा। स्पष्ट क्वेरी के अलावा हमने नियंत्रक में किया था, हम देख सकते हैं कि अगर हम इस अनुरोध के लिए रेल के लॉग की जांच करते हैं तो वेरिएंट लाने के लिए एक और क्वेरी निष्पादित की जाती है।
Started GET "/products/1" for 127.0.0.1 at 2018-04-19 08:49:13 +0200
Processing by ProductsController#show as HTML
Parameters: {"id"=>"1"}
Product Load (1.1ms) SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Rendering products/show.html.erb within layouts/application
Variant Load (1.1ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ? [["product_id", 1]]
Rendered products/show.html.erb within layouts/application (4.4ms)
Completed 200 OK in 64ms (Views: 56.4ms | ActiveRecord: 2.3ms)
इस अनुरोध ने एक उत्पाद को उसके सभी प्रकारों के साथ दिखाने के लिए दो प्रश्नों को निष्पादित किया।
SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1
SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 1
लूप्ड आलसी लोडिंग
आलसी लोडिंग अब तक बहुत अच्छी रही है। एक निहित क्वेरी का उपयोग करके, जब हम तय करते हैं कि हम इस दृश्य पर अब और वेरिएंट नहीं दिखाना चाहते हैं, तो हमें इसे नियंत्रक से निकालना याद रखना होगा।
मान लें कि हम ProductsController#index
. पर काम कर रहे हैं , जहां हम सभी उत्पादों की एक सूची उनके प्रत्येक प्रकार के साथ दिखाना चाहेंगे। हम इसे आलसी लोडिंग के साथ उसी तरह लागू कर सकते हैं जैसे हमने पहले किया था।
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all
end
end
# app/views/products/index.html.erb
<h1>Products</h1>
<% @products.each do |product| %>
<article>
<h1><%= product.title %></h1>
<ul>
<% product.variants.each do |variant| %>
<li><%= variant.description %></li>
<% end %>
</ul>
</article>
<% end %>
पहले उदाहरण के विपरीत अब हमें एक के बजाय नियंत्रक से उत्पादों की एक सूची मिलती है। दृश्य तब प्रत्येक उत्पाद पर लूप करता है, और आलसी प्रत्येक उत्पाद के लिए प्रत्येक प्रकार को लोड करता है।
जबकि यह काम करता है, एक पकड़ है। हमारी क्वेरी संख्या अब N+1 है ।
N+1 क्वेरी
पहले उदाहरण में, हमने एकल उत्पाद और उसके प्रकारों के लिए एक दृश्य प्रस्तुत किया। क्वेरी गिनती 2 था क्योंकि हमने दो प्रश्नों को निष्पादित किया था। इस अनुरोध ने डेटाबेस से सभी उत्पादों (इस उदाहरण में 3,) और उनके प्रत्येक रूप को लौटा दिया, और इसने दो के बजाय चार प्रश्न किए।
Started GET "/products" for 127.0.0.1 at 2018-04-19 09:49:02 +0200
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.3ms) SELECT "products".* FROM "products"
Variant Load (0.2ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ? [["product_id", 1]]
Variant Load (0.2ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ? [["product_id", 2]]
Variant Load (0.1ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ? [["product_id", 3]]
Rendered products/index.html.erb within layouts/application (5.6ms)
Completed 200 OK in 36ms (Views: 32.6ms | ActiveRecord: 0.8ms)
SELECT "products".* FROM "products"
SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 1
SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 2
SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 3
पहली क्वेरी, जिसे Product.all
. पर स्पष्ट कॉल द्वारा निष्पादित किया जाता है नियंत्रक में, सभी उत्पादों को ढूंढता है। प्रत्येक उत्पाद को देखने में लूप करते समय बाद वाले को आलसी रूप से निष्पादित किया जाता है।
इस उदाहरण के परिणामस्वरूप N+1 की एक क्वेरी गणना होती है, जहां N उत्पादों की संख्या है, और जोड़ा गया एक स्पष्ट क्वेरी है जो सभी उत्पादों को प्राप्त करती है। दूसरे शब्दों में; यह उदाहरण एक क्वेरी करता है, और फिर पहली क्वेरी में प्रत्येक परिणाम के लिए दूसरा। क्योंकि इस उदाहरण में N =3, परिणामी क्वेरी संख्या N + 1 = 3 + 1 = 4
है ।
हालांकि केवल तीन उत्पाद होने पर यह वास्तव में कोई समस्या नहीं हो सकती है, लेकिन क्वेरी की संख्या उत्पादों की संख्या के साथ बढ़ जाती है। चूंकि हम जानते हैं कि इस अनुरोध में N+1 क्वेरी हैं, इसलिए जब हमारे पास 100 उत्पाद हों (N + 1 = 100 + 1 = 101
तो हम 101 की क्वेरी संख्या का अनुमान लगा सकते हैं। ), उदाहरण के लिए।
उत्सुक लोडिंग एसोसिएशन
हमारे जैसे उत्पादों की संख्या के साथ प्रश्नों की संख्या बढ़ाने के बजाय, हम इस दृश्य में अनुरोधों की एक स्थिर संख्या चाहते हैं। हम दृश्य को प्रस्तुत करने से पहले नियंत्रक में वेरिएंट को स्पष्ट रूप से प्रीलोड करके ऐसा कर सकते हैं।
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all.includes(:variants)
end
end
ActiveRecord के includes
query विधि यह सुनिश्चित करती है कि संबंधित प्रकार उनके उत्पादों के साथ लोड किए गए हैं। क्योंकि यह जानता है कि कौन से वेरिएंट को पहले से लोड करने की आवश्यकता है, यह सभी अनुरोधित उत्पादों के सभी वेरिएंट को एक क्वेरी में प्राप्त कर सकता है।
Started GET "/products" for 127.0.0.1 at 2018-04-19 10:33:59 +0200
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.3ms) SELECT "products".* FROM "products"
Variant Load (0.4ms) SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (?, ?, ?) [["product_id", 1], ["product_id", 2], ["product_id", 3]]
Rendered products/index.html.erb within layouts/application (5.9ms)
Completed 200 OK in 45ms (Views: 40.8ms | ActiveRecord: 0.7ms)
विविधताओं को पहले से लोड करके, क्वेरी की संख्या वापस 2 पर आ जाती है, भले ही भविष्य में उत्पादों की संख्या बढ़ जाए।
SELECT "products".* FROM "products"
SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (1, 2, 3)
आलसी या उत्सुक?
अधिकांश स्थितियों में, डेटाबेस से सभी संबद्ध रिकॉर्ड एक ही क्वेरी में प्राप्त करना उन्हें आलसी लोड करने की तुलना में बहुत तेज़ है।
इस उदाहरण एप्लिकेशन में, डेटाबेस प्रदर्शन अंतर केवल तीन उत्पादों के साथ मापने योग्य है, प्रत्येक में दस प्रकार हैं। औसतन, उत्पादों की सूची में उत्सुक लोडिंग आलसी लोडिंग की तुलना में लगभग 12.5% तेज (0.7 एमएस बनाम 0.8 एमएस) है। दस उत्पादों के साथ, यह अंतर बढ़कर 59% (1.22 एमएस बनाम 2.98 एमएस) हो जाता है। 1000 उत्पादों के साथ, अंतर लगभग 80% है, क्योंकि उत्सुक प्रश्नों की घड़ी 58.4 एमएस में होती है, जबकि आलसी लोडिंग में उन्हें लगभग 290.12 एमएस लगता है।
हालांकि आलसी-भारित संघ नियंत्रक को अद्यतन किए बिना दृश्य में अधिक लचीलापन देते हैं, अंगूठे का एक अच्छा नियम यह है कि नियंत्रक डेटा को दृश्य में भेजने से पहले लोड कर रहा है।
दृश्य से आलसी लोडिंग उन दृश्यों के लिए काम करती है जो एक मॉडल ऑब्जेक्ट और उसके संघों को दिखाते हैं (जैसे ProductsController#show
हमारे पहले उदाहरण में) और उपयोगी हो सकता है जब एक ही नियंत्रक से अलग-अलग डेटा की आवश्यकता होती है, उदाहरण के लिए।
बिल्लियाँ और गुड़िया
बिल्लियाँ सहमत नहीं हो सकती हैं, लेकिन कभी-कभी यह आलसी के बजाय उत्सुक होने का भुगतान करती है। इस पोस्ट में हमने ActiveRecord में आलसी लोडिंग में प्रवेश किया और उन स्थितियों का एक उदाहरण दिखाया जिसमें यह एक प्रदर्शन समस्या पैदा कर सकता है। जैसे जब यह N+1 प्रश्नों की समस्या की ओर ले जाता है।
संक्षेप में:हमेशा विकास लॉग, या ऐपसिग्नल में इवेंट टाइमलाइन पर नज़र रखें, यह सुनिश्चित करने के लिए कि आप ऐसे प्रश्न नहीं कर रहे हैं जो आलसी लोड हो सकते हैं और आपके प्रतिक्रिया समय का ट्रैक रख सकते हैं, खासकर जब संसाधित होने वाले डेटा की मात्रा बढ़ जाती है ।
अगर आपको यह पसंद आया, तो प्रदर्शन और निगरानी पर हमारे द्वारा लिखी गई कुछ और चीजें देखें, जैसे कि रूसी गुड़िया कैशिंग के बारे में यह पसंदीदा या सशर्त प्राप्त अनुरोधों के बारे में यह एक।