Əsas məzmuna keçin

N+1 Problem

N+1 problem, ORM (Object-Relational Mapping) istifadə edən tətbiqlərdə rast gəlinən ən ümumi performans problemlərindən biridir. Bu problem əsasən lazy loading mexanizmi ilə bağlıdır və gözlənildiyindən daha çox database sorğusuna səbəb olur.

N+1 Problem Nədir?

N+1 problem belə adlanır çünki:

  • 1 sorğu əsas məlumatları əldə etmək üçün
  • N əlavə sorğu hər bir əsas məlumat üçün əlaqəli məlumatları əldə etmək üçün

Bu o deməkdir ki, N sayda record üçün N+1 sayda database sorğusu icra edilir.

N+1 Problem Nümunəsi

Tutaq ki, bizim müştərilər və onların sifarişləri var:

PostgreSQL cədvəl strukturları
-- Müştərilər cədvəli
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);

-- Sifarişlər cədvəli
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
order_date TIMESTAMP DEFAULT NOW(),
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending'
);

-- Test məlumatları
INSERT INTO customers (name, email) VALUES
('Əli Məmmədov', 'ali@example.com'),
('Ayşə Həsənova', 'ayshe@example.com'),
('Rəşad Quliyev', 'rashad@example.com');

INSERT INTO orders (customer_id, order_date, total_amount) VALUES
(1, NOW() - INTERVAL '2 days', 150.50),
(1, NOW() - INTERVAL '1 day', 89.99),
(2, NOW() - INTERVAL '3 days', 234.75),
(3, NOW() - INTERVAL '1 week', 99.00);

Problemli Kod Nümunəsi (Java/JPA)

// 1. Bütün müştəriləri əldə et (1 sorğu)
List<Customer> customers = customerRepository.findAll();

// 2. Hər müştəri üçün sifarişləri əldə et (N sorğu)
for (Customer customer : customers) {
List<Order> orders = customer.getOrders(); // Lazy loading
System.out.println(customer.getName() + " has " + orders.size() + " orders");
}

Nə Baş Verir?

İcra edilən SQL sorğuları
-- 1. Əsas sorğu (1)
SELECT * FROM customers;

-- 2. Hər müştəri üçün ayrıca sorğu (N)
SELECT * FROM orders WHERE customer_id = 1;
SELECT * FROM orders WHERE customer_id = 2;
SELECT * FROM orders WHERE customer_id = 3;
-- ... və s.

3 müştəri üçün 4 sorğu (1+3) icra edildi. Əgər 1000 müştəri olsaydı, 1001 sorğu icra ediləcəkdi!

N+1 Probleminin Həll Yolları

1. Eager Loading (Həvəsli Yükləmə)

JPA/Hibernate:

@Query("SELECT c FROM Customer c JOIN FETCH c.orders")
List<Customer> findAllWithOrders();

PostgreSQL sorğusu:

SELECT c.*, o.*
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;

2. Batch Loading

Hibernate Configuration:

@BatchSize(size = 10)
@OneToMany(mappedBy = "customer")
private List<Order> orders;

3. Projection istifadəsi

Custom DTO ilə optimallaşdırma
// DTO
public class CustomerOrderSummary {
private String customerName;
private Long orderCount;
private BigDecimal totalAmount;

// constructors, getters, setters
}

// Repository
@Query("""
SELECT new com.example.CustomerOrderSummary(
c.name,
COUNT(o.id),
COALESCE(SUM(o.totalAmount), 0)
)
FROM Customer c
LEFT JOIN c.orders o
GROUP BY c.id, c.name
""")
List<CustomerOrderSummary> getCustomerOrderSummaries();

4. PostgreSQL-də Advanced Həllər

JSON Aggregation:

SELECT 
c.id,
c.name,
c.email,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'id', o.id,
'order_date', o.order_date,
'total_amount', o.total_amount,
'status', o.status
)
) FILTER (WHERE o.id IS NOT NULL),
'[]'::json
) as orders
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name, c.email;

Window Functions istifadəsi:

SELECT DISTINCT
c.id,
c.name,
c.email,
COUNT(o.id) OVER (PARTITION BY c.id) as order_count,
SUM(o.total_amount) OVER (PARTITION BY c.id) as total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;

N+1 Probleminin Aşkarlanması

1. SQL Logging

PostgreSQL-də log aktivləşdirmə:

-- Bütün sorğuları log et
ALTER SYSTEM SET log_statement = 'all';

-- Yalnız yavaş sorğuları log et
ALTER SYSTEM SET log_min_duration_statement = 100;

SELECT pg_reload_conf();

2. Application Level Monitoring

Hibernate Statistics:

Statistics stats = sessionFactory.getStatistics();
stats.setStatisticsEnabled(true);

// Sorğu sayını yoxla
long queryCount = stats.getQueryExecutionCount();
System.out.println("Executed queries: " + queryCount);

3. Database Performance Monitoring

PostgreSQL sorğu statistikaları
-- Ən çox çağırılan sorğular
SELECT
query,
calls,
total_time,
mean_time,
rows
FROM pg_stat_statements
WHERE calls > 100
ORDER BY calls DESC;

-- Aktiv bağlantılar və sorğular
SELECT
pid,
usename,
application_name,
client_addr,
state,
query
FROM pg_stat_activity
WHERE state = 'active';

Best Practices

1. ORM Konfigurasiayası

// Lazy loading-i məcburi et
@OneToMany(fetch = FetchType.LAZY, mappedBy = "customer")
private List<Order> orders;

// Batch size təyin et
@BatchSize(size = 25)
@OneToMany(mappedBy = "customer")
private List<Order> orders;

2. Query Optimization Strategiyaları

  • Single Query Approach: Mümkün olduqda tək sorğu istifadə edin
  • Pagination: Böyük nəticələr üçün səhifəbəndi istifadə edin
  • Caching: Tez-tez istifadə olunan məlumatları cache edin
  • Indexing: Uyğun indekslər yaradın

3. PostgreSQL Specific Optimizations

-- Partial index istifadəsi
CREATE INDEX idx_active_orders ON orders (customer_id)
WHERE status = 'active';

-- Composite index
CREATE INDEX idx_customer_order_date ON orders (customer_id, order_date DESC);

-- EXPLAIN ANALYZE istifadə edərək performans analizi
EXPLAIN ANALYZE
SELECT c.name, COUNT(o.id) as order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name;

Nəticə

N+1 problem verilənlər bazası performansına ciddi təsir edə bilər. Düzgün ORM konfigurasiayası, eager loading strategiyaları və PostgreSQL-in güclü xüsusiyyətlərindən istifadə edərək bu problemi həll etmək mümkündür. Həmişə sorğu sayını monitor edin və lazım olduqda optimallaşdırma tətbiq edin.