ऐपसिग्नल पर हम डेवलपर्स को एप्लिकेशन प्रदर्शन में मदद करते हैं। हम बड़ी संख्या में ऐप्स की निगरानी करते हैं जो अरबों अनुरोध भेजते हैं। हमने सोचा कि हम रूबी और प्रदर्शन के बारे में कुछ ब्लॉग पोस्ट के साथ थोड़ी और मदद कर सकते हैं। N+1 क्वेरी समस्या रेल्स एप्लिकेशन में एक सामान्य एंटीपैटर्न है।
बहुत से ORMs, जैसे रेल्स का एक्टिवरेकॉर्ड, अंतर्निहित लेज़ी लोडिंग का उपयोग करते हैं ताकि आप एसोसिएशन को तब तक क्वेरी करने से टाल सकें जब तक उनकी आवश्यकता न हो। यह आपको इस निर्णय को व्यू पर छोड़कर अंतर्निहित रूप से यह तय करने की अनुमति देता है कि किन एसोसिएशन को लोड करना है।
N+1 क्वेरी समस्या एक सामान्य, लेकिन आमतौर पर आसानी से पहचानी जाने वाली, प्रदर्शन एंटीपैटर्न है जो प्रत्येक एसोसिएशन के लिए एक क्वेरी चलाने का परिणाम देती है, जिससे डेटाबेस से बड़ी संख्या में एसोसिएशन क्वेरी करते समय ओवरहेड होता है।
👋 वैसे, अगर आपको यह लेख पसंद आया, तो हमने रूबी (ऑन रेल्स) प्रदर्शन के बारे में बहुत कुछ लिखा है, हमारी रूबी प्रदर्शन निगरानी चेकलिस्ट देखें।
एक्टिवरेकॉर्ड में लेज़ी लोडिंग
एक्टिवरेकॉर्ड संबंधों के साथ काम करना आसान बनाने के लिए अंतर्निहित लेज़ी लोडिंग का उपयोग करता है। आइए एक वेबशॉप उदाहरण पर विचार करें, जहां प्रत्येक Product के कई Variants हो सकते हैं जिनमें उत्पाद का रंग या आकार होता है, उदाहरण के लिए।
# 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
इस एक्शन के लिए व्यू में, हम कंट्रोलर से प्राप्त @product वेरिएबल पर variants मेथड को कॉल करके उत्पाद के वेरिएंट पर लूप करेंगे।
# 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 1SELECT "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" = 1SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 2SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 3
पहली क्वेरी, जो कंट्रोलर में Product.all के स्पष्ट कॉल द्वारा निष्पादित की जाती है, सभी उत्पादों को ढूंढती है। बाद वाली व्यू में प्रत्येक उत्पाद पर लूप करते समय लेज़ी रूप से निष्पादित होती हैं।
इस उदाहरण का परिणाम N+1 की क्वेरी काउंट में होता है, जहां N उत्पादों की संख्या है, और जोड़ा गया एक वह स्पष्ट क्वेरी है जिसने सभी उत्पादों को प्राप्त किया। दूसरे शब्दों में; यह उदाहरण एक क्वेरी करता है, और फिर पहली क्वेरी में प्रत्येक परिणाम के लिए एक और। क्योंकि इस उदाहरण में N = 3 है, परिणामी क्वेरी काउंट N + 1 = 3 + 1 = 4 है।
हालांकि जब केवल तीन उत्पाद हों तो यह वास्तव में कोई समस्या नहीं हो सकती, लेकिन क्वेरी काउंट उत्पादों की संख्या के साथ बढ़ता है। क्योंकि हम जानते हैं कि इस अनुरोध में N+1 क्वेरी हैं, हम 100 उत्पादों के लिए 101 की क्वेरी काउंट का अनुमान लगा सकते हैं (N + 1 = 100 + 1 = 101), उदाहरण के लिए।
एसोसिएशन की ईगर लोडिंग
उत्पादों की संख्या के साथ क्वेरी की संख्या बढ़ाने के बजाय जैसे हम अभी करते हैं, हम इस व्यू में अनुरोधों की एक स्थिर संख्या रखना चाहेंगे। हम कंट्रोलर में व्यू रेंडर करने से पहले वेरिएंट को स्पष्ट रूप से प्रीलोड करके ऐसा कर सकते हैं।
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all.includes(:variants)
end
end
एक्टिवरेकॉर्ड का includes क्वेरी मेथड यह सुनिश्चित करता है कि संबंधित वेरिएंट उनके उत्पादों के साथ लोड हों। क्योंकि यह पहले से जानता है कि किन वेरिएंट को लोड करना है, यह सभी अनुरोधित उत्पादों के सभी वेरिएंट को एक क्वेरी में ला सकता है।
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 ms बनाम 0.8 ms)। दस उत्पादों के साथ, यह अंतर 59% (1.22 ms बनाम 2.98 ms) तक बढ़ जाता है। 1000 उत्पादों के साथ, अंतर लगभग 80% है, क्योंकि ईगर क्वेरी 58.4 ms पर होती हैं, जबकि उन्हें लेज़ी लोड करने में लगभग 290.12 ms लगते हैं।
हालांकि लेज़ी-लोडेड एसोसिएशन कंट्रोलर को अपडेट किए बिना व्यू में अधिक लचीलापन देते हैं, एक अच्छा नियम यह है कि कंट्रोलर डेटा को लोड करने को संभाले और फिर इसे व्यू को पास करे।
व्यू से लेज़ी लोडिंग उन व्यू के लिए काम करती है जो एक मॉडल ऑब्जेक्ट और उसके एसोसिएशन दिखाते हैं (जैसे हमारे पहले उदाहरण में ProductsController#show) और यह उपयोगी हो सकता है जब कई व्यू हों जिन्हें एक ही कंट्रोलर से अलग-अलग डेटा की आवश्यकता हो।
बिल्लियाँ और गुड़िया
बिल्लियाँ सहमत नहीं हो सकतीं, लेकिन कभी-कभी आलसी के बजाय उत्सुक होना फायदेमंद होता है। इस पोस्ट में हम एक्टिवरेकॉर्ड में लेज़ी लोडिंग में गहराई से गए और उन स्थितियों का एक उदाहरण दिखाया जिनमें यह प्रदर्शन समस्या पैदा कर सकता है। जैसे जब यह N+1 क्वेरी समस्या की ओर ले जाता है।
संक्षेप में: हमेशा विकास लॉग पर नज़र रखें, या ऐपसिग्नल में इवेंट टाइमलाइन, यह सुनिश्चित करने के लिए कि आप ऐसी क्वेरी नहीं कर रहे हैं जिन्हें लेज़ी लोड किया जा सकता है और अपने प्रतिक्रिया समय को ट्रैक करें, खासकर जब प्रोसेस किए गए डेटा की मात्रा बढ़ती है।
अगर आपको यह पसंद आया, तो प्रदर्शन और निगरानी के बारे में हमारी कुछ और चीज़ें देखें, जैसे रशियन डॉल कैशिंग के बारे में यह पसंदीदा या कंडीशनल गेट अनुरोधों के बारे में यह लेख।