# Sürekli güncellenen bir sessions tablosunda staging'de Index Scan alan sorgu production'da neden Seq Scan'e düşüyor, EXPLAIN ile nasıl teşhis ederim?

> Production'da EXPLAIN (ANALYZE, BUFFERS) çekip tabloyu ANALYZE edin ve n_dead_tup'a bakın; sessions için autovacuum'u sıkılaştırıp partial index ekleyin.

- Soruldu: 2026-07-09
- Yanıtlandı: 2026-07-14
- Soran: Zeynep
- Etiketler: postgresql, performans
- Kaynak: https://muhammetsafak.com/tr/sor-bakalim/surekli-guncellenen-bir-sessions-tablosunda-stagingde-index-scan-alan-sorgu-productionda/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
**Soru:** Aurora PostgreSQL kullanıyorum. Aynı sorgu staging'de anında dönüyor ve `EXPLAIN` çıktısında güzelce Index Scan alıyor; ama production primary'de gerçek trafik altında aynı sorgu Seq Scan'e düşüp timeout'a giriyor.

Sorgu, çok sık INSERT/UPDATE/DELETE aldığımız bir `sessions` tablosu üzerinde çalışıyor. Bu farkı nasıl teşhis ederim, `EXPLAIN`'i doğru şekilde nasıl okurum ve kalıcı çözümü nasıl kurarım?


Kısa cevap: Aynı sorgunun farklı plan alması, planner'ın maliyet tahmininin ortamlar arasında farklı olması demektir.

## Kısa cevap

Çok yazılan bir `sessions` tablosunda bunun nedeni neredeyse her zaman bayat istatistik ve tablo bloat'udur — "eksik index" değil. Bir index eklemeden veya sorguyu yeniden yazmadan önce şunu içselleştirin: plan, maliyet modelinin bir çıktısıdır ve maliyet modeli, kendisini besleyen istatistikler kadar iyidir. Girdileri düzeltirseniz plan çoğu zaman kendini toparlar. İstatistik tazelendikten sonra hâlâ index'ten şüphe ediyorsanız, kolon sırasının plana etkisini [PostgreSQL composite index kaydında](/tr/sor-bakalim/postgresql-composite-index-kolon-sirasi-nasil-secilir/) ayrıntılandırmıştım.

## Neden

1. **Yüksek devir hızlı tabloda istatistik bayatlar.** `sessions` sürekli INSERT/UPDATE/DELETE alır; autovacuum/autoanalyze buna yetişemeyince `n_distinct` ve histogramlar yanlış kalır. Planner "10 satır" derken gerçekte 2 milyon geliyorsa, planner yanıltılıyordur ve Seq Scan'i bu yüzden seçer.
2. **Tablo ve index bloat'u planı çevirir.** Sürekli update'lerden kalan dead tuple'lar heap'i ve index'leri şişirir; index canlı satırlara oranla o kadar büyür ki planner (haklı olarak) Seq Scan'i daha ucuz bulur.
3. **Veri dağılımı gerçekten farklı olabilir.** Staging'de küçük/tekdüze veri vardır; staging'de seçici olan bir predicate production'da satırların çoğuyla eşleşiyorsa Seq Scan orada gerçekten daha ucuzdur. Bu bir bug değildir — plan veriye göre doğrudur; planner'ı değil, sorguyu/index'i düzeltin.
4. **PostgreSQL'de native query hint yoktur.** Yani planı zorla çevirecek bir kaçış kapısı aramanın anlamı yok; asıl çözüm istatistik/bloat/index'i düzeltmektir.

## Ne yapmalı

1. **`EXPLAIN` değil, `EXPLAIN (ANALYZE, BUFFERS)` çalıştırın.** Düz `EXPLAIN` yalnızca tahmini gösterir; production'da `EXPLAIN (ANALYZE, BUFFERS)` ile tahmini ve gerçek satır sayısını yan yana görün.
2. **İstatistiği tazeleyin, ölü satırı ölçün.** `ANALYZE sessions;` çalıştırın; `pg_stat_user_tables`'ta `last_autoanalyze` ve `n_dead_tup`'a bakın ve bu tabloya özel daha agresif autovacuum düşünün.

   ```sql
   -- Teşhis: tahmin vs gerçek
   EXPLAIN (ANALYZE, BUFFERS) SELECT ... FROM sessions WHERE ...;

   -- Taze istatistik ve ölü satır kontrolü
   ANALYZE sessions;
   SELECT n_live_tup, n_dead_tup, last_autoanalyze
   FROM pg_stat_user_tables WHERE relname = 'sessions';

   -- Bu tabloya özel daha agresif autovacuum
   ALTER TABLE sessions SET (autovacuum_vacuum_scale_factor = 0.02,
                             autovacuum_analyze_scale_factor = 0.01);
   ```

3. **Aurora'ya özgü noktaları hesaba katın.** Aurora reader/writer ayrımı ve staging'i production istatistikleriyle birebir taklit edememe gerçeği vardır. `sessions` için `autovacuum_vacuum_scale_factor`/`analyze_scale_factor`'ı düşürün; hep aktif oturumları filtreliyorsanız partial index düşünün.
4. **İlk refleks hint olmasın.** Önce istatistiği ve bloat'ı düzeltin; `random_page_cost`'u (varsayılan 4.0) düşürmek de planner'ı index taramasına doğru iter; ama bunu istatistik düzeltildikten sonraki bir ayar adımı olarak görün, çözüm olarak değil.

**Sonuç:** Ben olsam önce production'da `EXPLAIN (ANALYZE, BUFFERS)` çekerdim, sonra tabloyu `ANALYZE` edip `n_dead_tup`'a bakardım; taze istatistikle plan çoğu zaman geri döner. `sessions` doğası gereği çok yazılan bir tabloysa, autovacuum'unu tabloya özel ayarlar ve aktif-oturum predicate'i için partial/covering index eklerdim.

## İlgili Yazılar

- [Heap erişimini önlemek ve index-only scan almak için covering index'e (INCLUDE) ne zaman geçmeliyim?](https://muhammetsafak.com/tr/sor-bakalim/index-only-scan-covering-indexe-ne-zaman-gecmeliyim/) — Sor Bakalım
- [Sadece 'pending' satırları taranan bir kuyruk tablosunda partial index kullanmalı mıyım?](https://muhammetsafak.com/tr/sor-bakalim/sadece-pending-satirlari-taranan-bir-kuyruk-tablosunda-partial-index-kullanmali-miyim/) — Sor Bakalım
- [PgBouncer'da Session/Transaction/Statement modlarından hangisini seçmeliyim?](https://muhammetsafak.com/tr/sor-bakalim/pgbouncer-session-transaction-statement-modu-ve-prepared-statement/) — Sor Bakalım
