Veritabanı performans sorunlarının %90''ı yavaş sorgudan kaynaklanır. Sunucuyu büyütmeden önce sorguyu düzeltmek 100 kat daha ucuz ve etkilidir. Bu yazı EXPLAIN çıktısını okumayı, JOIN tiplerini anlamayı ve gerçek dünyada index stratejisi kurmayı öğretir.

EXPLAIN ile Başlamak

İlgili rehberler: Yazılım geliştirme süreçleri · PostgreSQL optimizasyonu · Git ileri seviye komutlar · Redis nedir, nasıl kullanılır · Docker ile deploy

Her yavaş sorgunun başına EXPLAIN ANALYZE ekleyin. Planın kökünden yaprağına doğru okuyun (alt operator''lar önce çalışır). Aranacak kötü işaretler: Seq Scan büyük tabloda, Nested Loop çok satırla, high cost, rows estimate vs actual büyük fark.

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2026-01-01'
GROUP BY u.name
ORDER BY order_count DESC
LIMIT 20;

-- Çıktıda ara:
-- 'Seq Scan on users'    → index yok / işe yaramıyor
-- 'Nested Loop'          → küçük set için OK, büyük için yavaş
-- 'Hash Join'            → genellikle iyi
-- 'Sort' high cost       → index ile skip edilebilir
-- 'rows=100 actual=50000' → planner yanılmış, ANALYZE gerekli

JOIN Tipleri

  • Nested Loop: Dış tablodan her satır için iç tabloyu tara. Küçük tablolarda hızlı, büyük-büyük''de felaket
  • Hash Join: Küçük tabloyu hash''leyip büyük tabloyu tara. Eşitlik join''lerinde en iyi
  • Merge Join: İki sıralı tabloyu paralel yürü. Order by + index varsa mükemmel

Index Stratejileri

-- 1) Composite index — WHERE + ORDER BY birlikte
CREATE INDEX idx_orders_user_date
    ON orders(user_id, created_at DESC);

-- Aşağıdaki sorgu için mükemmel
SELECT * FROM orders WHERE user_id = 42
ORDER BY created_at DESC LIMIT 10;

-- 2) Partial index — sadece gerekli satırları indexle
CREATE INDEX idx_active_users ON users(email)
WHERE active = true;
-- %5'i aktif olan kullanıcı tablosunda index boyutu %95 küçük

-- 3) Covering index — her şey index''te, tabloya gidilmez
CREATE INDEX idx_orders_cover ON orders(user_id, created_at)
INCLUDE (total_amount, status);

-- 4) Expression index
CREATE INDEX idx_email_lower ON users(LOWER(email));
-- Artık şu sorgu index kullanır:
SELECT * FROM users WHERE LOWER(email) = 'admin@x.com';
Uyarı
Her index yazma performansını düşürür (INSERT/UPDATE/DELETE her index''i günceller). 10+ index''li tablo muhtemelen fazla index''lidir; kullanılmayanları pg_stat_user_indexes ile tespit et ve sil.

Subquery vs JOIN vs CTE

-- Correlated subquery (YAVAŞ: her satır için tekrar çalışır)
SELECT u.name,
    (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;

-- JOIN ile yeniden yaz (HIZLI)
SELECT u.name, COALESCE(c.cnt, 0) AS order_count
FROM users u
LEFT JOIN (
    SELECT user_id, COUNT(*) AS cnt
    FROM orders GROUP BY user_id
) c ON c.user_id = u.id;

-- Modern: Lateral JOIN (özellikle PG)
SELECT u.name, c.cnt
FROM users u
LEFT JOIN LATERAL (
    SELECT COUNT(*) AS cnt FROM orders WHERE user_id = u.id
) c ON true;

CTE Performans Tuzağı (PostgreSQL)

PG 12''den önce CTE''ler optimization barrier olarak çalışıyordu — planner CTE''yi alt sorgu gibi birleştiremiyor, her zaman materyalize ediyordu. PG 12+ bunu düzeltti; NOT MATERIALIZED ile garantilemek iyi fikir.

-- PG 12+
WITH recent AS NOT MATERIALIZED (
    SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'
)
SELECT u.name, COUNT(*)
FROM users u JOIN recent o ON o.user_id = u.id
GROUP BY u.name;

LIMIT + ORDER BY Optimizasyonu

-- 1 milyon satırda son 10 order'ı al
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- Index olmadan: full table scan + sort → çok yavaş
CREATE INDEX idx_orders_created ON orders(created_at DESC);
-- Şimdi index'ten 10 satır okunur, biter

Keyset Pagination (Offset yerine)

-- OFFSET YAVAŞ — 100.000. sayfada 100.000 satır atlamak gerekir
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 2000000;  -- 🐢

-- Keyset (cursor-based) pagination HIZLI
SELECT * FROM orders WHERE id > 2000000 ORDER BY id LIMIT 20;  -- ⚡
-- Her sayfa sonraki için 'son id'yi cursor olarak gönder

Denormalizasyon

Bazen 3. normal form''dan feragat etmek gerekir. Sık okunan ama nadiren değişen agregat veriyi ayrı kolon/tabloda tutmak (örn: user.order_count) JOIN''i bypass eder. Tradeoff: yazma karmaşıklığı artar (trigger veya app kodunda senkronize et).

Planner İstatistikleri

-- Büyük veri yüklemesinden sonra istatistikleri yenile
ANALYZE users;
ANALYZE orders;

-- Autovacuum'ın ANALYZE eşiğini ayarla (yüksek yazım olan tablolar için)
ALTER TABLE orders SET (autovacuum_analyze_scale_factor = 0.02);

-- Hangi tablolarda analyze gerekli?
SELECT schemaname, relname, n_mod_since_analyze
FROM pg_stat_user_tables
WHERE n_mod_since_analyze > 10000
ORDER BY n_mod_since_analyze DESC;

Slow Query Log

-- postgresql.conf
log_min_duration_statement = 500  -- 500ms üstü logla
auto_explain.log_min_duration = 500
auto_explain.log_analyze = on

-- pg_stat_statements extension (en çok kullanılan top 10 sorgu)
CREATE EXTENSION pg_stat_statements;
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

Modern Yazılım Geliştirme ve DevOps Pratikleri

Profesyonel yazılım geliştirme süreci üç pillar üzerine kuruludur: kaynak kontrolü (Git + GitHub/GitLab pull request akışı, code review zorunlu), CI/CD pipeline (otomatik test + lint + build + deploy), ve gözlemlenebilirlik (Sentry/Datadog/Grafana ile log, metric, trace toplama). Test piramidi (unit > integration > e2e) ile kod kalitesini garantilemek, mikroservis mimarisinde Docker container ve Kubernetes orkestrasyonu kullanmak, REST veya GraphQL API tasarımında OpenAPI/GraphQL Schema sözleşmesi tutmak modern standardlardır. Yazılım geliştirme yaşam döngüsü boyunca (gereksinim → tasarım → implementasyon → test → deploy → bakım) Agile/Scrum sprintleri 1-2 hafta, DevOps takımları sürekli teslim (continuous delivery) prensibiyle çalışır.

Veritabanı performans denetimi

Yavaş sorgularınızı EXPLAIN/ANALYZE ile inceleyip optimize etmemiz için bize yazın

WhatsApp