N+1 Problem কী?
N+1 Problem হলো ORM (Object-Relational Mapping) ফ্রেমওয়ার্কে একটি সাধারণ Performance সমস্যা। এতে একটি List লোড করতে ১টি Query চালানো হয় (১), এরপর সেই List-এর প্রতিটি Item-এর জন্য আলাদা আলাদা Query চালানো হয় (N)। মোট Query = N+1।
N+1 Problem-এর উদাহরণ
সমস্যাযুক্ত কোড:
@Entity
public class Author {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}
@Entity
public class Book {
@Id
private Long id;
private String title;
@ManyToOne
private Author author;
}
// N+1 সমস্যার উদাহরণ
@Service
public class AuthorService {
@Transactional
public void printAllAuthorsWithBooks() {
List<Author> authors = authorRepository.findAll(); // Query 1: সব Author লোড
for (Author author : authors) {
// প্রতিটি Author-এর জন্য আলাদা Query! (N টি Query)
System.out.println(author.getName() + ": " + author.getBooks().size());
}
}
}
Log-এ দেখা যাবে:
SELECT * FROM authors; -- ১টি Query
SELECT * FROM books WHERE author_id = 1; -- Author 1 এর জন্য
SELECT * FROM books WHERE author_id = 2; -- Author 2 এর জন্য
SELECT * FROM books WHERE author_id = 3; -- Author 3 এর জন্য
... (১০০ জন Author হলে ১০১টি Query!)
কেন N+1 Problem হয়?
FetchType.LAZY ব্যবহার করলে Related Entity সাথে সাথে লোড হয় না — যখন প্রথমবার Access করা হয় তখন লোড হয়। তাই author.getBooks() Call করলে ডেটাবেজে নতুন Query যায়।
সমাধান ১: JOIN FETCH (JPQL)
@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
// JOIN FETCH দিয়ে একটি Query-তেই সব ডেটা লোড
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
// LEFT JOIN FETCH — Books নেই এমন Author-ও আনবে
@Query("SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books")
List<Author> findAllWithBooksIncludingEmpty();
}
// সমাধান:
public void printAllAuthorsWithBooks() {
// একটিমাত্র Query-তে সব ডেটা লোড
List<Author> authors = authorRepository.findAllWithBooks();
for (Author author : authors) {
// আর কোনো Extra Query নেই!
System.out.println(author.getName() + ": " + author.getBooks().size());
}
}
Generated SQL:
SELECT a.*, b.* FROM authors a
JOIN books b ON a.id = b.author_id; -- একটিমাত্র Query!
সমাধান ২: @EntityGraph
@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
// @EntityGraph দিয়ে JOIN FETCH এর মতো কাজ
@EntityGraph(attributePaths = {"books"})
List<Author> findAll();
// নির্দিষ্ট Author খুঁজতে
@EntityGraph(attributePaths = {"books"})
Optional<Author> findById(Long id);
}
সমাধান ৩: @BatchSize (Hibernate)
@Entity
public class Author {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 50) // একসাথে ৫০টি Author-এর Books লোড করবে
private List<Book> books;
}
Generated SQL:
SELECT * FROM authors; -- Query 1
SELECT * FROM books WHERE author_id IN (1,2,...,50); -- Query 2 (৫০টি একসাথে)
SELECT * FROM books WHERE author_id IN (51,52,...,100); -- Query 3
-- মোট: ১ + (N/50) টি Query
সমাধান ৪: Fetch Join + Distinct (Pagination সহ)
Pagination ব্যবহার করলে JOIN FETCH সমস্যা তৈরি করে। সেক্ষেত্রে:
@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
// Pagination-এর জন্য আলাদা Count Query
@Query(
value = "SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books",
countQuery = "SELECT COUNT(DISTINCT a) FROM Author a"
)
Page<Author> findAllWithBooks(Pageable pageable);
}
N+1 Problem শনাক্ত করার উপায়
Hibernate Statistics
# application.properties
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
# Log Output:
Session Metrics {
1234567 nanoseconds spent acquiring 1 JDBC connections;
101 flushes as part of pre-partial-flush dirty checking;
101 dirty checking on flush;
101 queries executed to database; ← N+1 সমস্যার প্রমাণ!
}
Hibernate SQL Logging
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
সম্পূর্ণ বাস্তব উদাহরণ
সমস্যাযুক্ত:
@Service
public class OrderService {
// N+1 সমস্যা!
public List<String> getOrderSummaries() {
List<Order> orders = orderRepository.findAll(); // Query 1
return orders.stream()
.map(order -> {
// প্রতিটি Order-এর জন্য আলাদা Customer Query!
String customerName = order.getCustomer().getName();
return "Order #" + order.getId() + " by " + customerName;
})
.collect(Collectors.toList());
}
}
সমাধান:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();
// Pagination সহ
@Query(
value = "SELECT o FROM Order o JOIN FETCH o.customer",
countQuery = "SELECT COUNT(o) FROM Order o"
)
Page<Order> findAllWithCustomer(Pageable pageable);
}
@Service
public class OrderService {
// সমাধান: একটি Query-তে সব ডেটা
public List<String> getOrderSummaries() {
List<Order> orders = orderRepository.findAllWithCustomer(); // একটি Query
return orders.stream()
.map(order -> {
String customerName = order.getCustomer().getName(); // Extra Query নেই!
return "Order #" + order.getId() + " by " + customerName;
})
.collect(Collectors.toList());
}
}
সমাধান পদ্ধতির তুলনা
| পদ্ধতি | কখন ব্যবহার | সুবিধা |
|---|---|---|
JOIN FETCH |
JPQL Query-তে | সহজ, পরিষ্কার |
@EntityGraph |
Method-এ annotation | Repository পরিষ্কার থাকে |
@BatchSize |
Large collection | Automatic, কম কোড |
| DTO Projection | শুধু কিছু ফিল্ড দরকার | সবচেয়ে দ্রুত |
সংক্ষেপে
| বিষয় | বিবরণ |
|---|---|
| N+1 Problem | N টি Extra Query যায় Related Data-র জন্য |
| কারণ | FetchType.LAZY এবং Loop-এ Lazy Loading |
JOIN FETCH |
JPQL-এ সব ডেটা একসাথে আনে |
@EntityGraph |
Annotation দিয়ে Eager Fetch |
@BatchSize |
Batch করে Query কমায় |
| Statistics | Hibernate Metrics দিয়ে N+1 শনাক্ত |
উপসংহার
N+1 Problem Spring Boot/JPA অ্যাপ্লিকেশনে সবচেয়ে সাধারণ Performance সমস্যাগুলির একটি। JOIN FETCH বা @EntityGraph ব্যবহার করে এই সমস্যা সমাধান করুন। Hibernate Statistics সক্রিয় রেখে Query সংখ্যা Monitor করুন। সঠিকভাবে সমাধান করলে একটি অ্যাপ্লিকেশনের Query সংখ্যা শত শত থেকে মাত্র কয়েকটিতে নামিয়ে আনা সম্ভব।