İçeriğe geç
Muhammet Şafak
en

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 otuz beş 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 otuz beş 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.

Yüksek güven Tekrarlı ölçüm, denetimli ortam, ham veri paylaşıldı.
Ölçüm tarihi

25 gün önce ölçüldü

Yayın

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

PostgreSQL SQL Docker

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 otuz beş 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
Tek oturum, tek hazırlanmış deyim, kırk çalıştırma. Sayaçlar her çalıştırmadan sonra okundu.

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

Paylaş:

Diğer Kayıtlar

Tüm kayıtlar

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

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

25 gün önce ölçüldü

Yüksek güven

Partial index kuyruk tablosunu kırk bir kat küçültüyor — planner onu seçtiği sürece

Milyonlarca ölü satır biriken bir Postgres kuyruk tablosunda partial index ne kazandırıyor, ve planner onu ne zaman seçmiyor?

Bulgu

10 milyon ölü satırda partial index saniyede 11.537 iş çıkarıyor, index'siz tablo 7. Composite index'le hız farkı küçük (%6,9) ama boyut farkı değil: 7,6 MB'a karşı 310,4 MB, ve partial tabloyla birlikte büyümüyor çünkü yalnız 5.000 canlı satırı indeksliyor. Asıl bulgu bunların hiçbiri: planner hazırlanmış bir deyimde generic plana geçtiği anda partial index tamamen devre dışı kalıyor — 11.752 tps 7'ye, 0,68 ms 1,1 saniyeye düşüyor. Bin altı yüz otuz beş kat. Aynı koşulda composite index etkilenmiyor.

25 gün önce ölçüldü

Yüksek güven

PHP ile Go'yu aynı OAuth2 API'de ölçtüm: 10.000 yazmada fark yok, 50.000 okumada var

Her istekte OAuth2 token'ı doğrulayıp PostgreSQL'e yazan ya da okuyan aynı API, dört çekirdekte PHP-FPM, FrankenPHP worker ve Go ile saniyede 10.000 yazmayı ve 50.000 okumayı kaç CPU'yla karşılıyor?

Bulgu

Saniyede 10.000 yazmada üç aday da hedefi beş tekrarın beşinde tutturdu, p99 üçünde de 2,5 ms'yi aşmadı: bu yükte dil bir kapasite kalemi değil. Saniyede 50.000 okumayı dört çekirdekte yalnız Go tutturdu (p99 6,45 ms); PHP-FPM 23.528'de, FrankenPHP 22.859'da kaldı. İstek başına CPU okumada Go'da 64, FrankenPHP'de 117, PHP-FPM'de 168 mikrosaniye. 50.000 okuma için instance hesabı Go'ya 3,4, iki PHP adayına 8,2–8,8 çekirdek yazdırıyor. FrankenPHP'nin CPU tasarrufu kapasiteye dönmüyor: doygunken dört çekirdeğin birini boşta bırakıyor.

bugün ölçüldü

Orta güven

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi