PostgreSQL Performans Optimizasyonu: Sorgu Hızlandırma
PostgreSQL'de Performans Sorunlarını Anlamak
PostgreSQL, dünya genelinde en çok tercih edilen açık kaynaklı ilişkisel veritabanı yönetim sistemlerinden biridir. Ancak veritabanı büyüdükçe ve sorgu karmaşıklığı arttıkça performans sorunları kaçınılmaz hale gelir. Bu yazıda, PostgreSQL sorgularınızı hızlandırmak için uygulayabileceğiniz pratik teknikleri detaylı şekilde ele alacağız.
Performans optimizasyonuna başlamadan önce, sorunun kaynağını doğru tespit etmek kritik öneme sahiptir. PostgreSQL'in sunduğu araçları kullanarak yavaş sorguları belirlemek, optimizasyon sürecinin ilk adımıdır.
EXPLAIN ve EXPLAIN ANALYZE ile Sorgu Analizi
PostgreSQL'de bir sorgunun nasıl çalıştığını anlamanın en etkili yolu EXPLAIN komutunu kullanmaktır. Bu komut, sorgu planlayıcısının (query planner) oluşturduğu yürütme planını gösterir.
EXPLAIN ANALYZE SELECT u.ad, u.soyad, s.toplam_tutar
FROM kullanicilar u
JOIN siparisler s ON u.id = s.kullanici_id
WHERE s.tarih > '2025-01-01'
ORDER BY s.toplam_tutar DESC
LIMIT 100;
EXPLAIN ANALYZE çıktısında dikkat etmeniz gereken temel metrikler şunlardır:
- Seq Scan (Sequential Scan): Tablonun tamamının taranması. Büyük tablolarda performans düşürücüdür.
- Index Scan: İndeks kullanılarak yapılan tarama. Genellikle çok daha hızlıdır.
- Nested Loop / Hash Join / Merge Join: Tablo birleştirme yöntemleri. Veri boyutuna göre en uygun olanı otomatik seçilir.
- Actual Time: Gerçek yürütme süresi (milisaniye cinsinden).
- Rows: Tahmini ve gerçek satır sayısı arasındaki fark, istatistiklerin güncelliğini gösterir.
İndeks Stratejileri
Doğru indeks kullanımı, sorgu performansını onlarca hatta yüzlerce kat artırabilir. Ancak gereksiz indeksler de yazma performansını olumsuz etkiler. İndeks stratejinizi dikkatli planlamanız gerekir.
B-Tree İndeksler
PostgreSQL'in varsayılan indeks türüdür. Eşitlik (=) ve aralık (<, >, BETWEEN) sorgularında mükemmel performans sağlar:
-- Tek sütun indeks
CREATE INDEX idx_siparisler_tarih ON siparisler (tarih);
-- Çok sütunlu (composite) indeks
CREATE INDEX idx_siparisler_kullanici_tarih
ON siparisler (kullanici_id, tarih DESC);
Önemli: Çok sütunlu indekslerde sütun sırası kritiktir. İndeks, soldan sağa doğru kullanılır. Yukarıdaki örnekte sadece tarih üzerinden yapılan bir sorgu bu indeksten yararlanamaz, ancak sadece kullanici_id üzerinden yapılan bir sorgu yararlanabilir.
Kısmi (Partial) İndeksler
Tablonun yalnızca belirli bir alt kümesini indeksleyerek hem disk alanından hem de yazma performansından tasarruf sağlar:
-- Sadece aktif kullanıcıları indeksle
CREATE INDEX idx_kullanicilar_aktif_email
ON kullanicilar (email)
WHERE aktif = true;
-- Sadece son 1 yılın siparişlerini indeksle
CREATE INDEX idx_siparisler_son_yil
ON siparisler (kullanici_id, toplam_tutar)
WHERE tarih > CURRENT_DATE - INTERVAL '1 year';
GIN ve GiST İndeksler
JSONB sütunları veya tam metin araması (full-text search) için GIN indeksler idealdir:
-- JSONB sütunu için GIN indeks
CREATE INDEX idx_urunler_ozellikler ON urunler USING GIN (ozellikler);
-- Tam metin araması için
CREATE INDEX idx_makaleler_icerik ON makaleler USING GIN (to_tsvector('turkish', icerik));
Sorgu Yazım Optimizasyonları
SELECT * Kullanmaktan Kaçının
Yalnızca ihtiyacınız olan sütunları seçmek, hem ağ trafiğini hem de bellek kullanımını azaltır:
-- Kötü
SELECT * FROM siparisler WHERE kullanici_id = 42;
-- İyi
SELECT id, toplam_tutar, tarih FROM siparisler WHERE kullanici_id = 42;
EXISTS ve IN Karşılaştırması
Alt sorgu sonuç kümesi büyük olduğunda EXISTS, küçük olduğunda IN genellikle daha iyi performans gösterir:
-- Büyük alt sorgu sonuçlarında EXISTS tercih edin
SELECT u.ad, u.soyad
FROM kullanicilar u
WHERE EXISTS (
SELECT 1 FROM siparisler s
WHERE s.kullanici_id = u.id
AND s.toplam_tutar > 1000
);
Pagination için OFFSET Yerine Keyset Pagination
OFFSET kullanımı, büyük veri kümelerinde ciddi performans sorunlarına yol açar çünkü veritabanı atlanan tüm satırları yine de okur:
-- Yavaş: OFFSET ile sayfalama (sayfa 1000 için 999.000 satır okunur)
SELECT * FROM siparisler ORDER BY id LIMIT 100 OFFSET 999900;
-- Hızlı: Keyset (cursor-based) sayfalama
SELECT * FROM siparisler
WHERE id > 999900
ORDER BY id
LIMIT 100;
Yapılandırma (Configuration) Ayarları
PostgreSQL'in varsayılan yapılandırması, düşük kaynaklı sistemlerde bile çalışacak şekilde oldukça muhafazakâr ayarlanmıştır. Sunucunuzun donanım kapasitesine göre aşağıdaki parametreleri optimize edin:
- shared_buffers: Toplam RAM'in yaklaşık %25'i olarak ayarlayın. Örneğin 16 GB RAM için
shared_buffers = 4GB. - effective_cache_size: Toplam RAM'in %50-75'i. İşletim sistemi disk önbelleğini de hesaba katar:
effective_cache_size = 12GB. - work_mem: Sıralama ve hash işlemleri için ayrılan bellek. Dikkatli ayarlayın çünkü her bağlantı bu kadar bellek kullanabilir:
work_mem = 64MB. - maintenance_work_mem: VACUUM ve CREATE INDEX gibi bakım işlemleri için:
maintenance_work_mem = 512MB. - random_page_cost: SSD kullanıyorsanız varsayılan 4.0 değerini
1.1olarak düşürün. Bu, planlayıcının indeks kullanımını tercih etmesini sağlar.
VACUUM ve İstatistik Güncellemesi
PostgreSQL'in MVCC (Multi-Version Concurrency Control) mimarisi nedeniyle, silinen veya güncellenen satırlar hemen temizlenmez. VACUUM işlemi bu "ölü" satırları temizler:
-- Belirli bir tabloyu vacuum'la
VACUUM ANALYZE siparisler;
-- Tüm veritabanını vacuum'la (dikkatli kullanın)
VACUUM ANALYZE;
ANALYZE komutu ise tablo istatistiklerini günceller. Sorgu planlayıcısı bu istatistiklere dayanarak en verimli yürütme planını seçer. İstatistikler güncel değilse, planlayıcı yanlış kararlar alabilir. Büyük veri yüklemeleri veya toplu silme işlemlerinden sonra mutlaka ANALYZE çalıştırın.
Autovacuum Ayarlarını Optimize Edin
Yoğun yazma yapılan tablolar için autovacuum parametrelerini tablo bazında ayarlayabilirsiniz:
ALTER TABLE siparisler SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_cost_delay = 10
);
Bağlantı Havuzlama (Connection Pooling)
Her yeni PostgreSQL bağlantısı bir işletim sistemi süreci oluşturur ve bu işlem kaynak yoğundur. PgBouncer gibi bir bağlantı havuzlayıcı kullanarak bağlantı sayısını kontrol altında tutabilirsiniz. Özellikle yüzlerce eşzamanlı bağlantı alan uygulamalarda bu kritik bir optimizasyondur.
Yavaş Sorguları Tespit Etmek
pg_stat_statements eklentisi, tüm sorguların yürütme istatistiklerini toplar ve en çok kaynak tüketen sorguları tespit etmenizi sağlar:
-- Eklentiyi etkinleştir
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- En yavaş 10 sorguyu listele
SELECT query,
calls,
round(total_exec_time::numeric, 2) AS toplam_sure_ms,
round(mean_exec_time::numeric, 2) AS ortalama_sure_ms,
rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
Ayrıca postgresql.conf dosyasında log_min_duration_statement parametresini ayarlayarak belirli bir eşiği aşan sorguları loglamanız önerilir:
-- 500ms üzeri sorguları logla
log_min_duration_statement = 500
Sonuç
PostgreSQL performans optimizasyonu, tek seferlik bir işlem değil sürekli bir süreçtir. Veritabanınız büyüdükçe ve kullanım desenleri değiştikçe optimizasyon stratejilerinizi gözden geçirmeniz gerekir. Yukarıdaki teknikleri sistematik bir şekilde uygulayarak — önce EXPLAIN ANALYZE ile darboğazı tespit edin, ardından doğru indeks ve sorgu optimizasyonlarını uygulayın, son olarak yapılandırma ayarlarınızı sunucu kapasitenize göre ince ayar yapın. Bu yaklaşım, sorgularınızda dramatik hız artışları elde etmenizi sağlayacaktır.