İçeriğe geç
·12 dk okuma

PostgreSQL Sorgu Optimizasyonu: Yavaştan Milisaniye Altına

Üretim veritabanlarında kullandığım PostgreSQL optimizasyon teknikleri. EXPLAIN ANALYZE, index stratejileri, sorgu yeniden yazımları ve sorgu sürelerini saniyelerden mikrosaniyeye düşüren değişiklikler.

Paylaş:X / TwitterLinkedIn

Geçen ay gece 2'de bir Slack mesajı aldım: "Dashboard zaman aşımına uğruyor." Üretim sunucusuna SSH ile bağlandım, pg_stat_activity'yi açtım ve 12 sonuç döndürmek için 14 milyon satır tarayan tek bir sorgu buldum. Çözüm, oluşturması 30 saniye süren kısmi bir index'ti. Sorgu 4.2 saniyeden 0.3 milisaniyeye düştü.

PostgreSQL performansındaki mesele bu. Sorunlar neredeyse hiçbir zaman egzotik değil. Kaçırılmış index'ler, kötü istatistikler, tablo 10.000 satırken mantıklı olan ama şimdi 10 milyon satırı olan sorgular. Çözümler genellikle basit — nereye bakacağını bildiğinde.

Bu yazı, üretim veritabanlarını çalıştırarak öğrendiğim her şeyi içeriyor. Pratik olmayan teori yok. Buradaki her teknik, gerçek sistemlerde bana gerçek zaman kazandırdı.

Hata Ayıklama Zihniyeti: Tahmin Etme, Ölç#

Geliştiricilerin yavaş sorgularla yaptığı en büyük hata tahmin etmek. "Belki o sütuna index gerekiyor." "Belki JOIN yavaş." "Belki daha fazla RAM eklemeliyiz."

Tahmin etmeyi bırak. PostgreSQL, herhangi bir veritabanının en iyi sorgu analizcilerinden birine sahip. Kullan.

EXPLAIN — Plan#

Düz EXPLAIN, sorguyu gerçekten çalıştırmadan PostgreSQL'in ne yapmayı planladığını gösterir:

sql
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
                                    QUERY PLAN
---------------------------------------------------------------------------
 Index Scan using idx_orders_customer_id on orders  (cost=0.43..8.45 rows=1 width=128)
   Index Cond: (customer_id = 42)

Bu sana planı — bir index taraması — ve tahmini maliyeti söyler. Ama bu bir tahmin. PostgreSQL satır sayısı hakkında tamamen yanılıyor olabilir. Tahminin "1 satır" olduğu ama gerçek sonucun 50.000 satır olduğu durumlar gördüm. Bu tür bir yanlış tahmin, korkunç plan seçimlerine yol açar.

Sorguyu gerçekten çalıştırmadan plana hızlı bir bakış istediğinde düz EXPLAIN kullan. Bu, veri değiştirecek bir DELETE'i veya çalışması 30 dakika süren bir sorguyu analiz ederken önemlidir.

EXPLAIN ANALYZE — Gerçek#

sql
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
                                    QUERY PLAN
---------------------------------------------------------------------------
 Index Scan using idx_orders_customer_id on orders  (cost=0.43..8.45 rows=1 width=128)
                                                    (actual time=0.021..0.023 rows=1 loops=1)
   Index Cond: (customer_id = 42)
 Planning Time: 0.085 ms
 Execution Time: 0.041 ms

Şimdi gerçek çalışma süresini ve gerçek satır sayısını görüyorsun. Gerçek hata ayıklama burada başlıyor. rows=1 (tahmini) ile rows=1 (gerçek) karşılaştır — eşleşiyor, yani planlayıcı iyi bir karar vermiş. Eşleşmediğinde, ilk ipucun bu.

Uyarı: EXPLAIN ANALYZE sorguyu gerçekten çalıştırır. Bir UPDATE veya DELETE analiz ediyorsan, bir transaction'a sar:

sql
BEGIN;
EXPLAIN ANALYZE DELETE FROM sessions WHERE expires_at < now();
ROLLBACK;

EXPLAIN (ANALYZE, BUFFERS) — Tam Resim#

Zamanımın %90'ında gerçekte kullandığım bu:

sql
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at > '2026-01-01'
  AND o.status = 'completed';
                                    QUERY PLAN
---------------------------------------------------------------------------
 Hash Join  (cost=425.00..1893.25 rows=2150 width=48)
            (actual time=2.341..15.892 rows=2347 loops=1)
   Hash Cond: (o.customer_id = c.id)
   Buffers: shared hit=312 read=45
   ->  Bitmap Heap Scan on orders o  (cost=12.50..1450.75 rows=2150 width=32)
                                     (actual time=0.512..12.105 rows=2347 loops=1)
         Recheck Cond: (created_at > '2026-01-01'::date)
         Filter: (status = 'completed')
         Rows Removed by Filter: 893
         Heap Blocks: exact=189
         Buffers: shared hit=195 read=45
         ->  Bitmap Index Scan on idx_orders_created_at  (cost=0.00..11.96 rows=3043 width=0)
                                                         (actual time=0.287..0.287 rows=3240 loops=1)
               Index Cond: (created_at > '2026-01-01'::date)
               Buffers: shared hit=12
   ->  Hash  (cost=287.50..287.50 rows=10000 width=24)
             (actual time=1.753..1.753 rows=10000 loops=1)
         Buffers: shared hit=117
         ->  Seq Scan on customers c  (cost=0.00..287.50 rows=10000 width=24)
                                      (actual time=0.008..0.892 rows=10000 loops=1)
               Buffers: shared hit=117
 Planning Time: 0.215 ms
 Execution Time: 16.147 ms

BUFFERS çıktısı kritik. shared hit=312 demek 312 sayfa buffer önbelleğinden (RAM) geldi. read=45 demek 45 sayfa diskten okunmak zorunda kaldı. hit'e göre çok fazla read görüyorsan, shared_buffers çok küçük olabilir veya tablo önbelleğe sığamayacak kadar büyük.

Ayrıca planı explain.dalibo.com adresine görselleştirme için yapıştırmam gerektiğinde FORMAT JSON kullanıyorum. Ağaç görünümü karmaşık planları çok daha kolay okunur kılar:

sql
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT ...;

Gerçekten Kullandığım EXPLAIN Seçenekleri#

İşte herhangi bir yavaş sorgu araştırması için ilk çalıştırdığım tam tanılama sorgusu:

sql
EXPLAIN (ANALYZE, BUFFERS, TIMING, VERBOSE)
SELECT ...;
  • TIMING — düğüm başına zamanlama (ANALYZE ile varsayılan olarak açık, ama ben açıkça belirtiyorum)
  • VERBOSE — çıktı sütun listelerini ve şema-nitelikli tablo adlarını gösterir

Sorgu Planlarını Bir Dedektif Gibi Okumak#

Her sorgu planı bir ağaçtır. PostgreSQL onu aşağıdan yukarıya okur: en derin girintili düğümler önce çalışır ve sonuçları yukarı besler. Tarama tiplerini anlamak diğer her şeyin temelidir.

Seq Scan — Tam Tablo Taraması#

Seq Scan on orders  (cost=0.00..25000.00 rows=1000000 width=128)

Sıralı tarama, tablodaki her satırı okur. Bir milyon satırlık bir tablo için bu, diskteki her sayfa demek.

Ama işte nüans: Seq Scan her zaman kötü değildir. Tablonun %30'unu veya daha fazlasını seçiyorsan, sıralı I/O rastgele I/O'dan çok daha hızlı olduğu için sıralı tarama aslında index taramasından daha hızlıdır. PostgreSQL bunu bilir. Bir index'in olduğu halde Seq Scan seçiyorsa, WHERE koşulunun gerçekte ne kadar seçici olduğunu kontrol et.

Ne zaman sorun olur: büyük bir tablodan çok küçük bir satır kesiti seçerken ve index yokken.

Index Scan — Hedefli Arama#

Index Scan using idx_orders_customer_id on orders  (cost=0.43..8.45 rows=5 width=128)
  Index Cond: (customer_id = 42)

B-tree index'e gider, eşleşen girdileri bulur, sonra tablo heap'inden gerçek satırları getirir. Her satır getirme, heap'e rastgele bir I/O işlemidir. Çok seçici sorgular için harika ama her heap getirmenin bir maliyeti var.

Index Only Scan — Kutsal Kase#

Index Only Scan using idx_orders_customer_total on orders  (cost=0.43..4.15 rows=5 width=12)
  Index Cond: (customer_id = 42)
  Heap Fetches: 0

Bu en hızlı tarama tipi. PostgreSQL ihtiyacı olan her şeyi yalnızca index'ten alır, tablo heap'ine hiç dokunmaz. Bunu, SELECT ve WHERE yaptığın tüm sütunlar index'te olduğunda görürsün.

Tuzak: Heap Fetches: 0 görünürlük haritasının güncel olduğu anlamına gelir. Tablonda çok fazla ölü tuple varsa (yakın zamanda vacuum yapılmamış), PostgreSQL satır görünürlüğünü doğrulamak için yine heap'i kontrol etmek zorundadır. autovacuum'un performans için önemli olmasının bir nedeni budur, sadece disk alanı için değil.

Bitmap Scan — Orta Yol#

Bitmap Heap Scan on orders  (cost=45.00..1250.00 rows=5000 width=128)
  Recheck Cond: (created_at > '2026-01-01')
  ->  Bitmap Index Scan on idx_orders_created_at  (cost=0.00..43.75 rows=5000 width=0)
        Index Cond: (created_at > '2026-01-01')

Bitmap taramaları, PostgreSQL'in "index taraması için çok fazla satır, seq scan için çok az" problemine cevabıdır. Eşleşen satırları içeren sayfaların bir bitmap'ini oluşturur, fiziksel konuma göre sıralar, sonra sırayla getirir. Bu, rastgele I/O'yu sıralı I/O'ya dönüştürür.

İki veya daha fazla index birleştirildiğinde sıklıkla bitmap taramaları görürsün:

Bitmap Heap Scan on orders
  Recheck Cond: ((status = 'completed') AND (created_at > '2026-01-01'))
  ->  BitmapAnd
        ->  Bitmap Index Scan on idx_orders_status
              Index Cond: (status = 'completed')
        ->  Bitmap Index Scan on idx_orders_created_at
              Index Cond: (created_at > '2026-01-01')

Bu BitmapAnd iki ayrı index'i birleştirir. PostgreSQL'in "bileşik index'im yok ama bu iki tek sütunlu index'i birleştirebilirim" demesi. Çalışır, ama uygun bir bileşik index daha hızlı olur.

Kötü Tahminleri Tespit Etme#

Bir sorgu planında aradığım bir numaralı şey eşleşmeyen tahminler:

Nested Loop  (cost=0.43..50.12 rows=1 width=128)
             (actual time=0.025..4521.337 rows=48753 loops=1)

Planlayıcı 1 satır tahmin etti. Gerçek sonuç 48.753 satır. Bu beş büyüklük derecesi yanlış tahmin. Planlayıcı, 1 satıra karşı join yapacağını düşündüğü için Nested Loop seçti. 48.753 satırla, Hash Join büyüklük dereceleri daha hızlı olurdu.

Kötü tahminlerin yaygın nedenleri:

  • Eski istatistikler: Tabloda ANALYZE çalıştır
  • İlişkili sütunlar: Planlayıcı sütun değerlerinin bağımsız olduğunu varsayar. status = 'shipped' ve created_at > '2026-01-01' ilişkiliyse (en yeni siparişler gönderilmiştir), planlayıcı birleşik seçiciliği eksik tahmin eder
  • WHERE'de özel fonksiyonlar: Planlayıcı bir fonksiyonu analiz edemediğinde varsayılan seçicilik tahmini kullanır (eşitlik için genellikle %0.5, aralık için %33)
  • Genel planlarla parametrelenmiş sorgular: 5 çalıştırmadan sonra, PostgreSQL gerçek parametre değerini dikkate almayan genel bir plana geçebilir

Kötü tahminler gördüğünde, çözüm genellikle şunlardan biri: ANALYZE çalıştır, genişletilmiş istatistikler oluştur, sorguyu yeniden yaz veya bir CTE'yi optimizasyon çiti olarak kullan.

sql
-- İlişkili sütunlar için genişletilmiş istatistikler oluştur
CREATE STATISTICS orders_status_date (dependencies)
ON status, created_at FROM orders;
ANALYZE orders;

Index Stratejisi: Doğru İş İçin Doğru Index#

Index'ler bedava değil. Her index yazmaları yavaşlatır, disk alanı tüketir ve bakım gerektirir. Amaç "her şeyi indexle" değil — "tam olarak ihtiyacın olanı indexle."

B-tree — Varsayılan İş Atı#

B-tree varsayılandır ve vakaların büyük çoğunluğunu karşılar. Eşitlik ve aralık operatörlerini destekler (=, <, >, <=, >=, BETWEEN, IN, IS NULL).

sql
CREATE INDEX idx_orders_created_at ON orders (created_at);

Bileşik index'lerde B-tree index sütun sırası son derece önemlidir. En soldaki sütun birincil sıralama, sonraki ikinci vb. (a, b, c) üzerindeki bir index şunları verimli cevaplayabilir:

  • WHERE a = 1
  • WHERE a = 1 AND b = 2
  • WHERE a = 1 AND b = 2 AND c = 3
  • WHERE a = 1 AND b > 5
  • WHERE a = 1 ORDER BY b

Şunları verimli cevaplayamaz:

  • WHERE b = 2 (ilk sütunu atlar)
  • WHERE c = 3 (ilk iki sütunu atlar)
  • WHERE a = 1 AND c = 3 (ortada boşluk — a koşulu kullanılır, c koşulu filtre gerektirir)

Bunu soyadına göre, sonra ada göre sıralanmış bir telefon rehberi gibi düşün. Tüm "Smith" girişlerini veya özellikle "Smith, John"u hızlıca bulabilirsin. Ama tüm rehberi taramadan tüm "John" girişlerini hızlıca bulamazsın.

Kural: eşitlik koşullarını önce, sonra aralık koşullarını, sonra sıralama sütunlarını koy.

sql
-- Sorgu: WHERE status = 'active' AND created_at > '2026-01-01' ORDER BY total DESC
 
-- İyi: eşitlik önce, sonra aralık, sonra sıralama
CREATE INDEX idx_orders_optimized ON orders (status, created_at, total);
 
-- Kötü: aralık eşitlikten önce
CREATE INDEX idx_orders_bad ON orders (created_at, status, total);

INCLUDE ile Kapsayan Index'ler#

PostgreSQL 11+, INCLUDE ile bir index'e anahtar olmayan sütunlar eklemenize izin verir. Bu sütunlar yaprak sayfalarda saklanır ama B-tree yapısının parçası değildir. Ağacı şişirmeden index-only taramalar sağlar:

sql
-- Sık çalıştırıyoruz:
-- SELECT id, total, status FROM orders WHERE customer_id = 42;
 
CREATE INDEX idx_orders_customer_covering
ON orders (customer_id) INCLUDE (total, status);

Şimdi PostgreSQL tüm sorguyu yalnızca index'ten karşılayabilir. INCLUDE olmadan, heap'ten total ve status getirmesi gerekirdi. Soğuk önbellekte fark dramatik olabilir — sadece INCLUDE sütunları ekleyerek sorguların 50ms'den 0.2ms'ye düştüğünü gördüm.

Kısmi Index'ler — Sadece Önemli Olanı Indexle#

Bu benim en sevdiğim PostgreSQL özelliği ve çoğu geliştiricinin bilmediği.

sql
-- Siparişlerin sadece %2'si 'pending', ama onları sürekli sorguluyoruz
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';

Bu index, created_at üzerindeki tam bir index'e kıyasla minik. Sadece status = 'pending' olan satırları içerir. WHERE koşuluyla eşleşen sorgular bu küçük, hızlı index'i kullanır. Eşleşmeyen sorgular onu yok sayar.

Üretimden gerçek örnek: 50 milyon satırlık bir sessions tablom vardı. Sadece yaklaşık 200.000'i aktifti (süresi dolmamış). user_id üzerindeki tam index 1.2 GB'dı. WHERE expires_at > now() kısmi index'i 8 MB'dı. Sorgu 12ms'den 0.1ms'ye düştü çünkü tüm index önbelleğe sığıyordu.

sql
-- Başka bir yaygın kalıp: soft delete'ler
CREATE INDEX idx_users_active_email ON users (email)
WHERE deleted_at IS NULL;
 
-- Sadece aktif kayıtlarda benzersiz kısıtlama
CREATE UNIQUE INDEX idx_users_unique_email ON users (email)
WHERE deleted_at IS NULL;

Son örnek inanılmaz kullanışlı. Aktif kullanıcılarda benzersiz e-posta kısıtlaması olmasına izin verirken, aynı e-postanın silinen kayıtlarda görünmesine izin verir.

GIN Index'leri — Tam Metin Arama ve JSONB#

GIN (Generalized Inverted Index), değerlerin içinde arama yapman gerektiğinde cevaptır — diziler, JSONB belgeleri veya tam metin arama vektörleri.

sql
-- JSONB kapsama sorguları
CREATE INDEX idx_products_metadata ON products USING GIN (metadata);
 
-- Şimdi bu hızlı:
SELECT * FROM products WHERE metadata @> '{"color": "red"}';
 
-- Tam metin arama
CREATE INDEX idx_articles_search ON articles USING GIN (to_tsvector('english', title || ' ' || body));

GIN index'leri büyük ve oluşturması yavaş, ama sorgulamak için son derece hızlı. Karmaşık kapsama sorgularıyla okuma ağırlıklı iş yükleri için değer.

JSONB için, @> kapsama sorguları için daha küçük ve hızlı olan jsonb_path_ops da var, ama varlık operatörlerini desteklemez (?, ?|, ?&):

sql
CREATE INDEX idx_products_metadata_pathops
ON products USING GIN (metadata jsonb_path_ops);

GiST Index'leri — Geometrik ve Aralık Tipleri#

GiST (Generalized Search Tree) örtüşen veri tiplerini işler: geometrik şekiller, aralıklar, tam metin arama (GIN'e alternatif, daha küçük ama daha yavaş).

sql
-- IP aralık aramaları
CREATE INDEX idx_ip_ranges ON ip_blocks USING GiST (ip_range);
 
SELECT * FROM ip_blocks WHERE ip_range @> '192.168.1.50'::inet;
 
-- Zamansal aralıklar (zamanlama çakışmaları)
CREATE INDEX idx_bookings_period ON bookings USING GiST (
  tstzrange(start_time, end_time)
);
 
-- Örtüşen rezervasyonları bul:
SELECT * FROM bookings
WHERE tstzrange(start_time, end_time) && tstzrange('2026-03-01 10:00', '2026-03-01 12:00');

Hash Index'leri — Niş Ama Kullanışlı#

Hash index'leri sadece eşitlik karşılaştırmaları için kullanışlı. PostgreSQL 10'dan beri WAL-logged ve çökme güvenli. Geniş sütunlar için B-tree'den daha küçük ve saf eşitlik için biraz daha hızlı:

sql
-- Büyük metin sütunlarında sadece eşitlik aramaları için iyi
CREATE INDEX idx_sessions_token ON sessions USING HASH (session_token);
 
-- Bu hızlı:
SELECT * FROM sessions WHERE session_token = 'abc123def456...';
 
-- Bu hash index'i KULLANAMAZ:
SELECT * FROM sessions WHERE session_token LIKE 'abc%';

Pratikte, hash index'leri nadiren kullanıyorum. B-tree eşitliği gayet iyi halleder ve aralık sorguları veya sıralama yapamaması, hash index'lerini çoğu gerçek kullanım durumu için çok esnek olmayan kılar.

Index'ler Ne Zaman Zarar Verir#

Her index'in bir maliyeti var:

  • Yazma amplifikasyonu: Her INSERT, tablodaki her index'i günceller. 8 index'li bir tablo, INSERT başına 8 ek yazma demek
  • HOT güncellemeler engellenir: Heap-Only Tuple (HOT) güncellemeleri, PostgreSQL'in index'leri güncellemeden bir satırı güncelleyebildiği önemli bir optimizasyon, ama sadece index'lenmiş sütun değişmediyse. Daha fazla index = HOT güncellemelerin engellenmesi şansı artar
  • Vacuum yükü: Daha fazla index, vacuum'un daha uzun sürmesi demek
  • Planlayıcı yükü: Daha fazla index, planlayıcının değerlendirmesi gereken daha fazla seçenek demek

Üretim tablolarındaki index'leri düzenli olarak denetliyorum:

sql
-- Kullanılmayan index'leri bul
SELECT
  schemaname, tablename, indexname,
  idx_scan,
  pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexrelid NOT IN (
    SELECT conindid FROM pg_constraint
    WHERE contype IN ('p', 'u')  -- birincil anahtar ve benzersiz kısıtlamaları hariç tut
  )
ORDER BY pg_relation_size(indexrelid) DESC;

Tek seferlik bir migrasyon sırasında oluşturulmuş ve bir daha asla kullanılmamış çok gigabaytlık index'ler buldum. Onları kaldırmak yazmaları fark edilir şekilde hızlandırdı.

sql
-- Tekrarlanan index'leri bul (aynı sütunlar, aynı sıra)
SELECT
  a.indexrelid::regclass AS index1,
  b.indexrelid::regclass AS index2,
  a.indrelid::regclass AS table_name
FROM pg_index a
JOIN pg_index b ON a.indrelid = b.indrelid
  AND a.indexrelid < b.indexrelid
  AND a.indkey = b.indkey;

N+1 Sorgu Problemi#

Her ORM geliştiricisi eninde sonunda buna çarpar. Hata ayıkladığım en yaygın performans problemi.

N+1 Neye Benzer#

python
# Python / SQLAlchemy — klasik tuzak
orders = session.query(Order).filter_by(status='pending').all()
 
for order in orders:
    # Bu, HER sipariş için ayrı bir sorgu tetikler
    print(f"Customer: {order.customer.name}")

Bu şunu üretir:

sql
-- Sorgu 1: Tüm bekleyen siparişleri al
SELECT * FROM orders WHERE status = 'pending';
 
-- Sorgu 2: Sipariş 1 için müşteriyi al
SELECT * FROM customers WHERE id = 101;
 
-- Sorgu 3: Sipariş 2 için müşteriyi al
SELECT * FROM customers WHERE id = 102;
 
-- ...Sorgu 502: Sipariş 500 için müşteriyi al
SELECT * FROM customers WHERE id = 600;

1 veya 2 yerine 501 sorgu. Her sorgu bireysel olarak hızlı, belki 0.5ms. Ama 501 tanesi sadece veritabanı süresinde 250ms'ye ulaşır, artı her biri için ağ gidiş-dönüş gecikmesi.

Çözüm: Eager Loading veya JOIN'ler#

ORM yaklaşımı — ORM'ye ilişkili veriyi önceden yüklemesini söyle:

python
# SQLAlchemy — joinedload aynı sorguda müşterileri getirir
from sqlalchemy.orm import joinedload
 
orders = (
    session.query(Order)
    .options(joinedload(Order.customer))
    .filter_by(status='pending')
    .all()
)

Ham SQL yaklaşımı — sadece JOIN kullan:

sql
SELECT o.*, c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending';

Tek sorgu. Tamam.

Yaygın Sorgu Yeniden Yazımları#

Bazı sorgular eksik index'ler yüzünden değil, yazılma biçimleri yüzünden yavaş. PostgreSQL'in planlayıcısı iyi, ama sihir değil.

EXISTS vs IN vs JOIN#

Bu sürekli karşıma çıkar:

sql
-- EXISTS — ilk eşleşmede durur, varlık kontrolü için harika
SELECT * FROM orders o
WHERE EXISTS (
  SELECT 1 FROM returns r WHERE r.order_id = o.id
);
 
-- IN alt sorgu ile — önce tam sonuç setini oluşturur
SELECT * FROM orders
WHERE id IN (SELECT order_id FROM returns);
 
-- JOIN — returns'te sipariş başına birden fazla satır varsa tekrarlar üretebilir
SELECT DISTINCT o.* FROM orders o
JOIN returns r ON r.order_id = o.id;

Büyük tablolar için, EXISTS genellikle kazanır çünkü kısa devre yapar. IN versiyonu, filtrelemeden önce tüm iade sipariş_id'lerinin listesini oluşturmak zorunda. JOIN versiyonu tekrarlar üretebilir, bu da sıralama veya hash adımı ekleyen DISTINCT gerektirir.

Varsayılanım: ilişkili satırların varlığını kontrol ederken EXISTS kullan. En anlamsal olarak açık ve genellikle en hızlı.

OFFSET ile Sayfalama Yerine Keyset Sayfalama#

OFFSET büyük veri setleri için bir tuzak:

sql
-- Sayfa 1: hızlı
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 0;
 
-- Sayfa 500: yavaş — PostgreSQL 9.980 satırı tarayıp atmalı
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 9980;

Çözüm keyset sayfalama (imleç tabanlı sayfalama olarak da bilinir):

sql
-- İlk sayfa
SELECT * FROM products ORDER BY id LIMIT 20;
 
-- Sonraki sayfa: önceki sayfanın son id'sini kullan
SELECT * FROM products WHERE id > 12345 ORDER BY id LIMIT 20;

Bu her zaman hızlıdır çünkü hangi "sayfada" olursan ol doğrudan doğru konuma atlamak için index'i kullanır.

Window Fonksiyonları vs Alt Sorgular#

Window fonksiyonları PostgreSQL'in en güçlü özelliklerinden biri ve neredeyse her zaman ilişkili alt sorguları geride bırakır:

sql
-- Yavaş: ilişkili alt sorgu satır başına bir kez çalışır
SELECT
  o.*,
  (SELECT COUNT(*) FROM order_items i WHERE i.order_id = o.id) AS item_count
FROM orders o
WHERE o.status = 'completed';
 
-- Hızlı: DISTINCT ON (PostgreSQL'e özgü, son derece kullanışlı)
SELECT DISTINCT ON (user_id) *
FROM notifications
ORDER BY user_id, created_at DESC;

DISTINCT ON bu kalıp için benim varsayılanım. Kısa, okunabilir ve PostgreSQL doğru index ile iyi optimize eder.

Pratik Kontrol Listesi#

İşte bir "bu sorgu yavaş" raporu aldığımda izlediğim gerçek sürecim:

  1. Gerçek sorguyu al — "endpoint yavaş" değil, gerçek SQL
  2. EXPLAIN (ANALYZE, BUFFERS) çalıştır — kötü satır tahminleri, büyük tablolarda Seq Scan'ler, çok satırlı Nested Loop'lar ara
  3. Tablo istatistiklerini kontrol etlast_analyze eskiyse ANALYZE çalıştır
  4. Mevcut index'leri kontrol et — belki index var ama kullanılmıyor
  5. Index oluştur veya değiştirCONCURRENTLY kullan
  6. Sorgu yeniden yazımlarını düşün — JOIN'ler, keyset sayfalama, window fonksiyonları
  7. Sunucu ayarlarını kontrol etwork_mem, random_page_cost, effective_cache_size
  8. Düzeltmeden sonra izlepg_stat_statements ile doğrula

Son Düşünceler#

PostgreSQL sorgu optimizasyonu karanlık bir sanat değil. Sistematik bir süreç:

  1. Ölç — tahmin etme. EXPLAIN (ANALYZE, BUFFERS) en iyi dostun.
  2. Planı anla — tarama tiplerini, join tiplerini ve satır tahminlerini okumayı öğren.
  3. Stratejik indexle — doğru sütunlarda, doğru sırada doğru index. Kısmi index'ler ve kapsayan index'ler az kullanılan süper güçler.
  4. Daha iyi sorgular yaz — varlık kontrolleri için IN yerine EXISTS, OFFSET yerine keyset sayfalama, ilişkili alt sorgular yerine window fonksiyonları.
  5. Veritabanını bakımlı tut — autovacuum ayarlaması, istatistik güncellemeleri, bağlantı havuzlama.
  6. Sürekli izle — pg_stat_statements veritabanının zamanını nereye harcadığını söyler. Düzenli kontrol et.

4 saniye süren bir sorguyla 0.3 milisaniye süren bir sorgu arasındaki fark nadiren donanımdır. Neredeyse her zaman bilgidir — nereye bakacağını ve neyi değiştireceğini bilmek. Ve şimdi nereye bakacağını biliyorsun.

İlgili Yazılar