N+1 কোয়েরি সমস্যা ও Eager Loading
এই পাঠে যা শিখবেন
- N+1 কোয়েরি সমস্যা ঠিক কীভাবে ঘটে — একটি লুপের ভেতর প্রতিটি parent-এর জন্য আলাদা কোয়েরি
- eager loading কীভাবে সব children একসাথে ব্যাচড আনে সমস্যাটি সমাধান করে
- একটি সত্যিকারের কোয়েরি-কাউন্টার দিয়ে দুই পদ্ধতির প্রকৃত কোয়েরি সংখ্যা মেপে তুলনা করা
- N বাড়লে কেন নাইভ পদ্ধতির কোয়েরি সংখ্যা রৈখিকভাবে ($O(N)$) বাড়ে, কিন্তু eager loading-এর সংখ্যা স্থির থাকে
১ · N+1 কোয়েরি সমস্যা কী
N+1 কোয়েরি সমস্যাN+1 Query ProblemN-টি parent রেকর্ডের প্রতিটির সম্পর্কিত children আলাদাভাবে লোড করার ফলে মোট N+1টি কোয়েরি চালানো — যেখানে ব্যাচ করে মাত্র ২টি কোয়েরিতেই একই ডেটা পাওয়া সম্ভব ছিল।
সাধারণত ঘটে ORM-এর "লেজি লোডিং" (lazy loading) ডিফল্ট আচরণের কারণে — L30-এর Post.author-এর
মতো একটি রিলেশনশিপ অ্যাক্সেসর প্রতিবার কল হলেই একটি নতুন লুকআপ ("কোয়েরি") চালায়। এটি একটি রো-র জন্য ঠিক
আছে, কিন্তু N-টি parent-এর একটি তালিকার উপর লুপ করে প্রতিটির জন্য আলাদা করে এটি কল করলে কোয়েরি সংখ্যা দ্রুত
বেড়ে যায়।
২ · কোয়েরি-কাউন্টার দিয়ে বাস্তবে যাচাই
নিচের কোড সেলে একটি সত্যিকারের query_count কাউন্টার তৈরি করা হয়েছে — প্রতিবার কোনো "কোয়েরি"
ফাংশন কল হলে এটি একবার বাড়ে। ১০টি parent রেকর্ড (প্রতিটির ২-৪টি child) নিয়ে নাইভ ও eager, দুই পদ্ধতিই চালানো
হয়েছে এবং শেষে কাউন্টারের প্রকৃত মান প্রিন্ট ও যাচাই করা হয়েছে।
import random
random.seed(7) # প্রতিবার একই ফলাফল পুনরুৎপাদনযোগ্য রাখতে
# ---------- ডেটা প্রস্তুত করা: ১০ parent, প্রতিটির ২-৪টি child ----------
parents_table = [{"id": i, "name": f"Parent-{i}"} for i in range(1, 11)]
children_table = []
next_child_id = 1
for parent in parents_table:
n_children = random.choice([2, 3, 4])
for _ in range(n_children):
children_table.append({
"id": next_child_id,
"parent_id": parent["id"],
"name": f"Child-{next_child_id}",
})
next_child_id += 1
# ---------- "কোয়েরি" ফাংশন -- প্রতিটি কল একটি বাস্তব ডেটাবেস রাউন্ড-ট্রিপের সমতুল্য ----------
query_count = 0
def query_get_all_parents():
global query_count
query_count += 1
return list(parents_table)
def query_get_children_for_parent(parent_id):
global query_count
query_count += 1
return [c for c in children_table if c["parent_id"] == parent_id]
def query_get_all_children():
global query_count
query_count += 1
return list(children_table)
# ---------- নাইভ (N+1) পদ্ধতি ----------
query_count = 0
parents = query_get_all_parents() # ১টি কোয়েরি
naive_result = {}
for parent in parents: # প্রতিটি parent-এর জন্য আলাদা কোয়েরি -- মোট N-টি
naive_result[parent["id"]] = query_get_children_for_parent(parent["id"])
naive_query_count = query_count
print(f"নাইভ (N+1) পদ্ধতি -- মোট কোয়েরি সংখ্যা: {naive_query_count}")
assert naive_query_count == 1 + len(parents) # ১ (parent তালিকা) + ১০ (প্রতি parent-এর children) = ১১
# ---------- Eager loading পদ্ধতি ----------
query_count = 0
parents = query_get_all_parents() # ১টি কোয়েরি
all_children = query_get_all_children() # আরেকটি কোয়েরি -- সবার children একসাথে ব্যাচে
children_by_parent = {}
for child in all_children: # এই লুপ কোনো নতুন কোয়েরি চালায় না -- ইতিমধ্যে আনা ডেটার উপর কাজ
children_by_parent.setdefault(child["parent_id"], []).append(child)
eager_result = {parent["id"]: children_by_parent.get(parent["id"], []) for parent in parents}
eager_query_count = query_count
print(f"Eager loading পদ্ধতি -- মোট কোয়েরি সংখ্যা: {eager_query_count}")
assert eager_query_count == 2 # ১ (parent তালিকা) + ১ (সব children এক ব্যাচে) = ২, N নির্বিশেষে
# ---------- দুই পদ্ধতির ডেটা ফলাফল অভিন্ন কি না যাচাই ----------
same_data = naive_result == eager_result
print(f"\nদুই পদ্ধতির ফলাফল ডেটা হুবহু অভিন্ন: {same_data}")
assert same_data
print(f"\n--- সারাংশ (১০টি parent-এর জন্য) ---")
print(f"নাইভ (N+1): {naive_query_count} কোয়েরি")
print(f"Eager loading: {eager_query_count} কোয়েরি")
print(f"পার্থক্য: {naive_query_count - eager_query_count} কম কোয়েরি eager loading-এ")
query_count একটি সাধারণ গ্লোবাল কাউন্টার, কিন্তু এটি সত্যিকারভাবে প্রতিটি "কোয়েরি"
ফাংশন কলের সাথে বাড়ে — কোনো হার্ডকোড করা সংখ্যা নয়। নাইভ পদ্ধতিতে for parent in parents লুপের
ভেতর query_get_children_for_parent() ঠিক ১০ বার কল হয় (১০টি parent থাকায়), প্রতিটি কলে
কাউন্টার ১ বাড়ে — তার সাথে শুরুর query_get_all_parents()-এর ১ যোগ করলে মোট ১১। Eager
পদ্ধতিতে for child in all_children লুপ শুধু ইতিমধ্যে আনা all_children
লিস্টের উপর কাজ করে, কোনো নতুন "কোয়েরি" ফাংশন কল করে না — তাই কাউন্টার আটকে থাকে ২-এ।
N+1 কোয়েরি সমস্যা ঘটে যখন একটি রিলেশনশিপ N-বার, একবার করে প্রতিটি parent-এর জন্য, লোড করা হয় — মোট কোয়েরি সংখ্যা $O(N)$ হারে বাড়ে। Eager loading সব children একটি ব্যাচড কোয়েরিতে একসাথে এনে এই সংখ্যা $O(1)$-এ (parent সংখ্যা নির্বিশেষে স্থির) নামিয়ে আনে। উপরের উদাহরণে ১০টি parent-এর জন্য পার্থক্য ছিল ১১ বনাম ২ কোয়েরি — বাস্তব অ্যাপ্লিকেশনে হাজার হাজার parent থাকলে এই পার্থক্যই একটি পেজকে কয়েক মিলিসেকেন্ডে বনাম কয়েক সেকেন্ডে লোড হওয়ার পার্থক্য গড়ে দিতে পারে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
যদি parent সংখ্যা ১০-এর বদলে ১০০ হতো (children-এর সংখ্যা যাই হোক না কেন), তাহলে
naive_query_count ও eager_query_count-এর মান কী হতো?
naive_query_count হতো 1 + 100 = 101 — কারণ এটি সরাসরি parent সংখ্যার উপর
নির্ভরশীল (প্রতিটি parent-এর জন্য ঠিক একটি অতিরিক্ত কোয়েরি)। কিন্তু eager_query_count তখনো
2-ই থাকত — কারণ eager loading-এর কোয়েরি সংখ্যা parent সংখ্যার উপর নির্ভর করে না, সবসময় ঠিক
দুইটি (parent তালিকা + সব children এক ব্যাচে)। এটাই দেখায় eager loading-এর সুবিধা N বাড়ার সাথে সাথে
আরও বেশি গুরুত্বপূর্ণ হয়ে ওঠে।
প্র ০২
eager loading পদ্ধতিতে for child in all_children লুপটি কেন কোনো নতুন "কোয়েরি" গণনা
করায় না, যদিও এখানেও একটি লুপ আছে?
কারণ এই লুপটি এমন একটি লিস্টের (all_children) উপর কাজ করছে যা ইতিমধ্যে একবারের
query_get_all_children() কল দিয়ে সম্পূর্ণভাবে আনা হয়ে গেছে — লুপটি শুধু ইতিমধ্যে মেমরিতে
থাকা ডেটাকে parent_id অনুযায়ী পুনর্গঠন (গ্রুপিং) করছে, কোনো নতুন ডেটাবেস-স্টাইল কল করছে
না। নাইভ পদ্ধতির লুপের সাথে মূল পার্থক্য এখানেই — নাইভ লুপের প্রতিটি ধাপে একটি নতুন
query_...() ফাংশন কল হয়, eager-এর লুপে হয় না।
প্র ০৩
naive_result == eager_result সমতা যাচাই কেন গুরুত্বপূর্ণ — শুধু কোয়েরি সংখ্যা কম হলেই
কি যথেষ্ট না?
না — একটি অপটিমাইজেশন কোয়েরি সংখ্যা কমালেও যদি ভুল বা অসম্পূর্ণ ডেটা ফেরত দেয়, তাহলে সেটি একটি
পারফরম্যান্স উন্নতি নয়, একটি নতুন বাগ। naive_result == eager_result যাচাই নিশ্চিত করে
eager loading শুধু দ্রুতই নয়, বরং ঠিক একই ডেটা (প্রতিটি parent-এর ঠিক সঠিক children) ফেরত দেয় — অর্থাৎ
এটি একটি সঠিক অপটিমাইজেশন, ভুল শর্টকাট নয়।
অনুশীলন
-
চিন্তা করুন: যদি প্রতিটি parent-এর children-এর পাশাপাশি প্রতিটি child-এর জন্য আবার একটি
আলাদা "grandchild" রিলেশনশিপও নাইভভাবে (লুপের ভেতর) লোড করা হতো, তাহলে মোট কোয়েরি সংখ্যা আনুমানিক কেমন
দেখাত (parent সংখ্যা N ও গড় children সংখ্যা C হলে)?
আনুমানিক
1 + N + (N × C)— ১ (parent তালিকা) + N (প্রতি parent-এর children) + N×C (প্রতিটি child-এর জন্য আলাদা grandchild কোয়েরি, মোট children সংখ্যা প্রায় N×C)। এটিকে "N+1" এর একটি গভীরতর সংস্করণ বলা যায় — প্রতিটি অতিরিক্ত স্তরের নেস্টেড রিলেশনশিপ নাইভভাবে লোড করলে কোয়েরি সংখ্যা আরও দ্রুত বাড়ে, যা বাস্তব অ্যাপ্লিকেশনে এই সমস্যাটিকে আরও ক্ষতিকর করে তোলে। -
পরীক্ষা করুন: উপরের কোড সেলে
parents_table-এর সংখ্যা ১০ থেকে বাড়িয়ে ৩০ করুন (range(1, 11)-কেrange(1, 31)-এ বদলে), সেল আবার চালান, এবংnaive_query_countওeager_query_count-এর নতুন মান লক্ষ্য করুন।naive_query_countএখন31হবে (1 + 30), কিন্তুeager_query_countতখনো2-ই থাকবে — parent সংখ্যা তিনগুণ বাড়ানোর পরেও eager loading-এর কোয়েরি সংখ্যা একদমই বদলায়নি। এটি হাতে-কলমে প্রমাণ করে eager loading-এর কোয়েরি সংখ্যা $O(1)$ (parent সংখ্যা নির্বিশেষে স্থির), যেখানে নাইভ পদ্ধতির সংখ্যা $O(N)$ (parent সংখ্যার সাথে রৈখিকভাবে বাড়ে)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ L33 সেশন-ভিত্তিক অথেন্টিকেশন — মডিউল ৮-এ অথেন্টিকেশন ও অথোরাইজেশন শুরু হচ্ছে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
-
Database Management Systems কোর্স সহোদর কোর্স
JOIN-ভিত্তিক কোয়েরি লিখে একই ডেটা একটি মাত্র SQL কোয়েরিতে আনার প্র্যাকটিস সেই কোর্সে শেখানো হয়েছে। - সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।