PostgreSQL'de PITR (point-in-time recovery, belirli bir ana geri dönme), bir temel yedek (base backup) ile o yedekten sonra arşivlenen WAL dosyalarını birleştirerek veritabanını geçmişteki herhangi bir saniyeye geri getirme yöntemidir. Kurulum üç adımdan oluşur: archive_mode = on ve archive_command ile WAL arşivlemeyi açarsınız, pg_basebackup ile temel yedek alırsınız, felaket anında yedeği açıp recovery.signal dosyası ve recovery_target_time ayarıyla sunucuyu başlatırsınız.
Bu yazıda bu üç adımı PostgreSQL 18.1 üzerinde baştan sona uyguladık: bir tabloyu "yanlışlıkla" boşalttık ve silmeden hemen önceki ana döndük. Ayrıca PostgreSQL 17 ve sonrası sunucularda kullanılabilen artımlı (incremental) yedeği ve pg_combinebackup aracını, zaman çizelgelerini (timeline) ve sık yapılan hataları anlatıyoruz. Mantıksal yedek alan pg_dump için pg_dump ve pg_restore, yedekleme stratejisi için veritabanı yedekleme stratejileri yazılarına bakabilirsiniz.
WAL nedir ve neden yedeğe yarar?

WAL (write-ahead log, önden yazmalı günlük) PostgreSQL'in veri bütünlüğü mekanizmasıdır. Kural basittir: bir tablo ya da indeks dosyasındaki değişiklik diske yazılmadan önce, o değişikliği anlatan WAL kaydı kalıcı belleğe yazılmış olmalıdır. Böylece her commit'te bütün veri sayfalarını diske yazmak gerekmez; sunucu çökerse son checkpoint'ten sonraki WAL kayıtları yeniden oynatılarak (redo) veri tutarlı hale getirilir. WAL sıralı yazıldığı için bu, rastgele sayfa yazmaktan çok daha ucuzdur.
WAL veri dizinindeki pg_wal/ klasöründe, varsayılan olarak 16 MB'lık segment dosyaları halinde tutulur. Arşivleme kapalıyken sunucu birkaç segmenti döngüsel olarak yeniden kullanır. Arşivleme açıldığında ise dolan her segment, yeniden kullanılmadan önce sizin belirlediğiniz bir yere kopyalanır. Elinizde bir dosya sistemi yedeği ve o andan sonraki bütün WAL segmentleri varsa, yedeği açıp WAL'ı istediğiniz noktaya kadar oynatabilirsiniz. Yöntemin güçlü yanları kılavuzda şöyle sıralanıyor: temel yedeğin kusursuz bir anlık görüntü olması gerekmez, çünkü tutarsızlıkları WAL oynatması düzeltir; WAL arşivlemeyi sürdürerek sürekli yedek elde edersiniz; oynatmayı istediğiniz anda durdurabilirsiniz.
Bedeli de var. PITR yalnızca kümenin tamamını geri getirir, tek bir veritabanını ya da tabloyu değil. Temel yedek büyüktür ve yoğun bir sunucu çok miktarda WAL üretir. Ayrıca pg_dump ve pg_dumpall çıktıları mantıksal yedek oldukları için bu yöntemin parçası olarak kullanılamaz; WAL oynatması dosya düzeyinde bir başlangıç noktası ister.
| Özellik | pg_dump | Temel yedek + WAL arşivi (PITR) |
|---|---|---|
| Kapsam | Tek veritabanı, seçilebilir nesneler | Bütün küme |
| Geri dönülebilecek an | Yalnızca yedeğin alındığı an | Temel yedeğin bitişinden sonraki herhangi bir an |
| Farklı ana sürüme yükleme | Genellikle mümkün | Hayır, aynı ana sürüm gerekir |
| Gereken yetki | Okuma yetkisi | REPLICATION yetkisi veya süper kullanıcı |
| Yapılandırma | Gerekmez | archive_mode, archive_command, wal_level |
Adım 1: WAL arşivlemeyi açmak
Arşivleme için üç ayar gerekir: wal_level en az replica olmalı (bu zaten varsayılandır), archive_mode = on yapılmalı ve archive_command ile tamamlanan segmenti kopyalayacak kabuk komutu (ya da archive_library ile bir arşiv modülü) verilmeli. Komutta %p arşivlenecek dosyanın yolu, %f yalnızca dosya adı ile değiştirilir. Kılavuzdaki Unix ve Windows örnekleri şunlar:
# Unix
archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
# Windows
archive_command = 'copy "%p" "C:\\server\\archivedir\\%f"'Biz test kümemizde Windows biçimini kullandık ve sunucuyu yeniden başlattık (archive_mode ve wal_level yalnızca sunucu başlarken okunur; archive_command ise yeniden yüklemeyle değişebilir). Klasör yolunu okunabilirlik için kısalttık:
archive_mode = on
archive_command = 'copy "%p" "C:\\pgtest\\arsiv\\%f"'
summarize_wal = onArşivlemenin çalıştığını sınamanın en kolay yolu, pg_switch_wal() ile sunucuyu yeni bir segmente geçmeye zorlamak ve pg_stat_archiver görünümüne bakmaktır:
SELECT pg_switch_wal();
SELECT archived_count, last_archived_wal, failed_count FROM pg_stat_archiver; archived_count | last_archived_wal | failed_count
----------------+--------------------------+--------------
4 | 000000010000000000000005 | 0
(1 row)failed_count sıfırdan büyükse komut başarısız oluyordur ve sunucu aynı dosyayı aralıklarla tekrar dener. Bu sürede arşivlenemeyen segmentler pg_wal/ klasöründe birikir; disk dolarsa sunucu PANIC ile kapanır. Commit edilmiş veri kaybolmaz ama yer açılana kadar veritabanı çalışmaz. Bu yüzden arşivlemeyi izlemek kurulumun ayrılmaz parçasıdır.
Arşiv komutu için kılavuzun koyduğu kurallar şöyle özetlenebilir:
- Komut yalnızca gerçekten başarılı olduğunda 0 döndürmelidir. Sunucu 0 gördüğü anda dosyayı arşivlenmiş kabul eder ve geri dönüştürür.
- Var olan bir arşiv dosyasının üzerine yazmamalıdır. Unix örneğindeki
test ! -fadımı bunun içindir; iki farklı sunucunun aynı klasöre arşivlemesi gibi yönetici hatalarına karşı korur. Kılavuz, GNUcp -iseçeneğinin hedef dosya varken de 0 döndürdüğünü, bu yüzden ona güvenilmemesi gerektiğini not ediyor. - Aynı dosya içerikleri birebir aynı olarak yeniden arşivlenmek istenirse 0, içerik farklıysa sıfırdan farklı bir kod döndürmelidir. Çökme sonrası yeniden arşivleme nadiren de olsa olur.
- Arşivde WAL dosyalarının adı korunmalıdır; yolun korunması gerekmez. Dosya adları en fazla 64 karakterdir.
- Arşivlenen WAL veritabanındaki her şeyi içerir. Arşiv klasörünü başkalarının okuyamayacağı şekilde koruyun.
Az WAL üreten sunucularda bir segmentin dolması uzun sürebilir; bu sürede yapılan commit'ler henüz arşivde değildir. archive_timeout sunucuyu belirli aralıklarla yeni segmente geçmeye zorlar. Kılavuz bir dakika civarındaki değerleri makul buluyor; erken kapanan segmentler de tam 16 MB yer kapladığı için çok kısa değerler arşivi şişirir. Verinin daha hızlı karşıya geçmesi gerekiyorsa streaming replication daha uygun bir araçtır. Unutmayın, WAL arşivi postgresql.conf, pg_hba.conf gibi elle düzenlenen yapılandırma dosyalarını içermez; onları ayrıca yedeklemelisiniz.
Adım 2: pg_basebackup ile temel yedek
Arşivleme çalıştıktan sonra temel yedeği alırsınız. pg_basebackup sunucuya replikasyon protokolüyle bağlanır; kullanıcının REPLICATION yetkisi ya da süper kullanıcı olması, pg_hba.conf'un replikasyon bağlantısına izin vermesi gerekir. Sunucuyu yedek moduna alma ve çıkarma işini kendisi yapar. Örnek veritabanımızda hesap tablosuna iki satır ekledikten sonra yedeği aldık:
pg_basebackup -h 127.0.0.1 -p 46470 -U postgres -D temel -c fast -l "gece temel yedegi" -Pwaiting for checkpoint
24115/57399 kB (42%), 0/1 tablespace
57409/57409 kB (100%), 0/1 tablespace
57409/57409 kB (100%), 1/1 tablespace-c fast hemen bir checkpoint ister; varsayılan spread ise bir sonraki planlı checkpoint'i yayarak bekler ve sisteme daha az yük bindirir. -l yedeğe etiket verir, -P ilerlemeyi gösterir. -F belirtilmediği için çıktı, kaynak veri diziniyle aynı düzende düz dosyalardır (-Ft ile tar olarak da alınabilir). -X varsayılan olarak stream'dir: yedek sürerken ikinci bir bağlantıdan WAL de akıtılır ve yedek tek başına açılabilir hale gelir.
Yedek bittiğinde arşiv klasörüne bir yedek geçmişi (backup history) dosyası düşer. Bu küçük metin dosyası, yedeğin hangi WAL segmentinden başladığını ve hangisinde bittiğini, etiketi ve zamanları içerir:
START WAL LOCATION: 0/7000028 (file 000000010000000000000007)
STOP WAL LOCATION: 0/7000120 (file 000000010000000000000007)
CHECKPOINT LOCATION: 0/7000080
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2026-11-07 12:24:02 +03
LABEL: gece temel yedegi
START TIMELINE: 1
STOP TIME: 2026-11-07 12:24:03 +03
STOP TIMELINE: 1Dosya adı 000000010000000000000007.00000028.backup biçimindedir. Bu yedekle geri dönebilmek için 7 numaralı segmentten başlayan bütün WAL dosyalarına ihtiyacınız var; daha küçük numaralı segmentler bu yedek için gereksizdir ve silinebilir. Kılavuz yine de birkaç yedek setini birlikte saklamayı öneriyor. Temel yedekler arasındaki süreyi belirlerken iki şeyi tartarsınız: biriken WAL'ın kapladığı yer ve geri dönüşte oynatılması gereken WAL miktarı, yani kurtarma süresi.
pg_basebackup dışında düşük seviyeli bir API da var: aynı oturumda pg_backup_start() çağırıp veri dizinini tar gibi bir araçla kopyalar, ardından pg_backup_stop() çağırırsınız. Bu fonksiyonun döndürdüğü backup_label içeriği yedeğin kök dizinine bayt bayt aynen yazılmalıdır. Hazır yedekleme yazılımlarıyla entegrasyon dışında pg_basebackup çok daha az hataya açıktır.
Adım 3: Kaza ve belirli bir ana geri dönüş

Yedekten sonra bir satır daha ekledik, geri dönmek istediğimiz anı not ettik ve tabloyu boşalttık:
INSERT INTO hesap VALUES (3, 'Burak Demir', 980.00);
SELECT pg_create_restore_point('silmeden_once');
-- sonuç: 0/8000248
SELECT now() AS guvenli_an;
-- sonuç: 2026-11-07 12:24:09.610819+03
DELETE FROM hesap;
-- DELETE 3
SELECT pg_switch_wal();Gerçek bir kazada now() çıktısı elinizde olmaz; uygulama günlüklerinden ya da kullanıcı bildirimlerinden yaklaşık bir zaman bulursunuz. Riskli bir işlemden önce pg_create_restore_point() ile WAL'a isimli bir işaret koymak, daha sonra recovery_target_name ile tam o noktaya dönmeyi sağlar. Aynı adı birden fazla kez kullanmayın, kurtarma ilk eşleşmede durur.
Kurtarma için kılavuzdaki adımları kendi test ortamımıza uyguladık. Gerçek bir sunucuda önce sunucuyu durdurur, mevcut veri dizinini (en azından pg_wal/ içeriğini, arşivlenmemiş segmentler olabilir) güvenli bir yere kopyalar, sonra veri dizinini boşaltıp yedeği yerine koyarsınız. Biz yedeği ayrı bir klasöre kopyalayıp farklı bir portta açtık:
- Yedeği hedef dizine kopyalayın ve yedekle gelen
pg_wal/içeriğini silin; bu dosyalar büyük ihtimalle eskidir. postgresql.confiçinerestore_commandve kurtarma hedefini yazın.- Veri dizininde boş bir
recovery.signaldosyası oluşturun. - İsterseniz kurtarma doğrulanana kadar kullanıcıların bağlanmaması için
pg_hba.conf'u geçici olarak kısıtlayın. - Sunucuyu başlatın, sonucu kontrol edin.
port = 46472
archive_mode = off
restore_command = 'copy "C:\\pgtest\\arsiv\\%f" "%p"'
recovery_target_time = '2026-11-07 12:24:09.610819+03'
recovery_target_action = 'promote'Kurtarılan kopyada archive_mode'u kapattık ki deneme sunucusu asıl arşive yazmasın. restore_command, arşivden istenen dosyayı (%f) sunucunun beklediği yere (%p) kopyalar. Bu komut arşivde olmayan dosyalar için de çağrılır ve o durumda sıfırdan farklı kod döndürmesi gerekir; bu bir hata sayılmaz. Sunucunun günlüğünden ilgili satırlar:
LOG: starting backup recovery with redo LSN 0/7000028, checkpoint LSN 0/7000080, on timeline ID 1
LOG: restored log file "000000010000000000000007" from archive
LOG: starting point-in-time recovery to 2026-11-07 12:24:09.610819+03
LOG: consistent recovery state reached at 0/7000120
LOG: restored log file "000000010000000000000008" from archive
LOG: recovery stopping before commit of transaction 804, time 2026-11-07 12:24:11.618262+03
LOG: last completed transaction was at log time 2026-11-07 12:24:09.609743+03
LOG: selected new timeline ID: 2
LOG: archive recovery complete
LOG: database system is ready to accept connectionsGünlükte, aradaki satırlarda Windows copy komutunun "dosya bulunamadı" iletisi de birkaç kez görünüyor. Kılavuz bunun normal olduğunu söylüyor: kurtarma, arşivde bulunmayan bir sonraki segmenti ya da 00000002.history gibi zaman çizelgesi dosyalarını da ister ve bulamayınca durur. Kurtarma DELETE işleminin commit'inden hemen önce durdu. Tabloya baktığımızda üç satır da yerindeydi:
id | sahip | bakiye
----+-------------+---------
1 | Ali Kaya | 1500.00
2 | Elif Şahin | 320.50
3 | Burak Demir | 980.00
(3 rows)Dikkat edin, 3 numaralı satır temel yedekte yoktu; yedekten sonra eklendi ve WAL arşivinden geri geldi. PITR'ın pg_dump'tan farkı tam olarak budur. Kurtarma tamamlanınca sunucu recovery.signal dosyasını sildi ve pg_is_in_recovery() false döndürdü.
Kurtarma hedefi ayarları
| Ayar | Anlamı |
|---|---|
| recovery_target_time | Bu zaman damgasına kadar oynat; saat dilimi kısaltması yerine sayısal fark veya tam bölge adı kullanın |
| recovery_target_name | pg_create_restore_point() ile konmuş isimli noktaya kadar oynat |
| recovery_target_lsn | Belirli bir WAL konumuna (LSN) kadar oynat |
| recovery_target_xid | Belirli bir transaction kimliğine kadar oynat |
| recovery_target = 'immediate' | Tutarlı duruma ulaşılır ulaşılmaz, yani yedeğin bittiği noktada dur |
| recovery_target_inclusive | Hedefin kendisi dahil mi (on, varsayılan) hariç mi (off) |
| recovery_target_action | Hedefe gelince pause (varsayılan), promote veya shutdown |
| recovery_target_timeline | Hangi zaman çizelgesi; varsayılan latest |
Hedef ayarlarından en fazla biri kullanılabilir; birden fazlası yazılırsa sunucu hata verir. Hedef verilmezse kurtarma arşivdeki son WAL'a kadar ilerler. Varsayılan eylem pause'dur: sunucu hedefe gelince oynatmayı duraklatır, salt okunur sorgu kabul eder ve doğru noktada olup olmadığınızı kontrol etmenizi bekler. Memnunsanız pg_wal_replay_resume() ile kurtarmayı bitirirsiniz; değilseniz sunucuyu durdurup daha ileri bir hedefle yeniden başlatırsınız. hot_standby kapalıysa pause, shutdown gibi davranır. Kurtarma hedefe ulaşmadan arşiv biterse sunucu ölümcül (FATAL) bir hatayla kapanır.
Bir sınır daha: hedef, temel yedeğin bitiş anından sonra olmalıdır. Yedek alınırken geçen bir ana bu yedekle dönemezsiniz; bunun için bir önceki temel yedekten başlamanız gerekir.
Zaman çizelgeleri (timeline)
Kurtarma bittiğinde günlükte selected new timeline ID: 2 satırını gördük ve pg_wal/ klasöründe 00000002.history ile 000000020000000000000008 gibi dosyalar oluştu. WAL dosya adının ilk sekiz onaltılık hanesi zaman çizelgesi numarasıdır. Her arşiv kurtarması yeni bir zaman çizelgesi başlatır; böylece kurtarmadan sonra üretilen WAL, asıl geçmişin WAL dosyalarının üzerine yazılmaz.
Bunun pratik değeri şudur: yanlış noktaya döndüğünüzü fark ederseniz eski geçmiş hâlâ arşivdedir ve başka bir noktaya tekrar dönebilirsiniz. Her yeni zaman çizelgesi için hangi çizelgeden ve hangi noktadan ayrıldığını yazan küçük bir .history dosyası da arşive gider. Bu dosyalar küçük olduğu için kılavuz onları süresiz saklamayı öneriyor. Kurtarılan sunucunun arşivlemesini açık bırakacaksanız aynı arşiv klasörünü paylaşmanın sonuçlarını iyi düşünün; biz bu yüzden deneme kopyasında archive_mode = off kullandık.
Artımlı yedek ve pg_combinebackup
Büyük ve verisinin çoğu az değişen veritabanlarında her gün tam yedek almak pahalıdır. pg_basebackup --incremental, önceki bir yedeğin backup_manifest dosyasını referans alır ve yalnızca o yedekten beri değişen blokları içeren bir yedek üretir. Hangi blokların değiştiğini sunucu WAL özetlerinden (WAL summaries) bulur; bu yüzden summarize_wal = on ayarı, referans yedekten önce açılmış olmalıdır. Özetler pg_wal/summaries altında tutulur ve wal_summary_keep_time (varsayılan 10 gün) sonra silinir. Belgeye göre artımlı yedek PostgreSQL 17 ve sonrası sunucularla çalışır.
pg_basebackup -h 127.0.0.1 -p 46470 -U postgres -D artimli --incremental=temel/backup_manifest -c fast
du -sh temel/base artimli/base
# 55M temel/base
# 8.5M artimli/baseTest kümemizde veri dosyalarının bulunduğu base klasörü tam yedekte 55 MB, artımlı yedekte 8.5 MB tuttu; değişmeyen dosyalar INCREMENTAL. önekli küçük dosyalarla temsil edildi. Artımlı yedek tek başına açılamaz. Geri yüklemeden önce pg_combinebackup ile tam yedek ve sonraki bütün artımlı yedekler, eskiden yeniye sırayla verilerek sentetik bir tam yedek üretilir:
pg_combinebackup temel artimli -o birlesik
echo $?
# 0
pg_verifybackup -n birlesik
# backup successfully verifiedpg_combinebackup yedeklerin doğru bir zincir oluşturup oluşturmadığını denetler, ama her yedeğin sağlam olduğunu doğrulamaz; bunun için pg_verifybackup kullanılır (biz -n ile WAL denetimini atladık). Hangi artımlı yedeğin hangi tam yedeğe dayandığını PostgreSQL sizin yerinize takip etmez; zincirden bir halkayı silerseniz sonraki artımlı yedekler işe yaramaz. PostgreSQL: Release 18 notlarına göre PostgreSQL 18 ile pg_combinebackup'a kopyalama yerine sabit bağlantı (hard link) kullanan -k/--link seçeneği eklendi; belgeye göre bu durumda çıktı dizininde yapılan değişiklikler girdi dizinlerini de etkileyebileceği için girdiler yalnızca sonradan silinecek kopyalar olmalıdır.
Kılavuz artımlı yedeğin her zaman kazandırmadığını da belirtiyor: küçük veritabanlarında tam yedek almak yönetim açısından daha basittir, verisinin tamamı sürekli değişen büyük veritabanlarında ise artımlı yedek tam yedekten pek küçük olmaz. Artımlı yedek de tam yedeğin bütün gereksinimlerini taşır: WAL arşivi, recovery.signal ve kurtarma adımları yine gerekir.
Sık yapılan hatalar

- Arşivlemeyi test etmeden temel yedek almak. Kılavuz, ilk temel yedekten önce arşiv prosedürünün kurulup sınanmasını istiyor; yedek, başlangıcından itibaren kesintisiz WAL zinciri olmadan işe yaramaz.
archive_commandyerine "hiçbir şey yapmayıp başarı döndüren" bir komut koymak. Belgeye göre bu arşivlemeyi fiilen kapatır ve kurtarma için gereken WAL zincirini koparır.pg_stat_archiver.failed_countdeğerini izlememek;pg_wal/sessizce dolar.- Kurtarmadan önce mevcut
pg_wal/içeriğini kaydetmemek; arşivlenmemiş son segmentler orada olabilir. - Kurtarılan sunucuyu asıl arşive yazar halde bırakmak ya da kurtarma ayarlarını sonraki başlangıçlar için temizlememek. Bu ayarlar yalnızca o kurtarma için geçerlidir.
- Tablespace oluşturduktan veya sildikten sonra yeni temel yedek almamak.
CREATE TABLESPACEmutlak yolla WAL'a yazılır ve başka makinede oynatılırken sorun çıkarabilir. - Yedekleri hiç geri yükleyip denememek. Bu yazıdaki gibi ayrı bir portta deneme kurtarması yapmak birkaç dakika sürer.
Aynı temel yedek ve WAL akışı, ikinci bir sunucuyu sürekli güncel tutmak için de kullanılabilir; buna bekleme (standby) sunucusu denir ve streaming replication kurulumu yazısında anlatılıyor. Replikasyon ve arşivlemenin durumunu izlemek için pg_stat_activity ve izleme görünümleri yazısına bakabilirsiniz.
Sık Sorulan Sorular
PITR ile tek bir tabloyu geri getirebilir miyim?
Doğrudan hayır; PITR bütün kümeyi geri getirir. Pratik çözüm, kurtarmayı bu yazıdaki gibi ayrı bir dizinde ve portta yapıp ihtiyacınız olan tabloyu oradan pg_dump -t ile alıp asıl sunucuya yüklemektir.
archive_timeout kaç olmalı?
Kılavuz bir dakika civarındaki değerleri genellikle makul buluyor. Daha kısa değerler, erken kapanan segmentler tam boyutta saklandığı için arşivi şişirir. Saniyeler içinde kopya istiyorsanız replikasyon kullanın.
pg_basebackup -X stream yedeği tek başına yeterli mi?
-X ile alınan yedek, yedek süresince üretilen WAL'ı içerdiği için arşive ihtiyaç duymadan açılabilir. Kılavuz buna bağımsız sıcak yedek (standalone hot backup) diyor; ancak yalnızca yedeğin bittiği ana döner, sonraki bir ana dönmek için WAL arşivi gerekir.
recovery.conf dosyası nerede?
Güncel sürümlerde ayrı bir recovery.conf yoktur. Kurtarma ayarları postgresql.conf içine yazılır, kurtarma modunu recovery.signal (hedefli kurtarma) veya standby.signal (bekleme sunucusu) dosyası başlatır. İkisi birden varsa bekleme modu önceliklidir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: PostgreSQL öğrenme rehberi · pg_dump ve pg_restore · Streaming replication · postgresql.conf yapılandırması · Yedekleme stratejileri