N+1 problem
Spring Boot (বিশেষ করে JPA/Hibernate ব্যবহার করলে) একটি সাধারণ কিন্তু মারাত্মক পারফরম্যান্স সমস্যা দেখা যায় — যেটিকে বলা হয় N+1 Problem। এই সমস্যা ডাটাবেজে অপ্রয়োজনীয়ভাবে অসংখ্য query পাঠিয়ে অ্যাপ্লিকেশনকে ধীর করে ফেলে। চলো ধাপে ধাপে বুঝে নিই
N+1 Problem কী?
ধরা যাক, আমাদের দুটি entity আছে: Author এবং Book — যেখানে একজন লেখকের অনেকগুলো বই থাকতে পারে।
@Entity
public class Author {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "author")
private List<Book> books;
}
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String title;
@ManyToOne
private Author author;
}
এখন আমরা যদি সব লেখক ও তাদের বই দেখাতে চাই
List<Author> authors = authorRepository.findAll();
for (Author author : authors) {
System.out.println(author.getName() + " -> " + author.getBooks().size());
}
কী ঘটে এখানে?
Hibernate প্রথমে Author entity এর জন্য ১টি query চালাবে
SELECT * FROM author;
তারপর প্রতিটি Author এর জন্য তার books ফেচ করতে আলাদা query চালাবে
SELECT * FROM book WHERE author_id = 1;
SELECT * FROM book WHERE author_id = 2;
SELECT * FROM book WHERE author_id = 3;
...
এভাবে যদি ১০ জন author থাকে →
মোট query = 1 (Author) + 10 (Book per author) = 11 queries 😨
এটাই N+1 Problem
(১টি মূল query + N টি child query)।
কেন এটা সমস্যা?
- ডাটাবেজে অপ্রয়োজনীয় round trip হয়
- Network latency বেড়ে যায়
- Memory ও CPU usage বেড়ে যায়
- Response time নাটকীয়ভাবে বাড়ে
- এটি ছোট dataset এ বোঝা যায় না, কিন্তু বড় প্রোডাকশন সিস্টেমে এটি মারাত্মক।
এই সমস্যা কেন হয়?
এর মূল কারণ হলো — JPA এর Lazy Loading behavior। @OneToMany বা @ManyToOne রিলেশনগুলো default ভাবে LAZY লোড হয়। অর্থাৎ parent entity লোড হলে child entity তখনও লোড হয় না — যতক্ষণ না আমরা সেটি access করি। Hibernate তখন প্রতিবার নতুন query পাঠায়।
সমাধান কীভাবে করা যায়?
সমাধান ১: fetch = FetchType.EAGER ব্যবহার
@OneToMany(mappedBy = "author", fetch = FetchType.EAGER)
private List<Book> books;
এতে Author এর সাথে সাথে Book গুলোও লোড হবে। তবে সতর্কতা EAGER ব্যবহার করলে সব সময় child ডেটা লোড হবে — এমনকি দরকার না হলেও। ফলে এটি সব জায়গায় ব্যবহার করা ঠিক না।
সমাধান ২: JPQL Fetch Join ব্যবহার (Best Practice)
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
এখন Hibernate একটি মাত্র query চালাবে
SELECT a.*, b.*
FROM author a
JOIN book b ON a.id = b.author_id;
এতে N+1 সমস্যা থাকবে না, কারণ সব ডেটা একসাথে লোড হবে।
সমাধান ৩: EntityGraph ব্যবহার
@EntityGraph(attributePaths = {"books"})
@Query("SELECT a FROM Author a")
List<Author> findAllWithBooksGraph();
EntityGraph Hibernate কে বলে দেয় — “এই property গুলো eager ভাবে লোড করো”। এটি declarative এবং clean approach।
সমাধান ৪: Batch Size টিউন করা
Hibernate এর @BatchSize annotation ব্যবহার করা যায়
@OneToMany(mappedBy = "author")
@BatchSize(size = 10)
private List<Book> books;
এখন Hibernate প্রতিবার ১টি করে নয়, একসাথে ১০টি child entity লোড করবে। এটি query সংখ্যা কিছুটা কমায়, তবে পুরোপুরি N+1 প্রতিরোধ করে না।
Hibernate Logging দিয়ে কিভাবে N+1 ধরবে?
তুমি নিচের property গুলো application.yml এ যুক্ত করো 👇
spring:
jpa:
properties:
hibernate:
format_sql: true
show_sql: true
এখন কনসোলে সব SQL দেখতে পাবে।
যদি বারবার SELECT ... WHERE author_id = ? দেখা যায়, বুঝে নেবে N+1 সমস্যা হয়েছে 😅
Real-life Example (Service Layer)
@Service
public class AuthorService {
@Autowired
private AuthorRepository authorRepository;
public List<Author> getAuthorsWithBooks() {
return authorRepository.findAllWithBooks();
}
}
Controller থেকে একবার কল করলে সব ডেটা একসাথে চলে আসবে, N+1 query হবে না।
Performance তুলনা
| কনফিগারেশন | Query সংখ্যা | পারফরম্যান্স |
|---|---|---|
| Lazy loading | 11 | ❌ ধীর |
| Fetch Join | 1 | ✅ দ্রুত |
| EntityGraph | 1 | ✅ দ্রুত |
| BatchSize | 2–3 | ☑️ মাঝারি উন্নতি |
Bonus Tip — DTO Projection
Entity না এনে, সরাসরি DTO তে ডেটা ম্যাপ করে আনলে Hibernate সম্পর্কিত Lazy সমস্যাও এড়ানো যায়।
@Query("SELECT new com.example.dto.AuthorBookDTO(a.name, b.title) " +
"FROM Author a JOIN a.books b")
List<AuthorBookDTO> getAuthorBookDTOs();
এটি সব ডেটা একসাথে লোড করে এবং Hibernate context হালকা রাখে।