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 সংখ্যা শত শত থেকে মাত্র কয়েকটিতে নামিয়ে আনা সম্ভব।

Share