Veritabanı performansı, büyüyen uygulamaların en sık karşılaştığı darboğazdır. PostgreSQL'in güçlü araçlarını kullanarak saniyeler süren sorgular milisaniyelere indirilebilir. Bu makalede gerçek optimizasyon tekniklerini adım adım inceliyoruz.
EXPLAIN ANALYZE ile Başla
Kör optimizasyon yapmayın — önce sorgunun ne yaptığını anlayın.
// SQL //
EXPLAIN ANALYZESELECT m.baslik, k.ad, COUNT(y.id) AS yorum_sayisiFROM makaleler mJOIN kullanicilar k ON k.id = m.yazar_idLEFT JOIN yorumlar y ON y.makale_id = m.idWHERE m.yayinlandi = true AND m.olusturuldu > NOW() - INTERVAL '30 days'GROUP BY m.id, m.baslik, k.id, k.adORDER BY yorum_sayisi DESCLIMIT 20;
Çıktıyı okuma rehberi:
// PLAINTEXT //
Limit (cost=2341.52..2341.57 rows=20) (actual time=89.432..89.445 rows=20) -> Sort (cost=2341.52..2348.27 rows=2700) (actual time=89.428..89.431) -> HashAggregate (cost=2167.32..2194.82 rows=2700) -> Hash Left Join (cost=892.14..2099.82) Hash Cond: (y.makale_id = m.id) -> Hash Join (cost=45.20..1152.60) ← SEQ SCAN var mı? -> Seq Scan on makaleler m ← KÖTÜ işaret Filter: (yayinlandi AND olusturuldu > ...) Rows Removed by Filter: 12840Planning Time: 2.1 msExecution Time: 89.4 ms ← Hedef: < 10ms
Seq Scan (sıralı tarama), index yokluğunu gösterir. Index Scan veya Bitmap Index Scan olmalı.
Index Stratejisi
// SQL //
-- 1. Tek sütun index — WHERE koşullarınaCREATE INDEX CONCURRENTLY idx_makaleler_yayinlandiON makaleler(yayinlandi);CREATE INDEX CONCURRENTLY idx_makaleler_olusturulduON makaleler(olusturuldu DESC);-- 2. Composite index — birden fazla WHERE koşulu birlikte kullanılıyorsa-- Sıra önemli: eşitlik koşulları önce, range koşulları sonaCREATE INDEX CONCURRENTLY idx_makaleler_yayinlandi_tarihON makaleler(yayinlandi, olusturuldu DESC);-- 3. Partial index — WHERE koşulunun daraltılmış versiyonu-- Sadece yayınlananları indeksle, diğerleri büyüklüğü artırırCREATE INDEX CONCURRENTLY idx_makaleler_aktif_tarihON makaleler(olusturuldu DESC)WHERE yayinlandi = true;-- 4. Covering index — JOIN'ı ortadan kaldırır-- SELECT'te kullanılan tüm alanlar index içindeCREATE INDEX CONCURRENTLY idx_makaleler_listeleON makaleler(olusturuldu DESC, yayinlandi)INCLUDE (baslik, slug, yazar_id);
N+1 Problemi
// SQL //
-- KÖTÜ — her makale için ayrı yazar sorgusu (ORM'lerin tuzağı)-- Kod: makaleler.forEach(m => m.yazar)-- DB'ye giden: SELECT * FROM kullanicilar WHERE id = '1'-- SELECT * FROM kullanicilar WHERE id = '2'-- ... (100 makale = 101 sorgu!)-- İYİ — tek JOIN ile hepsini çekSELECT m.id, m.baslik, m.slug, k.id AS yazar_id, k.ad AS yazar_adi, k.avatar AS yazar_avatarFROM makaleler mINNER JOIN kullanicilar k ON k.id = m.yazar_idWHERE m.yayinlandi = trueORDER BY m.olusturuldu DESCLIMIT 20;-- Prisma'da: include ile N+1'i önleconst makaleler = await prisma.makale.findMany({ include: { yazar: { select: { ad: true, avatar: true } }, // JOIN kullanır etiketler: true, }, take: 20, orderBy: { olusturuldu: 'desc' }, where: { yayinlandi: true },});
Sayfalama Optimizasyonu
// SQL //
-- KÖTÜ — büyük offset'te performans katastrofik düşer-- OFFSET 10000 = 10000 satırı tarayıp atSELECT * FROM makalelerWHERE yayinlandi = trueORDER BY olusturuldu DESCLIMIT 20 OFFSET 10000; -- Yavaş!-- İYİ — Cursor-based pagination (keyset pagination)-- İlk sayfaSELECT id, baslik, slug, olusturulduFROM makalelerWHERE yayinlandi = trueORDER BY olusturuldu DESC, id DESCLIMIT 20;-- Sonraki sayfa — son elemanın cursor'ını kullanSELECT id, baslik, slug, olusturulduFROM makalelerWHERE yayinlandi = true AND (olusturuldu, id) < ('2025-06-01 12:00:00', 'abc-last-id')ORDER BY olusturuldu DESC, id DESCLIMIT 20;
Full-Text Search
// SQL //
-- GIN index ile Türkçe full-text searchALTER TABLE makaleler ADD COLUMN arama_vektoru tsvector;-- Otomatik güncelleme için triggerCREATE FUNCTION makaleler_arama_guncelle() RETURNS trigger AS $$BEGIN NEW.arama_vektoru := setweight(to_tsvector('simple', coalesce(NEW.baslik, '')), 'A') || setweight(to_tsvector('simple', coalesce(NEW.excerpt, '')), 'B') || setweight(to_tsvector('simple', coalesce(NEW.icerik, '')), 'C'); RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER makaleler_arama_triggerBEFORE INSERT OR UPDATE ON makalelerFOR EACH ROW EXECUTE FUNCTION makaleler_arama_guncelle();-- GIN indexCREATE INDEX idx_makaleler_fts ON makaleler USING GIN(arama_vektoru);-- Arama sorgusuSELECT baslik, excerpt, ts_rank(arama_vektoru, sorgu) AS skorFROM makaleler, to_tsquery('simple', 'javascript & react') sorguWHERE arama_vektoru @@ sorgu AND yayinlandi = trueORDER BY skor DESCLIMIT 10;
Aggregation Optimizasyonu
// SQL //
-- Yavaş: her sorguda COUNT hesaplaSELECT COUNT(*) FROM yorumlar WHERE makale_id = $1;-- Hızlı: denormalize yorum sayısını tablo üzerinde tutALTER TABLE makaleler ADD COLUMN yorum_sayisi INTEGER NOT NULL DEFAULT 0;-- Trigger ile senkronize etCREATE FUNCTION yorum_sayisi_guncelle() RETURNS trigger AS $$BEGIN IF TG_OP = 'INSERT' THEN UPDATE makaleler SET yorum_sayisi = yorum_sayisi + 1 WHERE id = NEW.makale_id; ELSIF TG_OP = 'DELETE' THEN UPDATE makaleler SET yorum_sayisi = yorum_sayisi - 1 WHERE id = OLD.makale_id; END IF; RETURN NULL;END;$$ LANGUAGE plpgsql;CREATE TRIGGER yorum_sayaciAFTER INSERT OR DELETE ON yorumlarFOR EACH ROW EXECUTE FUNCTION yorum_sayisi_guncelle();-- Artık JOIN'sız sorguSELECT baslik, yorum_sayisi FROM makaleler WHERE yayinlandi = trueORDER BY yorum_sayisi DESCLIMIT 10;-- Execution time: 2ms (önceki: 450ms)
Connection Pooling
// TYPESCRIPT //
// Prisma ile bağlantı havuzu — veritabanını aşırı yükleme// prisma/schema.prismadatasource db { provider = "postgresql" url = env("DATABASE_URL") // PgBouncer veya Prisma Accelerate için}// .envDATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10"// PgBouncer bağlantı URL'siDATABASE_URL="postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true"
Yavaş Sorgu Loglama
// SQL //
-- postgresql.conf — yavaş sorguları loglalog_min_duration_statement = 100 -- 100ms üstü sorguları loglalog_line_prefix = '%t [%p]: [%l-1] 'log_checkpoints = onlog_connections = on-- pg_stat_statements ile analizSELECT LEFT(query, 80) AS sorgu, calls AS cagri_sayisi, ROUND(total_exec_time::numeric, 2) AS toplam_ms, ROUND(mean_exec_time::numeric, 2) AS ortalama_ms, rows AS etkilenen_satirFROM pg_stat_statementsORDER BY mean_exec_time DESCLIMIT 20;
Sonuç
Veritabanı optimizasyonu sistematik bir süreçtir: EXPLAIN ANALYZE ile ölçün, Seq Scan'leri tespit edin, doğru index ekleyin. N+1 probleminden kaçının, cursor-based pagination kullanın ve sık okunan aggregation'ları denormalize edin. Bu adımlar, üretim veritabanınızı dakikalar içinde onlarca kat hızlandırabilir.