İçeriğe geç
Muhammet Şafak
en

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 yetmiş üç kat. Aynı koşulda composite index etkilenmiyor.

Partial index, 10M ölü satır
11.537 tps
Aynı tablo, index yokken
7 tps
Index boyutu, partial / composite
7,6 / 310,4 MB
Generic plana geçince
11.752 → 7 tps −1.673×

Yöntem

Postgres 17, tek konteyner, sabitlenmiş ayarlar (shared_buffers 1 GB, work_mem 64 MB, autovacuum açık — kapatmak sayıları güzelleştirir ve cevabı bozardı). `jobs` tablosunda canlı küme her kademede sabit 5.000 `pending` satır; ölü satır 100 bin, 1 milyon ve 10 milyon. Her boyut bir kez seed edilip **template veritabanı** olarak donduruluyor ve her koşu ondan kopyalanıyor: her stratejiyi yeniden seed etmek ölçümlerden uzun sürer ve her seferinde farklı karışmış bir tablo verir, yani sayılar stratejinin etkisini değil seed'in gürültüsünü taşırdı. Seed yüklerken `ORDER BY random()` ile karıştırıyor — sıralı tek geçiş, fiziksel düzeni `created_at`'e eşitleyip her index taramasını neredeyse sıralı okumaya çevirir ve dört stratejiyi de eşit biçimde kayırır. Tüketici pgbench: Postgres'le geliyor, gecikmeyi ortalama değil yüzdelikle veriyor, ve standart araç olduğu için tartışma tezgâha değil index'e kalıyor. 8 istemci, 30 saniye, üç tekrar, medyan. Talep edilen satır `done` yerine yeni bir `created_at` ile `pending`'e dönüyor: kuyruğunu kurutan bir tüketici koşunun ikinci yarısında boş tablo ölçerdi. Her koşuda throughput'un yanında `EXPLAIN` planı, index boyutu, gerçekleşen index tarama sayısı ve bırakılan ölü satır kaydediliyor — tek başına throughput, seçilmiş index ile yok sayılmış index'i ayırt edemez.

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

dün ölçüldü

Yayın
Güncelleme

Ortam

Postgres
17-alpine · shared_buffers 1 GB · work_mem 64 MB · autovacuum açık
Tablo
jobs · 5.000 canlı satır sabit · 100k / 1M / 10M ölü satır
Yük
pgbench · 8 istemci · 30 sn · FOR UPDATE SKIP LOCKED
Donanım
Apple M4 Pro · 12 çekirdek · 24 GB · macOS 26.6.1
Sanallaştırma
Docker Desktop 29.7.2 · aarch64
Tekrar
3 · medyan raporlanır
Determinizm
her koşu aynı template veritabanının kopyasından başlar

Teknolojiler

PostgreSQL SQL pgbench Docker

Tekrarlamak için

./bench/run.sh

Ağustos başında bir soru geldi: milyonlarca completed satırın arasında birkaç bin pending satırı sorgulayan bir kuyruk tablosunda partial index mi, düz index mi? “Evet, bu partial index’in ders kitabı örneğidir” diye cevapladım ve altı madde saydım. Hiçbirini ölçmemiştim.

Bu kayıt o cevabı sınıyor.

Üç kademe, dört strateji

Strateji 100k ölü satır 1M 10M Index boyutu (10M)
index yok 2.001 tps 247 tps 7 tps
(status) 6.499 tps 6.417 tps 6.426 tps 66,1 MB
(status, created_at) 12.414 tps 11.202 tps 10.795 tps 310,4 MB
partial (created_at) WHERE pending en hızlı ve en küçük 13.041 tps 11.707 tps 11.537 tps 7,6 MB
8 istemci, 30 saniye, üç tekrarın medyanı. Canlı küme her kademede 5.000 satır; değişen tek şey etrafındaki ölü satır sayısı.

İlk satır cevabın gerekçesini doğruluyor: index’siz bir kuyruk tablosu ölü satırla birlikte çökmüyor, yok oluyor. 2.001’den 7 tps’ye. 10 milyon satırın arasından 5.000 satır bulmak sequential scan ile saniyede yedi kez yapılabilir.

Düz (status) index’i de iddia edildiği gibi: kardinalitesi iki olan bir kolona index koymak yardım ediyor ama tavanı düşük — üç kademede de 6.400 civarında takılıyor.

Asıl fark hızda değil, boyutta

Partial ile composite arasında throughput farkı %6,9. Küçük. Ama boyut farkı tabloyla birlikte açılıyor:

Index boyutu, tablo büyüdükçe

Composite index bütün satırları indeksliyor, partial yalnız pending olanları. Canlı küme sabit olduğu için partial da sabit kalıyor.

  • (status, created_at)
  • partial

MB düşük olan iyi Kaynak: pg_relation_size, bench/run.sh

Veri tablosu
pg_relation_size, bench/run.sh
Seri 100k ölü satır1M10M
(status, created_at) 11,2 MB40,3 MB310,4 MB
partial 6,5 MB7,6 MB7,6 MB

Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.

Composite index tabloyla büyüyor: 11,2 → 40,3 → 310,4 MB. Partial 7,6 MB’da duruyor, çünkü indekslediği şey tablo değil kuyruk — ve kuyruk sabit. On milyon satırlık tabloda kırk bir kat fark.

Bu, cevabın “küçük olur, belleğe sığar, güncellemesi ucuzdur” cümlesinin sayısal karşılığı. 310 MB’lık bir index’i shared_buffers’da tutmak 7,6 MB’lık birini tutmakla aynı şey değil, ve her insert/update’te güncellenen ağacın boyutu da öyle.

Ve sonra planner fikrini değiştiriyor

Cevabın ikinci maddesi şuydu: status = $1 gibi parametreyle sorarsanız planner index’i seçemeyebilir; koşulu literal 'pending' olarak yazın.” Uyarıyı yazarken ne kadar ciddi olduğunu bilmiyordum.

10M ölü satır, parametreyle tps Ortalama gecikme Plan
partial · custom plan 11.752 0,68 ms Limit → LockRows → Index Scan
partial · generic plan index hiç taranmadı 7 1.113 ms Limit → LockRows → Sort → Seq Scan
composite · custom plan 11.105 0,72 ms Index Scan
composite · generic plan 11.416 0,70 ms Index Scan
Aynı tablo, aynı index, aynı sorgu. Değişen tek şey Postgres'in hazırlanmış deyim için genel mi yoksa o çağrıya özel mi plan kullandığı.

Bin altı yüz yetmiş üç kat. Partial index generic planda taranmıyor — index tarama sayacı sıfır — ve sorgu index’i hiç olmayan tablonun sayısına düşüyor: 7 tps. Gecikme 0,68 ms’den 1,1 saniyeye çıkıyor.

Composite index aynı koşulda hiç etkilenmiyor.

Mekanizma plan ağacında görünüyor. Generic plan, $1’in ne olduğunu bilmeden kurulur. Partial index’in kullanılabilmesi için planner’ın $1 = 'pending' olduğunu kanıtlaması gerekir — çünkü index yalnız o satırları içeriyor. Kanıtlayamaz, dolayısıyla index’i eleyip sequential scan’e düşer. Composite index’in kanıtlanacak bir predicate’i yoktur; status kolonu index’in içindedir ve $1 ne olursa olsun index taranabilir.

Bunun nasıl production’da patladığı

auto modunda Postgres ilk beş çalıştırmada custom plan kullanır, sonra generic planın maliyetini karşılaştırıp ona geçebilir. Yani bu profil ısındıkça bozulur: uygulama açılır, ilk istekler hızlıdır, worker’lar birkaç dakika çalıştıktan sonra aynı sorgu bin kat yavaşlar.

Staging’de görünmez, çünkü staging’de o deyim beş kez koşmadan test biter.

Sor Bakalım’da buna çok benzeyen başka bir soru vardı: staging’de Index Scan alan sorgu production’da neden Seq Scan’e düşüyor? Orada teşhisi bayat istatistik ve tablo bloat’u diye koymuştum. Bu ölçüm üçüncü bir sebep gösteriyor, ve bu sebebin ANALYZE ile ilgisi yok.

Cevabın karnesi

Sor Bakalım cevabındaki iddia Sonuç
Partial index küçük olur, belleğe sığar doğru — 7,6 MB, tabloyla büyümüyor
Düz (status) index'inden net üstün doğru — 11.537 vs 6.426 tps
ORDER BY kolonunu index'e koyun doğru — Index Scan sıralamayı bedavaya alıyor
Parametreyle sorarsanız planner seçemeyebilir doğru ama dar: sorun predicate'li index
Churn ve bloat'a dikkat ölçülmedi — 30 sn autovacuum için kısa
Altı maddenin dördü sınandı. Beşincisi kendi kaydını hak ediyor.

Bir tavsiyeyi ölçmenin değeri onu doğrulamak değil — dördü zaten doğruydu. Değeri, doğru olan bir cümlenin hangi koşulda tersine döndüğünü bulmakta.

İlgili yazılar

Paylaş:

Güncelleme:

Diğer Kayıtlar

Tüm kayıtlar

Partial index on beş dakikada üç yüz seksen 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,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.

dün ölçüldü

Yüksek güven

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.

dün ölçüldü

Yüksek güven

Laravel'in preload eğrisi: 123 dosya, 1.912 dosyadan sekiz kat fazla kazandırıyor

Laravel için elle seçilmiş bir preload nereye kadar iner, ve her dilim ne kadar açılış bedeline mal olur?

Bulgu

Eğri hacimle orantılı değil. İlk 1.592 dosya (Laravel çekirdeği) 30 ms kazandırıyor ve açılışa 1,2 saniye ekliyor. Sonraki 1.094 Symfony dosyası 9,5 ms kazandırıyor, bedava. Ondan sonraki **123 dosya** (psr, carbon) 15,7 ms kazandırıyor — kendinden önceki 1.094 dosyadan fazla. Ve son 1.912 dosya yalnız 1,8 ms kazandırıp açılışa 1,2 saniye daha yazıyor. Yani önceki kaydın tavan olarak ölçtüğü "hepsini derle", eğrinin başlangıç noktası dışındaki en kötü fiyat/performans bölgesi: 2.809 dosyada durmak 12,77 ms ve 1.514 ms açılış verirken, 4.721 dosya 10,96 ms için 2.691 ms istiyor.

dün ölçüldü

Yüksek güven

Sitede Ara

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

Esc ile kapat Pagefind ile güçlendirildi