কম্পিউটার

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

আমার মনে আছে প্রথমবার আমি রেলের অ্যাক্টিভ রেকর্ড দেখেছিলাম। এটি একটি উদ্ঘাটন ছিল. এটি 2005 সালে ফিরে এসেছিল এবং আমি একটি পিএইচপি অ্যাপের জন্য এসকিউএল কোয়েরি হ্যান্ড-কোডিং করছিলাম। হঠাৎ, ডাটাবেস ব্যবহার করা একটি ক্লান্তিকর কাজ থেকে সহজ এবং - আমি বলতে সাহসী - মজা হতে চলে গেছে।

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

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

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

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

আপনার কাছে যথেষ্ট ছোট ডেটাসেট থাকলে প্রতিটি DB ক্যোয়ারী কার্যকর হয়। তাই কার্যক্ষমতার জন্য সত্যিই অনুভূতি পেতে, আমাদের একটি উৎপাদন-আকারের ডাটাবেসের বিরুদ্ধে মানদণ্ড প্রয়োজন। আমাদের উদাহরণে, আমরা faults নামে একটি টেবিল ব্যবহার করতে যাচ্ছি প্রায় 22,000 রেকর্ড সহ।

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

আপাতত আমরা শুধুমাত্র "পরিকল্পনা সারি" এবং "প্রকৃত সারি" তে ফোকাস করতে যাচ্ছি।

  • পরিকল্পনা সারি সবচেয়ে খারাপ ক্ষেত্রে, আপনার প্রশ্নের উত্তর দেওয়ার জন্য DB কে কতগুলি সারি লুপ করতে হবে
  • প্রকৃত সারি যখন এটি ক্যোয়ারীটি চালায়, তখন কতগুলি সারি ডিবি প্রকৃতপক্ষে লুপ করেছিল?

যদি "পরিকল্পনা সারি" 1 হয়, যেমনটি উপরে আছে, তাহলে ক্যোয়ারীটি সম্ভবত ভালোভাবে স্কেল করতে যাচ্ছে। যদি "পরিকল্পনা সারি" ডাটাবেসের সারির সংখ্যার সমান হয়, তাহলে তার মানে কোয়েরিটি একটি "সম্পূর্ণ টেবিল স্ক্যান" করতে যাচ্ছে এবং ভালোভাবে স্কেল করতে যাচ্ছে না।

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

গণনা

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

Total Faults <%= Fault.count %>

এর ফলে এসকিউএল এমন কিছু দেখায়:

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)

বাহ! আমাদের সাধারণ গণনা ক্যোয়ারী 22,265 সারিগুলির উপর লুপ করছে — পুরো টেবিল! পোস্টগ্রেসে, গণনা সর্বদা পুরো রেকর্ড সেটের উপর লুপ করে।

আপনি where যোগ করে রেকর্ড সেটের আকার কমাতে পারেন ক্যোয়ারী শর্তাবলী. আপনার প্রয়োজনীয়তার উপর নির্ভর করে, কর্মক্ষমতা গ্রহণযোগ্য যেখানে আপনি যথেষ্ট কম আকার পেতে পারেন।

এই সমস্যাটির চারপাশে একমাত্র অন্য উপায় হল আপনার গণনার মানগুলি ক্যাশে করা। আপনি এটি সেট আপ করলে রেলগুলি আপনার জন্য এটি করতে পারে:

belongs_to :project, :counter_cache => true

কোয়েরি কোনো রেকর্ড ফেরত দেয় কিনা তা পরীক্ষা করার সময় আরেকটি বিকল্প পাওয়া যায়। Users.count > 0 এর পরিবর্তে , Users.exists? চেষ্টা করুন . ফলস্বরূপ ক্যোয়ারী অনেক বেশি কার্যকরী। (পাঠক গেরি শ'কে ধন্যবাদ এটা আমার দিকে তুলে ধরার জন্য।)

বাছাই

সূচী পাতা. প্রায় প্রতিটি অ্যাপে অন্তত একটি থাকে। আপনি ডাটাবেস থেকে নতুন 20টি রেকর্ড টানুন এবং সেগুলি প্রদর্শন করুন। এর চেয়ে সহজ কি হতে পারে?

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

@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 সমস্ত 22,265 সারি বাছাই করছে। নো বুয়েনো!

ডিফল্টরূপে, আপনার এসকিউএল-এর প্রতিটি "অর্ডার বাই" ক্লজের কারণে রেকর্ড সেটটি ঠিক তখনই, রিয়েল-টাইমে সাজানো হয়। কোন ক্যাশিং আছে. তোমাকে বাঁচাতে কোন জাদু নেই।

সমাধান হল সূচী ব্যবহার করা। এই ধরনের সাধারণ ক্ষেত্রে, create_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 এর মাধ্যমে সেগুলি চালানো একটি ভাল ধারণা। . এটা তাড়াতাড়ি এবং প্রায়ই করুন. আপনি খুঁজে পেতে পারেন যে ক্যোয়ারীতে কিছু সাধারণ পরিবর্তন পোস্টগ্রেসের জন্য সাজানোর জন্য সূচক ব্যবহার করা অসম্ভব করে তুলেছে।

সীমা এবং অফসেট

আমাদের সূচী পৃষ্ঠাগুলিতে আমরা খুব কমই ডাটাবেসের প্রতিটি আইটেম অন্তর্ভুক্ত করি। পরিবর্তে আমরা একটি সময়ে শুধুমাত্র 10 বা 30 বা 50টি আইটেম দেখাই, পৃষ্ঠা সংখ্যা করি। এটি করার সবচেয়ে সাধারণ উপায় হল limit ব্যবহার করা এবং offset একসাথে রেলে এটি এইরকম দেখায়:

Fault.limit(10).offset(100)

এটি এসকিউএল তৈরি করে যা এইরকম দেখায়:

select * from faults limit 10 offset 100;

এখন যদি আমরা ব্যাখ্যা চালাই, আমরা কিছু অদ্ভুত দেখতে. স্ক্যান করা সারির সংখ্যা 110, অফসেটের সীমার সমান।

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

আপনি যদি অফসেটটিকে 10,000-এ পরিবর্তন করেন তবে আপনি দেখতে পাবেন যে স্ক্যান করা সারিগুলির সংখ্যা 10010-এ চলে যাবে এবং কোয়েরিটি 64x ধীর হবে৷

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

এটি একটি বিরক্তিকর উপসংহারের দিকে নিয়ে যায়:পেজিন করার সময়, পরবর্তী পৃষ্ঠাগুলি আগের পৃষ্ঠাগুলির তুলনায় ধীরে ধীরে লোড হয়৷ যদি আমরা উপরের উদাহরণে প্রতি পৃষ্ঠায় 100 টি আইটেম ধরে নিই, পৃষ্ঠা 100 পৃষ্ঠা 1 এর চেয়ে 13 গুণ ধীর।

তো তুমি কি কর?

সত্যি বলতে, আমি একটি নিখুঁত সমাধান খুঁজে পেতে সক্ষম হয় নি। আমি ডেটাসেটের আকার কমাতে পারি কিনা তা দেখে শুরু করব তাই শুরু করার জন্য আমার 100 বা 1000 পৃষ্ঠা থাকতে হবে না।

আপনি যদি আপনার রেকর্ড সেট কমাতে অক্ষম হন, আপনার সেরা বাজি হতে পারে অফসেট/সীমা যেখানে ধারা রয়েছে তার সাথে প্রতিস্থাপন করা।

# You could use a date range
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)

# ...or even an id range
Fault.where("id > ? and id < ?", 100, 200)

উপসংহার

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


  1. ডাটাবেস কর্মক্ষমতা টিউনিং

  2. আপনার রেল অ্যাপের কর্মক্ষমতা বোঝার একটি নতুন উপায়

  3. কিভাবে একটি ডাটাবেস তৈরি করবেন যা এক্সেল এ স্বয়ংক্রিয়ভাবে আপডেট হয়

  4. Google Chrome-এর নতুন এক্সটেনশন প্ল্যান অ্যাড ব্লকারদের মেরে ফেলতে পারে