সোহান গ্রামে বড় হয়েছে, তার সবকিছুই গ্রামেই রয়েছে। হঠাৎ একদিন তার শহরে যাওয়ার দরকার হলো। তো সে তার বাবাকে জানালো কথাটা। তার বাবা রাজি হয়ে গেলো। তার বাবা তাকে জানালো, তার এক ফুফাতো ভাই নাকি ঐ শহরেই থাকে, কোনো একটা কোম্পানিতে কাজ করে। সোহান তার সাথে কথা বলে চলে গেলো। শহরে তার cousin-এর কাছে গিয়েই উঠলো। তার কাজ করতে লাগবে ২-৩ দিন, বাকি ২ দিন একটু ঘোরাফেরা করে back করবে। যাওয়ার পরে সোহান তার কাজ দ্রুতই শেষ করে ফেললো। সোহানের একটু curiosity জাগলো—তার ভাই কী কাজ করে আর এই কাজগুলো কেমন। সে তার সাথে এই কথা বললো, প্রথমে রাজি হয়নি কিন্তু অনেক জোরাজুরি করার পরে রাজি হলো।
তার ফুফাতো ভাই আসলে একটা কোম্পানিতে part-time data entry-র কাজ করে। প্রথমে সে তার boss-কে বিষয়টা জানালে তার boss রাজি হয়নি, কিন্তু একটু manage করে কোনো রকম নিয়ে গেলো। সোহান বসে আছে, আর ভাই computer-এ কাজ করছে। কিছু সময় পরে, ১ জন এসে বললো, "আমাদের অফিসের employee-দের list-গুলো দেন তো।" তার ভাই দিলো। Then আবার বললো, "এই সব employee-দের sales data-গুলো দেন।" তার ভাই সুন্দর করে সবার profile print করে দিয়ে দিলো।
সোহান এখানে একটা জিনিস খেয়াল করলো। ঐ লোক তো একবারেই তো সব বলতে পারতেন। প্রথমে ১বারেই সব employee-দের list পেয়ে গেলেন। এরপরে প্রত্যেক employee-র জন্য একটা করে কাজ করা লাগলো। এখানে কতগুলো operation দরকার হচ্ছে? প্রথমে ১টা action, then employee যদি ১০০ হয় তাহলে ১ + ১০০। ১০০০ হলে ১ + ১০০০। অর্থাৎ N + 1 action।
ওপরের যে problem-টা সোহান বুঝতে পারলো এটাকেই Database-এর context-এ N+1 problem বলে।
সমস্যাটা কী?
মনে করুন আপনাকে ১০০ জন user এবং তাদের post fetch করে দেখাতে হবে।
আপনি কী করতে পারেন? প্রথমে সবগুলা user fetch করবেন ১ বারে। Then ঐ users-গুলাকে iterate করে সেখান থেকে user id নিয়ে সবার জন্য posts fetch করবেন। নিচের example-টা দেখুন।
// প্রথমে সব user fetch করলাম - ১টা query
const users = await User.findAll();
// তারপর প্রতিটা user-এর posts - ১০০টা query!
for (const user of users) {
user.posts = await Post.findAll({ where: { userId: user.id } });
}
মোট query = ১ + ১০০ = ১০১টা query! বেশি হলে আরও বেশি। এখানে ১টা query এবং বাকি query operation-এর জন্য N বলা হচ্ছে। অর্থাৎ N + 1 query run হচ্ছে। এটাই হচ্ছে N+1 Problem।
কেন এটা এত বড় সমস্যা?
আগেই একটা বিষয় বলেছি, যে database query execute করতে হলে সেখানে একটা overhead থাকে (connections, network latency etc)।
মনে করুন আপনাকে ১০০ জন user এবং তাদের post fetch করে দেখাতে হবে। তাহলে এখানে মোট ১০১ query execute হবে। এখন প্রত্যেকটা query যদি 5ms করেও নেয় তাহলে কতটুকু সময় খরচ হবে? 5 \ 101 = 505ms = 0.505 seconds। এটা তো কোনো issue না। কিন্তু user যদি ১০০০ (১ হাজার) হয়ে যায় বা ১০,০০০ (১০ হাজার) হয়ে যায়, বা ১০০,০০০ (১ লাখ) হয়ে যায় তাহলে? তখন এই execution time হবে (1000 \ 5 + 1) ms = 5005ms = 5.005 seconds। (10000 \ 5 + 1) ms = 50005ms = 50.005 seconds। (100000 \ 5 + 1) ms = 500005ms = 500.005 seconds = 8.33 minutes! এই execution time exponential হারে increase হবে...
এখন আপনার কাছে আমার question, একটা request-এর জন্য যদি এত time লাগে তাহলে, আপনার user কি আপনার application use করবে? আপনার user-রা কি use করবে?
কেউই wait করবে না। সরকারি application হলে ব্যাপারটা আলাদা।
Solutions: Eager Loading / JOIN
এই problem-টা address করার জন্য eager loading / JOIN use করা হয়। সহজভাবে বললে JOIN, দুইটা table-কে একসাথে করে। JOIN একটা important জিনিস। অন্য কোনো সময় JOIN নিয়ে আলোচনা হবে।
নিচের example-গুলো দেখুন:
// Sequelize
const users = await User.findAll({
include: [{ model: Post }],
});
// Prisma
const users = await prisma.user.findMany({
include: { posts: true },
});
এখানে মনে হতে পারে, এখানে তো JOIN use-ই করলাম না। Sequelize ক্ষেত্রে এটি behind the scenes-এ SQL JOIN রান করে। তবে Prisma-তে include ব্যবহার করলে এটি সরাসরি JOIN না করে ব্যাকগ্রাউন্ডে দুটি আলাদা কুয়েরি চালিয়ে ডেটা নিয়ে আসে যার আউটপুট দিনশেষে JOIN-এর মতোই কাজ করে।
বি:দ্র: বিভিন্ন ল্যাঙ্গুয়েজ-এ solution ভিন্ন হতে পারে।
একটা বিষয় মনে রাখা উচিত N+1 problem এর solution শুধু মাত্র join-ই না। বিভিন্ন ভাবে এটা address করা যেতে পারে। যেমন টা prisma করেছে।
কীভাবে Detect করবেন?
এটা মূলত একটা performance-related problem, initially আপনার কোনো problem হবে না। Server crash-ও করবে না। এই জন্য এই problem-টা detect করা একটু difficult। এটা তখন বোঝা যাবে যখন data-র size বেড়ে যাবে। এটা address করতে Query Logging feature enable করতে হবে। Query-গুলো analyze করতে হবে। এখানে যদি, একটা request-এ একাধিক (৩-৪ টারও বেশি) similar type-এর query দেখেন তাহলে বুঝবেন এখানে N+1 problem আছে।
Query Logging অন করলে টার্মিনালে তাকালেই দেখতে পাবেন একই কুয়েরি বারবার এক্সিকিউট হচ্ছে:
-- প্রথমে ১টি কুয়েরি
Executing: SELECT * FROM users;
-- এরপর ইউজারের সংখ্যার ওপর ভিত্তি করে বারবার কুয়েরি (N times)
Executing: SELECT * FROM posts WHERE userId = 1;
Executing: SELECT * FROM posts WHERE userId = 2;
Executing: SELECT * FROM posts WHERE userId = 3;
...
Executing: SELECT * FROM posts WHERE userId = 100;
টার্মিনালে যদি একই ধরনের কুয়েরির এমন বন্যা দেখতে পান, নিশ্চিত বুঝবেন সেখানে N+1 Problem রয়েছে।
GraphQL এবং মাইক্রোসার্ভিস আর্কিটেকচারে সমাধান: DataLoader 📦
GraphQL বা মাইক্রোসার্ভিসের মতো আর্কিটেকচারে সবসময় সরাসরি SQL JOIN করা সম্ভব হয় না, কারণ ফিল্ডের ডাটা আলাদা আলাদা Resolver ফাংশন থেকে আসে। এখানে সমাধান দেয় Facebook-এর তৈরি DataLoader। DataLoader দুটি প্রধান কাজ করে:
- Batching: এক ফ্রেমের ভেতরে আসা সব
userId-কে সংগ্রহ করে একসাথে একটি ব্যাচ কুয়েরি চালায় (SELECT * FROM posts WHERE userId IN (1, 2, 3...))। - Caching: একই রিকোয়েস্টে একই ইউজারের ডাটা দ্বিতীয়বার লাগলে তা মেমোরি থেকে রিটার্ন করে।
import DataLoader from "dataloader";
// ১. Batch Loading Function তৈরি: সব IDs একসাথে রিসিভ করে একটিমাত্র SQL query চালায়
const userLoader = new DataLoader<number, User>(async (userIds) => {
// SELECT * FROM users WHERE id IN (1, 2, 3...)
const users = await db.user.findMany({
where: { id: { in: [...userIds] } },
});
// DataLoader-এর গুরুত্বপূর্ণ নিয়ম: keys যে অর্ডারে এসেছিল, results-ও ঠিক সেই অর্ডারে ম্যাপ করতে হবে
const userMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => userMap.get(id)!);
});
// ২. Resolver-এ ব্যবহার:
const resolvers = {
Post: {
author: async (post: Post) => {
// প্রতিটা পোস্টের জন্য আলাদা SQL কুয়েরি না চালিয়ে loader-এ জমা করবে
return await userLoader.load(post.authorId);
},
},
};
বটম লাইন
N + 1 problem-টা ছোট size-এর জন্য তেমন problem হয় না। এই জন্য beginner-রা easily এই N+1 problem avoid করতে পারে না। আবার অনেক senior developer-রাও এই problem-টা করে থাকে। এই জন্য logging বা query monitor করা উচিত। N+1 problem solve করলে আপনার application speed একেবারেই আকাশে উঠে যাবে। ORM use করার সময় একটু সাবধানে use করার পরামর্শ থাকলো।
আপনার project-এ N+1 খুঁজে পেয়েছেন কখনো? কতটা query কমাতে পেরেছিলেন? কমেন্টে শেয়ার করুন! 👇