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 হালকা রাখে।