Postgres partial index'i generic plana çevirmedi: kırk çalıştırma, kırk custom plan
Hazırlanmış bir deyimde Postgres partial index'li sorguyu kendiliğinden generic plana çeviriyor mu — yoksa bin altı yüz yetmiş üç katlık uçuruma ancak elle mi düşülüyor?
Bulgu
Postgres geçişi reddediyor. Partial index'te kırk çalıştırmanın kırkı da custom plan — sayaç 40/0. Reddetmesinin sebebi tam olarak felaketin kendisi: generic plan partial index'i kullanamaz, bu yüzden tahmini maliyeti yüksek çıkar ve planner onu seçmez. Composite index ise ders kitabındaki gibi altıncı çalıştırmada geçiyor (5/35) ve hiçbir şey kaybetmiyor. Yani bin altı yüz yetmiş üç katlık uçurum gerçek ama çitli: ona düşmek için `plan_cache_mode = force_generic_plan` yazmak gerekiyor.
- Partial index, generic plan
- 0 / 40 çalıştırma
- Composite index, geçiş
- 6. çalıştırma
- Partial, ilk 5 → son 5
- 0,36 → 0,20 ms
- Uçuruma düşmek için gereken
- force_generic_plan
Yöntem
Aynı tezgâh, aynı 10 milyon ölü + 5.000 canlı satırlık tablo, üç index stratejisi. Her strateji için tek bir psql oturumunda tek bir hazırlanmış deyim kırk kez çalıştırıldı ve her çalıştırmadan sonra `pg_prepared_statements` sorgulandı — sayaçlar oturuma özgü olduğu için bu ölçüm pgbench'ten yapılamaz. Karar zamanlamadan çıkarsanmıyor, sayaçtan okunuyor: `custom_plans` ve `generic_plans` alanları planner'ın o çalıştırmada ne seçtiğini doğrudan söylüyor. Zamanlamalar çift geliyor (deyimin kendisi, sonra sayaç sorgusu) ve ayrıştırıcı yalnız ilkini alıyor. psql'in hizalı çıktısı sayaç değerini boşlukla dolduruyor; çıktı bu yüzden hizasız moda alındı — ilk koşu bu yüzden boş seri üretmişti.
- Ölçüm tarihi
- Yayın
dün ölçüldü
Ortam
- Postgres
- 17-alpine · plan_cache_mode varsayılan (auto)
- Tablo
- 10M ölü + 5.000 canlı satır
- Ölçüm
- tek oturum · tek hazırlanmış deyim · 40 çalıştırma
- Karar kaynağı
- pg_prepared_statements.custom_plans / generic_plans
- Donanım
- Apple M4 Pro · 12 çekirdek · 24 GB · Docker Desktop 29.7.2
Teknolojiler
Tekrarlamak için
EXECUTIONS=40 ./bench/plan-switch.sh Partial index ölçümü plan_cache_mode’u
iki uca sabitledi ve aralarında bin altı yüz yetmiş üç kat buldu: custom planda
11.752 tps, generic planda 7. Ölçmediği şey herkesin gerçekten koştuğu ayardı —
auto.
O kayıt orada bir cümle kurdu: “profil ısındıkça bozulur… staging’de görünmez.” Makul bir çıkarımdı ve yanlış çıktı.
Karar tahmin edilmedi, okundu
Postgres hazırlanmış bir deyimi ilk beş çalıştırmada custom planla koşar, sonra
generic planın tahmini maliyetini custom planların ortalamasıyla karşılaştırır
ve ancak generic daha ucuz görünüyorsa ona geçer. pg_prepared_statements bu
kararı sayaç olarak tutuyor, yani zamanlamadan çıkarsamaya gerek yok.
| Strateji | İlk generic plan | Son sayaç (custom/generic) | İlk 5 (ms) | Son 5 (ms) |
|---|---|---|---|---|
| partial geçiş reddedildi | hiç | 40 / 0 | 0,36 | 0,20 |
| composite | 6. çalıştırma | 5 / 35 | 0,40 | 0,21 |
| (status) | hiç | 40 / 0 | 0,98 | 0,78 |
Composite’in geçişi belgelendiği gibi, tam beşinciden sonra:
#5 0,266 ms custom=5 generic=0
#6 0,275 ms custom=5 generic=1 ← geçti
#7 0,185 ms custom=5 generic=2
Partial index’te bu hiç olmuyor. Kırk çalıştırma, kırk custom plan.
Reddetmesinin sebebi felaketin kendisi
Planner generic planı seçmiyor çünkü maliyetini doğru tahmin ediyor. Generic
plan $1’in ne olduğunu bilmez; partial index’i kullanabilmek için
$1 = 'pending' olduğunu kanıtlaması gerekir, kanıtlayamaz, dolayısıyla o plan
sequential scan’e düşer ve tahmini maliyeti tavan yapar. Custom planların
ortalaması bunun çok altında kalınca karşılaştırma her seferinde custom lehine
sonuçlanıyor.
Gerçek takas daha küçük ve başka yerde
Korumanın bir bedeli var: partial index’li sorgu her çalıştırmada yeniden planlanıyor. Kırk çalıştırma, kırk plan üretimi. Composite altıncıdan sonra aynı planı tekrar kullanıyor.
Bu ölçekte fark ölçülemiyor — 0,20 ms’ye karşı 0,21 ms. Ama plan üretimi ucuz bir
sorguda ucuz; çok tablolu bir join’de ya da uzun bir IN listesinde değil. Partial
index’in gizli maliyeti bloat (bkz. dayanıklılık ölçümü)
ve sürekli yeniden planlama; ikisi de bu iş yükünde küçük, ikisi de başka bir iş
yükünde küçük kalmayabilir.
Önceki kaydı düzeltiyor
Partial index kaydının “ısındıkça bozulan profil” paragrafı fazla ileri gitmişti ve o kayda bu bulguya işaret eden bir not düşüldü. Ölçümün kendi yayınını düzeltmesi bu bölümün sözleşmesinin gereği: bir sayı yayınlanıyorsa, onu yanlışlayan sayı da yayınlanır.
İlgili yazılar
Dual-write'tan outbox'a: idempotent tüketim ve alan-seviyesi şifreleme günlüğü
Aynı projede dual-write yüzünden kaybolan event'ler beni transactional outbox'a, at-least-once teslim de idempotent tüketime götürdü. Bir de GDPR alanlarını x-gdpr-sensitive ile satır seviyesinde şifreledim. Gerçek bir projeden notlar.