DevOps & Cloud

PostgreSQL Optimizasyon Rehberi: İndeksleme, Sorgu Analizi ve Performans

13 dk okuma16 Mart 2026
PostgreSQL optimizasyonPostgreSQL indekslemePostgreSQL performansEXPLAIN ANALYZEPostgreSQL rehberiVeritabanı optimizasyonuPostgreSQL .NETConnection poolingPostgreSQL TürkiyeB-tree indexGIN indexSorgu optimizasyonu

PostgreSQL, acik kaynakli iliskisel veritabanlari arasinda performans, guvenilirlik ve ozellik zenginligi acisindan one cikan bir sistemdir. Ancak dogru yapilandirilmadiginda ve sorgular optimize edilmediginde, buyuk veri setlerinde ciddi performans sorunlari yasanabilir. Bu yazida PostgreSQL indeksleme stratejilerini, EXPLAIN ANALYZE ile sorgu analizi yapmayi ve production ortamlarinda sorgu optimizasyonu tekniklerini detayli olarak ele alacagim.

Projelerimde PostgreSQL sorgularini optimize ederek saniyeler suren sorgulari milisaniye seviyesine dusurdugumu defalarca deneyimledim. Dogru indeks tasarimi ve sorgu plani analizi, veritabani performansinin temel taslardir.

Indeksleme Stratejileri

Temel Indeks Turleri

PostgreSQL cok cesitli indeks turleri sunar. Her birinin kullanim alani farklidir:

  • B-Tree: Varsayilan indeks tipi. Esitlik ve aralik sorgulari icin idealdir
  • Hash: Sadece esitlik sorgulari icin. B-Tree'den biraz daha hizli olabilir
  • GIN (Generalized Inverted Index): JSONB, full-text search ve dizi sorgulari icin
  • GiST (Generalized Search Tree): Geometrik veri, full-text search ve range tipler icin
  • BRIN (Block Range Index): Buyuk, dogal olarak sirali tablolar icin (orn. zaman serisi verileri)
sql
-- B-Tree indeks: En yaygin kullanim
CREATE INDEX idx_orders_customer_id ON orders (customer_id);

-- Composite indeks: Birden fazla kolon
CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at DESC);

-- Partial indeks: Sadece belirli kosulu saglayan satirlar
CREATE INDEX idx_orders_active ON orders (status, created_at)
WHERE status = 'active';

-- Covering indeks: Index-only scan icin ek kolonlar
CREATE INDEX idx_orders_covering ON orders (customer_id)
INCLUDE (total_amount, status);

-- GIN indeks: JSONB alanlari icin
CREATE INDEX idx_products_metadata ON products USING gin (metadata jsonb_path_ops);

-- BRIN indeks: Zaman serisi verileri icin
CREATE INDEX idx_logs_created_at ON application_logs USING brin (created_at)
WITH (pages_per_range = 32);

-- Expression indeks: Fonksiyon bazli sorgular icin
CREATE INDEX idx_users_email_lower ON users (lower(email));

Indeks Secim Kriterleri

Dogru indeks secimi icin su sorulara cevap verin:

  • Sorgu hangi kolonlara WHERE kosuluyla erisiyor?
  • JOIN islemlerinde hangi kolonlar kullaniliyor?
  • ORDER BY ve GROUP BY hangi kolonlara uygulaniyor?
  • Tablodaki satir sayisi ve veri dagilimi nasil?

EXPLAIN ANALYZE ile Sorgu Analizi

Sorgu Planini Okumak

EXPLAIN ANALYZE, PostgreSQL'in bir sorguyu nasil yuruttugunun detayli planini gosterir. Performans sorunlarini teshis etmenin en etkili yoludur.

sql
-- Yavas sorgu ornegi
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total_amount, c.name, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2025-01-01'
  AND o.status = 'completed'
  AND o.total_amount > 100
ORDER BY o.created_at DESC
LIMIT 50;

/*
Ornek cikti (optimize edilmemis):

Limit  (cost=15234.56..15234.69 rows=50 width=72) (actual time=1523.456..1523.489 rows=50 loops=1)
  Buffers: shared hit=2341 read=8923
  ->  Sort  (cost=15234.56..15298.23 rows=25468 width=72) (actual time=1523.454..1523.478 rows=50 loops=1)
        Sort Key: o.created_at DESC
        Sort Method: top-N heapsort  Memory: 32kB
        ->  Hash Join  (cost=1234.56..14567.89 rows=25468 width=72) (actual time=45.123..1498.234 rows=25468 loops=1)
              Hash Cond: (o.customer_id = c.id)
              ->  Seq Scan on orders o  (cost=0.00..12345.67 rows=25468 width=48) (actual time=0.015..1423.567 rows=25468 loops=1)
                    Filter: ((created_at >= '2025-01-01') AND (status = 'completed') AND (total_amount > 100))
                    Rows Removed by Filter: 974532
                    Buffers: shared hit=1234 read=8923
              ->  Hash  (cost=934.56..934.56 rows=24000 width=28) (actual time=44.567..44.567 rows=24000 loops=1)
                    ->  Seq Scan on customers c  (cost=0.00..934.56 rows=24000 width=28)
Planning Time: 0.234 ms
Execution Time: 1523.678 ms
*/

-- Indeks ekledikten sonra:
CREATE INDEX idx_orders_status_date ON orders (status, created_at DESC)
INCLUDE (total_amount, customer_id)
WHERE status = 'completed';

-- Ayni sorgunun optimize edilmis plani:
/*
Limit  (cost=234.56..234.69 rows=50 width=72) (actual time=2.345..2.378 rows=50 loops=1)
  Buffers: shared hit=156
  ->  Nested Loop  (cost=234.56..1234.89 rows=12734 width=72) (actual time=2.343..2.371 rows=50 loops=1)
        ->  Index Only Scan using idx_orders_status_date on orders o
              (cost=0.42..567.89 rows=12734 width=48) (actual time=0.034..0.156 rows=50 loops=1)
              Index Cond: ((status = 'completed') AND (created_at >= '2025-01-01'))
              Filter: (total_amount > 100)
              Heap Fetches: 0
        ->  Index Scan using customers_pkey on customers c
              (cost=0.29..0.31 rows=1 width=28) (actual time=0.003..0.003 rows=1 loops=50)
              Index Cond: (id = o.customer_id)
Planning Time: 0.312 ms
Execution Time: 2.456 ms
*/

Dikkat Edilmesi Gereken Metrikleri

EXPLAIN ANALYZE ciktisinda su metriklere odaklanin:

  • Seq Scan: Buyuk tablolarda sequential scan performans sorununa isaret eder
  • Rows Removed by Filter: Cok yuksekse, indeks eksikligi vardir
  • Buffers read: Diskten okunan blok sayisi, yuksekse veri cache'de degildir
  • actual time: Gercek yurutme suresi, cost tahminleriyle karsilastirin
  • loops: Nested loop sayisi, yuksekse join stratejisini gozden gecirin

pg_stat_statements ile Sorgu Performansi Izleme

pg_stat_statements extension'i, veritabanindaki tum sorgularin istatistiklerini toplar. En yavas sorgulari, en cok cagirilan sorgulari ve en fazla kaynak tuketen sorgulari tespit etmek icin vazgecilmezdir:

sql
-- Extension'i etkinlestir
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- En yavas 10 sorguyu listele (toplam sure bazinda)
SELECT
    calls,
    round(total_exec_time::numeric, 2) AS total_time_ms,
    round(mean_exec_time::numeric, 2) AS avg_time_ms,
    round(stddev_exec_time::numeric, 2) AS stddev_ms,
    rows,
    query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- En cok cagirilan sorgular
SELECT
    calls,
    round(mean_exec_time::numeric, 2) AS avg_time_ms,
    query
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;

-- Istatistikleri sifirla (periyodik olarak yapin)
SELECT pg_stat_statements_reset();

Bu bilgileri duzenli olarak inceleyerek performans regresyonlarini erken tespit edebilir ve optimizasyon onceliklerinizi belirleyebilirsiniz.

N+1 Sorgu Problemi ve Cozumleri

N+1 problemi, ORM kullanilan uygulamalarda en yaygin performans sorunlarindan biridir. Ana sorgu 1 kez calisir, sonuc icindeki her satir icin ek sorgu ateslenirse toplam N+1 sorgu yurutulur:

sql
-- N+1 problemi: Her siparis icin ayri musteri sorgulama
-- Sorgu 1: SELECT * FROM orders WHERE status = 'active';
-- Sorgu 2..N+1: SELECT * FROM customers WHERE id = ? (her siparis icin)

-- Cozum: JOIN ile tek sorguda cozme
SELECT o.*, c.name, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'active';

-- Alternatif cozum: IN clause ile batch sorgulama
SELECT * FROM customers
WHERE id IN (
    SELECT DISTINCT customer_id FROM orders WHERE status = 'active'
);

EF Core gibi ORM'lerde .Include() kullanarak eager loading yapmak, N+1 problemini kod seviyesinde cozer. Ancak veritabani tarafinda da pg_stat_statements ile tekrar eden sorgu pattern'lerini izlemek onemlidir.

Tablo Partitioning

Buyuk tablolari fiziksel olarak bolumlemek, sorgu performansini onemli olcude artirabilir. PostgreSQL hem range hem de list partitioning destekler:

sql
-- Range partitioning: Tarih bazli bolumendirme
CREATE TABLE orders (
    id BIGSERIAL,
    customer_id INT NOT NULL,
    total_amount DECIMAL(10,2),
    status VARCHAR(20),
    created_at TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Aylik partitionlar olustur
CREATE TABLE orders_2025_01 PARTITION OF orders
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE orders_2025_02 PARTITION OF orders
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

CREATE TABLE orders_2025_03 PARTITION OF orders
    FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');

-- Varsayilan partition (tanimsiz araliklar icin)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

-- Her partition icin indeks olustur (otomatik miras alinir)
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_status ON orders (status, created_at DESC);

Partitioning sayesinde tarih filtreli sorgular sadece ilgili partition'i tarar, milyonlarca satirlik tablolarda bile hizli sonuc dondurur. Ancak partitioning overhead'i vardir ve kucuk tablolarda fayda saglamaz.

Connection Pooling

Production ortamlarinda connection pooling kritik oneme sahiptir. PgBouncer, PostgreSQL icin en yaygin kullanilan connection pooler'dir.

ini
; pgbouncer.ini
[databases]
myapp = host=localhost port=5432 dbname=myapp

[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

; Pool ayarlari
pool_mode = transaction
default_pool_size = 25
min_pool_size = 5
max_client_conn = 200
reserve_pool_size = 5

; Timeout ayarlari
server_idle_timeout = 600
client_idle_timeout = 0
query_timeout = 30

VACUUM ve ANALYZE Detaylari

PostgreSQL'in MVCC (Multi-Version Concurrency Control) mimarisi nedeniyle duzenli VACUUM islemi gereklidir. VACUUM isleminin nasil calistigini anlamak, veritabani bakim stratejinizin temelini olusturur:

  • VACUUM: Olu satirlari temizler ve alani geri kazanir, ancak alani isletim sistemine iade etmez
  • VACUUM FULL: Tabloyu tamamen yeniden yazar ve alani isletim sistemine iade eder (tablo kilidi gerektirir)
  • VACUUM ANALYZE: VACUUM + istatistik guncelleme (planner icin kritik)
  • autovacuum: Otomatik vacuum islemini yapilandirin, kapatmayin
  • REINDEX: Sismis indeksleri yeniden olusturur
sql
-- Autovacuum ayarlarini tablo bazinda ozellestir
ALTER TABLE orders SET (
    autovacuum_vacuum_threshold = 1000,
    autovacuum_vacuum_scale_factor = 0.05,
    autovacuum_analyze_threshold = 500,
    autovacuum_analyze_scale_factor = 0.02
);

-- Tablo sismesini (bloat) kontrol et
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
    n_dead_tup,
    n_live_tup,
    round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
    last_vacuum,
    last_autovacuum,
    last_analyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

Yuksek guncelleme oranlarina sahip tablolarda autovacuum parametrelerini agresif olarak yapilandirmak, tablo sismesini onler ve sorgu performansini korur.

Pratik Oneriler ve Ozet

PostgreSQL optimizasyonunda su noktalara dikkat edin:

  1. EXPLAIN ANALYZE aliskanligi: Her yeni sorguyu production'a almadan once analiz edin
  2. Indeks bakimi: Kullanilmayan indeksleri pg_stat_user_indexes ile tespit edip kaldirin
  3. Istatistik guncelleme: ANALYZE komutunu duzenli calistirarak planner'in dogru kararlar almasini saglayin
  4. work_mem ayari: Karmasik sorgularda sort ve hash islemleri icin yeterli bellek ayirin
  5. Slow query log: log_min_duration_statement ile yavas sorgulari kaydedin ve duzenli inceleyin

PostgreSQL, dogru optimize edildiginde milyonlarca satirlik tablolarda bile milisaniye seviyesinde yanit verebilir. Anahtar, veri erisim paternlerini anlamak ve buna uygun indeks stratejisi olusturmaktir.

İlgili Makaleler

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç