N+1 query সমস্যা ও সমাধান

তালিকার জন্য একটি query, তারপর প্রতিটি row-র জন্য আরও একটি। N+1 হলো API পেজ ধীর হওয়ার সবচেয়ে সাধারণ কারণ। কীভাবে ধরবেন ও ঠিক করবেন, জানুন।

একটি query ভেঙে অনেক ছোট query

N+1 সমস্যা হয় যখন কোড তালিকা আনতে ১টি query চালায়, তারপর প্রতিটি item-এর সম্পর্কিত data আনতে আরও N টি query চালায়।

একটি উদাহরণ

৫০টি article তাদের author-এর নামসহ দেখান:

const articles = await db.query("SELECT * FROM articles LIMIT 50");   // ১টি query
for (const a of articles) {
  a.author = await db.query("SELECT name FROM authors WHERE id = ?", a.author_id);  // ৫০টি query
}

এক পেজের জন্য ৫১টি query। ৫০০ article হলে ৫০১টি। প্রতিটি query দ্রুত, কিন্তু network round trip ও overhead জমে যায়, আর database অনেক পুনরাবৃত্ত কাজ করে।

কেন লুকিয়ে থাকে

ORM এটি সহজ করে দেয়। Loop-এ article.author ধরা property পড়ার মতো দেখায়, কিন্তু প্রতিবার lazily একটি query চালু করে। ডেভেলপারের ল্যাপটপে ৫টি row-এ এটি অদৃশ্য। প্রোডাকশনে হাজারো row-এ পেজ হামাগুড়ি দেয়।

কীভাবে ধরবেন

  • Query log: শুধু ID আলাদা অনেক একই রকম query।
  • প্রতি request-এ query গোনা টুল (Django Debug Toolbar, Rails Bullet, APM tracing)।
  • যে পেজের সময় row সংখ্যার সাথে বাড়ে।
  • মূল endpoint-এর test-এ query সংখ্যা assert করুন।

সমাধান

১. এক query-তে JOIN

SELECT a.*, au.name AS author_name
FROM articles a JOIN authors au ON au.id = a.author_id
LIMIT 50;

২. IN দিয়ে batch: মোট ২টি query

SELECT * FROM articles LIMIT 50;
SELECT * FROM authors WHERE id IN (3, 7, 9, 12);   -- দরকারি সব id একসাথে

তারপর কোডে মেলান। বেশিরভাগ ORM এটি আপনার হয়ে করে:

  • Django: select_related (join) ও prefetch_related (IN query)
  • Rails: includes
  • Sequelize / Prisma / TypeORM: include / relations
  • SQLAlchemy: joinedload, selectinload

৩. DataLoader (GraphQL): এক tick-এ চাওয়া সব ID একসাথে জমিয়ে একবারে লোড করে, প্রতি request-এ caching সহ।

৪. Cache করুন কম বদলানো সম্পর্কিত data (যেমন author-এর নাম)।

উল্টো দিকও দেখুন

অনেক "এক-থেকে-অনেক" table এক query-তে join করলে row গুণ হয়ে বিশাল ফল আসতে পারে। কখনো বেছে নেওয়া দুই-তিনটি query একটি দৈত্যাকার join-এর চেয়ে ভালো।

মোটামুটি নিয়ম

Loop-এর ভেতরে database call থাকলে থামুন এবং ভাবুন সব item-এর জন্য এটি loop-এর আগে একবারে করা যায় কি না।

মূল কথা: প্রতি request-এ query গুনুন, সম্পর্কিত data বাল্কে (join বা IN) আনুন, এবং আপনার ORM-এর eager loading সচেতনভাবে ব্যবহার করুন।


Backend ও Database সিরিজ, পর্ব ২ (performance ও operations): 2 নম্বর (মোট 6)। আগের লেখা: EXPLAIN: query plan পড়ে ধীর query খোঁজা.

আরও পড়ুন

Recursion ও backtracking সহজ উদাহরণে

Recursion সমস্যার ছোট কপি সমাধান করে সমাধান করে। অনেক interview সমস্যায় ব্যবহৃত চিন্তার ধাপ ও backtracking টেমপ্লেট শিখুন।

Interview ও DSA•৩ মিনিটে পড়া