PostgreSQL logical replication, seçtiğiniz tabloların satır değişikliklerini (INSERT, UPDATE, DELETE, TRUNCATE) bir sunucudan diğerine aktaran yayın/abonelik (publish/subscribe) mekanizmasıdır. Kaynak tarafta wal_level = logical yapıp CREATE PUBLICATION yayin FOR TABLE tablo ile bir yayın, hedef tarafta CREATE SUBSCRIPTION abone CONNECTION '...' PUBLICATION yayin ile bir abonelik oluşturursunuz. Abonelik önce mevcut veriyi kopyalar, sonra değişiklikleri sürekli uygular. Fiziksel replikasyondan farkı, tablo, satır ve sütun düzeyinde seçim yapabilmeniz ve hedef sunucunun yazılabilir kalmasıdır.

Bu yazıda iki ayrı PostgreSQL 18.1 kümesi arasında logical replication kurduk: bir tablodan hassas bir sütunu dışarıda bırakan sütun listesi, başka bir tabloda yalnızca bir ile ait siparişleri gönderen satır filtresi kullandık; replica identity hatasını, bir UPDATE'in abonede DELETE'e dönüşmesini, birincil anahtar çakışmasını ve şema değişikliğinin replikasyonu durdurmasını gerçek çıktılarla gösteriyoruz. Bütün kümeyi bayt düzeyinde kopyalamak istiyorsanız streaming replication kurulumu yazısı daha uygun.

Logical replication ne zaman kullanılır?

Logical replication ne zaman kullanılır? Bir veritabanının tamamını veya bir alt kümesini, değişiklikler oldukça başka..., Abonede, gelen her değişiklik için tetikleyici...

Fiziksel replikasyon veri dosyalarının blok adresleriyle birebir kopyasını üretir. Logical replication ise değişiklikleri satır olarak, satırın kimliğine (replica identity, genellikle birincil anahtar) dayanarak taşır. Abone sunucu sıradan bir PostgreSQL kümesidir: kendi tabloları, indeksleri ve kullanıcıları olabilir, yazma kabul eder ve kendisi de yayın oluşturabilir. Kılavuzun saydığı tipik kullanım alanları şunlardır:

  • Bir veritabanının tamamını veya bir alt kümesini, değişiklikler oldukça başka sunuculara göndermek.
  • Abonede, gelen her değişiklik için tetikleyici çalıştırmak.
  • Birden fazla veritabanını analiz için tek bir veritabanında birleştirmek.
  • Farklı PostgreSQL ana sürümleri arasında veri aktarmak (sürüm yükseltme için de kullanılır).
  • Farklı platformlar arasında, örneğin Linux'tan Windows'a replikasyon yapmak.
  • Verinin bir kısmını farklı kullanıcı gruplarıyla paylaşmak.

Yayın kavramı bir değişiklik kümesidir. Her yayın tek bir veritabanında bulunur, bir tablo birden fazla yayına eklenebilir ve her yayının birden fazla abonesi olabilir. Yayın, tablonun nasıl sorgulandığını etkilemez.

Hazırlık: ayarlar ve yetkiler

Yayıncı (publisher) tarafında tek zorunlu değişiklik wal_level = logical ayarıdır ve sunucunun yeniden başlatılmasını gerektirir. max_replication_slots ve max_wal_senders varsayılanları basit bir kurulum için yeterlidir; her abonelik bir slot kullanır, ilk kopyalama sırasında geçici ek slotlar açılır. Abone tarafında ise max_logical_replication_workers ve PostgreSQL 18 ile eklenen max_active_replication_origins abonelik sayısına yetmelidir.

ini
wal_level = logical

Yetkiler konusunda kılavuzun güvenlik bölümü şunları söylüyor: replikasyon bağlantısı için kullanılan rolün REPLICATION özniteliği (veya süper kullanıcı) ve LOGIN hakkı olmalı, ilk veri kopyası için yayınlanan tablolarda SELECT yetkisi bulunmalıdır. Fiziksel replikasyondan farklı olarak logical replication bağlantısı belirli bir veritabanına yapılır; bu yüzden pg_hba.conf'ta replication değil, veritabanı adı veya all yazan bir satır gerekir. Yayın oluşturmak için veritabanında CREATE yetkisi ve tabloların sahibi olmak, abonelik oluşturmak için pg_create_subscription rolünün yetkileri ile CREATE yetkisi gerekir. Biz yayıncıda, önceki testten kalan replikator rolüne tablolarda SELECT yetkisi verdik.

Yayın oluşturmak: sütun listesi ve satır filtresi

Yayıncıda bir müşteri ve bir sipariş tablosu oluşturduk. Müşteri tablosundaki telefon sütununu göndermek istemiyoruz, sipariş tablosundan ise yalnızca İzmir siparişlerini:

sql
CREATE TABLE musteri (
    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    ad text NOT NULL,
    il text NOT NULL,
    telefon text
);
CREATE TABLE siparis (
    id integer PRIMARY KEY,
    il text NOT NULL,
    tutar numeric(10,2) NOT NULL
);
INSERT INTO musteri (ad, il, telefon) VALUES
    ('Ayşe Yılmaz', 'İzmir', '0532 000 00 01'),
    ('Mehmet Öztürk', 'Ankara', '0532 000 00 02');
INSERT INTO siparis VALUES (1, 'İzmir', 250.00), (2, 'Ankara', 99.90), (3, 'İzmir', 1200.00);

CREATE PUBLICATION musteri_yayini FOR TABLE musteri (id, ad, il);
CREATE PUBLICATION izmir_yayini FOR TABLE siparis WHERE (il = 'İzmir');

psql'in \dRp+ komutu yayınları ayrıntılı listeler:

text
                                    Publication izmir_yayini
  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
----------+------------+---------+---------+---------+-----------+-------------------+----------
 postgres | f          | t       | t       | t       | t         | none              | f
Tables:
    "public.siparis" WHERE (il = 'İzmir'::text)

                                   Publication musteri_yayini
  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root 
----------+------------+---------+---------+---------+-----------+-------------------+----------
 postgres | f          | t       | t       | t       | t         | none              | f
Tables:
    "public.musteri" (id, ad, il)

Varsayılan olarak dört DML türünün hepsi yayınlanır; WITH (publish = 'insert') ile bunu kısıtlayabilirsiniz. Bu ayar yalnızca sonraki değişiklikleri etkiler, ilk veri kopyasını etkilemez. Tablolar tek tek verilebileceği gibi FOR TABLES IN SCHEMA ile bir şemanın tamamı ya da FOR ALL TABLES ile veritabanındaki bütün tablolar yayınlanabilir; bu son iki biçim süper kullanıcı ister ve satır filtresi kabul etmez. Tablo eklemek veya çıkarmak için ALTER PUBLICATION ... ADD TABLE / DROP TABLE kullanılır.

Replica identity hatası

Yayını oluşturduktan sonra yayıncıda sıradan bir güncelleme yaptığımızda şu hatayı aldık:

sql
UPDATE siparis SET tutar = 260.00 WHERE id = 1;
text
ERROR:  cannot update table "siparis"
DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.

Hata yayıncıdaki uygulamanın kendisini durdurdu; bu, logical replication kurarken üretim sistemini etkileyebilecek en önemli tuzaklardan biridir. Kural şu: bir yayın UPDATE veya DELETE gönderiyorsa, satır filtresinde yalnızca replica identity içindeki sütunlar kullanılabilir. Replica identity varsayılan olarak birincil anahtardır (id), filtre ise il sütununu kullanıyor. Aynı kural sütun listeleri için de geçerlidir: UPDATE/DELETE yayınlanıyorsa liste replica identity sütunlarını içermelidir. Çözüm seçenekleri:

  • Yayını yalnızca INSERT gönderecek şekilde kurmak (WITH (publish = 'insert')); bu durumda filtre her sütunu kullanabilir.
  • Replica identity'yi filtre sütunlarını kapsayacak şekilde değiştirmek: ALTER TABLE siparis REPLICA IDENTITY FULL; bütün satırı kimlik yapar.
  • Filtreyi birincil anahtar sütunlarına dayandırmak.

Biz REPLICA IDENTITY FULL seçtik ve güncelleme çalıştı. Bunun bir maliyeti var: her UPDATE ve DELETE için bütün eski satır WAL'a yazılır ve abonede satır uygun bir indeks yoksa çok verimsiz biçimde aranır. Replica identity'si NOTHING olan veya birincil anahtarı olmayan tablolarda UPDATE ve DELETE, bu işlemleri yayınlayan bir yayına dahil edildiğinde hata verir; INSERT ise her durumda çalışır.

Abone tarafı: şemayı kopyalamak ve abonelik oluşturmak

Abone tarafı: şemayı kopyalamak ve abonelik oluşturmak: copy_data, create_slot, enabled, connect

Logical replication şemayı ve DDL komutlarını kopyalamaz. Hedef tabloların abonede önceden var olması gerekir; tablolar tam nitelikli adla (şema.tablo) eşleşir, sütunlar adla eşleşir. Kılavuz ilk şemanın pg_dump --schema-only ile aktarılmasını öneriyor. Biz yalnızca iki tabloyu, yetki ve yayın tanımları olmadan aktardık:

bash
pg_dump -h 127.0.0.1 -p 46470 -U postgres --schema-only -x --no-publications -t musteri -t siparis ornek_yayin \
  | psql -h 127.0.0.1 -p 46471 -U postgres -X -q -d ornek_abone

Abonedeki tablonun telefon sütunu da var, çünkü şemayı olduğu gibi kopyaladık. Abonedeki tablonun yayınlanan bütün sütunları içermesi şarttır; fazladan sütunlar ise sorun değildir ve tanımlı varsayılan değerle doldurulur. Sütun tiplerinin aynı olması da gerekmez, metin gösterimi hedef tipe çevrilebildiği sürece (örneğin integer'dan bigint'e) aktarım çalışır. Şimdi aboneliği oluşturuyoruz:

sql
CREATE SUBSCRIPTION musteri_abone
    CONNECTION 'host=127.0.0.1 port=46470 dbname=ornek_yayin user=replikator'
    PUBLICATION musteri_yayini, izmir_yayini;
-- NOTICE:  created replication slot "musteri_abone" on publisher

Abonelik yayıncıda kendi adıyla bir mantıksal replikasyon slotu oluşturdu, mevcut veriyi kopyaladı ve akışa geçti. İki saniye sonra abonedeki tablolar:

text
 id |      ad       |   il   | telefon 
----+---------------+--------+---------
  1 | Ayşe Yılmaz   | İzmir  | 
  2 | Mehmet Öztürk | Ankara | 
(2 rows)

 id |  il   |  tutar  
----+-------+---------
  1 | İzmir |  260.00
  3 | İzmir | 1200.00
(2 rows)

telefon sütunu boş geldi, Ankara siparişi hiç kopyalanmadı. Satır filtresi ilk kopyalamada da uygulanır; aynı tablo birden fazla yayında farklı filtrelerle yayınlanıyorsa, filtreler VEYA ile birleştirilir. Abonelik oluşturmada kullanılan başlıca parametreler şunlardır:

ParametreVarsayılanNe yapar
copy_datatrueMevcut veriyi ilk başta kopyalar
create_slottrueYayıncıda slotu oluşturur
enabledtrueAboneliği hemen başlatır
connecttruefalse ise yayıncıya hiç bağlanmaz; pg_dump abonelikleri böyle yazar
slot_nameabonelik adıYayıncıdaki slotun adı
binaryfalseVeriyi ikili biçimde ister; daha hızlı olabilir ama sürüm ve mimariler arasında daha az taşınabilir
streamingparallel (PG18)Devam eden büyük transaction'ların akıtılma biçimi

Değişikliklerin akışı ve UPDATE dönüşümü

Yayıncıda yeni bir müşteri, bir Ankara ve bir İzmir siparişi ekledik; ayrıca 3 numaralı İzmir siparişinin ilini Ankara olarak güncelledik:

sql
INSERT INTO musteri (ad, il, telefon) VALUES ('Zeynep Çelik', 'Muğla', '0532 000 00 03');
INSERT INTO siparis VALUES (4, 'Ankara', 45.50), (5, 'İzmir', 80.00);
UPDATE siparis SET il = 'Ankara' WHERE id = 3;
text
 id |  il   | tutar  
----+-------+--------
  1 | İzmir | 260.00
  5 | İzmir |  80.00
(2 rows)

3 numaralı sipariş abonede silindi. Bu bilinçli bir davranıştır: UPDATE işlendiğinde satır filtresi hem eski hem yeni satır için değerlendirilir. Eski satır filtreye uyup yeni satır uymuyorsa, abonede tutarlılık için UPDATE bir DELETE'e dönüştürülür; tersi durumda INSERT'e dönüşür. İkisi de uymuyorsa değişiklik gönderilmez, ikisi de uyuyorsa normal UPDATE olarak gider.

Eski satırYeni satırAbonede
uymuyoruymuyorgönderilmez
uymuyoruyuyorINSERT
uyuyoruymuyorDELETE
uyuyoruyuyorUPDATE

Abonede durumu pg_stat_subscription görünümü, yayıncıda ise pg_replication_slots gösterir:

text
    subname    | worker_type | received_lsn | latest_end_lsn 
---------------+-------------+--------------+----------------
 musteri_abone | apply       | 0/104A91A0   | 0/104A91A0
(1 row)

   slot_name   |  plugin  | slot_type |  database   | active 
---------------+----------+-----------+-------------+--------
 musteri_abone | pgoutput | logical   | ornek_yayin | t
(1 row)

Sequence değerleri kopyalanmaz

Abonedeki musteri tablosunda 3 satır vardı, ama tablonun identity sequence'ine baktığımızda:

sql
SELECT last_value FROM musteri_id_seq;
-- sonuç: 1

Kılavuz bunu kısıtlamalar arasında sayıyor: sequence'e bağlı sütunların değerleri tablo verisinin parçası olarak gelir, ama sequence'in kendisi abonede başlangıç değerinde kalır. Abone salt okunur kullanılıyorsa sorun olmaz. Aboneye geçiş (switchover) planlıyorsanız, geçişten önce sequence'leri yayıncıdaki güncel değerlere veya tablolardaki en büyük değerin üzerine ayarlamanız gerekir (setval için identity, serial ve sequence yazısına bakın). Aksi halde abonede ilk INSERT, var olan bir anahtarla çakışır.

Çakışmalar: abone tarafında yazılan veri

Abone yazılabilir olduğu için, abonede doğrudan yazılan bir satır gelen değişiklikle çakışabilir. Bunu görmek için abonede 6 numaralı siparişi elle ekledik, ardından yayıncıda aynı anahtarla bir sipariş ve onun arkasından 7 numaralı siparişi ekledik. Abonenin günlüğünde şu satırlar belirdi:

text
ERROR:  conflict detected on relation "public.siparis": conflict=insert_exists
DETAIL:  Key already exists in unique index "siparis_pkey", modified in transaction 785.
	Key (id)=(6); existing local row (6, İzmir, 10.00); remote row (6, İzmir, 300.00).
CONTEXT:  processing remote data for replication origin "pg_16406" during message type "INSERT" for replication target relation "public.siparis" in transaction 825, finished at 0/104A9260

Çakışma hata ürettiği için replikasyon durdu: 7 numaralı sipariş de gelmedi, çünkü abone değişiklikleri yayıncıdaki sırayla uygular. İşçi süreç belirli aralıklarla yeniden denedi; pg_stat_subscription_stats görünümünde apply_error_count ve confl_insert_exists değerleri 2'ye çıkmıştı. PostgreSQL: Release 18 notlarına göre çakışmaların bu ayrıntıyla günlüğe yazılması ve pg_stat_subscription_stats içindeki çakışma sütunları PostgreSQL 18 ile geldi.

Çözmenin iki yolu var. Birincisi abonedeki veriyi düzeltmek: biz yerel satırı DELETE FROM siparis WHERE id = 6 ile sildik, birkaç saniye içinde işçi yeniden denedi ve hem 300.00 tutarlı 6 numaralı sipariş hem de 7 numaralı sipariş geldi. İkincisi, çakışan uzak transaction'ı tamamen atlamak: günlükteki finished at 0/104A9260 değeri o transaction'ın bitiş LSN'idir ve ALTER SUBSCRIPTION musteri_abone SKIP (lsn = '0/104A9260') ile atlanabilir. Atlanan transaction'ın içindeki bütün değişiklikler kaybolur, bu yüzden bu yolu bilerek seçin.

Her çakışma hata üretmez. Güncellenecek veya silinecek satır abonede bulunamazsa (update_missing, delete_missing) değişiklik sessizce atlanır ve yalnızca sayaçlara yansır. Birincil anahtar ve benzersizlik ihlalleri (insert_exists, update_exists) ise elle çözülene kadar replikasyonu durdurur. Çakışmayı başka bir kaynağın yaptığını ayırt etmek için abonede track_commit_timestamp açılması gerekir. Hatada aboneliği otomatik devre dışı bırakmak isterseniz disable_on_error parametresini kullanabilirsiniz.

Şema değişiklikleri replikasyonu durdurur

Yayıncıda siparis tablosuna yeni bir sütun ekleyip o sütunu dolduran bir satır yazdık:

sql
ALTER TABLE siparis ADD COLUMN kargo text;
INSERT INTO siparis VALUES (8, 'İzmir', 120.00, 'Aras');
text
ERROR:  logical replication target relation "public.siparis" is missing replicated column: "kargo"

Abone bu hatayı verdi ve replikasyon durdu. Abonede aynı ALTER TABLE komutunu çalıştırdığımızda bekleyen satır geldi. Kılavuzun önerisi şu: ekleme türündeki şema değişikliklerini önce aboneye, sonra yayıncıya uygulayın; böylece aradaki hatalardan kaçınırsınız. Şemaların iki tarafta birebir aynı olması gerekmez, ama yayınlanan her sütun abonede bulunmalıdır.

Diğer kısıtlamalar

Diğer kısıtlamalar: Sequence verisi, büyük nesneler (large objects) ve DDL kopyalanmaz., Yalnızca tablolar (bölümlenmiş tablolar dahil) replike edilebilir; view..., Yabancı...
  • Sequence verisi, büyük nesneler (large objects) ve DDL kopyalanmaz.
  • Yalnızca tablolar (bölümlenmiş tablolar dahil) replike edilebilir; view, materialized view ve foreign table edilemez. Abonede hedef de normal bir tablo olmalıdır.
  • Yabancı anahtarla bağlı tablolarda TRUNCATE, aboneliğe dahil olmayan bir tabloya referans varsa abonede başarısız olur.
  • Bölümlenmiş tablolarda değişiklikler varsayılan olarak yaprak bölümlerden gelir; publish_via_partition_root ile kök tablonun kimliği kullanılabilir (tablo bölümleme).
  • Abonelik silinip yeniden oluşturulursa senkronizasyon bilgisi kaybolur ve veri yeniden kopyalanır.

Aboneliği kaldırmak ve slotları temizlemek

sql
DROP SUBSCRIPTION musteri_abone;
-- NOTICE:  dropped replication slot "musteri_abone" on publisher

DROP SUBSCRIPTION yayıncıdaki slotu da siler. Yayıncıya ulaşılamıyorsa önce ALTER SUBSCRIPTION ... DISABLE ve ALTER SUBSCRIPTION ... SET (slot_name = NONE) ile slot bağını koparıp sonra aboneliği silmeniz gerekir; bu durumda yayıncıda kalan slotu elle silmezseniz WAL birikmeye devam eder ve disk dolabilir. Kullanılmayan slotlar için PostgreSQL 18 ile gelen idle_replication_slot_timeout ayarı mantıksal slotları da etkiler. Güvenlik tarafında PostgreSQL: Release 18.6 sürümünde mantıksal kod çözme eklentilerini sınırlayan output_plugin_libraries parametresi eklendi; aboneliklerin kullandığı eklentinin adı pgoutput'tur ve bu listede bulunmalıdır.

Sık yapılan hatalar

  • wal_level'ı logical yapıp sunucuyu yeniden başlatmayı unutmak; bu ayar yalnızca başlangıçta okunur.
  • Satır filtresini veya sütun listesini replica identity'yi hesaba katmadan yazmak; yayıncıdaki UPDATE/DELETE komutları hata verir.
  • Aboneye şemayı aktarmamak; tablolar önceden var olmalıdır.
  • Abonede, replike edilen tablolara uygulama tarafından yazmak; anahtar çakışmaları replikasyonu durdurur.
  • Yayıncıda sütun ekleyip aboneyi unutmak.
  • Aboneye geçişte sequence değerlerini güncellememek.
  • Silinen veya erişilemeyen aboneliklerin slotlarını yayıncıda bırakmak.

Replikasyon gecikmesini ve slotları izlemek için izleme görünümleri, bağlantı ve kimlik doğrulama sorunları için pg_hba.conf yazılarına bakabilirsiniz.

Sık Sorulan Sorular

Logical replication ile sürüm yükseltme yapılabilir mi?

Evet; kılavuz farklı ana sürümler arasında replikasyonu tipik kullanım alanları arasında sayıyor ve PostgreSQL 18 sürüm notları da önceki sürümlerden geçiş için pg_dumpall, pg_upgrade veya logical replication gerektiğini belirtiyor. Ayrıntılar için sürüm yükseltme yazısına bakın.

Abone salt okunur mu olmalı?

Zorunlu değil, abone normal bir sunucudur. Ancak replike edilen tablolara başka bir kaynaktan yazılırsa çakışma çıkabilir. Kılavuza göre abone, uygulama tarafından salt okunur kullanıldığında tek bir abonelikten çakışma doğmaz.

Birincil anahtarı olmayan tablo yayınlanabilir mi?

INSERT için evet. UPDATE ve DELETE yayınlanacaksa tabloya birincil anahtar, uygun bir benzersiz indeks (REPLICA IDENTITY USING INDEX) veya REPLICA IDENTITY FULL tanımlamanız gerekir.

Üretilmiş (generated) sütunlar replike edilir mi?

PostgreSQL 18 ile, sütun listesinde adı geçen üretilmiş sütunlar yayınlanır; liste yoksa publish_generated_columns parametresi (varsayılan none, diğer değer stored) karar verir. Daha önce bu sütunlar hiç replike edilmiyordu.

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 · Streaming replication · Sürüm yükseltme · Sequence · pg_dump · İzleme