AppSignal-এ আমরা অ্যাপ্লিকেশন পারফরম্যান্সে বিকাশকারীদের সাহায্য করি। আমরা বিপুল সংখ্যক অ্যাপের উপর নজর রাখছি যেগুলি কোটি কোটি অনুরোধ পাঠায়। আমরা ভেবেছিলাম রুবি এবং পারফরম্যান্স সম্পর্কে কয়েকটি ব্লগপোস্ট দিয়েও আমরা কিছুটা সাহায্য করতে পারি। N+1 ক্যোয়ারী সমস্যা হল রেল অ্যাপ্লিকেশনের একটি সাধারণ অ্যান্টিপ্যাটার্ন।
রেলের অ্যাক্টিভরেকর্ডের মতো অনেক ওআরএম-এ অলস লোডিং বিল্ট-ইন থাকে যাতে আপনি প্রয়োজনের মুহুর্ত পর্যন্ত কোয়েরি অ্যাসোসিয়েশনগুলিকে পিছিয়ে দিতে পারেন। এই সিদ্ধান্তটি ভিউতে অফলোড করে কোন অ্যাসোসিয়েশনগুলিকে লোড করতে হবে সে সম্পর্কে এটি অন্তর্নিহিত হওয়ার অনুমতি দেয়৷
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
ক্যোয়ারী পদ্ধতি নিশ্চিত করে যে সংশ্লিষ্ট ভেরিয়েন্টগুলি তাদের পণ্যগুলির সাথে লোড করা হয়েছে। কারণ এটি আগে থেকেই জানে কোন ভেরিয়েন্টগুলিকে লোড করতে হবে, তাই এটি একটি ক্যোয়ারীতে সমস্ত অনুরোধ করা পণ্যের সমস্ত ভেরিয়েন্ট আনতে পারে৷
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
আমাদের প্রথম উদাহরণে) এবং একই কন্ট্রোলার থেকে ভিন্ন ডেটার প্রয়োজন হলে একাধিক ভিউ থাকলে কাজে লাগতে পারে।
বিড়াল এবং পুতুল
বিড়ালরা একমত নাও হতে পারে, তবে কখনও কখনও এটি অলসের পরিবর্তে আগ্রহী হওয়ার জন্য অর্থ প্রদান করে। এই পোস্টে আমরা ActiveRecord-এ অলস লোডিংয়ে প্রবেশ করেছি এবং এমন পরিস্থিতিগুলির একটি উদাহরণ দেখিয়েছি যেখানে এটি একটি কর্মক্ষমতা সমস্যা তৈরি করতে পারে। যেমন যখন এটি N+1 কোয়েরি সমস্যার দিকে নিয়ে যায়।
সংক্ষেপে:সর্বদা ডেভেলপমেন্ট লগ বা অ্যাপসিগন্যালে ইভেন্ট টাইমলাইনের উপর নজর রাখুন, নিশ্চিত করুন যে আপনি অলস লোড হতে পারে এমন প্রশ্নগুলি করছেন না এবং আপনার প্রতিক্রিয়ার সময়গুলি ট্র্যাক রাখুন, বিশেষ করে যখন প্রক্রিয়াকৃত ডেটার পরিমাণ বৃদ্ধি পায় .
আপনি যদি এটি পছন্দ করেন তবে পারফরম্যান্স এবং নিরীক্ষণের বিষয়ে আমরা লিখেছি এমন আরও কিছু জিনিস দেখুন, যেমন রাশিয়ান ডল ক্যাশিং সম্পর্কে এই প্রিয় বা শর্তসাপেক্ষ পান অনুরোধ সম্পর্কে।