অ্যাপসিগন্যালে আমরা ডেভেলপারদের অ্যাপ্লিকেশন পারফরম্যান্স নিয়ে সাহায্য করি। আমরা অনেকগুলি অ্যাপ মনিটর করি যারা বিলিয়ন বিলিয়ন রিকোয়েস্ট পাঠায়। আমরা মনে করি রুবি এবং পারফরম্যান্স নিয়ে কিছু ব্লগ পোস্ট লিখে আমরা আরও কিছু সাহায্য করতে পারি। N+1 কোয়েরি সমস্যা রেলস অ্যাপ্লিকেশনে একটি সাধারণ অ্যান্টিপ্যাটার্ন।
অ্যাক্টিভ রেকর্ডে লেজি লোডিং
অ্যাক্টিভ রেকর্ড ন मालूम ডেটা লোড করার জন্য ইমপ্লিসিট লেজি লোডিং ব্যবহার করে যাতে আপনি অ্যাসোসিয়েশন কোয়েরি করলে সেটি ডিফার করতে পারেন। এটা ভিউতে কোন অ্যাসোসিয়েশন লোড করতে হবে সে ডিসিশন অফলোড করে আপনাকে ইমপ্লিসিট থাকতে দেয়।
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 কোয়ারী
প্রথম উদাহরণে, আমরা একটি প্রোডাক্ট এবং তার ভ্যারিয়েন্টসহ ভিউ রেন্ডার করেছিলাম। কোয়ারী কাউন্ট ছিল ২ কারণ দুটি কোয়ারী এক্সিকিউট হয়েছিল। এই রিকোয়েস্ট সব প্রোডাক্ট (এই উদাহরণে ৩টি) ডেটাবেস থেকে রিটার্ন করেছিল, এবং প্রতিটির ভ্যারিয়েন্ট, এবং সেটি ২টির বদলে ৪টি কোয়ারী করেছিল।
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 কোয়ারী আছে, আমরা প্রেডিক্ট করতে পারি ১০০টি প্রোডাক্ট থাকলে কোয়ারী কাউন্ট ১০১ হবে (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)
ভ্যারিয়েন্টগুলো প্রিলোড করলে, কোয়ারী কাউন্ট আবার ২-এ নামে আসে, এমনকি ভবিষ্যতে প্রোডাক্টের সংখ্যা বাড়লেও।
SELECT "products".* FROM "products"SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (1, 2, 3)
লেজি নাকি ইগার?
অधিকাংশ পরিস্থিতিতে, ডেটাবেস থেকে সব অ্যাসোসিয়েটেড রেকর্ড এক কোয়ারীতে পাওয়া লেজি লোডিং চেয়ে অনেক দ্রুত।
এই উদাহরণ অ্যাপ্লিকেশনে, ডেটাবেস পারফরম্যান্স পার্থক্য শুধু ৩টি প্রোডাক্ট থাকলেও মাপা যায়, প্রতিটির ১০টি ভ্যারিয়েন্ট। গড়ে, প্রোডাক্ট লিস্ট ইগার লোড করলে প্রায় ১২.৫% দ্রুত (০.৭ ms বনাম ০.৮ ms) লেজি লোডিং চেয়ে। ১০টি প্রোডাক্ট থাকলে পার্থক্য ৫৯% (১.২২ ms বনাম ২.৯৮ ms)। ১০০০টি প্রোডাক্ট থাকলে পার্থক্য প্রায় ৮০%, কারণ ইগার কোয়ারী ৫৮.৪ ms নেয়, যখন লেজি লোডিং ২৯০.১২ ms নেয়।
যদিও লেজি-লোডেড অ্যাসোসিয়েশন ভিউতে বেশি ফ্লেক্সিবিলিটি দেয় কন্ট্রোলার আপডেট ছাড়া, একটি ভালো রুল অফ থাম্ব হলো কন্ট্রোলার ডেটা লোডিং হ্যান্ডেল করবে ভিউতে পাস করার আগে।
ভিউ থেকে লেজি লোডিং সিঙ্গল মডেল অবজেক্ট এবং তার অ্যাসোসিয়েশন দেখানোর ভিউর জন্য কাজ করে (যেমন আমাদের প্রথম উদাহরণের ProductsController#show) এবং ব্যবহারী হতে পারে যখন একই কন্ট্রোলারের জন্য একাধিক ভিউ ভিন্ন ডেটা চায়।
বিড়াল এবং গুড়িয়া
বিড়ালরা একমত নাও হলে, কখনো কখনো লেজি হইতে ইগার হয়া বোস। এই পোস্টে আমরা অ্যাক্টিভ রেকর্ডের লেজি লোডিংয়ে গোতা লাগিয়েছি এবং দেখিয়েছি কোন পরিস্থিতিতে এটা পারফরম্যান্স সমস্যা তৈরি করতে পারে। যেমন যখন এটা N+1 কোয়ারী সমস্যায় নিয়ে যায়।
সংক্ষেপে: ডেভেলপমেন্ট লগ বা অ্যাপসিগন্যালের ইভেন্ট টাইমলাইন সবসময় চোখ রাখুন যেন আপনি এমন কোয়ারী না করছেন যা লেজি লোডেড হতে পারে এবং আপনার রেসপন্স টাইম ট্র্যাক রাখুন, বিশেষ করে যখন প্রসেস করা ডেটার পরিমাণ বাড়ে।
আপনি যদি এটি পছন্দ করেন, তবে পারফরম্যান্স এবং মনিটরিং নিয়ে আরও কিছু পড়ুন, যেমন রাশিয়ান ডল ক্যাশিং বা কন্ডিশনাল GET রিকোয়েস্টস নিয়ে লেখা।