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.