# Partial index on beş dakikada üç yüz beş kat şişti — ve autovacuum bir kez bile gelmedi

> Aynı kuyruk tablosu on beş dakika churn altında, on beş saniyede bir örneklendi: partial index'in bloat eğrisi ve autovacuum ona yetişiyor mu?

- Tür: Ölçüm
- Soru: Sürekli churn altındaki bir kuyruk tablosunda partial index'in küçüklüğü kalıcı mı, ve varsayılan autovacuum ayarları ona yetişiyor mu?
- Bulgu: Canlı küme yaklaşık 845. saniyeye kadar beş bin satırda sabit dururken partial index 0,125 MB'dan 38,2 MB'a çıktı — üç yüz beş kat. Küçüklüğü canlı satır sayısından geliyor, şişme hızı iş hacminden, ve ikisi arasında hiçbir bağ yok. Composite index oransal olarak daha az şişti (%42) ama mutlak olarak daha çok (+126 MB) ve şişerken çöktü — büyük olasılıkla belleğe sığmadığı için, ki bu ölçümde nedeni ölçülmedi: gecikmesi 0,52 ms'den 61 saniyeye çıktı, kuyruğu 126 bine tırmandı. On beş dakikada 1,75 milyon ölü satır birikti ve autovacuum **bir kez bile koşmadı** — varsayılan eşik tablonun tamamına göre ölçekleniyor (50 + 0,2 × 10 milyon ≈ 2 milyon), churn ise küçük canlı kümede oluyor.
- Yöntem: Partial index ölçümüyle aynı tezgâh, aynı Postgres 17 ayarları, aynı 10 milyon ölü + 5.000 canlı satırlık tablo. Üç fark var, üçü de bu soruyu sorabilmek için zorunlu. Tüketici satırı `pending`'e geri koymuyor, `done` işaretliyor — sınanan iddia satırın index'ten DÜŞMESİYLE ilgili ve hiç düşmeyen satırda ölçülemez. Yanında sabit varış hızında (2.000/sn) bir üretici koşuyor, yani tablo önceki kaydın eksik ilan etmek zorunda kaldığı insert trafiğini de görüyor. Ve talep, `\gset` yerine alt sorgulu `UPDATE` biçiminde: kısıtsız bir tüketici kuyruğu bir saniyenin altında boşaltıyor ve `\gset` boş sonuçta istemciyi öldürüyor. Tüketici de üretici hızına bağlandı, böylece kuyruk derinliği sabit kalıyor — ve sabit kaldığı örneklenerek doğrulanıyor, varsayılmıyor. On beş saniyede bir tablo boyutu, index boyutu, kuyruk derinliği, ölü satır sayısı ve autovacuum sayaçları kaydedildi; strateji başına 60 örnek.
- Metrikler: Partial index, 15 dakikada: 0,125 → 38,2 MB (305×) · Composite index, aynı sürede: 301 → 427 MB (+%42) · Autovacuum koşma sayısı: 0 · Composite gecikmesi, 900. saniye: 61.533 ms
- Ölçüm tarihi: 2026-08-22
- Güven: Yüksek güven
- Durum: Yürürlükte
- Program: Veritabanı & sorgu
- Ortam: Postgres 17-alpine · shared_buffers 1 GB · autovacuum varsayılan ayarlarda · Tablo 10M ölü + 5.000 canlı satır · 1.421 MB başlangıç · Yük üretici 2.000/sn sabit · tüketici 8 istemci, aynı hızda · Süre strateji başına 900 sn · 15 saniyede bir örnek · 60 örnek · Donanım Apple M4 Pro · 12 çekirdek · 24 GB · Docker Desktop 29.7.2
- Teknolojiler: PostgreSQL, SQL, pgbench, Docker
- Tekrarlamak için: DURATION=900 ./bench/endurance.sh
- Kaynak kodu: https://github.com/muhammetsafak/pg-queue-bench
- Ham veri: https://github.com/muhammetsafak/pg-queue-bench/blob/main/results/2026-08-22/endurance-partial.jsonl
- Ham veri lisansı: https://github.com/muhammetsafak/pg-queue-bench/blob/main/LICENSE
- Yayın: 2026-08-23
- Kaynak: https://muhammetsafak.com/tr/research/kuyruk-tablosunda-bloat-ve-autovacuum/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
[Partial index ölçümü](/tr/research/partial-index-kuyruk-tablosu/) otuz saniyelik
pencerelerle çalıştı ve sınırlar kutusunda şunu yazdı: *"Autovacuum açık bırakıldı; 30 saniyelik koşular
onun uzun vadeli etkisini ölçmeye yetmez, bu ayrı bir kayıt konusu."*
Bu o kayıt.

Sınanan iddia, Sor Bakalım cevabının tek ölçülmemiş maddesiydi: *"Bir satır
pending'den done'a döndüğünde partial index'ten düşer — bu iyi. Ama yoğun update
trafiği bloat üretebilir; autovacuum'un sağlıklı çalışması önemli. İyi haber:
index küçük olduğu için vacuum'u da ucuzdur."*

## Bloat eğrisi

**Index boyutu, sürekli churn altında**

Kuyruk derinliği yaklaşık 845. saniyeye kadar beş bin satır civarında sabit. Büyüyen tek şey ölü index girdileri.

Kaynak: pg_relation_size, 15 saniyede bir örnek, bench/endurance.sh

|  | 0 sn | 135 | 286 | 436 | 587 | 738 | 888 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| partial | 0.1 MB | 5.9 MB | 12.4 MB | 18.8 MB | 25.3 MB | 31.7 MB | 38.2 MB |
| composite | 301 MB | 321 MB | 344 MB | 366 MB | 388 MB | 409 MB | 427 MB |

Partial index **üç yüz beş kat** büyüdü. Composite **%42**. Mutlak artış
composite'te daha fazla (+126 MB), oransal olarak partial'da karşılaştırılamaz.

Sebep tek cümlede duruyor: `pending → done` geçişi satırı partial index'ten
düşürüyor, ama ölü girdi vacuum gelene kadar orada kalıyor. Yani index'in
**küçüklüğü** canlı satır sayısından, **şişme hızı** iş hacminden geliyor — ve
ikisi arasında hiçbir bağ yok. Beş bin satırlık bir kuyruk, saniyede iki bin iş
işlerken, on beş dakikada otuz sekiz megabaytlık ölü girdi biriktiriyor.

> **Sonuç**
>
> Cevabın "index küçük olduğu için vacuum'u da ucuzdur" cümlesi doğru ama eksik.
> Vacuum ucuz — ama **sık** gerekiyor, çünkü bloat oranı index boyutuna değil
> throughput'a bağlı. Küçük index, az vacuum demek değil; ucuz vacuum demek.

## Autovacuum hiç gelmedi

On beş dakikada 1.753.949 ölü satır birikti. Autovacuum sayacı: **0.**

Aritmetik acımasız:

```
eşik = autovacuum_vacuum_threshold + scale_factor × canlı satır
     = 50 + 0,2 × 10.005.000
     ≈ 2.001.050 ölü satır
```

Eşiğin hemen altında kaldık. Varsayılan ayarlarla bu tablo autovacuum'u yaklaşık
**on yedi dakikada bir** tetikleyecek ve arada index'ler serbestçe şişecek.

Asıl mesele oran değil, **ölçekleme yönü**: eşik tablonun tamamına göre büyüyor,
churn ise küçük canlı kümede oluyor ve tablo büyüdükçe hızlanmıyor. Yani tablo ne
kadar büyükse autovacuum o kadar geç geliyor.

> **Sınır**
>
> Kuyruk tablosunun en çok vacuum'a ihtiyaç duyduğu an, varsayılan ayarların ona
> en az vacuum verdiği andır. `autovacuum_vacuum_scale_factor`'ı tablo bazında
> düşürmek (ör. 0,01) ya da `autovacuum_vacuum_threshold`'u sabit bir sayıya
> çekmek bu tabloya özgü bir ayar gerektiriyor — global varsayılan bu erişim
> deseni için tasarlanmamış.

## Şişen index sürdürülebilir yükü taşımıyor

Her iki koşu da saniyede 2.000 iş hedefiyle başladı.

| Strateji | Başlangıç | 900. saniye | Kuyruk derinliği |
| --- | --- | --- | --- |
| partial | 2.024 tps · 1,7 ms | 2.016 tps · 4.906 ms | 5.003 → 14.364 |
| composite | 2.000 tps · 0,52 ms | 1.204 tps · 61.533 ms | 5.000 → 126.024 |

Gecikme pgbench'in zamanlama gecikmesini içeriyor: hedeflenen hıza yetişemeyen bir istemcinin borcu buraya yazılıyor.

Composite hedefi tutamadı — 2.000'den 1.204 tps'ye düştü, gecikmesi 61 saniyeye
çıktı, kuyruğu 126 bine tırmandı. Partial hedef hızında bitirdi ama
gecikmesini koruyamadı: 855. saniye civarında o da geriye düşmeye başladı, 4,9
saniyelik gecikme ve 14.364'lük kuyrukla bitirdi.

Bu, otuz saniyelik ölçümün **tersi** yönde bir sonuç. Orada ikisi neredeyse eşitti
(10.795 vs 11.537 tps). Fark sürdürülebilir yükte açılıyor, büyük olasılıkla
427 megabayta şişmiş index artık belleğe sığmadığı ve taramalar diske gittiği için.
Bu koşu cache isabetlerini kaydetmedi, yani neden bir çıkarım; fark ise ölçülmüş.

> **Sınır**
>
> Ölçüm tek makinede, tek Postgres sürümünde ve tek varış hızında alındı. 2.000
> iş/sn bu donanımın tavanının çok altında bir hız — seçilme sebebi tüketicinin
> kuyruğu boşaltıp boş tablo ölçmesini engellemek. Daha yüksek hızlarda bloat daha
> hızlı birikir ve autovacuum eşiği daha erken gelir; buradaki "on yedi dakika"
> sayısı bu iş yüküne özgüdür, evrensel değil. Autovacuum bir kez de koşmadığı
> için vacuum'un **maliyeti** ölçülmedi; ölçülen, ne zaman geldiği.

## Cevaba dönüş

Sor Bakalım cevabının altı maddesinden beşincisi buydu ve tek sınanmayan oydu.
Sonuç: madde doğru ama uyarısı zayıf. "Autovacuum'un sağlıklı çalışması önemli"
cümlesi, varsayılan ayarlarla autovacuum'un bu tabloda **sağlıklı çalışmadığını**
söylemiyor.

Partial index'i seçmenin bedeli iki kalemde: ölü index girdileri iş hacmiyle
birikiyor, ve o birikimi temizleyecek mekanizma tablo bazında ayarlanmadıkça
gelmiyor.
