Mert Özen Her Satırda Daha İleri
Veritabanı PostgreSQL — #17

PostgreSQL 19 Yenilikleri: REPACK CONCURRENTLY, Sequence Replikasyonu, WAIT, ON CONFLICT DO SELECT ve Davranış Değişiklikleri

Mert Özen 15 Kas 2026 11 dk 6 görüntülenme
PostgreSQL 19 Yenilikleri: REPACK CONCURRENTLY, Sequence Replikasyonu, WAIT, ON CONFLICT DO SELECT ve Davranış Değişiklikleri

PostgreSQL 19'un REPACK (CONCURRENTLY) komutu tabloyu VACUUM FULL gibi küçültür ama yazmaları durdurmaz: pgbench yükü altında VACUUM FULL saniyede 4 transaction'a düşürdü, REPACK trafiği sürdürdü. 19 RC 2 üzerinde ON CONFLICT DO SELECT, standby'da WAIT FOR LSN ile read-your-writes, sequence replikasyonu, pg_plan_advice, varsayılan değişiklikleri ve Npgsql/EF Core 11'de beklenenler.

Sürüm notu: Bu yazıdaki örnekler PostgreSQL 19'un ikinci RC'si (release candidate; kararlı sürümden önceki test sürümü), yani resmî postgres:19rc2 Docker image'ı (12 Kasım 2026), .NET 10 ve Npgsql 10.0.3 ile çalıştırılmıştır; 18 ile karşılaştırmalar 18.7 üzerindedir. Yazının yazıldığı gün (14 Kasım 2026) PostgreSQL 19'un GA (genel kullanıma açık kararlı sürüm) tarihi açıklanmamıştı; anlatılanlar resmî release notes taslağına ("as of 2026-10-09") ve RC 2'deki gerçek denemelere dayanıyor. Not: PostgreSQL 19 henüz kararlı sürüm değil; kararlı sürüm çıktığında bu yazı resmî sürüm notlarıyla karşılaştırılıp güncellenecek.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) gün içinde orders tablosunu sürekli güncelliyor. 08'de şişen bir tabloyu küçültmenin tek çekirdek yolunun VACUUM FULL olduğunu ve tabloyu kilitlediğini görmüştük. PostgreSQL 19 RC 2'de iki bağlantının saniyede binlerce UPDATE yaptığı bir pgbench yükü altında önce VACUUM FULL orders, sonra 19'un REPACK (CONCURRENTLY) orders komutunu çalıştırdık:

\set id random(1, 1000000)
UPDATE orders SET status = status WHERE order_id = :id;
pgbench -U bookstore_owner -n -c 2 -T 8 -P 1 -f /lab/17-writer.pgbench bookstore

İki komut da tabloyu baştan yazıyor ve süreleri neredeyse aynı. Yazma trafiği iki durumda nasıl etkilenir?

/* VACUUM FULL orders: 1485 ms */
progress: 3.0 s, 5303.6 tps, lat 0.187 ms stddev 0.088, 0 failed
progress: 4.0 s, 4.0 tps, lat 751.253 ms stddev 749.326, 0 failed
progress: 5.0 s, 10663.9 tps, lat 0.187 ms stddev 0.127, 0 failed
/* REPACK (CONCURRENTLY) orders: 1441 ms */
progress: 3.0 s, 10681.5 tps, lat 0.187 ms stddev 0.273, 0 failed
progress: 4.0 s, 9550.7 tps, lat 0.209 ms stddev 0.238, 0 failed
progress: 5.0 s, 13886.2 tps, lat 0.144 ms stddev 0.103, 0 failed

VACUUM FULL sırasında yazmalar durdu: o saniyede yalnızca 4 transaction tamamlandı ve gecikme 751 ms'ye çıktı. REPACK (CONCURRENTLY) sırasında ise trafik biraz yavaşladı ama durmadı. Bu, serinin son yazısının ilk konusu. Ardından ON CONFLICT DO SELECT, standby'da WAIT komutu, sequence replikasyonu, autovacuum ve planner yenilikleri, davranış değişiklikleri ve Npgsql/EF Core 11 tarafında beklenenler geliyor.

REPACK ve REPACK (CONCURRENTLY)

19, VACUUM FULL ve CLUSTER'ın yaptığı işi tek bir komutta topluyor: REPACK tabloyu dead tuple'lar olmadan yeni bir dosyaya yazıyor, USING INDEX verilirse satırları o index'in sırasına diziyor. Eski iki komut uyumluluk için duruyor. 08'deki denemeyi tekrarladık:

docker exec -it bookstore-pg19 psql -U bookstore_owner -d bookstore -f /lab/17-repack.sql
 fresh                      2040 kB
 after_update_and_vacuum    4080 kB
 after_repack               2040 kB
 after_repack_concurrently  2048 kB

Tüm satırları güncelleyip VACUUM çalıştırınca tablo 4080 kB'ta kaldı; REPACK onu 2040 kB'a indirdi. 08'deki VACUUM FULL gibi REPACK da n_dead_tup sayacını sıfırlamıyor (80.000 kaldı); bu sayaca bakan bir izleme, tablonun hâlâ şişkin olduğunu sanabilir.

CONCURRENTLY seçeneği işin büyük kısmını SHARE UPDATE EXCLUSIVE lock ile yapıyor; bu lock okuma ve yazmayı engellemiyor. Kopyalama sürerken gelen değişiklikleri logical decoding (WAL'daki değişiklikleri satır düzeyinde okuma mekanizması) ile yakalıyor ve en sonda kısa bir ACCESS EXCLUSIVE lock alarak dosyaları değiştiriyor. Tahmindeki 4 tps'lik saniye bu yüzden yaşanmadı. Kullanmadan önce bilinmesi gerekenler dokümanda açık: tabloda primary key ya da index tabanlı replica identity olmalı, işlem bir replication slot kullanıyor (max_repack_replication_slots, varsayılan 5), transaction bloğu içinde çalışmıyor ve MVCC (Multi-Version Concurrency Control; satırın birden çok sürümünü tutarak okumayı yazmadan ayıran mekanizma, 08) açısından güvenli değil: REPACK başlamadan önce snapshot almış uzun bir transaction, iş bittikten sonra tabloyu boş görebilir. 08'de önerdiğimiz üçüncü parti pg_repack extension'ının çekirdekteki karşılığı bu; ilerlemesi pg_stat_progress_repack view'ında görünüyor.

ON CONFLICT DO SELECT: Get-or-Create

Sepet servisi, müşterinin sepeti varsa onu döndürmek, yoksa oluşturmak istiyor. 18'e kadar bunun temiz bir yolu yoktu; DO NOTHING var olan satırı döndürmüyor:

INSERT INTO carts (customer_id) VALUES (42)
ON CONFLICT (customer_id) DO SELECT
RETURNING cart_id, customer_id, old.cart_id IS NULL AS created;
 cart_id | customer_id | created
---------+-------------+---------
       1 |          42 | t

/* aynı müşteri, ON CONFLICT DO NOTHING RETURNING cart_id */
 cart_id
---------
(0 rows)

/* aynı müşteri, ON CONFLICT DO SELECT FOR UPDATE */
 cart_id | customer_id | created
---------+-------------+---------
       1 |          42 | f

İlk çağrı sepeti oluşturdu, üçüncüsü var olanı döndürdü ve FOR UPDATE ile kilitledi. 16'daki RETURNING OLD/NEW burada da işe yarıyor: old.cart_id IS NULL satırın yeni mi olduğunu söylüyor. 18'deki alternatifler ya DO NOTHING sonrası ikinci bir SELECT (iki sorgu ve arada yarış) ya da DO UPDATE SET customer_id = excluded.customer_id hilesiydi; ikincisi her çağrıda satırı gerçekten güncelliyor, dead tuple ve WAL üretiyor, trigger'ları çalıştırıyor. Bir ayrıntı: komut etiketi var olan satır için de INSERT 0 1 diyor, yani etkilenen satır sayısına bakan kod yeni kayıt eklendiğini sanabilir; ayrımı RETURNING'den yap.

WAIT FOR LSN: Standby'da Read-Your-Writes

Okumaları replica'ya yönlendiren bir uygulamada kullanıcı fiyatı değiştirir ve sayfa yenilendiğinde eski fiyatı görür: asenkron replica henüz o değişikliği uygulamamıştır. Veritabanı Performansı ve Scaling serisinin read/write splitting yazısı bu sorunu ve uygulama tarafındaki çözümleri anlatıyor. 19, standby'a "şu LSN'e (Log Sequence Number; WAL'daki bir konumun numarası) kadar uygulayana dek bekle" diyen WAIT komutunu getiriyor. db/17-standby.ps1 bir primary ve replikasyonu 3 saniye geciktirilmiş (recovery_min_apply_delay) bir standby kuruyor. .NET tarafı:

newPrice = (decimal)(await update.ExecuteScalarAsync())!;
// UPDATE commit edildi (autocommit); şu anki WAL konumu commit kaydıyla aynı ya da ondan ileride
await using NpgsqlCommand lsn = new("SELECT pg_current_wal_lsn()", connection);
commitLsn = (NpgsqlLogSequenceNumber)(await lsn.ExecuteScalarAsync())!;
...
// WAIT bir utility komutu, parametre almıyor; LSN kullanıcıdan değil sunucudan geliyor
await using (NpgsqlCommand wait = new(
    $"WAIT FOR LSN '{commitLsn}' WITH (TIMEOUT '10s', NO_THROW)", replica))
dotnet run --project src/Bookstore.PgLab -- read-your-writes
primary: price = 40.00, commit LSN 0/5001F208
standby, no wait: price = 39.00
standby, WAIT FOR LSN: success after 2980 ms
standby, after wait: price = 40.00

Beklemeden okunan fiyat eski; WAIT 2,98 saniye bekleyip success döndü ve sonraki okuma yeni fiyatı gördü. NO_THROW olmadan zaman aşımı hata, NO_THROW ile timeout durumu dönüyor; komutu primary'de çalıştırırsan recovery is not in progress hatası alıyorsun:

 status
---------
 timeout

ERROR:  timed out while waiting for target LSN 0/500211C0 to be replayed; current standby_replay LSN 0/50021198

Pratikte LSN'i yazma isteğinin cevabıyla istemciye (cookie, header ya da oturum) taşır, sonraki okumada replica'da WAIT ile beklersin; zaman aşımında primary'den okumak makul bir geri dönüş. Dokümanın önemli bir uyarısı var: WAIT'i transaction'ın başında, lock alan bir komuttan önce çalıştır; aksi hâlde replay'i bekleten bir döngü oluşabileceği için reddediliyor. Bu deneme Npgsql 10.0.3 ile, komut düz SQL olarak gönderilerek yapıldı.

Logical Replication: Sequence'ler ve Restart Gerektirmeyen Açılış

Logical replication ile başka bir sunucuya geçerken klasik sorunlardan biri sequence'lerdi: tablolar kopyalanır, ama sequence değerleri kopyalanmaz ve geçişten sonra ilk INSERT duplicate key hatası verir. 19'da publication'a sequence'ler eklenebiliyor. Önceki denemenin standby'ını promote edip ayrı bir sunucu yaptık ve primary'nin sequence'lerine abone ettik:

CREATE PUBLICATION bookstore_sequences FOR ALL SEQUENCES;                 /* primary */
CREATE SUBSCRIPTION bookstore_sequences CONNECTION 'host=bookstore-pg19 dbname=bookstore user=postgres password=<password>'
    PUBLICATION bookstore_sequences;                                       /* yeni sunucu */
ALTER SUBSCRIPTION bookstore_sequences REFRESH SEQUENCES;                  /* sonraki eşitlemeler */
primary   orders_order_id_seq.last_value  1000005
subscriber, before CREATE SUBSCRIPTION     1000000
subscriber, after CREATE SUBSCRIPTION      1000005
primary, three more orders                 1000008
subscriber, before REFRESH SEQUENCES       1000005
subscriber, after REFRESH SEQUENCES        1000008

Sequence'ler sürekli akmıyor; abonelik oluşturulurken ve REFRESH SEQUENCES ile eşitleniyor. Geçiş planında bu komut, uygulamayı yeni sunucuya çevirmeden hemen önceki adım olmalı. İkinci yenilik aynı denemede görünüyor: wal_level replica iken effective_wal_level başta replica idi, abonelik primary'de logical bir slot oluşturunca restart olmadan logical oldu. 18'e kadar logical replication'ı açmak wal_level = logical ve restart demekti. Upgrade ve geçiş senaryolarının ayrıntısı Veritabanı Performansı ve Scaling serisinde.

Autovacuum ve Planner: Paralel Autovacuum, Skor Sistemi ve pg_plan_advice

Autovacuum artık bir tablonun index'lerini paralel worker'larla temizleyebiliyor; ama autovacuum_max_parallel_workers varsayılan olarak 0, yani açmak bilinçli bir karar. Hangi tablonun önce işleneceğini de yeni bir skor sistemi belirliyor ve pg_stat_autovacuum_scores view'ı her tablonun skorunu (xid_score, vacuum_score, analyze_score…) gösteriyor; 08'deki "autovacuum neden bu tabloya gelmedi" sorusunun cevabı burada.

pg_plan_advice contrib modülü planner'ın kararlarını okunabilir bir "advice" diliyle gösteriyor ve istersen sabitliyor. Modülü LOAD ile yüklemek superuser istiyor (owner rolüyle access to library "pg_plan_advice" is not allowed aldık):

LOAD 'pg_plan_advice';
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT b.title, count(*) FROM order_items oi JOIN books b ON b.book_id = oi.book_id
WHERE oi.order_id BETWEEN 1 AND 100 GROUP BY b.title;
         ->  Nested Loop
               ->  Index Scan using ix_order_items_order_id on order_items oi
               ->  Index Scan using books_pkey on books b
 Generated Plan Advice:
   JOIN_ORDER(oi b)
   NESTED_LOOP_PLAIN(b)
   INDEX_SCAN(oi public.ix_order_items_order_id b public.books_pkey)
   NO_GATHER(oi b)

/* SET pg_plan_advice.advice = 'HASH_JOIN(b)'; */
         ->  Hash Join
               Hash Cond: (oi.book_id = b.book_id)
 Supplied Plan Advice:
   HASH_JOIN(b) /* matched */

07'deki gibi bir sorgunun planı veri değiştikçe bozuluyorsa, iyi planın advice'ını alıp sabitlemek mümkün; 19 ile gelen pg_stash_advice extension'ı bunu sorgu ID'sine göre otomatik uyguluyor. Doküman uyarıyor: veri dağılımı değişince planner'ın uyum sağlama imkânını elinden alırsın; bu bir son çare, istatistik ve index düzeltmesinin yerine geçmez.

Davranış Değişiklikleri

Upgrade'de seni etkileyecek varsayılanlar, 18.7 ve 19 RC 2'de aynı sorguyla:

Ayar / davranış18.719 RC 2
jitonoff
default_toast_compressionpglzlz4
log_lock_waitsoffon
max_locks_per_transaction64128
io_workers3kaldırıldı; io_min_workers 2, io_max_workers 8
json_array(SELECT ... WHERE false)NULL[]
effective_wal_levelyokreplica (gerekince logical)

JIT (just-in-time compilation; sorgu ifadelerini çalışma anında makine koduna derleme) kapatıldı, çünkü ne zaman devreye gireceğine karar veren maliyet hesabı güvenilmez bulundu; büyük analitik sorgular çalıştıran sistemler onu elle açmalı. max_locks_per_transaction'ın 128 olması daha fazla kapasite demek değil: lock tablosunun hesaplanma şekli değişti ve eski ayarı korumak için değeri iki katına çıkarmak gerekiyor. 09'da 18'de kapalı olduğunu söylediğimiz log_lock_waits artık açık. json_array()'in sonucuna IS NULL ile bakan kod 19'da farklı davranır. 11'deki COPY'nin JSON çıktısı da geldi: COPY ... TO STDOUT (FORMAT json) her satırı bir JSON nesnesi olarak yazıyor, FORCE_ARRAY ile tek bir array.

Kaldırılanlar: RADIUS (merkezi kimlik doğrulama protokolü) desteği; yalnızca UDP (bağlantısız, şifresiz taşıma protokolü) üzerinden çalışıyordu ve güvenli hâle getirilemiyordu; standard_conforming_strings = off artık mümkün değil ve böyle alınmış eski dump'lar 19'a doğru yüklenmiyor, dump'ı 19'un pg_dump'ı ile al. md5 parolayla başarılı bir girişten sonra artık uyarı veriliyor (14). Full-text search ve pg_trgm karakter sınıflandırmasında veritabanının locale provider'ını kullanmaya başladı ve Türkçe dahil birkaç stemmer güncellendi; 10'daki kelimelerde (kasaba, öğretmenler, aşkı…) fark görmedik, ama notlar upgrade sonrası bu index'leri yeniden oluşturmayı ve saklanan tsvector'ları yeniden hesaplamayı öneriyor.

Npgsql ve EF Core 11 Tarafında Beklenenler

Bu yazının yazıldığı gün NuGet'teki son Npgsql 10.0.3; Npgsql 11 geliştirme aşamasında ve henüz NuGet'te yok. Taslak sürüm notları multiplexing modunun kaldırılacağını ve senkron API'lerin (Open, ExecuteNonQuery) varsayılan olarak hata verecek şekilde deprecate edilmesinin değerlendirildiğini söylüyor; serideki bütün örnekler zaten async. Npgsql.EntityFrameworkCore.PostgreSQL 11 ise 11.0.0-rc.3 aşamasında. Notlarına göre gelecek olanlar: varsayılan hedef PostgreSQL 14'ten 16'ya çıkıyor; 04 ve 16'da gördüğümüz temporal key'ler WithoutOverlaps() ve WithPeriod() ile modellenebilecek; SaveChanges sırasındaki 40001 ve 40P01 hataları (09) DbUpdateConcurrencyException olarak gelecek; ToJson() ile eşlenen JSON üyelerine index tanımlanabilecek (12'deki Details.Pages alıştırması gibi). rc.3 notlarında PostgreSQL 19'a özgü bir madde yok. Bunlar sürüm adayı bilgisi; GA'da değişebilir. Bu yazıdaki WAIT ve REPACK gibi komutlar düz SQL olduğu için Npgsql 10 ile de çalışıyor.

Üretimde Dikkat

19'a GA çıkmadan production'da geçme; RC, test ortamında uygulamanın testlerini ve izleme sorgularını denemek içindir. GA çıktığında 15. yazıdaki yöntemle bir deneme dosyası hazırla ve yukarıdaki tabloyu kendi sisteminde doğrula.

REPACK (CONCURRENTLY)'yi büyük bir tabloda ilk kez çalıştırmadan önce disk alanını hesapla: tablo ve bütün index'leri ikinci kez yazılıyor, değişiklikler ayrıca diske tutuluyor. Uzun transaction'ların bittiğinden emin ol.

JIT'e güvenen raporlama sorguların varsa upgrade sonrası sürelerini ölç; gerekiyorsa jit = on'u yalnızca o rol ya da veritabanı için aç.

Sık Düşülen Tuzaklar

Beta haberlerinde okunan özellikleri 19'da var sanmak: GROUP BY ALL ya da SQL/PGQ gibi betada konuşulan maddeler RC notlarında yok. Kaynak olarak yalnızca resmî release notes'u kullan.

REPACK (CONCURRENTLY)'yi eski snapshot'lar için güvenli sanmak: o sırada açık ve eski snapshot kullanan bir transaction tabloyu boş görebilir.

Etkilenen satır sayısına bakarak ON CONFLICT DO SELECT'in satır eklediğini düşünmek: komut etiketi iki durumda da INSERT 0 1.

WAIT'i lock almış bir transaction'ın ortasında çağırmak: reddedilir. Okumadan önce, transaction'ın ilk komutu olarak çağır.

Küçük Bir Deneme

★☆☆ COPY (SELECT ...) TO STDOUT (FORMAT json) ile Bookstore'un ilk on kitabını dışa aktar ve jsonb kolonu olan bir tabloda null değerlerinin nasıl göründüğüne bak.

★★☆ Standby'ı recovery_min_apply_delay = '3s' ile kur, read-your-writes komutunu TIMEOUT '1s' ile çalıştır ve zaman aşımında primary'den okuyan bir geri dönüş ekle.

★★★ REPACK (CONCURRENTLY) orders çalışırken başka bir oturumda, REPACK'ten önce başlamış bir REPEATABLE READ transaction'ında SELECT count(*) FROM orders çalıştır. REPACK bittikten sonra aynı transaction'da sayı ne oluyor? Dokümanın MVCC uyarısını kendin gör.

Sonraki yazılar PostgreSQL'i günlük işte kullanmaya dönüyor: string, regex ve biçimlendirme function'larıyla başlayıp function, procedure, trigger ve psql ile devam ediyoruz. Replication, partitioning ve connection pooling gibi ölçek konuları Veritabanı Performansı ve Scaling serisinde devam ediyor.