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

> Partial index ölçümü otuz saniyelik pencerelerle çalışmıştı — bloat ve vacuum hakkında hiçbir şey söyleyemeyecek kadar kısa. Bu kayıt aynı tabloyu on beş dakika koşturup on beş saniyede bir örnekliyor.

- 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 beş bin satırda sabit dururken partial index 0,1 MB'dan 38,2 MB'a çıktı — üç yüz seksen 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 belleğe sığmayı bıraktı: 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,1 → 38,2 MB (380×) · 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://www.muhammetsafak.com.tr/research/kuyruk-tablosunda-bloat-ve-autovacuum/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
[Partial index ölçümü](/research/partial-index-kuyruk-tablosu/) otuz saniyelik
pencerelerle çalıştı ve sınırlar kutusunda şunu yazdı: *"30 saniyelik koşular
autovacuum'un 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 koşu boyunca 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 seksen 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 hedefi tuttu.

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, çünkü 427 megabayta
şişmiş bir index artık belleğe sığmıyor ve her tarama diske gidiyor.

> **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.
