কম্পিউটার টিউটোরিয়াল

ডাটাবেস পারফরম্যান্স নষ্ট করে এমন রেলস ইডিয়ম এড়িয়ে চলুন

আমি আজও স্মরণ করি প্রথমবারের মতো যখন আমি রেলসের ActiveRecord দেখেছিলাম। এটি একটি প্রকাশ ছিল। এটা ২০০৫ সালের কথা এবং আমি একটি PHP অ্যাপের জন্য হ্যান্ড-কোডিং SQL কোয়েরি করতাম। হঠাৎ, ডাটাবেস ব্যবহার করা একটি ক্লান্তিকর কাজ থেকে সহজ এবং - ডের আই সে - মজাদার হয়ে উঠেছিল।

...তারপর আমি পারফরম্যান্স ইস্যুগুলো লক্ষ্য করলাম।

ActiveRecord নিজে স্লো ছিল না। আমি শুধু যে কোয়েরি আসলে রান হচ্ছে সেগুলোর ওপর মনোযোগ দিতে বন্ধ করে দিয়েছিলাম। এবং খুলে আসে, রেলস CRUD অ্যাপে ব্যবহৃত সবচেয়ে ইডিয়োম্যাটিক ডাটাবেস কোয়েরিগুলোর কিছু ডিফল্টরূপে বড় ডেটাসেটে স্কেল আপ করার জন্য খুব খারাপ।

এই আর্টিকেলে আমরা তিনটি বড় আপরাধী নিয়ে আলোচনা করব। কিন্তু প্রথমে, চলুন কথা বলি কীভাবে আপনি বুঝতে পারেন আপনার DB কোয়েরিগুলো ভালো স্কেল করবে কিনা।

পারফরম্যান্স মাপা

যদি আপনার ডেটাসেট достаточно ছোট হয়, তবে যেকোনো DB কোয়েরি পারফর্ম্যান্ট। তাই আসলে পারফরম্যান্স বুঝতে হলে, আমাদের প্রোডাকশন-সাইজ ডাটাবেসের বিপরীতে বেঞ্চмар্ক করতে হবে। আমাদের উদাহরণে, আমরা faults নামের একটি টেবিল ব্যবহার করব প্রায় ২২,০০০ রেকর্ডের সাথে।

আমরা পোস্টগ্রেস ব্যবহার করছি। পোস্টগ্রেসে, পারফরম্যান্স মাপার উপায় হলো 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)

এখন আমরা শুধু "Plan Rows" এবং "Actual Rows"-এ ফোকাস করব।

  • Plan Rows ворস্ট কেসে, DB কতগুলো রোদর মাধ্যমে লুপ করবে আপনার কোয়ারি রেসপন্স দিতে
  • Actual Rows কোয়ারি এক্সিকিউট করার সময়, DB আসলে কতগুলো রোদর মাধ্যমে লুপ করেছিল?

"Plan Rows" ১ হলে, যেমন উপরে, তবে কোয়ারি সম্ভবত ভালো স্কেল করবে। যদি "Plan Rows" ডাটাবেসের রো সংখ্যার সমান হয়, তবে মানে কোয়ারি একটি "ফুল টেবিল স্ক্যান" করবে এবং ভালো স্কেল করবে না।

এখন আপনি জানেন কীভাবে কোয়ারি পারফরম্যান্স মাপতে হয়, চলুন কিছু সাধারণ রেলস ইডিয়ম দেখি এবং তারা কীভাবে স্ট্যাক আপ করে।

কাউন্টিং

রেলস ভিউতে এত্যাদি কোড দেখতে খুব সাধারণ:

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)

ওহ! আমাদের সিম্পল কাউন্ট কোয়ারি ২২,২৬৫ রো - পুরো টেবিল! - দিয়ে লুপ করছে! পোস্টগ্রেসে, কাউন্ট সবসময় পুরো রেকর্ড সেটের ওপর লুপ করে।

আপনি কোয়ারিতে where কন্ডিশন যোগ করে রেকর্ড সেটের আকার কমানো পারেন। আপনার রিকোয়ারেমেন্টের ওপর নির্ভর করে, আপনি আকারটিকে পর্যাপ্ত নিচে নামাতে পারেন যেখানে পারফরম্যান্স গ্রহণযোগ্য।

এই ইস্যুতে কাবু পाने का একমাত্র অন্য উপায় হলো আপনার কাউন্ট ভ্যালু ক্যাশ করা। রেলস আপনার জন্য এটি করতে পারে আপনি সেট আপ করলে:

belongs_to :project, :counter_cache => true

একটি অল্টারনেটিভ আছে যখন চেক করবেন কোয়ারি কোনো রেকর্ড রিটার্ন করে কিনা। Users.count > 0 এর বদলে, Users.exists? ট্রাই করুন। ফলাফল কোয়ারি অনেক বেশি পারফরম্যান্ট। (রিডার Gerry Shaw-কে ধন্যবাদ এই পয়েন্টটি আমাকে দেখানোর জন্য।)

সর্টিং

ইন্ডেক্স পেজ। প্রায় প্রতিটি অ্যাপে কমপক্ষে একটি থাকে। আপনি ডাটাবেস থেকে সবচেয়ে নতুন ২০টি রেকর্ড টানেন এবং প্রদর্শন করেন। इससे সহজ কী হতে পারে?

রেকর্ড লোড করার কোড কিছুটা এরকম দেখতে পারে:

@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)

এখানে আমরা দেখি DB প্রতিবার এই কোয়ারি করার সময় ২২,২৬৫ রো সর্ট করছে। নো বুয়েনো!

ডিফল্টরূপে, আপনার SQL-এ যেকোনো "order by" ক্লজ রেকর্ড সেটকে রিয়েল-টাইমে সর্ট করে। কোনো ক্যাশিং নেই। আপনাকে বাঁচাতে কোনো ম্যাজিক নেই।

সমাধান হলো ইনডেক্স ব্যবহার করা। এইরকম সিম্পল কেসের জন্য, created_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-এ রান করা ভালো। এটি আর্লি এবং ওফেন করুন। আপনি খুঁজে পেতে পারেন যে কোয়ারিতে কিছু সিম্পল পরিবর্তন এটিকে পোস্টগ্রেসের জন্য ইনডেক্স ব্যবহার করা অসম্ভব করে দিয়েছে সর্টিং-এর জন্য।

লিমিটস এবং অফসেটস

আমাদের ইন্ডেক্স পেজে আমরা কখনোই ডাটাবেসের সব আইটেম ইনক্লুড করি না। বদলে আমরা পেজিনেট করি, একবারে শুধু ১০ বা ৩০ বা ৫০ আইটেম দেখাই। এটি করার সবচেয়ে সাধারণ উপায় হলো limit এবং offset একসাথে ব্যবহার করা। রেলসে এটি এইরকম দেখায়:

Fault.limit(10).offset(100)

এটি এমন SQL তৈরি করে:

select * from faults limit 10 offset 100;

এখন যদি আমরা explain রান করি, তবে আমরা কিছু অদ্ভুত দেখি। স্ক্যান করা রো সংখ্যা ১১০, যা লিমিট প্লাস অফসেটের সমান।

# 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
      + ...

আপনি যদি অফসেট ১০,০০০ করলে, আপনি দেখবেন স্ক্যান করা রো সংখ্যা ১০০১০-এ যাবে, এবং কোয়ারি ৬৪ গুণ স্লো হবে।

# 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
      + ...

এটি একটি বিচলিত করানোর সমাধান দিকে নিয়ে যায়: পেজিনেট করার সময়, পেছনের পেজগুলো সামনের পেজগুলোর চেয়ে স্লো লোড হয়। যদি আমরা উপরের উদাহরণে ১০০ আইটেম পার পেজ অনুমান করি, তবে পেজ ১০০, পেজ ১-এর চেয়ে ১৩ গুণ স্লো।

তাহলে আপনি কী করবেন?

স্পষ্টভাবেই, আমি একটি পারফেক্ট সমাধান খুঁজে পাইনি। আমি শুরু করব দেখে যদি আমি ডেটাসেটের আকার কমানো পারি যাতে আমাকে ১০০ বা ১০০০ পেজ রাখতে হয় না।

আপনি যদি আপনার রেকর্ড সেট কমানো না পারেন, তবে আপনার সেরা বিট হতে পারে অফসেট/লিমিটকে where ক্লজ দিয়ে রিপ্লেস করা।

# আপনি একটি ডেট রেঞ্জ ব্যবহার করতে পারেন
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)
# ...অথবা এমনকি একটি আইডি রেঞ্জ
Fault.where("id > ? and id < ?", 100, 200)

উপসংহার

আশা করি এই আর্টিকেল আপনাকে বুঝিয়ে দিয়েছে যে আপনার খুব সতর্ক থাকা উচিত পোস্টগ্রেসের explain ফাংশন আডভান্টেজ নেওয়ায় আপনার ডিবি কোয়ারির সম্ভাব্য পারফরম্যান্স ইস্যু খুঁজে বের করতে। এমনকি সিম্পল কোয়ারিও বড় পারফরম্যান্স ইস্যু সৃষ্টি করতে পারে, তাই চেক করা দামি। :)

  1. 데이터베이스 최적화: 전문가 성능 튜닝 기법

  2. রেইলস পারফরম্যান্সে দক্ষতা: অ্যাপ্স ডায়াগনোজ ও ত্বরান্বিত করার প্রামাণিক পদ্ধতি

  3. স্বয়ংক্রিয়ভাবে এক্সেল ডেটাবেস আপডেট: ৪টি প্রমাণিত পদ্ধতি

  4. গুগল ক্রোমের এক্সটেনশন নীতিতে পরিবর্তন: পপুলার অ্যাড ব্লকারদের হুমকি